diff --git a/.babelrc b/.babelrc index abf4798b2..bca0e3c2c 100644 --- a/.babelrc +++ b/.babelrc @@ -4,8 +4,8 @@ "@babel/env", { "targets": { - "chrome": "106", - "node": "16.16.0" + "chrome": "122", + "node": "20.9.0" } } ] diff --git a/.eslintrc.js b/.eslintrc.js index 2729411b7..1846e7791 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,3 +1,8 @@ +const path = require('path') +const { readFileSync } = require('fs') + +const activeLocales = JSON.parse(readFileSync(path.join(__dirname, './static/locales/activeLocales.json'))) + module.exports = { // https://eslint.org/docs/user-guide/configuring#using-configuration-files-1 root: true, @@ -47,11 +52,12 @@ module.exports = { 'plugin:vue/recommended', 'standard', 'plugin:jsonc/recommended-with-json', - 'plugin:vuejs-accessibility/recommended' + 'plugin:vuejs-accessibility/recommended', + 'plugin:@intlify/vue-i18n/recommended' ], // https://eslint.org/docs/user-guide/configuring#configuring-plugins - plugins: ['vue', 'vuejs-accessibility', 'n', 'unicorn'], + plugins: ['vue', 'vuejs-accessibility', 'n', 'unicorn', '@intlify/vue-i18n'], rules: { 'space-before-function-paren': 'off', @@ -77,6 +83,40 @@ module.exports = { 'unicorn/no-array-push-push': 'error', 'unicorn/prefer-keyboard-event-key': 'error', 'unicorn/prefer-regexp-test': 'error', - 'unicorn/prefer-string-replace-all': 'error' + 'unicorn/prefer-string-replace-all': 'error', + '@intlify/vue-i18n/no-dynamic-keys': 'error', + // TODO: enable at a later date. currently disabled to prevent massive conflicts for initial PR + // '@intlify/vue-i18n/no-unused-keys': [ + // 'error', + // { + // extensions: ['.js', '.vue', 'yaml'] + // } + // ], + '@intlify/vue-i18n/no-duplicate-keys-in-locale': 'error', + '@intlify/vue-i18n/no-raw-text': [ + 'error', + { + attributes: { + '/.+/': [ + 'title', + 'aria-label', + 'aria-placeholder', + 'aria-roledescription', + 'aria-valuetext', + 'tooltip', + 'message' + ], + input: ['placeholder', 'value'], + img: ['alt'] + }, + ignoreText: ['-', '•', '/', 'YouTube', 'Invidious', 'FreeTube'] + } + ], + }, + settings: { + 'vue-i18n': { + localeDir: `./static/locales/{${activeLocales.join(',')}}.yaml`, + messageSyntaxVersion: '^8.0.0' + } } } diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 3ee6b6eaf..aef914ee3 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -92,7 +92,7 @@ body: - Portable - .rpm - .zip - - .apk (Android, FreeTubeCordova Unofficial) + - .apk (FreeTubeAndroid Unofficial) - AUR (Unofficial) - Chocolatey (Unofficial) - Homebrew (Unofficial) diff --git a/.github/issue-labeler.yml b/.github/issue-labeler.yml index 1d08f2788..38178571e 100644 --- a/.github/issue-labeler.yml +++ b/.github/issue-labeler.yml @@ -2,7 +2,7 @@ - '(visual bug)' 'B: Unofficial Download': - - '(AUR \(Unofficial\)|Chocolatey \(Unofficial\)|\.apk \(Android, FreeTubeCordova Unofficial\)|Homebrew \(Unofficial\)|PortableApps \(Unofficial\)|WAPT \(Unofficial\)|winget \(Unofficial\)|Scoop \(Unofficial\)|Snapcraft \(Unofficial\)|MPR \(Unofficial\)|Nix \(Unofficial\))' + - '(AUR \(Unofficial\)|Chocolatey \(Unofficial\)|\.apk \(FreeTubeAndroid Unofficial\)|Homebrew \(Unofficial\)|PortableApps \(Unofficial\)|WAPT \(Unofficial\)|winget \(Unofficial\)|Scoop \(Unofficial\)|Snapcraft \(Unofficial\)|MPR \(Unofficial\)|Nix \(Unofficial\))' 'B: keyboard control': - '(keyboard control not working)' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 569ded447..3bbef6630 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,7 +12,7 @@ jobs: build: strategy: matrix: - node-version: [18.x] + node-version: [20.x] runtime: - linux-x64 - linux-armv7l diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 53ea38937..a10f2f3e2 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -18,10 +18,12 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: - uses: actions/checkout@v4 - - name: Use Node.js 18.x + - name: Use Node.js 20.x uses: actions/setup-node@v4 with: - node-version: 18.x + node-version: 20.x cache: "yarn" - run: yarn run ci - run: yarn run lint + # let's verify that webpack is able to package the project + - run: yarn run pack diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 519492e79..87237a364 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: build: strategy: matrix: - node-version: [18.x] + node-version: [20.x] runtime: - linux-x64 - linux-armv7l diff --git a/README.md b/README.md index 3f55240f1..196960948 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ 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. +Available for Windows (10 and later), Mac (macOS 10.15 and later) & Linux thanks to Electron.
@@ -78,6 +78,10 @@ FreeTube is supported by the [Privacy Redirect](https://github.com/SimonBrazell/ ## Download Links ### Official Downloads + +> [!CAUTION] +> FreeTube is only supported on Windows 10 and later, macOS 10.15 and above, and various Linux distributions. Installing it on unsupported systems may result in unexpected issues. + * [GitHub Releases](https://github.com/FreeTubeApp/FreeTube/releases) * [FreeTube Website](https://freetubeapp.io/#download) @@ -103,7 +107,7 @@ The first build with a green check mark is the latest build. * Chocolatey: [Download](https://chocolatey.org/packages/freetube/) -* FreeTubeCordova (FreeTube port for Android and PWA): [Download](https://github.com/MarmadileManteater/FreeTubeCordova/releases) and [Source Code](https://github.com/MarmadileManteater/FreeTubeCordova) +* FreeTubeAndroid (FreeTube port for Android and PWA): [Download](https://github.com/MarmadileManteater/FreeTubeAndroid/releases) and [Source Code](https://github.com/MarmadileManteater/FreeTubeAndroid) * Homebrew Formulae (Mac only): [Download](https://formulae.brew.sh/cask/freetube) diff --git a/package.json b/package.json index f1ed1c968..052bd7086 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "build-release": "node _scripts/build.js", "build-release:arm64": "node _scripts/build.js arm64", "build-release:arm32": "node _scripts/build.js arm32", - "clean": "rimraf build/ static/dashFiles/ dist/ static/storyboards/", + "clean": "rimraf build/ dist/", "debug": "run-s rebuild:electron debug-runner", "debug-runner": "node _scripts/dev-runner.js --remote-debug", "dev": "run-s rebuild:electron dev-runner", @@ -53,9 +53,9 @@ "ci": "yarn install --silent --frozen-lockfile" }, "dependencies": { - "@fortawesome/fontawesome-svg-core": "^6.5.1", - "@fortawesome/free-brands-svg-icons": "^6.5.1", - "@fortawesome/free-solid-svg-icons": "^6.5.1", + "@fortawesome/fontawesome-svg-core": "^6.5.2", + "@fortawesome/free-brands-svg-icons": "^6.5.2", + "@fortawesome/free-solid-svg-icons": "^6.5.2", "@fortawesome/vue-fontawesome": "^2.0.10", "@seald-io/nedb": "^4.0.4", "@silvermine/videojs-quality-selector": "^1.3.1", @@ -80,16 +80,17 @@ "youtubei.js": "^9.2.0" }, "devDependencies": { - "@babel/core": "^7.24.3", + "@babel/core": "^7.24.4", "@babel/eslint-parser": "^7.24.1", "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/preset-env": "^7.24.3", + "@babel/preset-env": "^7.24.4", "@double-great/stylelint-a11y": "^3.0.2", + "@intlify/eslint-plugin-vue-i18n": "^2.0.0", "babel-loader": "^9.1.3", "copy-webpack-plugin": "^12.0.2", - "css-loader": "^6.10.0", + "css-loader": "^7.0.0", "css-minimizer-webpack-plugin": "^6.0.0", - "electron": "^29.1.6", + "electron": "^29.2.0", "electron-builder": "^24.13.3", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", @@ -106,14 +107,14 @@ "html-webpack-plugin": "^5.6.0", "js-yaml": "^4.1.0", "json-minimizer-webpack-plugin": "^5.0.0", - "lefthook": "^1.6.7", + "lefthook": "^1.6.8", "mini-css-extract-plugin": "^2.8.1", "npm-run-all": "^4.1.5", "postcss": "^8.4.38", "postcss-scss": "^4.0.9", "prettier": "^2.8.8", "rimraf": "^5.0.5", - "sass": "^1.72.0", + "sass": "^1.74.1", "sass-loader": "^14.1.1", "stylelint": "^16.3.1", "stylelint-config-sass-guidelines": "^11.1.0", diff --git a/src/constants.js b/src/constants.js index b281fb168..322472503 100644 --- a/src/constants.js +++ b/src/constants.js @@ -23,7 +23,10 @@ const IpcChannels = { SYNC_SETTINGS: 'sync-settings', SYNC_HISTORY: 'sync-history', SYNC_PROFILES: 'sync-profiles', - SYNC_PLAYLISTS: 'sync-playlists' + SYNC_PLAYLISTS: 'sync-playlists', + + GET_REPLACE_HTTP_CACHE: 'get-replace-http-cache', + TOGGLE_REPLACE_HTTP_CACHE: 'toggle-replace-http-cache' } const DBActions = { diff --git a/src/main/index.js b/src/main/index.js index 51f8b7d56..aa3b241a3 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -10,6 +10,7 @@ import { IpcChannels, DBActions, SyncEvents } from '../constants' import baseHandlers from '../datastores/handlers/base' import { extractExpiryTimestamp, ImageCache } from './ImageCache' import { existsSync } from 'fs' +import asyncFs from 'fs/promises' import packageDetails from '../../package.json' @@ -177,7 +178,8 @@ function runApp() { // command line switches need to be added before the app ready event first // that means we can't use the normal settings system as that is asynchronous, // doing it synchronously ensures that we add it before the event fires - const replaceHttpCache = existsSync(`${app.getPath('userData')}/experiment-replace-http-cache`) + const REPLACE_HTTP_CACHE_PATH = `${app.getPath('userData')}/experiment-replace-http-cache` + const replaceHttpCache = existsSync(REPLACE_HTTP_CACHE_PATH) if (replaceHttpCache) { // the http cache causes excessive disk usage during video playback // we've got a custom image cache to make up for disabling the http cache @@ -662,7 +664,7 @@ function runApp() { } }) - ipcMain.once('relaunchRequest', () => { + function relaunch() { if (process.env.NODE_ENV === 'development') { app.exit(parseInt(process.env.FREETUBE_RELAUNCH_EXIT_CODE)) return @@ -693,6 +695,10 @@ function runApp() { } app.quit() + } + + ipcMain.once('relaunchRequest', () => { + relaunch() }) nativeTheme.on('updated', () => { @@ -780,6 +786,22 @@ function runApp() { child.unref() }) + ipcMain.handle(IpcChannels.GET_REPLACE_HTTP_CACHE, () => { + return replaceHttpCache + }) + + ipcMain.once(IpcChannels.TOGGLE_REPLACE_HTTP_CACHE, async () => { + if (replaceHttpCache) { + await asyncFs.rm(REPLACE_HTTP_CACHE_PATH) + } else { + // create an empty file + const handle = await asyncFs.open(REPLACE_HTTP_CACHE_PATH, 'w') + await handle.close() + } + + relaunch() + }) + // ************************************************* // // DB related IPC calls // *********** // diff --git a/src/renderer/App.js b/src/renderer/App.js index 2076c162d..a80cebcf1 100644 --- a/src/renderer/App.js +++ b/src/renderer/App.js @@ -15,6 +15,7 @@ import { marked } from 'marked' import { IpcChannels } from '../constants' import packageDetails from '../../package.json' import { openExternalLink, openInternalPath, showToast } from './helpers/utils' +import { translateWindowTitle } from './helpers/strings' let ipcRenderer = null @@ -77,14 +78,13 @@ export default defineComponent({ return this.$store.getters.getShowCreatePlaylistPrompt }, windowTitle: function () { - const routeTitle = this.$route.meta.title - if (routeTitle !== 'Channel' && routeTitle !== 'Watch' && routeTitle !== 'Hashtag') { - let title = - this.$route.meta.path === '/home' - ? packageDetails.productName - : `${this.$t(this.$route.meta.title)} - ${packageDetails.productName}` + const routePath = this.$route.path + if (!routePath.startsWith('/channel/') && !routePath.startsWith('/watch/') && !routePath.startsWith('/hashtag/')) { + let title = translateWindowTitle(this.$route.meta.title, this.$i18n) if (!title) { title = packageDetails.productName + } else { + title = `${title} - ${packageDetails.productName}` } return title } else { @@ -477,12 +477,7 @@ export default defineComponent({ 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) - } - - showToast(message) + showToast(this.$t('Unknown YouTube url type, cannot be opened in app')) } } }) diff --git a/src/renderer/components/data-settings/data-settings.js b/src/renderer/components/data-settings/data-settings.js index d7f2605f7..b0790d550 100644 --- a/src/renderer/components/data-settings/data-settings.js +++ b/src/renderer/components/data-settings/data-settings.js @@ -524,7 +524,7 @@ export default defineComponent({ ] } - await this.promptAndWriteToFile(options, subscriptionsDb, 'Subscriptions have been successfully exported') + await this.promptAndWriteToFile(options, subscriptionsDb, this.$t('Settings.Data Settings.Subscriptions have been successfully exported')) }, exportYouTubeSubscriptions: async function () { @@ -577,7 +577,7 @@ export default defineComponent({ return object }) - await this.promptAndWriteToFile(options, JSON.stringify(subscriptionsObject), 'Subscriptions have been successfully exported') + await this.promptAndWriteToFile(options, JSON.stringify(subscriptionsObject), this.$t('Settings.Data Settings.Subscriptions have been successfully exported')) }, exportOpmlYouTubeSubscriptions: async function () { @@ -605,7 +605,7 @@ export default defineComponent({ opmlData += '