From 2154255ec37221fbd1766c46a176e7d31951fcd3 Mon Sep 17 00:00:00 2001 From: efb4f5ff-1298-471a-8973-3d47447115dc <73130443+efb4f5ff-1298-471a-8973-3d47447115dc@users.noreply.github.com> Date: Sun, 9 Oct 2022 03:03:02 +0200 Subject: [PATCH 1/4] Advertise FT better in README (#2677) * logical structure * swap headings * remove redundant info * remove disclaimer * concrete wording * swap nightly and unofficial * smaller header * redirect users to discussions * list chapters as feature * update localization image * added some features * more features and reordering * Change wording in features * correction on screenshot feature * condense show/hide into one * me -> us * correction on tor proxy * family * update proxy feature * clarify extension builds * revert previous commit and add statement --- README.md | 75 ++++++++++++++++++++++++++----------------------------- 1 file changed, 35 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 167058ef2381..9b03d688e04b 100644 --- a/README.md +++ b/README.md @@ -6,63 +6,71 @@ FreeTube is an open source desktop YouTube player built with privacy in mind. Use YouTube without advertisements and prevent Google from tracking you with their cookies and JavaScript. Available for Windows, Mac & Linux thanks to Electron. -Please note that FreeTube is currently in Beta. While it should work well for -most users, there are still bugs and missing features that need to be -addressed. -

Download FreeTube


-

Browser ExtensionHow does it work?ScreenshotsFeaturesDownload LinksContributingLocalizationContactDonateLicense

+

ScreenshotsHow does it work?FeaturesDownload LinksContributingLocalizationContactDonateLicense

WebsiteBlogDocumentationFAQDiscussions


-## Browser Extension - -FreeTube is supported by the [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect) and [LibRedirect](https://github.com/libredirect/libredirect) extensions, which will allow you to open YouTube links into FreeTube. You must enable the option within the advanced settings for it to work. +Please note that FreeTube is currently in Beta. While it should work well for most users, there are still bugs and missing features that need to be addressed. If you have an idea or if you found a bug, please submit a [GitHub issue](https://github.com/FreeTubeApp/FreeTube/issues/new/choose) so that +we can track it. Please search [the existing issues](https://github.com/FreeTubeApp/FreeTube/issues) before submitting to +prevent duplicates! -* Download Privacy Redirect for [Firefox](https://addons.mozilla.org/en-US/firefox/addon/privacy-redirect/) or [Google Chrome](https://chrome.google.com/webstore/detail/privacy-redirect/pmcmeagblkinmogikoikkdjiligflglb). - -* Download LibRedirect for [Firefox](https://addons.mozilla.org/firefox/addon/libredirect/) or [Google Chrome](https://github.com/libredirect/libredirect/blob/master/chromium.md). - -Disclaimer: Learn more about why a browser extension is bad for your [privacy](https://www.privacyguides.org/desktop-browsers/#additional-resources). - -If you have issues with the extension working with FreeTube, please create an issue in this repository instead of the extension repository. +## Screenshots + ## How does it work? FreeTube uses a built in extractor to grab and serve data / videos. The [Invidious API](https://github.com/iv-org/invidious) can also optionally be used. FreeTube does not use any official APIs to obtain data. While YouTube can still see your video requests, it can no longer track you using cookies or JavaScript. Your subscriptions and history are stored locally on your computer and never sent out. Using a VPN or Tor is highly recommended to hide your IP while using FreeTube. -Go to [FreeTube's Documentation](https://docs.freetubeapp.io/) if you'd like to know more about how to operate FreeTube and its features. - -## Screenshots - - ## Features * Watch videos without ads * Use YouTube without Google tracking you using cookies and JavaScript * Two extractor APIs to choose from (Built in or Invidious) * Subscribe to channels without an account -* Local subscriptions, history, and saved videos +* Connect to an externally setup proxy such as Tor +* View and search your local subscriptions, history, and saved videos * Organize your subscriptions into "Profiles" to create a more focused feed * Export & import subscriptions +* Youtube Trending +* Youtube Chapters +* Most popular videos page based on the set Invidious instance +* SponsorBlock * Open videos from your browser directly into FreeTube (with extension) -* Mini Player +* Watch videos using an external player * Full Theme support +* Make a screenshot of a video +* Multiple windows +* Mini Player (Picture-in-Picture) +* Keyboard shortcuts +* Option to show only family friendly content +* Show/hide functionality or elements within the app using the distraction free settings -## Download Links +### Browser Extension +FreeTube is supported by the [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect) and [LibRedirect](https://github.com/libredirect/libredirect) extensions, which will allow you to open YouTube links into FreeTube. You must enable the option within the advanced settings of the extension for it to work. -### Official Downloads +* Download Privacy Redirect for [Firefox](https://addons.mozilla.org/en-US/firefox/addon/privacy-redirect/) or [Google Chrome](https://chrome.google.com/webstore/detail/privacy-redirect/pmcmeagblkinmogikoikkdjiligflglb). + +* Download LibRedirect for [Firefox](https://addons.mozilla.org/firefox/addon/libredirect/) or [Google Chrome](https://github.com/libredirect/libredirect/blob/master/chromium.md). + +If you have issues with the extension working with FreeTube, please create an issue in this repository instead of the extension repository. This extension does not work on Linux portable builds! +## Download Links +### Official Downloads * [GitHub Releases](https://github.com/FreeTubeApp/FreeTube/releases) * [FreeTube Website](https://freetubeapp.io/#download) * Flatpak on Flathub: [Download](https://flathub.org/apps/details/io.freetubeapp.FreeTube) [Source](https://github.com/flathub/io.freetubeapp.FreeTube) -### Unofficial Downloads +#### Automated Builds (Nightly / Weekly) +Builds are automatically created from changes to our development branch via [GitHub Actions](https://github.com/FreeTubeApp/FreeTube/actions?query=workflow%3ABuild). + +The first build with a green check mark is the latest build. You will need to have a GitHub account to download these builds. +### Unofficial Downloads These builds are maintained by the community. While they should be safe, download at your own risk. There may be issues with using these versus the official builds. Any issues specific with these builds should be sent to their respective maintainer. * Arch User Repository (AUR): [Download](https://aur.archlinux.org/packages/freetube-bin/) @@ -77,17 +85,7 @@ These builds are maintained by the community. While they should be safe, downlo * Windows Package Manager (winget): [Usage](https://docs.microsoft.com/en-us/windows/package-manager/winget/) -### Automated Builds (Nightly / Weekly) - -Builds are automatically created from changes to our development branch via [GitHub Actions](https://github.com/FreeTubeApp/FreeTube/actions?query=workflow%3ABuild). - -The first build with a green check mark is the latest build. You will need to have a GitHub account to download these builds. - ## Contributing -If you have an idea or if you found a bug, please submit a GitHub issue so that -we can track it. Please search the existing issues before submitting to -prevent duplicates. - If you like to get your hands dirty and want to contribute, we would love to have your help. Send a pull request and someone will review your code. Please follow the [Contribution @@ -98,7 +96,7 @@ Thank you very much to the [People and Projects](https://docs.freetubeapp.io/cre ## Localization -Translation status +Translation status We are actively looking for translations! We use [Weblate](https://hosted.weblate.org/engage/free-tube/) to make it easy for translators to get involved. Click on the badge above to learn how to get involved. @@ -106,10 +104,7 @@ We are actively looking for translations! We use [Weblate](https://hosted.webla For the Linux Flatpak, the desktop entry comment string can be translated at our [Flatpak repository](https://github.com/flathub/io.freetubeapp.FreeTube/blob/master/io.freetubeapp.FreeTube.desktop). ## Contact - -If you ever have any questions, feel free to make an issue here on GitHub. Alternatively, you can email me at FreeTubeApp@protonmail.com or you can join our [Matrix Community](https://matrix.to/#/+freetube:matrix.org). Don't forget to check out the [rules](https://docs.freetubeapp.io/community/matrix/) before joining. - -You can also stay up to date by reading the [FreeTube Blog](https://write.as/freetube/). [View the welcome blog](https://write.as/freetube/welcome-to-freetube-blogs). +If you ever have any questions, feel free to ask it on our [Discussions](https://github.com/FreeTubeApp/FreeTube/discussions) page. Alternatively, you can email us at FreeTubeApp@protonmail.com or you can join our [Matrix Community](https://matrix.to/#/+freetube:matrix.org). Don't forget to check out the [rules](https://docs.freetubeapp.io/community/matrix/) before joining. ## Donate If you enjoy using FreeTube, you're welcome to leave a donation using the following methods. From 41fee01217ad79c04de23857771429c4ae2f1df1 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Sat, 8 Oct 2022 21:08:34 -0400 Subject: [PATCH 2/4] Improve Importing Subscriptions (#2604) * improve import * fix merge conflicts * dont add duplicate subscriptions, remove redundant "uniqueId" * Update src/renderer/components/data-settings/data-settings.js Co-authored-by: PikachuEXE * fix unexpected errors dont show toast when no errors. dont error when already subscribed * remove check for legacy subscriptions * rename method * deduplicate importing code * remove unused code Co-authored-by: PikachuEXE --- .../components/data-settings/data-settings.js | 581 ++++++++---------- .../data-settings/data-settings.vue | 14 +- static/locales/en-US.yaml | 6 +- 3 files changed, 246 insertions(+), 355 deletions(-) diff --git a/src/renderer/components/data-settings/data-settings.js b/src/renderer/components/data-settings/data-settings.js index c77e830f86da..d85c03d34eb4 100644 --- a/src/renderer/components/data-settings/data-settings.js +++ b/src/renderer/components/data-settings/data-settings.js @@ -59,18 +59,6 @@ export default Vue.extend({ allPlaylists: function () { return this.$store.getters.getAllPlaylists }, - importSubscriptionsPromptNames: function () { - const importFreeTube = this.$t('Settings.Data Settings.Import FreeTube') - const importYouTube = this.$t('Settings.Data Settings.Import YouTube') - const importNewPipe = this.$t('Settings.Data Settings.Import NewPipe') - return [ - `${importFreeTube} (.db)`, - `${importYouTube} (.csv)`, - `${importYouTube} (.json)`, - `${importYouTube} (.opml)`, - `${importNewPipe} (.json)` - ] - }, exportSubscriptionsPromptNames: function () { const exportFreeTube = this.$t('Settings.Data Settings.Export FreeTube') const exportYouTube = this.$t('Settings.Data Settings.Export YouTube') @@ -85,6 +73,9 @@ export default Vue.extend({ }, usingElectron: function () { return process.env.IS_ELECTRON + }, + primaryProfile: function () { + return JSON.parse(JSON.stringify(this.profileList[0])) } }, methods: { @@ -94,33 +85,21 @@ export default Vue.extend({ }) }, - importSubscriptions: function (option) { - this.showImportSubscriptionsPrompt = false - - if (option === null) { - return + importSubscriptions: async function () { + const options = { + properties: ['openFile'], + filters: [ + { + name: this.$t('Settings.Data Settings.Subscription File'), + extensions: ['db', 'csv', 'json', 'opml', 'xml'] + } + ] } - switch (option) { - case 'freetube': - this.importFreeTubeSubscriptions() - break - case 'youtubenew': - this.importCsvYouTubeSubscriptions() - break - case 'youtube': - this.importYouTubeSubscriptions() - break - case 'youtubeold': - this.importOpmlYouTubeSubscriptions() - break - case 'newpipe': - this.importNewPipeSubscriptions() - break + const response = await this.showOpenDialog(options) + if (response.canceled || response.filePaths?.length === 0) { + return } - }, - - handleFreetubeImportFile: async function (response) { let textDecode try { textDecode = await this.readFileFromDialog({ response }) @@ -131,6 +110,25 @@ export default Vue.extend({ }) return } + response.filePaths.forEach(filePath => { + if (filePath.endsWith('.csv')) { + this.importCsvYouTubeSubscriptions(textDecode) + } else if (filePath.endsWith('.db')) { + this.importFreeTubeSubscriptions(textDecode) + } else if (filePath.endsWith('.opml') || filePath.endsWith('.xml')) { + this.importOpmlYouTubeSubscriptions(textDecode) + } else if (filePath.endsWith('.json')) { + textDecode = JSON.parse(textDecode) + if (textDecode.subscriptions) { + this.importNewPipeSubscriptions(textDecode) + } else { + this.importYouTubeSubscriptions(textDecode) + } + } + }) + }, + + importFreeTubeSubscriptions: async function (textDecode) { textDecode = textDecode.split('\n') textDecode.pop() textDecode = textDecode.map(data => JSON.parse(data)) @@ -141,8 +139,6 @@ export default Vue.extend({ textDecode = await this.convertOldFreeTubeFormatToNew(textDecode) } - const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0])) - textDecode.forEach((profileData) => { // We would technically already be done by the time the data is parsed, // however we want to limit the possibility of malicious data being sent @@ -175,15 +171,15 @@ export default Vue.extend({ }) } else { if (profileObject.name === 'All Channels' || profileObject._id === MAIN_PROFILE_ID) { - primaryProfile.subscriptions = primaryProfile.subscriptions.concat(profileObject.subscriptions) - primaryProfile.subscriptions = primaryProfile.subscriptions.filter((sub, index) => { - const profileIndex = primaryProfile.subscriptions.findIndex((x) => { + this.primaryProfile.subscriptions = this.primaryProfile.subscriptions.concat(profileObject.subscriptions) + this.primaryProfile.subscriptions = this.primaryProfile.subscriptions.filter((sub, index) => { + const profileIndex = this.primaryProfile.subscriptions.findIndex((x) => { return x.name === sub.name }) return profileIndex === index }) - this.updateProfile(primaryProfile) + this.updateProfile(this.primaryProfile) } else { const existingProfileIndex = this.profileList.findIndex((profile) => { return profile.name.includes(profileObject.name) @@ -204,15 +200,15 @@ export default Vue.extend({ this.updateProfile(profileObject) } - primaryProfile.subscriptions = primaryProfile.subscriptions.concat(profileObject.subscriptions) - primaryProfile.subscriptions = primaryProfile.subscriptions.filter((sub, index) => { - const profileIndex = primaryProfile.subscriptions.findIndex((x) => { + this.primaryProfile.subscriptions = this.primaryProfile.subscriptions.concat(profileObject.subscriptions) + this.primaryProfile.subscriptions = this.primaryProfile.subscriptions.filter((sub, index) => { + const profileIndex = this.primaryProfile.subscriptions.findIndex((x) => { return x.name === sub.name }) return profileIndex === index }) - this.updateProfile(primaryProfile) + this.updateProfile(this.primaryProfile) } } }) @@ -222,41 +218,12 @@ export default Vue.extend({ }) }, - importFreeTubeSubscriptions: async function () { - const options = { - properties: ['openFile'], - filters: [ - { - name: 'Database File', - extensions: ['db'] - } - ] - } - - const response = await this.showOpenDialog(options) - if (response.canceled || response.filePaths?.length === 0) { - return - } - - this.handleFreetubeImportFile(response) - }, - - handleYoutubeCsvImportFile: async function(response) { // first row = header, last row = empty - let textDecode - try { - textDecode = await this.readFileFromDialog({ response }) - } catch (err) { - const message = this.$t('Settings.Data Settings.Unable to read file') - this.showToast({ - message: `${message}: ${err}` - }) - return - } + importCsvYouTubeSubscriptions: async function(textDecode) { // first row = header, last row = empty const youtubeSubscriptions = textDecode.split('\n').filter(sub => { return sub !== '' }) - const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0])) const subscriptions = [] + const errorList = [] this.showToast({ message: this.$t('Settings.Data Settings.This might take a while, please wait') @@ -265,67 +232,62 @@ export default Vue.extend({ this.updateShowProgressBar(true) this.setProgressBarPercentage(0) let count = 0 - for (let i = 1; i < (youtubeSubscriptions.length - 1); i++) { - const channelId = youtubeSubscriptions[i].split(',')[0] - const subExists = primaryProfile.subscriptions.findIndex((sub) => { - return sub.id === channelId - }) - if (subExists === -1) { - let channelInfo - if (this.backendPreference === 'invidious') { // only needed for thumbnail - 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 - } + const ytsubs = youtubeSubscriptions.slice(1).map(yt => { + const splitCSVRegex = /(?:,|\n|^)("(?:(?:"")*[^"]*)*"|[^",\n]*|(?:\n|$))/g + return [...yt.matchAll(splitCSVRegex)].map(s => { + let newVal = s[1] + if (newVal.startsWith('"')) { + newVal = newVal.substring(1, newVal.length - 2).replace('""', '"') + } + return newVal + }) + }).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) } - } - - count++ - - const progressPercentage = (count / (youtubeSubscriptions.length - 1)) * 100 - this.setProgressBarPercentage(progressPercentage) - if (count + 1 === (youtubeSubscriptions.length - 1)) { - primaryProfile.subscriptions = primaryProfile.subscriptions.concat(subscriptions) - this.updateProfile(primaryProfile) - - if (subscriptions.length < count + 2) { - this.showToast({ - message: this.$t('Settings.Data Settings.One or more subscriptions were unable to be imported') - }) - } else { - this.showToast({ - message: this.$t('Settings.Data Settings.All subscriptions have been successfully imported') - }) + finishCount++ + if (finishCount === ytsubs.length) { + resolve(true) } - - this.updateShowProgressBar(false) + }) + }).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]}.`) + }) + this.showToast({ + message: this.$t('Settings.Data Settings.One or more subscriptions were unable to be imported') + }) + } else { + this.showToast({ + message: this.$t('Settings.Data Settings.All subscriptions have been successfully imported') + }) } - } + }).finally(_ => { + this.updateShowProgressBar(false) + }) }, - handleYoutubeImportFile: async function (response) { - let textDecode - try { - textDecode = await this.readFileFromDialog({ response }) - } catch (err) { - const message = this.$t('Settings.Data Settings.Unable to read file') - this.showToast({ - message: `${message}: ${err}` - }) - return - } - textDecode = JSON.parse(textDecode) - - const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0])) + importYouTubeSubscriptions: async function (textDecode) { const subscriptions = [] + const errorList = [] this.showToast({ message: this.$t('Settings.Data Settings.This might take a while, please wait') @@ -335,125 +297,56 @@ export default Vue.extend({ this.setProgressBarPercentage(0) let count = 0 - - textDecode.forEach((channel) => { - const snippet = channel.snippet - - if (typeof snippet === 'undefined') { - const message = this.$t('Settings.Data Settings.Invalid subscriptions file') - this.showToast({ - message: message - }) - - throw new Error('Unable to find channel data') - } - - const subscription = { - id: snippet.resourceId.channelId, - name: snippet.title, - thumbnail: snippet.thumbnails.default.url - } - - const subExists = primaryProfile.subscriptions.findIndex((sub) => { - return sub.id === subscription.id || sub.name === subscription.name - }) - - const subDuplicateExists = subscriptions.findIndex((sub) => { - return sub.id === subscription.id || sub.name === subscription.name - }) - - if (subExists === -1 && subDuplicateExists === -1) { - subscriptions.push(subscription) - } - - count++ - - const progressPercentage = (count / textDecode.length) * 100 - this.setProgressBarPercentage(progressPercentage) - - if (count === textDecode.length) { - primaryProfile.subscriptions = primaryProfile.subscriptions.concat(subscriptions) - this.updateProfile(primaryProfile) - - if (subscriptions.length < count) { - this.showToast({ - message: this.$t('Settings.Data Settings.One or more subscriptions were unable to be imported') - }) - } else { + 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') this.showToast({ - message: this.$t('Settings.Data Settings.All subscriptions have been successfully imported') + message: message }) + throw new Error('Unable to find channel data') } - - this.updateShowProgressBar(false) - } - }) - }, - - importCsvYouTubeSubscriptions: async function () { - const options = { - properties: ['openFile'], - filters: [ - { - name: 'Database File', - extensions: ['csv'] - } - ] - } - const response = await this.showOpenDialog(options) - if (response.canceled || response.filePaths?.length === 0) { - return - } - - this.handleYoutubeCsvImportFile(response) - }, - - importYouTubeSubscriptions: async function () { - const options = { - properties: ['openFile'], - filters: [ - { - name: 'Database File', - extensions: ['json'] + 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]) } - ] - } - - const response = await this.showOpenDialog(options) - if (response.canceled || response.filePaths?.length === 0) { - return - } - - this.handleYoutubeImportFile(response) - }, - - importOpmlYouTubeSubscriptions: async function () { - const options = { - properties: ['openFile'], - filters: [ - { - name: 'Database File', - extensions: ['opml', 'xml'] + finishCount++ + if (finishCount === textDecode.length) { + resolve(true) } - ] - } - - const response = await this.showOpenDialog(options) - if (response.canceled || response.filePaths?.length === 0) { - return - } - - let data - try { - data = await this.readFileFromDialog({ response }) - } catch (err) { - const message = this.$t('Settings.Data Settings.Unable to read file') - this.showToast({ - message: `${message}: ${err}` }) - return - } + }).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]}.`) + }) + this.showToast({ + message: this.$t('Settings.Data Settings.One or more subscriptions were unable to be imported') + }) + } else { + this.showToast({ + message: this.$t('Settings.Data Settings.All subscriptions have been successfully imported') + }) + } + }).finally(_ => { + this.updateShowProgressBar(false) + }) + }, + importOpmlYouTubeSubscriptions: async function (data) { let json try { json = await opmlToJSON(data) @@ -476,12 +369,10 @@ export default Vue.extend({ this.showToast({ message: message }) - return } } - const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0])) const subscriptions = [] this.showToast({ @@ -495,7 +386,7 @@ export default Vue.extend({ feedData.forEach(async (channel, index) => { const channelId = channel.xmlurl.replace('https://www.youtube.com/feeds/videos.xml?channel_id=', '') - const subExists = primaryProfile.subscriptions.findIndex((sub) => { + const subExists = this.primaryProfile.subscriptions.findIndex((sub) => { return sub.id === channelId }) if (subExists === -1) { @@ -522,8 +413,8 @@ export default Vue.extend({ this.setProgressBarPercentage(progressPercentage) if (count === feedData.length) { - primaryProfile.subscriptions = primaryProfile.subscriptions.concat(subscriptions) - this.updateProfile(primaryProfile) + this.primaryProfile.subscriptions = this.primaryProfile.subscriptions.concat(subscriptions) + this.updateProfile(this.primaryProfile) if (subscriptions.length < count) { this.showToast({ @@ -541,35 +432,7 @@ export default Vue.extend({ } }, - importNewPipeSubscriptions: async function () { - const options = { - properties: ['openFile'], - filters: [ - { - name: 'Database File', - extensions: ['json'] - } - ] - } - - const response = await this.showOpenDialog(options) - if (response.canceled || response.filePaths?.length === 0) { - return - } - - let data - try { - data = await this.readFileFromDialog({ response }) - } catch (err) { - const message = this.$t('Settings.Data Settings.Unable to read file') - this.showToast({ - message: `${message}: ${err}` - }) - return - } - - const newPipeData = JSON.parse(data) - + importNewPipeSubscriptions: async function (newPipeData) { if (typeof newPipeData.subscriptions === 'undefined') { this.showToast({ message: this.$t('Settings.Data Settings.Invalid subscriptions file') @@ -582,8 +445,8 @@ export default Vue.extend({ return channel.service_id === 0 }) - const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0])) const subscriptions = [] + const errorList = [] this.showToast({ message: this.$t('Settings.Data Settings.This might take a while, please wait') @@ -594,51 +457,45 @@ export default Vue.extend({ let count = 0 - newPipeSubscriptions.forEach(async (channel, index) => { - const channelId = channel.url.replace(/https:\/\/(www\.)?youtube\.com\/channel\//, '') - const subExists = 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 - } + 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) } - } - - count++ - - const progressPercentage = (count / newPipeSubscriptions.length) * 100 - this.setProgressBarPercentage(progressPercentage) - - if (count === newPipeSubscriptions.length) { - primaryProfile.subscriptions = primaryProfile.subscriptions.concat(subscriptions) - this.updateProfile(primaryProfile) - - if (subscriptions.length < count) { - this.showToast({ - message: this.$t('Settings.Data Settings.One or more subscriptions were unable to be imported') - }) - } else { - this.showToast({ - message: this.$t('Settings.Data Settings.All subscriptions have been successfully imported') - }) + if (result === -1) { + errorList.push([channelId, channel.url, channel.name]) } - - this.updateShowProgressBar(false) + 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]}.`) + }) + this.showToast({ + message: this.$t('Settings.Data Settings.One or more subscriptions were unable to be imported') + }) + } else { + this.showToast({ + message: this.$t('Settings.Data Settings.All subscriptions have been successfully imported') + }) } + }).finally(_ => { + this.updateShowProgressBar(false) }) }, @@ -679,7 +536,7 @@ export default Vue.extend({ defaultPath: exportFileName, filters: [ { - name: 'Database File', + name: this.$t('Settings.Data Settings.Subscription File'), extensions: ['db'] } ] @@ -726,7 +583,7 @@ export default Vue.extend({ defaultPath: exportFileName, filters: [ { - name: 'Database File', + name: this.$t('Settings.Data Settings.Subscription File'), extensions: ['json'] } ] @@ -799,7 +656,7 @@ export default Vue.extend({ defaultPath: exportFileName, filters: [ { - name: 'Database File', + name: this.$t('Settings.Data Settings.Subscription File'), extensions: ['opml'] } ] @@ -851,7 +708,7 @@ export default Vue.extend({ defaultPath: exportFileName, filters: [ { - name: 'Database File', + name: this.$t('Settings.Data Settings.Subscription File'), extensions: ['csv'] } ] @@ -860,8 +717,8 @@ export default Vue.extend({ 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 if channel has comma in name - channelName = `"${channelName}"` + if (channelName.search(',') !== -1) { // add quotations and escape existing quotations if channel has comma in name + channelName = `"${channelName.replaceAll('"', '""')}"` } exportText += `${channel.id},${channelUrl},${channelName}\n` }) @@ -896,7 +753,7 @@ export default Vue.extend({ defaultPath: exportFileName, filters: [ { - name: 'Database File', + name: this.$t('Settings.Data Settings.Subscription File'), extensions: ['json'] } ] @@ -942,23 +799,12 @@ export default Vue.extend({ }) }, - checkForLegacySubscriptions: async function () { - let dbLocation = await this.getUserDataPath() - dbLocation = dbLocation + '/subscriptions.db' - this.handleFreetubeImportFile({ canceled: false, filePaths: [dbLocation] }) - fs.unlink(dbLocation, (err) => { - if (err) { - console.error(err) - } - }) - }, - importHistory: async function () { const options = { properties: ['openFile'], filters: [ { - name: 'Database File', + name: this.$t('Settings.Data Settings.History File'), extensions: ['db'] } ] @@ -1040,7 +886,7 @@ export default Vue.extend({ defaultPath: exportFileName, filters: [ { - name: 'Database File', + name: this.$t('Settings.Data Settings.Playlist File'), extensions: ['db'] } ] @@ -1084,7 +930,7 @@ export default Vue.extend({ properties: ['openFile'], filters: [ { - name: 'Database File', + name: this.$t('Settings.Data Settings.Playlist File'), extensions: ['db'] } ] @@ -1329,6 +1175,63 @@ export default Vue.extend({ }) }, + /* + 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) => { + return sub.id === channelId + }) !== -1 + + const subDuplicateExists = subscriptions.findIndex((sub) => { + return sub.id === channelId + }) !== -1 + return subExists || subDuplicateExists + }, + ...mapActions([ 'invidiousAPICall', 'updateProfile', diff --git a/src/renderer/components/data-settings/data-settings.vue b/src/renderer/components/data-settings/data-settings.vue index aa65ee28ca1c..2b8b9a5e77a5 100644 --- a/src/renderer/components/data-settings/data-settings.vue +++ b/src/renderer/components/data-settings/data-settings.vue @@ -5,12 +5,7 @@ - - Date: Sun, 9 Oct 2022 15:09:56 +0200 Subject: [PATCH 3/4] Cleanup the web webpack config (#2690) --- _scripts/webpack.web.config.js | 12 +----------- package.json | 2 -- yarn.lock | 7 +------ 3 files changed, 2 insertions(+), 19 deletions(-) diff --git a/_scripts/webpack.web.config.js b/_scripts/webpack.web.config.js index b9d05f00b44c..5e28947d2029 100644 --- a/_scripts/webpack.web.config.js +++ b/_scripts/webpack.web.config.js @@ -35,17 +35,7 @@ const config = { }, { test: /\.vue$/, - use: { - loader: 'vue-loader', - options: { - extractCSS: true, - loaders: { - sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1', - scss: 'vue-style-loader!css-loader!sass-loader', - less: 'vue-style-loader!css-loader!less-loader', - }, - }, - }, + loader: 'vue-loader' }, { test: /\.s(c|a)ss$/, diff --git a/package.json b/package.json index 63fa30673a2a..d13cf5242701 100644 --- a/package.json +++ b/package.json @@ -113,12 +113,10 @@ "rimraf": "^3.0.2", "sass": "^1.54.9", "sass-loader": "^13.0.2", - "style-loader": "^3.2.1", "tree-kill": "1.2.2", "vue-devtools": "^5.1.4", "vue-eslint-parser": "^9.1.0", "vue-loader": "^15.10.0", - "vue-style-loader": "^4.1.3", "webpack": "^5.74.0", "webpack-cli": "^4.10.0", "webpack-dev-server": "^4.10.1" diff --git a/yarn.lock b/yarn.lock index aa05ab33deed..cfb6dbd17a8a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7807,11 +7807,6 @@ strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= -style-loader@^3.2.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575" - integrity sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ== - stylehacks@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.1.0.tgz#a40066490ca0caca04e96c6b02153ddc39913520" @@ -8450,7 +8445,7 @@ vue-router@^3.6.5: resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.6.5.tgz#95847d52b9a7e3f1361cb605c8e6441f202afad8" integrity sha512-VYXZQLtjuvKxxcshuRAwjHnciqZVoXAjTjcqBTz4rKc8qih9g9pI3hbDjmqXaHdgL3v8pV6P8Z335XvHzESxLQ== -vue-style-loader@^4.1.0, vue-style-loader@^4.1.3: +vue-style-loader@^4.1.0: version "4.1.3" resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.3.tgz#6d55863a51fa757ab24e89d9371465072aa7bc35" integrity sha512-sFuh0xfbtpRlKfm39ss/ikqs9AbKCoXZBpHeVZ8Tx650o0k0q/YCM7FRvigtxpACezfq6af+a7JeqVTWvncqDg== From 7ca6440a888008401caeb5ebdc892afa0f318e37 Mon Sep 17 00:00:00 2001 From: Aiz <66974576+Aiz0@users.noreply.github.com> Date: Sun, 9 Oct 2022 13:10:57 +0000 Subject: [PATCH 4/4] Add shortcuts for refresh buttons on Subscription, Trending, and Popular views (#2689) * add shortcut to subscription refresh button * add shortcut to most popular refresh button * add shortcut to trending refresh button * prevent refresh if currently loading --- src/renderer/views/Popular/Popular.js | 20 ++++++++++++++++++ .../views/Subscriptions/Subscriptions.js | 20 ++++++++++++++++++ src/renderer/views/Trending/Trending.js | 21 ++++++++++++++++++- 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/renderer/views/Popular/Popular.js b/src/renderer/views/Popular/Popular.js index 634a12e52fb3..a70e4f578f1f 100644 --- a/src/renderer/views/Popular/Popular.js +++ b/src/renderer/views/Popular/Popular.js @@ -25,11 +25,16 @@ export default Vue.extend({ } }, mounted: function () { + document.addEventListener('keydown', this.keyboardShortcutHandler) + this.shownResults = this.popularCache if (!this.shownResults || this.shownResults.length < 1) { this.fetchPopularInfo() } }, + beforeDestroy: function () { + document.removeEventListener('keydown', this.keyboardShortcutHandler) + }, methods: { fetchPopularInfo: async function () { const searchPayload = { @@ -56,6 +61,21 @@ export default Vue.extend({ this.$store.commit('setPopularCache', this.shownResults) }, + // This function should always be at the bottom of this file + keyboardShortcutHandler: function (event) { + if (event.ctrlKey || document.activeElement.classList.contains('ft-input')) { + return + } + switch (event.key) { + case 'r': + case 'R': + if (!this.isLoading) { + this.fetchPopularInfo() + } + break + } + }, + ...mapActions([ 'invidiousAPICall' ]) diff --git a/src/renderer/views/Subscriptions/Subscriptions.js b/src/renderer/views/Subscriptions/Subscriptions.js index df35a0ed52f8..9c8f5f0ce61a 100644 --- a/src/renderer/views/Subscriptions/Subscriptions.js +++ b/src/renderer/views/Subscriptions/Subscriptions.js @@ -98,6 +98,8 @@ export default Vue.extend({ } }, mounted: async function () { + document.addEventListener('keydown', this.keyboardShortcutHandler) + this.isLoading = true const dataLimit = sessionStorage.getItem('subscriptionLimit') if (dataLimit !== null) { @@ -132,6 +134,9 @@ export default Vue.extend({ this.isLoading = false } }, + beforeDestroy: function () { + document.removeEventListener('keydown', this.keyboardShortcutHandler) + }, methods: { goToChannel: function (id) { this.$router.push({ path: `/channel/${id}` }) @@ -471,6 +476,21 @@ export default Vue.extend({ sessionStorage.setItem('subscriptionLimit', this.dataLimit) }, + // This function should always be at the bottom of this file + keyboardShortcutHandler: function (event) { + if (event.ctrlKey || document.activeElement.classList.contains('ft-input')) { + return + } + switch (event.key) { + case 'r': + case 'R': + if (!this.isLoading) { + this.getSubscriptions() + } + break + } + }, + ...mapActions([ 'showToast', 'invidiousAPICall', diff --git a/src/renderer/views/Trending/Trending.js b/src/renderer/views/Trending/Trending.js index e41ede95eae4..803836a77734 100644 --- a/src/renderer/views/Trending/Trending.js +++ b/src/renderer/views/Trending/Trending.js @@ -42,12 +42,17 @@ export default Vue.extend({ } }, mounted: function () { + document.addEventListener('keydown', this.keyboardShortcutHandler) + if (this.trendingCache[this.currentTab] && this.trendingCache[this.currentTab].length > 0) { this.getTrendingInfoCache() } else { this.getTrendingInfo() } }, + beforeDestroy: function () { + document.removeEventListener('keydown', this.keyboardShortcutHandler) + }, methods: { changeTab: function (tab) { this.currentTab = tab @@ -125,7 +130,6 @@ export default Vue.extend({ }) }) }, - getTrendingInfoInvidious: function () { this.isLoading = true @@ -176,6 +180,21 @@ export default Vue.extend({ }) }, + // This function should always be at the bottom of this file + keyboardShortcutHandler: function (event) { + if (event.ctrlKey || document.activeElement.classList.contains('ft-input')) { + return + } + switch (event.key) { + case 'r': + case 'R': + if (!this.isLoading) { + this.getTrendingInfo() + } + break + } + }, + ...mapActions([ 'showToast', 'invidiousAPICall',