diff --git a/_icons/iconBlack.svg b/_icons/iconBlack.svg new file mode 100644 index 0000000000000..e61b30d675bb0 --- /dev/null +++ b/_icons/iconBlack.svg @@ -0,0 +1 @@ +logomark \ No newline at end of file diff --git a/_icons/iconColor.svg b/_icons/iconColor.svg new file mode 100644 index 0000000000000..86014119a00b6 --- /dev/null +++ b/_icons/iconColor.svg @@ -0,0 +1,95 @@ + + + + + + diff --git a/_icons/iconWhite.svg b/_icons/iconWhite.svg new file mode 100644 index 0000000000000..285f9ee67aa2b --- /dev/null +++ b/_icons/iconWhite.svg @@ -0,0 +1,95 @@ + + + + + + diff --git a/_icons/logoBlack.svg b/_icons/logoBlack.svg new file mode 100644 index 0000000000000..8d167add15645 --- /dev/null +++ b/_icons/logoBlack.svg @@ -0,0 +1,193 @@ + + + +logotype + + + diff --git a/_icons/logoColor.svg b/_icons/logoColor.svg new file mode 100644 index 0000000000000..35d5e45a8a65d --- /dev/null +++ b/_icons/logoColor.svg @@ -0,0 +1,224 @@ + + + + + + diff --git a/_icons/logoWhite.svg b/_icons/logoWhite.svg new file mode 100644 index 0000000000000..d95bc268fb171 --- /dev/null +++ b/_icons/logoWhite.svg @@ -0,0 +1,210 @@ + + + + + + diff --git a/package.json b/package.json index 7e67c19355737..48f8e92492e42 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "autolinker": "^4.0.0", "electron-context-menu": "^3.6.1", "lodash.debounce": "^4.0.8", - "marked": "^9.0.2", + "marked": "^9.0.3", "path-browserify": "^1.0.1", "process": "^0.11.10", "stylelint-use-logical-spec": "^5.0.0", @@ -81,7 +81,7 @@ "youtubei.js": "^6.4.0" }, "devDependencies": { - "@babel/core": "^7.22.20", + "@babel/core": "^7.23.0", "@babel/eslint-parser": "^7.22.15", "@babel/plugin-proposal-class-properties": "^7.18.6", "@babel/preset-env": "^7.22.20", @@ -90,9 +90,9 @@ "copy-webpack-plugin": "^11.0.0", "css-loader": "^6.8.1", "css-minimizer-webpack-plugin": "^5.0.1", - "electron": "^22.3.21", + "electron": "^22.3.24", "electron-builder": "^24.6.4", - "eslint": "^8.49.0", + "eslint": "^8.50.0", "eslint-config-prettier": "^9.0.0", "eslint-config-standard": "^17.1.0", "eslint-plugin-import": "^2.28.1", @@ -107,14 +107,14 @@ "html-webpack-plugin": "^5.5.3", "js-yaml": "^4.1.0", "json-minimizer-webpack-plugin": "^4.0.0", - "lefthook": "^1.4.11", + "lefthook": "^1.5.0", "mini-css-extract-plugin": "^2.7.6", "npm-run-all": "^4.1.5", - "postcss": "^8.4.29", + "postcss": "^8.4.30", "postcss-scss": "^4.0.8", "prettier": "^2.8.8", "rimraf": "^5.0.1", - "sass": "^1.67.0", + "sass": "^1.68.0", "sass-loader": "^13.3.2", "stylelint": "^15.10.3", "stylelint-config-sass-guidelines": "^10.0.0", diff --git a/src/renderer/components/distraction-settings/distraction-settings.js b/src/renderer/components/distraction-settings/distraction-settings.js index 659409d12f84c..7f5e3741fc962 100644 --- a/src/renderer/components/distraction-settings/distraction-settings.js +++ b/src/renderer/components/distraction-settings/distraction-settings.js @@ -92,6 +92,9 @@ export default defineComponent({ hideSubscriptionsLive: function () { return this.$store.getters.getHideSubscriptionsLive }, + hideSubscriptionsCommunity: function() { + return this.$store.getters.getHideSubscriptionsCommunity + }, showDistractionFreeTitles: function () { return this.$store.getters.getShowDistractionFreeTitles }, @@ -155,6 +158,7 @@ export default defineComponent({ 'updateHideSubscriptionsVideos', 'updateHideSubscriptionsShorts', 'updateHideSubscriptionsLive', + 'updateHideSubscriptionsCommunity', 'updateBlurThumbnails' ]) } diff --git a/src/renderer/components/distraction-settings/distraction-settings.vue b/src/renderer/components/distraction-settings/distraction-settings.vue index 4989782c0eda3..5b6f06db4a4a1 100644 --- a/src/renderer/components/distraction-settings/distraction-settings.vue +++ b/src/renderer/components/distraction-settings/distraction-settings.vue @@ -66,6 +66,12 @@ :tooltip="hideLiveStreams ? hideSubscriptionsLiveTooltip : ''" v-on="!hideLiveStreams ? { change: updateHideSubscriptionsLive } : {}" /> +

{ + authorThumbnails.forEach(thumbnail => { thumbnail.url = youtubeImageUrlToInvidious(thumbnail.url) - return thumbnail }) } else { - authorThumbnails = authorThumbnails.map(thumbnail => { + authorThumbnails.forEach(thumbnail => { if (thumbnail.url.startsWith('//')) { thumbnail.url = 'https:' + thumbnail.url } - return thumbnail }) } this.authorThumbnails = authorThumbnails @@ -104,6 +103,7 @@ export default defineComponent({ this.commentCount = this.data.commentCount this.type = (this.data.postContent !== null && this.data.postContent !== undefined) ? this.data.postContent.type : 'text' this.author = this.data.author + this.authorId = this.data.authorId this.isLoading = false }, diff --git a/src/renderer/components/ft-community-post/ft-community-post.scss b/src/renderer/components/ft-community-post/ft-community-post.scss index 1f15d6d6ae529..7c7a6a8470ac4 100644 --- a/src/renderer/components/ft-community-post/ft-community-post.scss +++ b/src/renderer/components/ft-community-post/ft-community-post.scss @@ -34,6 +34,10 @@ font-weight: bold; margin-block: 5px 0; margin-inline: 5px 6px; + .authorNameLink { + color: inherit; + text-decoration: none; + } } .publishedText { diff --git a/src/renderer/components/ft-community-post/ft-community-post.vue b/src/renderer/components/ft-community-post/ft-community-post.vue index 870a66807e553..986591cf6f23c 100644 --- a/src/renderer/components/ft-community-post/ft-community-post.vue +++ b/src/renderer/components/ft-community-post/ft-community-post.vue @@ -8,16 +8,43 @@
- + + +

- {{ author }} + + {{ author }} + +

{ - if (store.getters.getProxyVideos) { + if (store.getters.getProxyVideos && !options.uri.startsWith('data:application/dash+xml')) { const { uri } = options options.uri = getProxyUrl(uri) } diff --git a/src/renderer/components/subscriptions-community/subscriptions-community.js b/src/renderer/components/subscriptions-community/subscriptions-community.js new file mode 100644 index 0000000000000..e975e97c79e62 --- /dev/null +++ b/src/renderer/components/subscriptions-community/subscriptions-community.js @@ -0,0 +1,221 @@ +import { defineComponent } from 'vue' +import { mapActions, mapMutations } from 'vuex' +import SubscriptionsTabUI from '../subscriptions-tab-ui/subscriptions-tab-ui.vue' + +import { calculatePublishedDate, copyToClipboard, showToast } from '../../helpers/utils' +import { getLocalChannelCommunity } from '../../helpers/api/local' +import { invidiousGetCommunityPosts } from '../../helpers/api/invidious' + +export default defineComponent({ + name: 'SubscriptionsCommunity', + components: { + 'subscriptions-tab-ui': SubscriptionsTabUI + }, + data: function () { + return { + isLoading: false, + postList: [], + errorChannels: [], + attemptedFetch: false, + } + }, + computed: { + backendPreference: function () { + return this.$store.getters.getBackendPreference + }, + + backendFallback: function () { + return this.$store.getters.getBackendFallback + }, + + currentInvidiousInstance: function () { + return this.$store.getters.getCurrentInvidiousInstance + }, + + activeProfile: function () { + return this.$store.getters.getActiveProfile + }, + activeProfileId: function () { + return this.activeProfile._id + }, + + cacheEntriesForAllActiveProfileChannels() { + const entries = [] + this.activeSubscriptionList.forEach((channel) => { + const cacheEntry = this.$store.getters.getPostsCacheByChannel(channel.id) + if (cacheEntry == null) { return } + + entries.push(cacheEntry) + }) + return entries + }, + postCacheForAllActiveProfileChannelsPresent() { + if (this.cacheEntriesForAllActiveProfileChannels.length === 0) { return false } + if (this.cacheEntriesForAllActiveProfileChannels.length < this.activeSubscriptionList.length) { return false } + + return this.cacheEntriesForAllActiveProfileChannels.every((cacheEntry) => { + return cacheEntry.posts != null + }) + }, + + activeSubscriptionList: function () { + return this.activeProfile.subscriptions + }, + + fetchSubscriptionsAutomatically: function() { + return this.$store.getters.getFetchSubscriptionsAutomatically + }, + }, + watch: { + activeProfile: async function (_) { + this.isLoading = true + this.loadpostsFromCacheSometimes() + }, + }, + mounted: async function () { + this.isLoading = true + + this.loadpostsFromCacheSometimes() + }, + methods: { + loadpostsFromCacheSometimes() { + // This method is called on view visible + if (this.postCacheForAllActiveProfileChannelsPresent) { + this.loadPostsFromCacheForAllActiveProfileChannels() + return + } + + this.maybeLoadPostsForSubscriptionsFromRemote() + }, + + async loadPostsFromCacheForAllActiveProfileChannels() { + const postList = [] + this.activeSubscriptionList.forEach((channel) => { + const channelCacheEntry = this.$store.getters.getPostsCacheByChannel(channel.id) + + postList.push(...channelCacheEntry.posts) + }) + + postList.sort((a, b) => { + return calculatePublishedDate(b.publishedText) - calculatePublishedDate(a.publishedText) + }) + + this.postList = postList + this.isLoading = false + }, + + loadPostsForSubscriptionsFromRemote: async function () { + if (this.activeSubscriptionList.length === 0) { + this.isLoading = false + this.postList = [] + return + } + + const channelsToLoadFromRemote = this.activeSubscriptionList + const postList = [] + let channelCount = 0 + this.isLoading = true + + this.updateShowProgressBar(true) + this.setProgressBarPercentage(0) + this.attemptedFetch = true + + this.errorChannels = [] + const postListFromRemote = (await Promise.all(channelsToLoadFromRemote.map(async (channel) => { + let posts = [] + if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') { + posts = await this.getChannelPostsInvidious(channel) + } else { + posts = await this.getChannelPostsLocal(channel) + } + + channelCount++ + const percentageComplete = (channelCount / channelsToLoadFromRemote.length) * 100 + this.setProgressBarPercentage(percentageComplete) + + this.updateSubscriptionPostsCacheByChannel({ + channelId: channel.id, + posts: posts, + }) + return posts + }))).flatMap((o) => o) + postList.push(...postListFromRemote) + postList.sort((a, b) => { + return calculatePublishedDate(b.publishedText) - calculatePublishedDate(a.publishedText) + }) + + this.postList = postList + this.isLoading = false + this.updateShowProgressBar(false) + }, + + maybeLoadPostsForSubscriptionsFromRemote: async function () { + if (this.fetchSubscriptionsAutomatically) { + // `this.isLoading = false` is called inside `loadPostsForSubscriptionsFromRemote` when needed + await this.loadPostsForSubscriptionsFromRemote() + } else { + this.postList = [] + this.attemptedFetch = false + this.isLoading = false + } + }, + + getChannelPostsLocal: async function (channel) { + try { + const entries = await getLocalChannelCommunity(channel.id) + + if (entries === null) { + this.errorChannels.push(channel) + return [] + } + entries.forEach(post => { + post.authorId = channel.id + }) + return entries + } catch (err) { + console.error(err) + const errorMessage = this.$t('Local API Error (Click to copy)') + showToast(`${errorMessage}: ${err}`, 10000, () => { + copyToClipboard(err) + }) + if (this.backendPreference === 'local' && this.backendFallback) { + showToast(this.$t('Falling back to Invidious API')) + return await this.getChannelPostsInvidious(channel) + } + return [] + } + }, + + getChannelPostsInvidious: function (channel) { + return new Promise((resolve, reject) => { + invidiousGetCommunityPosts(channel.id).then(result => { + result.posts.forEach(post => { + post.authorId = channel.id + }) + resolve(result.posts) + }).catch((err) => { + console.error(err) + const errorMessage = this.$t('Invidious API Error (Click to copy)') + showToast(`${errorMessage}: ${err}`, 10000, () => { + copyToClipboard(err) + }) + if (process.env.IS_ELECTRON && this.backendPreference === 'invidious' && this.backendFallback) { + showToast(this.$t('Falling back to the local API')) + resolve(this.getChannelPostsLocal(channel)) + } else { + resolve([]) + } + }) + }) + }, + + ...mapActions([ + 'updateShowProgressBar', + 'updateSubscriptionPostsCacheByChannel', + ]), + + ...mapMutations([ + 'setProgressBarPercentage' + ]) + } +}) diff --git a/src/renderer/components/subscriptions-community/subscriptions-community.vue b/src/renderer/components/subscriptions-community/subscriptions-community.vue new file mode 100644 index 0000000000000..8c2e504c25ef1 --- /dev/null +++ b/src/renderer/components/subscriptions-community/subscriptions-community.vue @@ -0,0 +1,13 @@ + + +