diff --git a/.github/workflows/autoLabelDuplicate.yml b/.github/workflows/autoLabelDuplicate.yml index 756a982258751..4bbbc9b4cf622 100644 --- a/.github/workflows/autoLabelDuplicate.yml +++ b/.github/workflows/autoLabelDuplicate.yml @@ -7,7 +7,9 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: Amwam/issue-comment-action@v1.3.1 + - name: Check Comment Author + if: github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER' + uses: Amwam/issue-comment-action@v1.3.1 with: keywords: '["duplicate of #", "duplicate of https://github.com/FreeTubeApp/FreeTube/issues/", "duplicate of https://github.com/FreeTubeApp/FreeTube/pulls/"]' labels: '["U: duplicate"]' diff --git a/.github/workflows/autoLabelIssue.yaml b/.github/workflows/autoLabelIssue.yaml index f5f14d54b01f9..7981deafb087d 100644 --- a/.github/workflows/autoLabelIssue.yaml +++ b/.github/workflows/autoLabelIssue.yaml @@ -9,7 +9,7 @@ jobs: steps: - uses: Naturalclar/issue-action@v2.0.2 with: - body: "both" + title-or-body: "body" parameters: >- [ { diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml index 492a3ab71ec72..dc2ce54205e5b 100644 --- a/.github/workflows/label-pr.yml +++ b/.github/workflows/label-pr.yml @@ -1,7 +1,7 @@ name: "Pull Request Labeler" on: pull_request_target: - types: [opened, reopened] + types: [opened, reopened, ready_for_review] jobs: triage: @@ -9,6 +9,7 @@ jobs: contents: read pull-requests: write runs-on: ubuntu-latest + if: ${{ !github.event.pull_request.draft }} steps: - uses: actions/labeler@v4 with: diff --git a/.github/workflows/remove-outdated-labels.yml b/.github/workflows/remove-outdated-labels.yml index ebeacffc78cff..c72ce19e1e2d4 100644 --- a/.github/workflows/remove-outdated-labels.yml +++ b/.github/workflows/remove-outdated-labels.yml @@ -1,9 +1,10 @@ name: Remove outdated labels on: - # https://github.community/t/github-actions-are-severely-limited-on-prs/18179/15 pull_request_target: types: - closed + - converted_to_draft + - ready_for_review jobs: remove-merged-pr-labels: name: Remove merged pull request labels @@ -23,7 +24,7 @@ jobs: remove-closed-pr-labels: name: Remove closed pull request labels - if: github.event_name == 'pull_request_target' && (! github.event.pull_request.merged) + if: github.event_name == 'pull_request_target' && (! github.event.pull_request.merged) && (github.event.action != 'converted_to_draft') && (github.event.action != 'ready_for_review') runs-on: ubuntu-latest steps: - uses: mondeja/remove-labels-gh-action@v1.1.1 @@ -36,3 +37,25 @@ jobs: PR: merge conflicts / rebase needed PR/Issue: dependent PR: stale + + remove-draft-pr-labels: + name: Remove labels from draft pull requests + if: github.event_name == 'pull_request_target' && github.event.action == 'converted_to_draft' + runs-on: ubuntu-latest + steps: + - uses: mondeja/remove-labels-gh-action@v1.1.1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + labels: | + PR: waiting for review + + remove-ready-pr-labels: + name: Remove labels when draft pr is marked ready for review + if: github.event_name == 'pull_request_target' && github.event.action == 'ready_for_review' + runs-on: ubuntu-latest + steps: + - uses: mondeja/remove-labels-gh-action@v1.1.1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + labels: | + PR: WIP diff --git a/.github/workflows/report.yml b/.github/workflows/report.yml index fddf6630d47fd..cbe16d3bd5ed4 100644 --- a/.github/workflows/report.yml +++ b/.github/workflows/report.yml @@ -13,7 +13,7 @@ jobs: # For bug reports - name: New bug issue - uses: alex-page/github-project-automation-plus@v0.8.2 + uses: alex-page/github-project-automation-plus@v0.8.3 if: contains(github.event.issue.labels.*.name, 'bug') && github.event.action == 'opened' with: project: Bug Reports @@ -22,7 +22,7 @@ jobs: action: update - name: Bug issue closed - uses: alex-page/github-project-automation-plus@v0.8.2 + uses: alex-page/github-project-automation-plus@v0.8.3 if: github.event.action == 'closed' || github.event.action == 'deleted' with: action: delete @@ -31,7 +31,7 @@ jobs: repo-token: ${{ secrets.PUSH_TOKEN }} - name: Bug issue reopened - uses: alex-page/github-project-automation-plus@v0.8.2 + uses: alex-page/github-project-automation-plus@v0.8.3 if: contains(github.event.issue.labels.*.name, 'bug') && github.event.action == 'reopened' with: project: Bug Reports @@ -41,7 +41,7 @@ jobs: # For feature requests - name: New feature issue - uses: alex-page/github-project-automation-plus@v0.8.2 + uses: alex-page/github-project-automation-plus@v0.8.3 if: contains(github.event.issue.labels.*.name, 'enhancement') && github.event.action == 'opened' with: project: Feature Requests @@ -50,7 +50,7 @@ jobs: action: update - name: Feature request issue closed - uses: alex-page/github-project-automation-plus@v0.8.2 + uses: alex-page/github-project-automation-plus@v0.8.3 if: github.event.action == 'closed' || github.event.action == 'deleted' with: action: delete @@ -59,7 +59,7 @@ jobs: repo-token: ${{ secrets.PUSH_TOKEN }} - name: Feature request issue reopened - uses: alex-page/github-project-automation-plus@v0.8.2 + uses: alex-page/github-project-automation-plus@v0.8.3 if: contains(github.event.issue.labels.*.name, 'enhancement') && github.event.action == 'reopened' with: project: Feature Requests diff --git a/README.md b/README.md index 02a38f0791aaf..9f87731852aaa 100644 --- a/README.md +++ b/README.md @@ -79,21 +79,21 @@ The first build with a green check mark is the latest build. You will need to ha ## How to build and test ### Commands for the Android APK ```bash - # 📦 Packs the project using `webpack.cordova.config.js` - yarn pack:cordova - # 🏗 Builds the debug APK and launches it on a connected device - yarn run:cordova - # 🚧 Builds the development APK - yarn build:cordova - # 🏦 Builds the release APK - yarn build:cordova --release +# 📦 Packs the project using `webpack.cordova.config.js` +yarn pack:cordova +# 🏗 Builds the debug APK and launches it on a connected device +yarn run:cordova +# 🚧 Builds the development APK +yarn build:cordova +# 🏦 Builds the release APK +yarn build:cordova --release ``` ### Commands for the PWA (progressive web app) ```bash - # 🐛 Debugs the project using `webpack.web.config.js` - yarn dev:web - # 📦 Packs the project using `webpack.web.config.js` - yarn pack:web +# 🐛 Debugs the project using `webpack.web.config.js` +yarn dev:web +# 🎁 Packs the project using `webpack.web.config.js` +yarn pack:web ``` ## Contributing diff --git a/_scripts/dev-runner.js b/_scripts/dev-runner.js index 24322bdc9931e..d030c8b814035 100644 --- a/_scripts/dev-runner.js +++ b/_scripts/dev-runner.js @@ -9,16 +9,23 @@ const kill = require('tree-kill') const path = require('path') const { spawn } = require('child_process') -const mainConfig = require('./webpack.main.config') -const rendererConfig = require('./webpack.renderer.config') -const webConfig = require('./webpack.web.config') - let electronProcess = null let manualRestart = null const remoteDebugging = process.argv.indexOf('--remote-debug') !== -1 const web = process.argv.indexOf('--web') !== -1 +let mainConfig +let rendererConfig +let webConfig + +if (!web) { + mainConfig = require('./webpack.main.config') + rendererConfig = require('./webpack.renderer.config') +} else { + webConfig = require('./webpack.web.config') +} + if (remoteDebugging) { // disable dvtools open in electron process.env.RENDERER_REMOTE_DEBUGGING = true diff --git a/_scripts/webpack.cordova.config.js b/_scripts/webpack.cordova.config.js index 96da473f9807c..3c3aea38bb80e 100644 --- a/_scripts/webpack.cordova.config.js +++ b/_scripts/webpack.cordova.config.js @@ -28,12 +28,6 @@ const config = { electron: '{}', cordova: 'cordova', 'music-controls': 'MusicControls' - }, - ({ request }, callback) => { - if (request.startsWith('youtubei.js')) { - return callback(null, '{}') - } - callback() } ], module: { @@ -130,11 +124,12 @@ const config = { filename: 'index.html', template: path.resolve(__dirname, '../src/index.ejs'), nodeModules: false, + inject: false }), new VueLoaderPlugin(), new MiniCssExtractPlugin({ - filename: isDevMode ? '[name].css' : '[name].[contenthash].css', - chunkFilename: isDevMode ? '[id].css' : '[id].[contenthash].css', + filename: '[name].css', + chunkFilename: '[id].css', }) ], resolve: { @@ -180,6 +175,10 @@ config.plugins.push( from: path.join(__dirname, '../static/pwabuilder-sw.js'), to: path.join(__dirname, '../dist/cordova/www/pwabuilder-sw.js'), }, + { + from: path.join(__dirname, '../_icons/iconColor.ico'), + to: path.join(__dirname, '../dist/cordova/www/favicon.ico'), + }, { from: path.join(__dirname, '../static'), to: path.join(__dirname, '../dist/cordova/www/static'), diff --git a/_scripts/webpack.renderer.config.js b/_scripts/webpack.renderer.config.js index 7325a7e8bb91a..6a79da855a298 100644 --- a/_scripts/webpack.renderer.config.js +++ b/_scripts/webpack.renderer.config.js @@ -28,15 +28,12 @@ const config = { level: isDevMode ? 'info' : 'none' }, output: { - publicPath: '', libraryTarget: 'commonjs2', path: path.join(__dirname, '../dist'), filename: '[name].js', }, externals: { - // ignore linkedom's unnecessary broken canvas import, as youtubei.js only uses linkedom to generate DASH manifests - canvas: '{}', - 'cordova': 'browserify/lib/_empty.js', + cordova: 'browserify/lib/_empty.js', 'music-controls': 'browserify/lib/_empty.js' }, module: { @@ -49,6 +46,11 @@ const config = { { test: /\.vue$/, loader: 'vue-loader', + options: { + compilerOptions: { + whitespace: 'condense', + } + } }, { test: /\.scss$/, @@ -136,10 +138,7 @@ const config = { alias: { vue$: 'vue/dist/vue.common.js', - // defaults to the prebundled browser version which causes webpack to error with: - // "Critical dependency: require function is used in a way in which dependencies cannot be statically extracted" - // webpack likes to bundle the dependencies itself, could really have a better error message though - 'youtubei.js$': 'youtubei.js/dist/browser.js', + 'youtubei.js$': 'youtubei.js/web', }, extensions: ['.js', '.vue'] }, diff --git a/_scripts/webpack.web.config.js b/_scripts/webpack.web.config.js index fddbe1446f63f..64239477b3425 100644 --- a/_scripts/webpack.web.config.js +++ b/_scripts/webpack.web.config.js @@ -19,23 +19,15 @@ const config = { web: path.join(__dirname, '../src/renderer/main.js'), }, output: { - publicPath: '', path: path.join(__dirname, '../dist/web'), filename: '[name].js', }, - externals: [ - { + externals: { electron: '{}', cordova: '{}', - 'music-controls': '{}' + 'music-controls': '{}', + 'youtubei.js': '{}' }, - ({ request }, callback) => { - if (request.startsWith('youtubei.js')) { - return callback(null, '{}') - } - callback() - } - ], module: { rules: [ { @@ -45,7 +37,12 @@ const config = { }, { test: /\.vue$/, - loader: 'vue-loader' + loader: 'vue-loader', + options: { + compilerOptions: { + whitespace: 'condense', + } + } }, { test: /\.scss$/, @@ -173,26 +170,22 @@ config.plugins.push( 'process.env.GEOLOCATION_NAMES': JSON.stringify(fs.readdirSync(path.join(__dirname, '..', 'static', 'geolocations'))) }), new CopyWebpackPlugin({ - patterns: [ - { - from: path.join(__dirname, '../static/pwabuilder-sw.js'), - to: path.join(__dirname, '../dist/web/pwabuilder-sw.js'), - }, - { - // webmanifest expected to be in root - from: path.join(__dirname, '../static/manifest.webmanifest'), - to: path.join(__dirname, '../dist/web/manifest.webmanifest'), - }, - { - from: path.join(__dirname, '../static'), - to: path.join(__dirname, '../dist/web/static'), - globOptions: { - dot: true, - ignore: ['**/.*', '**/locales/**', '**/pwabuilder-sw.js', '**/dashFiles/**', '**/storyboards/**'], + patterns: [ + { + from: path.join(__dirname, '../static/pwabuilder-sw.js'), + to: path.join(__dirname, '../dist/web/pwabuilder-sw.js'), + }, + { + from: path.join(__dirname, '../static'), + to: path.join(__dirname, '../dist/web/static'), + globOptions: { + dot: true, + ignore: ['**/.*', '**/locales/**', '**/pwabuilder-sw.js', '**/dashFiles/**', '**/storyboards/**'], + }, }, - }, ] }) ) + module.exports = config diff --git a/package.json b/package.json index 4f4a114163b55..04361498dd2f9 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "pack": "run-p pack:main pack:renderer", "pack:main": "webpack --mode=production --node-env=production --config _scripts/webpack.main.config.js", "pack:renderer": "webpack --mode=production --node-env=production --config _scripts/webpack.renderer.config.js", + "pack:cordova:dev": "webpack --mode=development --node-env=development --config _scripts/webpack.cordova.config.js", "pack:cordova": "webpack --mode=production --node-env=production --config _scripts/webpack.cordova.config.js", "pack:web": "webpack --mode=production --node-env=production --config _scripts/webpack.web.config.js", "postinstall": "yarn run --silent rebuild:electron", @@ -54,11 +55,10 @@ "ci": "yarn install --silent --frozen-lockfile" }, "dependencies": { - "@fortawesome/fontawesome-svg-core": "^6.2.1", - "@fortawesome/free-brands-svg-icons": "^6.2.1", - "@fortawesome/free-solid-svg-icons": "^6.2.1", - "@fortawesome/vue-fontawesome": "^2.0.9", - "@freetube/yt-comment-scraper": "^6.2.0", + "@fortawesome/fontawesome-svg-core": "^6.3.0", + "@fortawesome/free-brands-svg-icons": "^6.3.0", + "@fortawesome/free-solid-svg-icons": "^6.3.0", + "@fortawesome/vue-fontawesome": "^2.0.10", "@silvermine/videojs-quality-selector": "^1.2.5", "autolinker": "^4.0.0", "browserify": "^17.0.0", @@ -68,7 +68,7 @@ "marked": "^4.2.12", "nedb-promises": "^6.2.1", "process": "^0.11.10", - "video.js": "7.20.3", + "video.js": "7.21.2", "videojs-contrib-quality-levels": "^3.0.0", "videojs-http-source-selector": "^1.1.6", "videojs-mobile-ui": "^0.8.0", @@ -78,12 +78,12 @@ "vue-i18n": "^8.28.2", "vue-observe-visibility": "^1.0.0", "vue-router": "^3.6.5", + "vue-tiny-slider": "^0.1.39", "vuex": "^3.6.2", - "youtubei.js": "^2.9.0", - "yt-channel-info": "^3.2.1" + "youtubei.js": "^3.1.1" }, "devDependencies": { - "@babel/core": "^7.20.12", + "@babel/core": "^7.21.0", "@babel/eslint-parser": "^7.19.1", "@babel/plugin-proposal-class-properties": "^7.18.6", "@babel/preset-env": "^7.20.2", @@ -91,14 +91,15 @@ "babel-loader": "^9.1.2", "copy-webpack-plugin": "^11.0.0", "cordova": "^11.0.0", + "core-js": "^3.27.2", "css-loader": "^6.7.3", "css-minimizer-webpack-plugin": "^4.2.2", - "electron": "^22.0.2", + "electron": "^22.3.2", "electron-builder": "^23.6.0", - "eslint": "^8.32.0", + "eslint": "^8.35.0", "eslint-config-prettier": "^8.6.0", "eslint-config-standard": "^17.0.0", - "eslint-plugin-import": "^2.27.4", + "eslint-plugin-import": "^2.27.5", "eslint-plugin-jsonc": "^2.6.0", "eslint-plugin-n": "^15.6.1", "eslint-plugin-prettier": "^4.2.1", @@ -110,19 +111,19 @@ "html-webpack-plugin": "^5.3.2", "js-yaml": "^4.1.0", "json-minimizer-webpack-plugin": "^4.0.0", - "lefthook": "^1.2.7", + "lefthook": "^1.3.1", "mini-css-extract-plugin": "^2.7.2", "npm-run-all": "^4.1.5", "postcss": "^8.4.21", "postcss-scss": "^4.0.6", - "prettier": "^2.8.3", - "rimraf": "^4.1.0", - "sass": "^1.57.1", + "prettier": "^2.8.4", + "rimraf": "^4.1.2", + "sass": "^1.58.3", "sass-loader": "^13.2.0", "stylelint": "^14.16.1", "stylelint-config-sass-guidelines": "^9.0.1", "stylelint-config-standard": "^29.0.0", - "stylelint-high-performance-animation": "^1.7.0", + "stylelint-high-performance-animation": "^1.8.0", "tree-kill": "1.2.2", "vue-devtools": "^5.1.4", "vue-eslint-parser": "^9.1.0", diff --git a/src/constants.js b/src/constants.js index b5956cc9af36a..bd80da2ffdc17 100644 --- a/src/constants.js +++ b/src/constants.js @@ -38,7 +38,8 @@ const DBActions = { }, HISTORY: { - UPDATE_WATCH_PROGRESS: 'db-action-history-update-watch-progress' + UPDATE_WATCH_PROGRESS: 'db-action-history-update-watch-progress', + UPDATE_PLAYLIST: 'db-action-history-update-playlist', }, PLAYLISTS: { @@ -59,7 +60,8 @@ const SyncEvents = { }, HISTORY: { - UPDATE_WATCH_PROGRESS: 'sync-history-update-watch-progress' + UPDATE_WATCH_PROGRESS: 'sync-history-update-watch-progress', + UPDATE_PLAYLIST: 'sync-history-update-playlist', }, PLAYLISTS: { diff --git a/src/cordova/config.xml b/src/cordova/config.xml index 2419a575c2413..c77368988cac4 100644 --- a/src/cordova/config.xml +++ b/src/cordova/config.xml @@ -19,4 +19,5 @@ + diff --git a/src/cordova/config.xml.js b/src/cordova/config.xml.js index 1daf8e49c80b6..d05a6a50fda2d 100644 --- a/src/cordova/config.xml.js +++ b/src/cordova/config.xml.js @@ -17,8 +17,10 @@ module.exports = (async () => { const [major, minor, patch] = versionNumbers let build = 0 if (versionParts.length > 2) { + // if the build number comes after -{environment}- build = versionParts[2] - } else { + } else if (versionNumbers.length > 3) { + // if the build number comes after a final . build = parseInt(versionNumbers[3]) } // eslint-disable-next-line diff --git a/src/cordova/package.js b/src/cordova/package.js index 90ec05a73559b..1022ee198b112 100644 --- a/src/cordova/package.js +++ b/src/cordova/package.js @@ -17,12 +17,13 @@ module.exports = { devDependencies: { 'cordova-android': '^11.0.0', 'cordova-clipboard': '^1.3.0', - 'cordova-plugin-advanced-background-mode': '^1.1.1', + 'cordova-plugin-background-mode': 'git+https://bitbucket.org/TheBosZ/cordova-plugin-run-in-background.git', 'cordova-plugin-android-permissions': '^1.1.4', - 'cordova-plugin-music-controls2': '3.0.5', + 'cordova-plugin-music-controls2': '3.0.7', 'cordova-plugin-save-dialog': '^1.1.1', 'cordova-plugin-theme-detection': '^1.3.0', - 'cordova-plugin-device': '^2.1.0' + 'cordova-plugin-device': '^2.1.0', + 'cordova-plugin-advanced-http': '^3.3.1' }, cordova: { platforms: [ @@ -32,9 +33,10 @@ module.exports = { 'cordova-plugin-android-permissions': {}, 'cordova-plugin-music-controls2': {}, 'cordova-clipboard': {}, - 'cordova-plugin-advanced-background-mode': {}, + 'cordova-plugin-background-mode': {}, 'cordova-plugin-theme-detection': {}, - 'cordova-plugin-save-dialog': {} + 'cordova-plugin-save-dialog': {}, + 'cordova-plugin-advanced-http': {} } } } diff --git a/src/datastores/handlers/base.js b/src/datastores/handlers/base.js index 285307017abf9..f24580a5eb6ed 100644 --- a/src/datastores/handlers/base.js +++ b/src/datastores/handlers/base.js @@ -58,6 +58,10 @@ class History { return db.history.update({ videoId }, { $set: { watchProgress } }, { upsert: true }) } + static updateLastViewedPlaylist(videoId, lastViewedPlaylistId) { + return db.history.update({ videoId }, { $set: { lastViewedPlaylistId } }, { upsert: true }) + } + static delete(videoId) { return db.history.remove({ videoId }) } diff --git a/src/datastores/handlers/electron.js b/src/datastores/handlers/electron.js index a054d7b5f5d81..ddb90ff82434e 100644 --- a/src/datastores/handlers/electron.js +++ b/src/datastores/handlers/electron.js @@ -42,6 +42,16 @@ class History { ) } + static updateLastViewedPlaylist(videoId, lastViewedPlaylistId) { + return ipcRenderer.invoke( + IpcChannels.DB_HISTORY, + { + action: DBActions.HISTORY.UPDATE_PLAYLIST, + data: { videoId, lastViewedPlaylistId } + } + ) + } + static delete(videoId) { return ipcRenderer.invoke( IpcChannels.DB_HISTORY, diff --git a/src/datastores/handlers/web.js b/src/datastores/handlers/web.js index bf7ee41febd18..a81eb305d1dd9 100644 --- a/src/datastores/handlers/web.js +++ b/src/datastores/handlers/web.js @@ -33,6 +33,10 @@ class History { return baseHandlers.history.updateWatchProgress(videoId, watchProgress) } + static updateLastViewedPlaylist(videoId, lastViewedPlaylistId) { + return baseHandlers.history.updateLastViewedPlaylist(videoId, lastViewedPlaylistId) + } + static delete(videoId) { return baseHandlers.history.delete(videoId) } diff --git a/src/datastores/index.js b/src/datastores/index.js index a54fd0b4ad3c0..2f1485cf97ae2 100644 --- a/src/datastores/index.js +++ b/src/datastores/index.js @@ -5,8 +5,18 @@ let dbPath = null if (process.env.IS_ELECTRON_MAIN) { const { app } = require('electron') const { join } = require('path') + // this code only runs in the electron main process, so hopefully using sync fs code here should be fine 😬 + const { existsSync, statSync, realpathSync } = require('fs') const userDataPath = app.getPath('userData') // This is based on the user's OS - dbPath = (dbName) => join(userDataPath, `${dbName}.db`) + dbPath = (dbName) => { + let path = join(userDataPath, `${dbName}.db`) + + if (existsSync(path) && statSync(path).isSymbolicLink) { + path = realpathSync(path) + } + + return path + } } else { dbPath = (dbName) => `${dbName}.db` } diff --git a/src/index.ejs b/src/index.ejs index 4b631ffad8531..cf3d87041fd50 100644 --- a/src/index.ejs +++ b/src/index.ejs @@ -2,10 +2,24 @@ + <% if (process.env.IS_CORDOVA) { %> + + + + <% } %> - <% if (!process.env.IS_ELECTRON) { %> + <% if (!process.env.IS_ELECTRON && !process.env.IS_CORDOVA) { %> <% } %> @@ -21,7 +35,7 @@
- <% if (!process.env.IS_ELECTRON) { %> + <% if (!process.env.IS_ELECTRON && !process.env.IS_CORDOVA) { %> <% } %> - <% if (process.env.IS_CORDOVA) { %> - - <% } %> diff --git a/src/main/index.js b/src/main/index.js index 6bdff568c4709..627d916e20d9b 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -508,11 +508,12 @@ function runApp() { const boundsDoc = await baseHandlers.settings._findBounds() if (typeof boundsDoc?.value === 'object') { const { maximized, fullScreen, ...bounds } = boundsDoc.value - const allDisplaysSummaryWidth = screen - .getAllDisplays() - .reduce((accumulator, { size: { width } }) => accumulator + width, 0) + const windowVisible = screen.getAllDisplays().some(display => { + const { x, y, width, height } = display.bounds + return !(bounds.x > x + width || bounds.x + bounds.width < x || bounds.y > y + height || bounds.y + bounds.height < y) + }) - if (allDisplaysSummaryWidth >= bounds.x) { + if (windowVisible) { newWindow.setBounds({ x: bounds.x, y: bounds.y, @@ -789,6 +790,15 @@ function runApp() { ) return null + case DBActions.HISTORY.UPDATE_PLAYLIST: + await baseHandlers.history.updateLastViewedPlaylist(data.videoId, data.lastViewedPlaylistId) + syncOtherWindows( + IpcChannels.SYNC_HISTORY, + event, + { event: SyncEvents.HISTORY.UPDATE_PLAYLIST, data } + ) + return null + case DBActions.GENERAL.DELETE: await baseHandlers.history.delete(data) syncOtherWindows( @@ -1017,7 +1027,16 @@ function runApp() { } function baseUrl(arg) { - return arg.replace('freetube://', '') + let newArg = arg.replace('freetube://', '') + // add support for authority free url + .replace('freetube:', '') + + // fix for Qt URL, like `freetube://https//www.youtube.com/watch?v=...` + // For details see https://github.com/FreeTubeApp/FreeTube/pull/3119 + if (newArg.startsWith('https') && newArg.charAt(5) !== ':') { + newArg = 'https:' + newArg.substring(5) + } + return newArg } function getLinkUrl(argv) { diff --git a/src/renderer/App.js b/src/renderer/App.js index 28637e1831f57..10b1b32a6f95e 100644 --- a/src/renderer/App.js +++ b/src/renderer/App.js @@ -14,6 +14,7 @@ import { IpcChannels } from '../constants' import packageDetails from '../../package.json' import { openExternalLink, openInternalPath, showToast } from './helpers/utils' import cordova from 'cordova' +import 'core-js/stable' let ipcRenderer = null @@ -145,15 +146,21 @@ export default defineComponent({ }, mounted: function () { if (process.env.IS_CORDOVA) { - const { backgroundMode } = cordova.plugins - backgroundMode.setDefaults({ - title: 'FreeTube' - }) - backgroundMode.enable() - backgroundMode.on('activate', () => { - // By default android wants to pause videos in the background, this disables that "optimization" - backgroundMode.disableWebViewOptimizations() - }) + if ('plugins' in cordova && 'backgroundMode' in cordova.plugins) { + const { backgroundMode } = cordova.plugins + backgroundMode.setDefaults({ + title: 'FreeTube', + // TODO ✏ add this to locale strings + text: 'FreeTube is running in the background.' + }) + backgroundMode.enable() + backgroundMode.on('activate', () => { + // By default android wants to pause videos in the background, this disables that "optimization" + backgroundMode.disableWebViewOptimizations() + }) + } else { + console.error('Background mode plugin failed to load.') + } } this.grabUserSettings().then(async () => { this.checkThemeSettings() @@ -166,6 +173,7 @@ export default defineComponent({ this.grabAllProfiles(this.$t('Profile.All Channels')).then(async () => { this.grabHistory() this.grabAllPlaylists() + this.watchSystemTheme() document.addEventListener('visibilitychange', () => { if (!document.hidden) { // if the window was unfocused, the system theme might have changed @@ -449,11 +457,14 @@ export default defineComponent({ } case 'channel': { - const { channelId, subPath } = result + const { channelId, subPath, url } = result openInternalPath({ path: `/channel/${channelId}/${subPath}`, - doCreateNewWindow + doCreateNewWindow, + query: { + url + } }) break } @@ -486,13 +497,17 @@ export default defineComponent({ document.body.dataset.systemTheme = shouldUseDarkColors ? 'dark' : 'light' }) } else if (process.env.IS_CORDOVA) { - cordova.plugins.ThemeDetection.isAvailable((isThemeDetectionAvailable) => { - if (isThemeDetectionAvailable) { - cordova.plugins.ThemeDetection.isDarkModeEnabled((message) => { - document.body.dataset.systemTheme = message.value ? 'dark' : 'light' - }) - } - }, console.error) + if ('plugins' in cordova && 'ThemeDetection' in cordova.plugins) { + cordova.plugins.ThemeDetection.isAvailable((isThemeDetectionAvailable) => { + if (isThemeDetectionAvailable) { + cordova.plugins.ThemeDetection.isDarkModeEnabled((message) => { + document.body.dataset.systemTheme = message.value ? 'dark' : 'light' + }) + } + }, console.error) + } else { + console.error('Theme detection plugin failed to load.') + } } }, diff --git a/src/renderer/components/cordova-settings/cordova-settings.js b/src/renderer/components/cordova-settings/cordova-settings.js index 1e335c89a8996..300e26eb42616 100644 --- a/src/renderer/components/cordova-settings/cordova-settings.js +++ b/src/renderer/components/cordova-settings/cordova-settings.js @@ -13,11 +13,15 @@ export default defineComponent({ computed: { getDisableBackgroundModeNotification: function () { return this.$store.getters.getDisableBackgroundModeNotification + }, + getShowThumbnailInMediaControls: function () { + return this.$store.getters.getShowThumbnailInMediaControls } }, methods: { ...mapActions([ 'updateDisableBackgroundModeNotification', + 'updateShowThumbnailInMediaControls' ]) } }) diff --git a/src/renderer/components/cordova-settings/cordova-settings.vue b/src/renderer/components/cordova-settings/cordova-settings.vue index 34e596d723bb6..1664ec0552f04 100644 --- a/src/renderer/components/cordova-settings/cordova-settings.vue +++ b/src/renderer/components/cordova-settings/cordova-settings.vue @@ -10,6 +10,13 @@ :default-value="getDisableBackgroundModeNotification" @change="updateDisableBackgroundModeNotification" /> + diff --git a/src/renderer/components/data-settings/data-settings.js b/src/renderer/components/data-settings/data-settings.js index eca9b0f963f07..442b71ded6633 100644 --- a/src/renderer/components/data-settings/data-settings.js +++ b/src/renderer/components/data-settings/data-settings.js @@ -6,17 +6,18 @@ import FtFlexBox from '../ft-flex-box/ft-flex-box.vue' import FtPrompt from '../ft-prompt/ft-prompt.vue' import { MAIN_PROFILE_ID } from '../../../constants' -import ytch from 'yt-channel-info' import { calculateColorLuminance, getRandomColor } from '../../helpers/colors' import { copyToClipboard, + getTodayDateStrLocalTimezone, readFileFromDialog, showOpenDialog, showSaveDialog, showToast, - writeFileFromDialog + writeFileFromDialog, } from '../../helpers/utils' import { invidiousAPICall } from '../../helpers/api/invidious' +import { getLocalChannel } from '../../helpers/api/local' export default defineComponent({ name: 'DataSettings', @@ -76,9 +77,6 @@ export default defineComponent({ `${exportNewPipe} (.json)` ] }, - usingElectron: function () { - return process.env.IS_ELECTRON - }, primaryProfile: function () { return JSON.parse(JSON.stringify(this.profileList[0])) } @@ -512,8 +510,8 @@ export default defineComponent({ const subscriptionsDb = this.profileList.map((profile) => { return JSON.stringify(profile) }).join('\n') + '\n'// a trailing line is expected - const date = new Date().toISOString().split('T')[0] - const exportFileName = 'freetube-subscriptions-' + date + '.db' + const dateStr = getTodayDateStrLocalTimezone() + const exportFileName = 'freetube-subscriptions-' + dateStr + '.db' const options = { defaultPath: exportFileName, @@ -525,24 +523,12 @@ export default defineComponent({ ] } - const response = await showSaveDialog(options) - if (response.canceled || response.filePath === '') { - // User canceled the save dialog - return - } - try { - await writeFileFromDialog(response, subscriptionsDb) - } catch (writeErr) { - const message = this.$t('Settings.Data Settings.Unable to read file') - showToast(`${message}: ${writeErr}`) - return - } - showToast(this.$t('Settings.Data Settings.Subscriptions have been successfully exported')) + await this.promptAndWriteToFile(options, subscriptionsDb, 'Subscriptions have been successfully exported') }, exportYouTubeSubscriptions: async function () { - const date = new Date().toISOString().split('T')[0] - const exportFileName = 'youtube-subscriptions-' + date + '.json' + const dateStr = getTodayDateStrLocalTimezone() + const exportFileName = 'youtube-subscriptions-' + dateStr + '.json' const options = { defaultPath: exportFileName, @@ -590,25 +576,12 @@ export default defineComponent({ return object }) - const response = await showSaveDialog(options) - if (response.canceled || response.filePath === '') { - // User canceled the save dialog - return - } - - try { - await writeFileFromDialog(response, JSON.stringify(subscriptionsObject)) - } catch (writeErr) { - const message = this.$t('Settings.Data Settings.Unable to write file') - showToast(`${message}: ${writeErr}`) - return - } - showToast(this.$t('Settings.Data Settings.Subscriptions have been successfully exported')) + await this.promptAndWriteToFile(options, JSON.stringify(subscriptionsObject), 'Subscriptions have been successfully exported') }, exportOpmlYouTubeSubscriptions: async function () { - const date = new Date().toISOString().split('T')[0] - const exportFileName = 'youtube-subscriptions-' + date + '.opml' + const dateStr = getTodayDateStrLocalTimezone() + const exportFileName = 'youtube-subscriptions-' + dateStr + '.opml' const options = { defaultPath: exportFileName, @@ -636,25 +609,12 @@ export default defineComponent({ opmlData += '' - const response = await showSaveDialog(options) - if (response.canceled || response.filePath === '') { - // User canceled the save dialog - return - } - - try { - await writeFileFromDialog(response, opmlData) - } catch (writeErr) { - const message = this.$t('Settings.Data Settings.Unable to write file') - showToast(`${message}: ${writeErr}`) - return - } - showToast(this.$t('Settings.Data Settings.Subscriptions have been successfully exported')) + await this.promptAndWriteToFile(options, opmlData, 'Subscriptions have been successfully exported') }, exportCsvYouTubeSubscriptions: async function () { - const date = new Date().toISOString().split('T')[0] - const exportFileName = 'youtube-subscriptions-' + date + '.csv' + const dateStr = getTodayDateStrLocalTimezone() + const exportFileName = 'youtube-subscriptions-' + dateStr + '.csv' const options = { defaultPath: exportFileName, @@ -675,25 +635,13 @@ export default defineComponent({ exportText += `${channel.id},${channelUrl},${channelName}\n` }) exportText += '\n' - const response = await showSaveDialog(options) - if (response.canceled || response.filePath === '') { - // User canceled the save dialog - return - } - try { - await writeFileFromDialog(response, exportText) - } catch (writeErr) { - const message = this.$t('Settings.Data Settings.Unable to write file') - showToast(`${message}: ${writeErr}`) - return - } - showToast(this.$t('Settings.Data Settings.Subscriptions have been successfully exported')) + await this.promptAndWriteToFile(options, exportText, 'Subscriptions have been successfully exported') }, exportNewPipeSubscriptions: async function () { - const date = new Date().toISOString().split('T')[0] - const exportFileName = 'newpipe-subscriptions-' + date + '.json' + const dateStr = getTodayDateStrLocalTimezone() + const exportFileName = 'newpipe-subscriptions-' + dateStr + '.json' const options = { defaultPath: exportFileName, @@ -722,19 +670,7 @@ export default defineComponent({ newPipeObject.subscriptions.push(subscription) }) - const response = await showSaveDialog(options) - if (response.canceled || response.filePath === '') { - // User canceled the save dialog - return - } - try { - await writeFileFromDialog(response, JSON.stringify(newPipeObject)) - } catch (writeErr) { - const message = this.$t('Settings.Data Settings.Unable to write file') - showToast(`${message}: ${writeErr}`) - return - } - showToast(this.$t('Settings.Data Settings.Subscriptions have been successfully exported')) + await this.promptAndWriteToFile(options, JSON.stringify(newPipeObject), 'Subscriptions have been successfully exported') }, importHistory: async function () { @@ -809,8 +745,8 @@ export default defineComponent({ const historyDb = this.historyCache.map((historyEntry) => { return JSON.stringify(historyEntry) }).join('\n') + '\n' - const date = new Date().toISOString().split('T')[0] - const exportFileName = 'freetube-history-' + date + '.db' + const dateStr = getTodayDateStrLocalTimezone() + const exportFileName = 'freetube-history-' + dateStr + '.db' const options = { defaultPath: exportFileName, @@ -822,19 +758,7 @@ export default defineComponent({ ] } - const response = await showSaveDialog(options) - if (response.canceled || response.filePath === '') { - // User canceled the save dialog - return - } - - try { - await writeFileFromDialog(response, historyDb) - } catch (writeErr) { - const message = this.$t('Settings.Data Settings.Unable to write file') - showToast(`${message}: ${writeErr}`) - } - showToast(this.$t('Settings.Data Settings.All watched history has been successfully exported')) + await this.promptAndWriteToFile(options, historyDb, 'All watched history has been successfully exported') }, importPlaylists: async function () { @@ -952,8 +876,8 @@ export default defineComponent({ }, exportPlaylists: async function () { - const date = new Date().toISOString().split('T')[0] - const exportFileName = 'freetube-playlists-' + date + '.db' + const dateStr = getTodayDateStrLocalTimezone() + const exportFileName = 'freetube-playlists-' + dateStr + '.db' const options = { defaultPath: exportFileName, @@ -965,19 +889,7 @@ export default defineComponent({ ] } - const response = await showSaveDialog(options) - if (response.canceled || response.filePath === '') { - // User canceled the save dialog - return - } - try { - await writeFileFromDialog(response, JSON.stringify(this.allPlaylists)) - } catch (writeErr) { - const message = this.$t('Settings.Data Settings.Unable to write file') - showToast(`${message}: ${writeErr}`) - return - } - showToast(`${this.$t('Settings.Data Settings.All playlists has been successfully exported')}`) + await this.promptAndWriteToFile(options, JSON.stringify(this.allPlaylists), 'All playlists has been successfully exported') }, convertOldFreeTubeFormatToNew(oldData) { @@ -1011,6 +923,24 @@ export default defineComponent({ return convertedData }, + promptAndWriteToFile: async function (saveOptions, content, successMessageKeySuffix) { + const response = await showSaveDialog(saveOptions) + if (response.canceled || response.filePath === '') { + // User canceled the save dialog + return + } + + try { + await writeFileFromDialog(response, content) + } catch (writeErr) { + const message = this.$t('Settings.Data Settings.Unable to write file') + showToast(`${message}: ${writeErr}`) + return + } + + showToast(this.$t(`Settings.Data Settings.${successMessageKeySuffix}`)) + }, + getChannelInfoInvidious: function (channelId) { return new Promise((resolve, reject) => { const subscriptionsPayload = { @@ -1038,25 +968,32 @@ export default defineComponent({ }) }, - getChannelInfoLocal: function (channelId) { - return new Promise((resolve, reject) => { - ytch.getChannelInfo({ channelId: channelId }).then(async (response) => { - resolve(response) - }).catch((err) => { - console.error(err) - const errorMessage = this.$t('Local API Error (Click to copy)') - showToast(`${errorMessage}: ${err}`, 10000, () => { - copyToClipboard(err) - }) + getChannelInfoLocal: async function (channelId) { + try { + const channel = await getLocalChannel(channelId) - if (this.backendFallback && this.backendPreference === 'local') { - showToast(this.$t('Falling back to the Invidious API')) - resolve(this.getChannelInfoInvidious(channelId)) - } else { - resolve([]) - } + if (channel.alert) { + return undefined + } + + 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 the Invidious API')) + return await this.getChannelInfoInvidious(channelId) + } else { + return [] + } + } }, /* diff --git a/src/renderer/components/download-settings/download-settings.scss b/src/renderer/components/download-settings/download-settings.css similarity index 100% rename from src/renderer/components/download-settings/download-settings.scss rename to src/renderer/components/download-settings/download-settings.css diff --git a/src/renderer/components/download-settings/download-settings.vue b/src/renderer/components/download-settings/download-settings.vue index b4cd9dc1167e9..4ca6c15605f85 100644 --- a/src/renderer/components/download-settings/download-settings.vue +++ b/src/renderer/components/download-settings/download-settings.vue @@ -44,4 +44,4 @@