diff --git a/.eslintrc.js b/.eslintrc.js index f342b62dd1104..a0c8c79b28a3a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -112,6 +112,9 @@ module.exports = { ignoreText: ['-', '•', '/', 'YouTube', 'Invidious', 'FreeTube'] } ], + // Only applicable when we upgrade to Vue 3 and vue-i18n 9+ + '@intlify/vue-i18n/no-deprecated-tc': 'off', + 'vue/require-explicit-emits': 'error', 'vue/no-unused-emit-declarations': 'error', }, diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 5e4e8b3f2b0e5..0000000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,3 +0,0 @@ -# These are supported funding model platforms - -liberapay: FreeTube diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 9f926e9737198..dcb395c41cd27 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -77,25 +77,25 @@ jobs: date +"%Y-%m-%d" >> $GITHUB_ENV echo 'EOF' >> $GITHUB_ENV - name: Update x64 File Location in yml File - uses: mikefarah/yq@4.0.0-beta1 + uses: mikefarah/yq@v4.44.2 with: # The Command which should be run - cmd: yq w -i io.freetubeapp.FreeTube.yml modules[0].sources[0].url 'https://github.com/FreeTubeApp/FreeTube/releases/download/v${{ steps.sub.outputs.result }}-beta/freetube-${{ steps.sub.outputs.result }}-linux-portable-x64.zip' + cmd: yq -i '.modules[0].sources[0].url = "https://github.com/FreeTubeApp/FreeTube/releases/download/v${{ steps.sub.outputs.result }}-beta/freetube-${{ steps.sub.outputs.result }}-linux-portable-x64.zip"' io.freetubeapp.FreeTube.yml - name: Update x64 Hash in yml File - uses: mikefarah/yq@4.0.0-beta1 + uses: mikefarah/yq@v4.44.2 with: # The Command which should be run - cmd: yq w -i io.freetubeapp.FreeTube.yml modules[0].sources[0].sha256 ${{ env.HASH_X64 }} + cmd: yq -i '.modules[0].sources[0].sha256 = "${{ env.HASH_X64 }}"' io.freetubeapp.FreeTube.yml - name: Update ARM File Location in yml File - uses: mikefarah/yq@4.0.0-beta1 + uses: mikefarah/yq@v4.44.2 with: # The Command which should be run - cmd: yq w -i io.freetubeapp.FreeTube.yml modules[0].sources[1].url 'https://github.com/FreeTubeApp/FreeTube/releases/download/v${{ steps.sub.outputs.result }}-beta/freetube-${{ steps.sub.outputs.result }}-linux-portable-arm64.zip' + cmd: yq -i '.modules[0].sources[1].url = "https://github.com/FreeTubeApp/FreeTube/releases/download/v${{ steps.sub.outputs.result }}-beta/freetube-${{ steps.sub.outputs.result }}-linux-portable-arm64.zip"' io.freetubeapp.FreeTube.yml - name: Update ARM Hash in yml File - uses: mikefarah/yq@4.0.0-beta1 + uses: mikefarah/yq@v4.44.2 with: # The Command which should be run - cmd: yq w -i io.freetubeapp.FreeTube.yml modules[0].sources[1].sha256 ${{ env.HASH_ARM64 }} + cmd: yq -i '.modules[0].sources[1].sha256 = "${{ env.HASH_ARM64 }}"' io.freetubeapp.FreeTube.yml - name: Add Patch Notes to XML File run: xmlstarlet ed -L -i /application/releases/release[1] -t elem -n releaseTMP -v "" -i //releaseTMP -t attr -n version -v "${{ steps.sub.outputs.result }} Beta" -i //releaseTMP -t attr -n date -v "${{ env.CURRENT_DATE }}" -s //releaseTMP -t elem -n url -v "" -s //releaseTMP/url -t text -n "" -v "https://github.com/FreeTubeApp/FreeTube/releases/tag/v${{ steps.sub.outputs.result }}-beta" -r //releaseTMP -v "release" io.freetubeapp.FreeTube.metainfo.xml - name: Remove Release Files diff --git a/README.md b/README.md index 45101488864b8..5a42b6c49d877 100644 --- a/README.md +++ b/README.md @@ -150,14 +150,10 @@ If you ever have any questions, feel free to ask it on our [Discussions](https:/ > 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. - -* [FreeTube on Liberapay](https://liberapay.com/FreeTube) +If you enjoy using FreeTube, you're welcome to leave a donation using the following method. * Bitcoin Address: `1Lih7Ho5gnxb1CwPD4o59ss78pwo2T91eS` -* Monero Address: `48WyAPdjwc6VokeXACxSZCFeKEXBiYPV6GjfvBsfg4CrUJ95LLCQSfpM9pvNKy5GE5H4hNaw99P8RZyzmaU9kb1pD7kzhCB` - While your donations are much appreciated, only donate if you really want to. Donations are used for keeping the website up and running and eventual code signing costs. > [!TIP] diff --git a/package.json b/package.json index 35607377f9276..ec10d8fdf0185 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "freetube", "productName": "FreeTube", "description": "A private YouTube client", - "version": "0.20.0", + "version": "0.21.0", "license": "AGPL-3.0-or-later", "main": "./dist/main.js", "private": true, @@ -63,11 +63,11 @@ "autolinker": "^4.0.0", "electron-context-menu": "^4.0.0", "lodash.debounce": "^4.0.8", - "marked": "^12.0.2", + "marked": "^13.0.0", "path-browserify": "^1.0.1", "portal-vue": "^2.1.7", "process": "^0.11.10", - "swiper": "^11.1.3", + "swiper": "^11.1.4", "video.js": "7.21.5", "videojs-contrib-quality-levels": "^3.0.0", "videojs-http-source-selector": "^1.1.6", @@ -79,55 +79,55 @@ "vue-observe-visibility": "^1.0.0", "vue-router": "^3.6.5", "vuex": "^3.6.2", - "youtubei.js": "^9.4.0" + "youtubei.js": "^10.0.0" }, "devDependencies": { - "@babel/core": "^7.24.6", - "@babel/eslint-parser": "^7.24.6", + "@babel/core": "^7.24.7", + "@babel/eslint-parser": "^7.24.7", "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/preset-env": "^7.24.6", + "@babel/preset-env": "^7.24.7", "@double-great/stylelint-a11y": "^3.0.2", - "@intlify/eslint-plugin-vue-i18n": "^2.0.0", + "@intlify/eslint-plugin-vue-i18n": "^3.0.0", "babel-loader": "^9.1.3", "copy-webpack-plugin": "^12.0.2", "css-loader": "^7.1.2", "css-minimizer-webpack-plugin": "^7.0.0", - "electron": "^30.0.8", + "electron": "^31.0.1", "electron-builder": "^24.13.3", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-config-standard": "^17.1.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jsonc": "^2.16.0", - "eslint-plugin-n": "^17.7.0", + "eslint-plugin-n": "^17.9.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-unicorn": "^53.0.0", + "eslint-plugin-promise": "^6.2.0", + "eslint-plugin-unicorn": "^54.0.0", "eslint-plugin-vue": "^9.26.0", "eslint-plugin-vuejs-accessibility": "^2.3.0", "eslint-plugin-yml": "^1.14.0", "html-webpack-plugin": "^5.6.0", "js-yaml": "^4.1.0", "json-minimizer-webpack-plugin": "^5.0.0", - "lefthook": "^1.6.13", + "lefthook": "^1.6.16", "mini-css-extract-plugin": "^2.9.0", "npm-run-all2": "^6.2.0", "postcss": "^8.4.38", "postcss-scss": "^4.0.9", "prettier": "^2.8.8", "rimraf": "^5.0.7", - "sass": "^1.77.2", + "sass": "^1.77.5", "sass-loader": "^14.2.1", - "stylelint": "^16.6.0", + "stylelint": "^16.6.1", "stylelint-config-sass-guidelines": "^11.1.0", "stylelint-config-standard": "^36.0.0", "stylelint-high-performance-animation": "^1.10.0", "stylelint-use-logical-spec": "^5.0.1", "tree-kill": "1.2.2", "vue-devtools": "^5.1.4", - "vue-eslint-parser": "^9.4.2", + "vue-eslint-parser": "^9.4.3", "vue-loader": "^15.10.0", - "webpack": "^5.91.0", + "webpack": "^5.92.0", "webpack-cli": "^5.1.4", "webpack-dev-server": "^5.0.4", "yaml-eslint-parser": "^1.2.3" diff --git a/src/constants.js b/src/constants.js index c3c3e164edc5e..7fc27d8e342ff 100644 --- a/src/constants.js +++ b/src/constants.js @@ -12,6 +12,14 @@ const IpcChannels = { CREATE_NEW_WINDOW: 'create-new-window', OPEN_IN_EXTERNAL_PLAYER: 'open-in-external-player', NATIVE_THEME_UPDATE: 'native-theme-update', + APP_READY: 'app-ready', + RELAUNCH_REQUEST: 'relaunch-request', + + SEARCH_INPUT_HANDLING_READY: 'search-input-handling-ready', + UPDATE_SEARCH_INPUT_TEXT: 'update-search-input-text', + + OPEN_URL: 'open-url', + CHANGE_VIEW: 'change-view', DB_SETTINGS: 'db-settings', DB_HISTORY: 'db-history', @@ -26,6 +34,8 @@ const IpcChannels = { GET_REPLACE_HTTP_CACHE: 'get-replace-http-cache', TOGGLE_REPLACE_HTTP_CACHE: 'toggle-replace-http-cache', + SHOW_VIDEO_STATISTICS: 'show-video-statistics', + PLAYER_CACHE_GET: 'player-cache-get', PLAYER_CACHE_SET: 'player-cache-set' } @@ -45,6 +55,11 @@ const DBActions = { UPDATE_PLAYLIST: 'db-action-history-update-playlist', }, + PROFILES: { + ADD_CHANNEL: 'db-action-profiles-add-channel', + REMOVE_CHANNEL: 'db-action-profiles-remove-channel' + }, + PLAYLISTS: { UPSERT_VIDEO: 'db-action-playlists-upsert-video-by-playlist-name', UPSERT_VIDEOS: 'db-action-playlists-upsert-videos-by-playlist-name', @@ -67,6 +82,11 @@ const SyncEvents = { UPDATE_PLAYLIST: 'sync-history-update-playlist', }, + PROFILES: { + ADD_CHANNEL: 'sync-profiles-add-channel', + REMOVE_CHANNEL: 'sync-profiles-remove-channel' + }, + PLAYLISTS: { UPSERT_VIDEO: 'sync-playlists-upsert-video', DELETE_VIDEO: 'sync-playlists-delete-video', @@ -85,6 +105,9 @@ const PLAYLIST_HEIGHT_FORCE_LIST_THRESHOLD = 500 // YouTube search character limit is 100 characters const SEARCH_CHAR_LIMIT = 100 +// Displayed on the about page and used in the main.js file to only allow bitcoin URLs with this wallet address to be opened +const ABOUT_BITCOIN_ADDRESS = '1Lih7Ho5gnxb1CwPD4o59ss78pwo2T91eS' + export { IpcChannels, DBActions, @@ -92,5 +115,6 @@ export { MAIN_PROFILE_ID, MOBILE_WIDTH_THRESHOLD, PLAYLIST_HEIGHT_FORCE_LIST_THRESHOLD, - SEARCH_CHAR_LIMIT + SEARCH_CHAR_LIMIT, + ABOUT_BITCOIN_ADDRESS, } diff --git a/src/datastores/handlers/base.js b/src/datastores/handlers/base.js index b630327899ee1..4a7db5cbb8c3d 100644 --- a/src/datastores/handlers/base.js +++ b/src/datastores/handlers/base.js @@ -86,6 +86,36 @@ class Profiles { return db.profiles.updateAsync({ _id: profile._id }, profile, { upsert: true }) } + static addChannelToProfiles(channel, profileIds) { + if (profileIds.length === 1) { + return db.profiles.updateAsync( + { _id: profileIds[0] }, + { $push: { subscriptions: channel } } + ) + } else { + return db.profiles.updateAsync( + { _id: { $in: profileIds } }, + { $push: { subscriptions: channel } }, + { multi: true } + ) + } + } + + static removeChannelFromProfiles(channelId, profileIds) { + if (profileIds.length === 1) { + return db.profiles.updateAsync( + { _id: profileIds[0] }, + { $pull: { subscriptions: { id: channelId } } } + ) + } else { + return db.profiles.updateAsync( + { _id: { $in: profileIds } }, + { $pull: { subscriptions: { id: channelId } } }, + { multi: true } + ) + } + } + static delete(id) { return db.profiles.removeAsync({ _id: id }) } diff --git a/src/datastores/handlers/electron.js b/src/datastores/handlers/electron.js index 6f57a7d83ba10..cc0b473a3b990 100644 --- a/src/datastores/handlers/electron.js +++ b/src/datastores/handlers/electron.js @@ -89,6 +89,26 @@ class Profiles { ) } + static addChannelToProfiles(channel, profileIds) { + return ipcRenderer.invoke( + IpcChannels.DB_PROFILES, + { + action: DBActions.PROFILES.ADD_CHANNEL, + data: { channel, profileIds } + } + ) + } + + static removeChannelFromProfiles(channelId, profileIds) { + return ipcRenderer.invoke( + IpcChannels.DB_PROFILES, + { + action: DBActions.PROFILES.REMOVE_CHANNEL, + data: { channelId, profileIds } + } + ) + } + static delete(id) { return ipcRenderer.invoke( IpcChannels.DB_PROFILES, diff --git a/src/datastores/handlers/web.js b/src/datastores/handlers/web.js index d6073dc7039f3..93ffa3d68c8ff 100644 --- a/src/datastores/handlers/web.js +++ b/src/datastores/handlers/web.js @@ -59,6 +59,14 @@ class Profiles { return baseHandlers.profiles.upsert(profile) } + static addChannelToProfiles(channel, profileIds) { + return baseHandlers.profiles.addChannelToProfiles(channel, profileIds) + } + + static removeChannelFromProfiles(channelId, profileIds) { + return baseHandlers.profiles.removeChannelFromProfiles(channelId, profileIds) + } + static delete(id) { return baseHandlers.profiles.delete(id) } diff --git a/src/main/index.js b/src/main/index.js index c97a7eb3f2aa4..a781c33db3ddb 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -6,7 +6,12 @@ import { import path from 'path' import cp from 'child_process' -import { IpcChannels, DBActions, SyncEvents } from '../constants' +import { + IpcChannels, + DBActions, + SyncEvents, + ABOUT_BITCOIN_ADDRESS, +} from '../constants' import * as baseHandlers from '../datastores/handlers/base' import { extractExpiryTimestamp, ImageCache } from './ImageCache' import { existsSync } from 'fs' @@ -56,7 +61,7 @@ function runApp() { label: 'Show / Hide Video Statistics', visible: parameters.mediaType === 'video', click: () => { - browserWindow.webContents.send('showVideoStatistics') + browserWindow.webContents.send(IpcChannels.SHOW_VIDEO_STATISTICS) } }, { @@ -243,7 +248,7 @@ function runApp() { const url = getLinkUrl(commandLine) if (url) { - mainWindow.webContents.send('openUrl', url) + mainWindow.webContents.send(IpcChannels.OPEN_URL, url) } } }) @@ -745,8 +750,8 @@ function runApp() { } if (typeof searchQueryText === 'string' && searchQueryText.length > 0) { - ipcMain.once('searchInputHandlingReady', () => { - newWindow.webContents.send('updateSearchInputText', searchQueryText) + ipcMain.once(IpcChannels.SEARCH_INPUT_HANDLING_READY, () => { + newWindow.webContents.send(IpcChannels.UPDATE_SEARCH_INPUT_TEXT, searchQueryText) }) } @@ -792,9 +797,9 @@ function runApp() { }) } - ipcMain.once('appReady', () => { + ipcMain.once(IpcChannels.APP_READY, () => { if (startupUrl) { - mainWindow.webContents.send('openUrl', startupUrl) + mainWindow.webContents.send(IpcChannels.OPEN_URL, startupUrl) } }) @@ -831,7 +836,7 @@ function runApp() { app.quit() } - ipcMain.once('relaunchRequest', () => { + ipcMain.once(IpcChannels.RELAUNCH_REQUEST, () => { relaunch() }) @@ -855,8 +860,35 @@ function runApp() { session.defaultSession.closeAllConnections() }) - ipcMain.on(IpcChannels.OPEN_EXTERNAL_LINK, (_, url) => { - if (typeof url === 'string') shell.openExternal(url) + ipcMain.handle(IpcChannels.OPEN_EXTERNAL_LINK, (_, url) => { + if (typeof url === 'string') { + let parsedURL + + try { + parsedURL = new URL(url) + } catch { + // If it's not a valid URL don't open it + return false + } + + if ( + parsedURL.protocol === 'http:' || parsedURL.protocol === 'https:' || + + // Email address on the about page and Autolinker detects and links email addresses + parsedURL.protocol === 'mailto:' || + + // Autolinker detects and links phone numbers + parsedURL.protocol === 'tel:' || + + // Donation links on the about page + (parsedURL.protocol === 'bitcoin:' && parsedURL.pathname === ABOUT_BITCOIN_ADDRESS) + ) { + shell.openExternal(url) + return true + } + } + + return false }) ipcMain.handle(IpcChannels.GET_SYSTEM_LOCALE, () => { @@ -1090,6 +1122,24 @@ function runApp() { ) return null + case DBActions.PROFILES.ADD_CHANNEL: + await baseHandlers.profiles.addChannelToProfiles(data.channel, data.profileIds) + syncOtherWindows( + IpcChannels.SYNC_PROFILES, + event, + { event: SyncEvents.PROFILES.ADD_CHANNEL, data } + ) + return null + + case DBActions.PROFILES.REMOVE_CHANNEL: + await baseHandlers.profiles.removeChannelFromProfiles(data.channelId, data.profileIds) + syncOtherWindows( + IpcChannels.SYNC_PROFILES, + event, + { event: SyncEvents.PROFILES.REMOVE_CHANNEL, data } + ) + return null + case DBActions.GENERAL.DELETE: await baseHandlers.profiles.delete(data) syncOtherWindows( @@ -1298,7 +1348,7 @@ function runApp() { event.preventDefault() if (mainWindow && mainWindow.webContents) { - mainWindow.webContents.send('openUrl', baseUrl(url)) + mainWindow.webContents.send(IpcChannels.OPEN_URL, baseUrl(url)) } else { startupUrl = baseUrl(url) } @@ -1359,7 +1409,7 @@ function runApp() { } browserWindow.webContents.send( - 'change-view', + IpcChannels.CHANGE_VIEW, { route: path } ) } @@ -1460,9 +1510,7 @@ function runApp() { click: (_menuItem, browserWindow, _event) => { if (browserWindow == null) { return } - browserWindow.webContents.send( - 'history-back', - ) + browserWindow.webContents.goBack() }, type: 'normal', }, @@ -1472,9 +1520,7 @@ function runApp() { click: (_menuItem, browserWindow, _event) => { if (browserWindow == null) { return } - browserWindow.webContents.send( - 'history-forward', - ) + browserWindow.webContents.goForward() }, type: 'normal', }, diff --git a/src/renderer/App.js b/src/renderer/App.js index c669960c5c88d..e2ecc75e4140b 100644 --- a/src/renderer/App.js +++ b/src/renderer/App.js @@ -1,5 +1,5 @@ import { defineComponent } from 'vue' -import { mapActions, mapMutations } from 'vuex' +import { mapActions } from 'vuex' import FtFlexBox from './components/ft-flex-box/ft-flex-box.vue' import TopNav from './components/top-nav/top-nav.vue' import SideNav from './components/side-nav/side-nav.vue' @@ -183,7 +183,6 @@ export default defineComponent({ ipcRenderer = require('electron').ipcRenderer this.setupListenersToSyncWindows() this.activateKeyboardShortcuts() - this.activateIPCListeners() this.openAllLinksExternally() this.enableSetSearchQueryText() this.enableOpenUrl() @@ -199,10 +198,6 @@ export default defineComponent({ }, 500) }) - this.$router.afterEach((to, from) => { - this.$refs.topNav?.navigateHistory() - }) - this.$router.onReady(() => { if (this.$router.currentRoute.path === '/') { this.$router.replace({ path: this.landingPage }) @@ -334,16 +329,6 @@ export default defineComponent({ }) }, - activateIPCListeners: function () { - // handle menu event updates from main script - ipcRenderer.on('history-back', (_event) => { - this.$refs.topNav.historyBack() - }) - ipcRenderer.on('history-forward', (_event) => { - this.$refs.topNav.historyForward() - }) - }, - handleKeyboardShortcuts: function (event) { if (event.altKey) { switch (event.key) { @@ -505,23 +490,23 @@ export default defineComponent({ }, enableSetSearchQueryText: function () { - ipcRenderer.on('updateSearchInputText', (event, searchQueryText) => { + ipcRenderer.on(IpcChannels.UPDATE_SEARCH_INPUT_TEXT, (event, searchQueryText) => { if (searchQueryText) { this.$refs.topNav.updateSearchInputText(searchQueryText) } }) - ipcRenderer.send('searchInputHandlingReady') + ipcRenderer.send(IpcChannels.SEARCH_INPUT_HANDLING_READY) }, enableOpenUrl: function () { - ipcRenderer.on('openUrl', (event, url) => { + ipcRenderer.on(IpcChannels.OPEN_URL, (event, url) => { if (url) { this.handleYoutubeLink(url) } }) - ipcRenderer.send('appReady') + ipcRenderer.send(IpcChannels.APP_READY) }, handleExternalLinkOpeningPromptAnswer: function (option) { @@ -551,10 +536,6 @@ export default defineComponent({ } }, - ...mapMutations([ - 'setInvidiousInstancesList' - ]), - ...mapActions([ 'grabUserSettings', 'grabAllProfiles', diff --git a/src/renderer/App.vue b/src/renderer/App.vue index fb54f93110337..759c91240926e 100644 --- a/src/renderer/App.vue +++ b/src/renderer/App.vue @@ -12,6 +12,7 @@ >