diff --git a/.eslintrc.js b/.eslintrc.js index be982e46f7c8b..f50b44c1d73ff 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -11,13 +11,25 @@ module.exports = { // https://eslint.org/docs/user-guide/configuring#specifying-parser parser: 'vue-eslint-parser', - // https://vuejs.github.io/eslint-plugin-vue/user-guide/#faq + // https://eslint.vuejs.org/user-guide/#faq parserOptions: { - parser: 'babel-eslint', - ecmaVersion: 2018, - sourceType: 'module' + parser: '@babel/eslint-parser', + ecmaVersion: 2022, + sourceType: 'module', + requireConfigFile: false }, + overrides: [ + { + files: ['*.json'], + parser: 'jsonc-eslint-parser', + rules: { + 'no-tabs': 'off', + 'comma-spacing': 'off' + } + } + ], + // https://eslint.org/docs/user-guide/configuring#extending-configuration-files // order matters: from least important to most important in terms of overriding // Prettier + Vue: https://medium.com/@gogl.alex/how-to-properly-set-up-eslint-with-prettier-for-vue-or-nuxt-in-vscode-e42532099a9c @@ -25,11 +37,13 @@ module.exports = { 'prettier', 'eslint:recommended', 'plugin:vue/recommended', - 'standard' + 'standard', + 'plugin:jsonc/recommended-with-json', + // 'plugin:vuejs-accessibility/recommended' // uncomment once issues are fixed ], // https://eslint.org/docs/user-guide/configuring#configuring-plugins - plugins: ['vue'], + plugins: ['vue', 'vuejs-accessibility', 'n', 'unicorn'], rules: { 'space-before-function-paren': 'off', @@ -38,8 +52,22 @@ module.exports = { 'no-console': ['error', { allow: ['warn', 'error'] }], 'no-unused-vars': 'warn', 'no-undef': 'warn', + 'object-shorthand': 'off', 'vue/no-template-key': 'warn', 'vue/no-useless-template-attributes': 'off', - 'vue/multi-word-component-names': 'off' + 'vue/multi-word-component-names': 'off', + 'vuejs-accessibility/no-onchange': 'off', + 'vuejs-accessibility/label-has-for': ['error', { + required: { + some: ['nesting', 'id'] + } + }], + 'n/no-callback-literal': 'warn', + 'n/no-path-concat': 'warn', + 'unicorn/better-regex': 'error', + 'unicorn/no-array-push-push': 'error', + 'unicorn/prefer-keyboard-event-key': 'error', + 'unicorn/prefer-regexp-test': 'error', + 'unicorn/prefer-string-replace-all': 'error' } } diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 396fc42b3a9af..c90fb342d97af 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,6 +7,7 @@ updates: labels: - "PR: waiting for review" - "PR: dependencies" + open-pull-requests-limit: 10 - package-ecosystem: "github-actions" directory: "/" schedule: diff --git a/.github/workflows/autoMerge.yml b/.github/workflows/autoMerge.yml index dc6ced128b2cf..a5391e7245551 100644 --- a/.github/workflows/autoMerge.yml +++ b/.github/workflows/autoMerge.yml @@ -1,7 +1,7 @@ name: Auto Merge PR on: pull_request_target: - types: [opened, synchronize, reopened, auto_merge_disabled] + types: [opened, synchronize, reopened, auto_merge_disabled, ready_for_review] jobs: build: @@ -9,7 +9,7 @@ jobs: steps: - name: Auto Merge PR - if: contains(${{ github.event.pull_request.base.ref }}, 'development') || contains(${{ github.event.pull_request.base.ref }}, 'RC') + if: github.event.pull_request.draft == false && (contains(${{ github.event.pull_request.base.ref }}, 'development') || contains(${{ github.event.pull_request.base.ref }}, 'RC')) run: | echo ${{ secrets.PUSH_TOKEN }} >> auth.txt gh auth login --with-token < auth.txt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 077dad8147e4e..f6a14f1678372 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -57,7 +57,7 @@ jobs: - run: yarn run ci - run: yarn run lint - name: Get Version Number - uses: nyaayaya/package-version@v1 + uses: jozsefsallai/node-package-version@v1.0.4 with: path: 'package.json' follow-symlinks: false @@ -84,7 +84,7 @@ jobs: # script: if ${{ env.IS_DEV }} then echo "::set-output name=VERSION_NUMBER::${{ env.VERSION_NUMBER_NIGHTLY }}" else echo "::set-output name=VERSION_NUMBER::${{ env.VERSION_NUMBER }}" fi - name: Update package.json version - uses: jossef/action-set-json-field@v2 + uses: jossef/action-set-json-field@v2.1 with: file: package.json field: version diff --git a/.github/workflows/buildCordova.yml b/.github/workflows/buildCordova.yml index b6cadeeff7e8a..7bb71e4d17016 100644 --- a/.github/workflows/buildCordova.yml +++ b/.github/workflows/buildCordova.yml @@ -9,66 +9,79 @@ on: jobs: build: - strategy: - matrix: - node-version: [16.x] - runtime: [ linux-x64 ] - include: - - runtime: linux-x64 - os: ubuntu-18.04 - - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest environment: development steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 + - uses: actions/checkout@v3 + - name: Use Node.js + uses: actions/setup-node@v3 with: - node-version: ${{ matrix.node-version }} + node-version: 16.x cache: "yarn" - - run: npm run ci + - name: 🧶 Yarn install + run: yarn ci - - name: Lint code - run: npm run lint + - name: 🔍Lint code + run: yarn lint - - name: Get Version Number - uses: nyaayaya/package-version@v1 - with: - path: 'package.json' - follow-symlinks: false + - name: 📚Read package.json + id: pkg + uses: jaywcjlove/github-action-package@v1.3.0 - name: Set Version Number Variable id: versionNumber uses: actions/github-script@v6 - env: - VERSION_NUMBER_DEVELOPMENT: ${{ env.PACKAGE_VERSION }}-development-${{ github.run_number }} with: result-encoding: string script: | - return "${{ env.VERSION_NUMBER_DEVELOPMENT }}" + return '${{ steps.pkg.outputs.version }}-nightly-${{ github.run_number }}' + - name: Set App ID Variable + id: appId + uses: actions/github-script@v6 + with: + result-encoding: string + script: | + return '${{ steps.pkg.outputs.name }}.nightly' - - name: Update package.json version - uses: jossef/action-set-json-field@v2 + - name: ⬆ Update package.json version + uses: jossef/action-set-json-field@v2.1 with: file: package.json field: version value: ${{ steps.versionNumber.outputs.result }} + - name: ⬆ Update package.json app environment + uses: jossef/action-set-json-field@v2.1 + with: + file: package.json + field: name + value: ${{ steps.appId.outputs.result }} + + + - name: ⬆ Update package.json product name + uses: jossef/action-set-json-field@v2.1 + with: + file: package.json + field: productName + value: ${{ steps.pkg.outputs.productName }} Nightly - - name: Install libarchive-tools - if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64') - run: sudo apt -y install libarchive-tools; echo "Version Number ${{ toJson(job) }} ${{ toJson(needs) }}" + - name: 📦 Pack for 🕸web with Node.js + run: yarn pack:web - - name: Pack with Node.js ${{ matrix.node-version}} - if: contains(matrix.runtime, 'x64') - run: npm run pack:browser + - name: 📡 Upload PWA Artifact + uses: actions/upload-artifact@v3 + with: + name: freetube-${{ steps.versionNumber.outputs.result }}-PWA + path: dist/web - - name: Setup Android SDK Tools - if: contains(matrix.runtime, 'x64') + - name: 🚧 Setup Android SDK Tools uses: android-actions/setup-android@v2.0.9 - - name: Fetch keystore from secrets + - name: 📦 Pack for 📱Android with Node.js & Cordova + run: yarn pack:cordova + + - name: 🦴 Fetch keystore from secrets run: | while read -r line; do @@ -76,24 +89,11 @@ jobs: done <<< '${{ secrets.KEYSTORE }}' gpg -d --passphrase '${{ secrets.KEYSTORE_PASSWORD }}' --batch freetube.keystore.asc >> freetube.keystore - - name: Build APK with Cordova with Node.js ${{ matrix.node-version}} - if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64') - run: npm run build:cordova freetube-${{ steps.versionNumber.outputs.result }}.apk cordova ./freetube.keystore ${{ secrets.KEYSTORE_PASSWORD }} - - - name: Upload Cordova APK Artifact - uses: actions/upload-artifact@v3 - if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64') - with: - name: freetube-${{ steps.versionNumber.outputs.result }}.apk - path: build/freetube-${{ steps.versionNumber.outputs.result }}.apk - - - name: Build HTML5 with Node.js ${{ matrix.node-version}} - if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64') - run: npm run build:cordova freetube browser + - name: 👷‍♀️ Build APK with Cordova with Node.js + run: yarn build:cordova freetube-${{ steps.versionNumber.outputs.result }}.apk ./freetube.keystore ${{ secrets.KEYSTORE_PASSWORD }} - - name: Upload Cordova HTML5 Artifact + - name: 📡 Upload Cordova APK Artifact uses: actions/upload-artifact@v3 - if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64') with: - name: freetube-${{ steps.versionNumber.outputs.result }}-HTML5 - path: build/freetube + name: freetube-${{ steps.versionNumber.outputs.result }}-Android.apk + path: dist/freetube-${{ steps.versionNumber.outputs.result }}.apk diff --git a/.github/workflows/conflicts.yml b/.github/workflows/conflicts.yml index c0f2210258c8d..cceb14d31c46f 100644 --- a/.github/workflows/conflicts.yml +++ b/.github/workflows/conflicts.yml @@ -7,9 +7,6 @@ on: # In `pull_request` we wouldn't be able to change labels of fork PRs pull_request_target: types: [synchronize] - workflow_run: - workflows: ['Dummy workflow for conflicts'] - types: [requested] jobs: main: @@ -23,3 +20,4 @@ jobs: repoToken: "${{ secrets.GITHUB_TOKEN }}" commentOnDirty: "This pull request has conflicts, please resolve those before we can evaluate the pull request." commentOnClean: "Conflicts have been resolved. A maintainer will review the pull request shortly." + diff --git a/.github/workflows/dummy-conflicts.yml b/.github/workflows/dummy-conflicts.yml deleted file mode 100644 index cc4ba42504ee4..0000000000000 --- a/.github/workflows/dummy-conflicts.yml +++ /dev/null @@ -1,9 +0,0 @@ -name: Dummy workflow for conflicts -on: - pull_request_review: - types: [submitted] -jobs: - dummy: - runs-on: ubuntu-latest - steps: - - run: echo "this is a dummy workflow that triggers a workflow_run; it's necessary because otherwise the repo secrets will not be in scope for externally forked pull requests" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 000f9172a39b1..a2b08ad6e3e3b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,7 +58,7 @@ jobs: - run: yarn run lint - name: Get Version Number - uses: nyaayaya/package-version@v1 + uses: jozsefsallai/node-package-version@v1.0.4 with: path: 'package.json' follow-symlinks: false diff --git a/.github/workflows/releaseCordova.yml b/.github/workflows/releaseCordova.yml index fa06c020f216f..59706302f11b2 100644 --- a/.github/workflows/releaseCordova.yml +++ b/.github/workflows/releaseCordova.yml @@ -2,82 +2,86 @@ # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions name: Release Cordova + on: push: branches: [ release ] jobs: - build: - strategy: - matrix: - node-version: [16.x] - runtime: [ linux-x64 ] - include: - - runtime: linux-x64 - os: ubuntu-18.04 - - runs-on: ${{ matrix.os }} - environment: nightly + release: + runs-on: ubuntu-latest + environment: release steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 + - uses: actions/checkout@v3 + - name: Use Node.js + uses: actions/setup-node@v3 with: - node-version: ${{ matrix.node-version }} + node-version: 16.x cache: "yarn" - - run: npm run ci - - name: Get Version Number - uses: nyaayaya/package-version@v1 - with: - path: 'package.json' - follow-symlinks: false + - name: 🧶 Yarn install + run: yarn ci + + - name: 🔍Lint code + run: yarn lint + + - name: 📚Read package.json + id: pkg + uses: jaywcjlove/github-action-package@v1.3.0 - name: Set Version Number Variable id: versionNumber uses: actions/github-script@v6 - env: - IS_DEV: ${{ contains(github.ref, 'development') }} - IS_NIGHTLY: ${{ contains(github.ref, 'release') }} - VERSION_NUMBER_DEVELOPMENT: ${{ env.PACKAGE_VERSION }}-development-${{ github.run_number }} - VERSION_NUMBER_NIGHTLY: ${{ env.PACKAGE_VERSION }}-nightly-${{ github.run_number }} - VERSION_NUMBER_RELEASE: ${{ env.PACKAGE_VERSION }}.${{ github.run_number }} with: result-encoding: string script: | - if (${{ env.IS_DEV }}) { - return "${{ env.VERSION_NUMBER_DEVELOPMENT }}" - } else if (${{ env.IS_NIGHTLY }}) { - return "${{ env.VERSION_NUMBER_NIGHTLY }}" - } else { - return "${{env.VERSION_NUMBER_RELEASE }}" - } - # script: if ${{ env.IS_DEV }} then echo ":token :set-output name=VERSION_NUMBER::${{ env.VERSION_NUMBER_NIGHTLY }}" else echo "::set-output name=VERSION_NUMBER::${{ env.VERSION_NUMBER }}" fi - - - name: Update package.json version - uses: jossef/action-set-json-field@v2 + return '${{ steps.pkg.outputs.version }}.${{ github.run_number }}' + - name: Set App ID Variable + id: appId + uses: actions/github-script@v6 + with: + result-encoding: string + script: | + return '${{ steps.pkg.outputs.name }}' + + - name: ⬆ Update package.json version + uses: jossef/action-set-json-field@v2.1 with: file: package.json field: version value: ${{ steps.versionNumber.outputs.result }} + - name: ⬆ Update package.json app environment + uses: jossef/action-set-json-field@v2.1 + with: + file: package.json + field: name + value: ${{ steps.appId.outputs.result }} + - - name: Install libarchive-tools - if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64') - run: sudo apt -y install libarchive-tools; echo "Version Number ${{ toJson(job) }} ${{ toJson(needs) }}" + - name: ⬆ Update package.json product name + uses: jossef/action-set-json-field@v2.1 + with: + file: package.json + field: productName + value: ${{ steps.pkg.outputs.productName }} - - name: Lint code - run: npm run lint + - name: 📦 Pack for 🕸web with Node.js + run: yarn pack:web - - name: Pack with Node.js ${{ matrix.node-version}} - if: contains(matrix.runtime, 'x64') - run: npm run pack:browser + - name: 📡 Upload PWA Artifact + uses: actions/upload-artifact@v3 + with: + name: freetube-${{ steps.versionNumber.outputs.result }}-PWA + path: dist/web - - name: Setup Android SDK Tools - if: contains(matrix.runtime, 'x64') + - name: 🚧 Setup Android SDK Tools uses: android-actions/setup-android@v2.0.9 - - name: Fetch keystore from secrets + - name: 📦 Pack for 📱Android with Node.js & Cordova + run: yarn pack:cordova + + - name: 🦴 Fetch keystore from secrets run: | while read -r line; do @@ -85,43 +89,28 @@ jobs: done <<< '${{ secrets.KEYSTORE }}' gpg -d --passphrase '${{ secrets.KEYSTORE_PASSWORD }}' --batch freetube.keystore.asc >> freetube.keystore - - name: Build APK with Cordova with Node.js ${{ matrix.node-version}} - if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64') - run: npm run build:cordova freetube-${{ steps.versionNumber.outputs.result }}.apk cordova ./freetube.keystore ${{ secrets.KEYSTORE_PASSWORD }} + - name: 👷‍♀️ Build APK with Cordova with Node.js + run: yarn build:cordova freetube-${{ steps.versionNumber.outputs.result }}.apk ./freetube.keystore ${{ secrets.KEYSTORE_PASSWORD }} - - name: Upload Cordova APK Artifact + - name: 📡 Upload Cordova APK Artifact uses: actions/upload-artifact@v3 - if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64') with: - name: freetube-${{ steps.versionNumber.outputs.result }}.apk - path: build/freetube-${{ steps.versionNumber.outputs.result }}.apk - - - name: Build HTML5 with Node.js ${{ matrix.node-version}} - if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64') - run: npm run build:cordova freetube browser + name: freetube-${{ steps.versionNumber.outputs.result }}-Android.apk + path: dist/freetube-${{ steps.versionNumber.outputs.result }}.apk - - name: Setup Zip Action - if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64') + - name: 🔨 Setup Zip Action uses: montudor/action-zip@v1.0.0 - - name: Zip output - if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64') + - name: 🤐 Zip output run: zip -qq -r freetube-${{ steps.versionNumber.outputs.result }}.zip * - working-directory: build/freetube/ - - - name: Upload Cordova HTML5 Artifact - uses: actions/upload-artifact@v3 - if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64') - with: - name: freetube-${{ steps.versionNumber.outputs.result }}.zip - path: build/freetube/freetube-${{ steps.versionNumber.outputs.result }}.zip + working-directory: dist/web/ - name: Create release body run: | echo "${{ github.event.head_commit.message }}" >> release.txt echo "" >> release.txt - - name: Create Draft Release + - name: 📝 Create Draft Release id: create_release uses: actions/create-release@v1 env: @@ -133,28 +122,22 @@ jobs: prerelease: false body_path: release.txt - - name: Upload HTML5 Artifact to Release + - name: ⬆ Upload HTML5 Artifact to Release uses: actions/upload-release-asset@v1.0.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: build/freetube/freetube-${{ steps.versionNumber.outputs.result }}.zip + asset_path: dist/web/freetube-${{ steps.versionNumber.outputs.result }}.zip asset_name: freetube-${{ steps.versionNumber.outputs.result }}-pwa.zip asset_content_type: application/zip - - name: Upload Android APK Artifact to Release + - name: ⬆ Upload Android APK Artifact to Release uses: actions/upload-release-asset@v1.0.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: build/freetube-${{ steps.versionNumber.outputs.result }}.apk + asset_path: dist/freetube-${{ steps.versionNumber.outputs.result }}.apk asset_name: freetube-${{ steps.versionNumber.outputs.result }}-android.apk asset_content_type: application/apk - - - uses: eregon/publish-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - release_id: ${{ steps.create_release.outputs.id }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 02a937fe39dfe..04fa47539ab2e 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -3,11 +3,15 @@ on: schedule: - cron: '30 1 * * *' +permissions: + issues: write + pull-requests: write + jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v6 + - uses: actions/stale@v7 with: stale-issue-message: 'This issue is stale because it has been open 28 days with no activity. Remove stale label or comment or this will be closed in 7 days.' stale-pr-message: 'This PR is stale because it has been open 28 days with no activity. Remove stale label or comment or this will be closed in 14 days.' @@ -19,3 +23,5 @@ jobs: days-before-pr-close: 14 stale-issue-label: 'U: stale' stale-pr-label: 'PR: stale' + exempt-pr-labels: 'PR: WIP' + exempt-issue-labels: 'enhancement' diff --git a/.gitignore b/.gitignore index 79dfea94c23f0..2dbcd0748b75a 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ __coverage__ csak-timelog.json .idea/ debug/ + +# Lefthook +lefthook-local.yml diff --git a/.stylelintignore b/.stylelintignore new file mode 100644 index 0000000000000..18256bd0f5487 --- /dev/null +++ b/.stylelintignore @@ -0,0 +1,7 @@ +src/data/ +src/datastores/ +src/main/ +src/renderer/videoJS.css +dist/ +static/ +node_modules/ diff --git a/.stylelintrc.json b/.stylelintrc.json new file mode 100644 index 0000000000000..bf2da3b109eeb --- /dev/null +++ b/.stylelintrc.json @@ -0,0 +1,33 @@ +{ + "plugins": ["stylelint-high-performance-animation", "@double-great/stylelint-a11y"], + "extends": ["stylelint-config-standard", "stylelint-config-sass-guidelines"], + "overrides": [ + { + "files": ["**/*.scss"], + "customSyntax": "postcss-scss", + "rules": { + "max-nesting-depth": null, + "selector-max-compound-selectors": null + } + }, + { + "files": ["**/*.css"], + "rules": { + } + } + ], + "rules": { + "selector-class-pattern": null, + "selector-id-pattern": null, + "plugin/no-low-performance-animation-properties": null, + "selector-pseudo-class-no-unknown": [ + true, + { + "ignorePseudoClasses": ["deep"] + } + ], + "a11y/no-outline-none": true, + "a11y/selector-pseudo-class-focus": true, + "a11y/font-size-is-readable": true + } +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000000..5dc5d72c0a43a --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,11 @@ +{ + "recommendations": [ + "editorconfig.editorconfig", + "dbaeumer.vscode-eslint", + "stylelint.vscode-stylelint", + "syler.sass-indented", + "redhat.vscode-yaml", + "vue.volar", + "eamodio.gitlens" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000000..a48f03e30aeb2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,16 @@ +{ + "stylelint.packageManager": "yarn", + "stylelint.snippet": [ + "css", + "less", + "postcss", + "sass", + "scss" + ], + "stylelint.validate": [ + "css", + "less", + "postcss", + "scss" + ] +} diff --git a/README.md b/README.md index 0ce199f377edf..02a38f0791aaf 100644 --- a/README.md +++ b/README.md @@ -77,26 +77,24 @@ Builds are automatically created from changes to our development branch via [Git The first build with a green check mark is the latest build. You will need to have a GitHub account to download these builds. ## How to build and test - +### Commands for the Android APK ```bash - yarn pack:browser - # This creates the cordova project, - # copies the dist folder, browserifies - # it, and replaces a bunch of functions - # with cordova equivalents. - # After your first build, most of the - # build components will be recycled - # from that build. + # 📦 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 - # This opens up the cordova application - # in a web browser - yarn run:cordova browser - # This opens up the cordova application - # on an android device connected with - # debugging enabled - yarn run:cordova android + # 🏦 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 ``` - ## Contributing **NOTICE: MOST CHANGES SHOULD PROBABLY BE MADE TO [UPSTREAM](https://www.github.com/freetubeapp/freetube) UNLESS DIRECTLY RELATED TO CORDOVA CODE OR WORKFLOWS.** diff --git a/_scripts/CordovaPlugin.js b/_scripts/CordovaPlugin.js new file mode 100644 index 0000000000000..7c10dae48ac60 --- /dev/null +++ b/_scripts/CordovaPlugin.js @@ -0,0 +1,44 @@ + +// #region Imports +const { mkdir, writeFile } = require('fs/promises') +const fse = require('fs-extra') +const path = require('path') +const util = require('util') +const copy = util.promisify(fse.cp) +const exists = fse.existsSync +const { execWithLiveOutput } = require('./helpers') +// #endregion + +class CordovaPlugin { + apply(compiler) { + compiler.hooks.afterDone.tap('CordovaPlugin', async (afterDone) => { + const wwwRoot = afterDone.compilation.options.output.path + const outputDirectory = path.join(wwwRoot, '..') + const configXML = await require('../src/cordova/config.xml.js') + const packageJSON = require('../src/cordova/package') + + if (!exists(path.join(outputDirectory, 'node_modules'))) { + await writeFile(path.join(outputDirectory, 'package.json'), JSON.stringify(packageJSON, null, 2)) + await writeFile(path.join(outputDirectory, 'config.xml'), configXML.string) + // Copy the icons into the cordova directory + await mkdir(path.join(outputDirectory, 'res')) + await mkdir(path.join(outputDirectory, 'res', 'icon')) + await copy(path.join(__dirname, '..', '_icons', '.icon-set'), path.join(outputDirectory, 'res', 'icon', 'android'), { recursive: true, force: true }) + await copy(path.join(__dirname, '..', '_icons', 'icon.svg'), path.join(outputDirectory, 'res', 'icon', 'android', 'background.xml')) + // These next commands require the environment to be development + const environment = process.env.NODE_ENV + process.env.NODE_ENV = 'development' + // Install all of the cordova plugins + await execWithLiveOutput(`cd ${outputDirectory} && yarn install`) + // Restore the platform specific data + await execWithLiveOutput(`cd ${outputDirectory} && yarn restore`) + process.env.NODE_ENV = environment + } else { + await copy(path.join(__dirname, '..', '_icons', '.icon-set'), path.join(outputDirectory, 'res', 'icon', 'android'), { recursive: true, force: true }) + await copy(path.join(__dirname, '..', '_icons', 'icon.svg'), path.join(outputDirectory, 'res', 'icon', 'android', 'background.xml')) + } + }) + } +} + +module.exports = CordovaPlugin diff --git a/_scripts/ProcessLocalesPlugin.js b/_scripts/ProcessLocalesPlugin.js index 4d471cd21014e..988d9e4df9f84 100644 --- a/_scripts/ProcessLocalesPlugin.js +++ b/_scripts/ProcessLocalesPlugin.js @@ -1,7 +1,10 @@ const { existsSync, readFileSync } = require('fs') -const { brotliCompressSync, constants } = require('zlib') +const { brotliCompress, constants } = require('zlib') +const { promisify } = require('util') const { load: loadYaml } = require('js-yaml') +const brotliCompressAsync = promisify(brotliCompress) + class ProcessLocalesPlugin { constructor(options = {}) { this.compress = !!options.compress @@ -34,9 +37,9 @@ class ProcessLocalesPlugin { }, async (_assets) => { const promises = [] - + for (const { locale, data } of this.locales) { - promises.push(new Promise((resolve) => { + promises.push(new Promise(async (resolve) => { if (Object.prototype.hasOwnProperty.call(data, 'Locale Name')) { delete data['Locale Name'] } @@ -46,7 +49,7 @@ class ProcessLocalesPlugin { if (this.compress) { filename += '.br' - output = this.compressLocale(output) + output = await this.compressLocale(output) } compilation.emitAsset( @@ -78,10 +81,10 @@ class ProcessLocalesPlugin { } } - compressLocale(data) { + async compressLocale(data) { const buffer = Buffer.from(data, 'utf-8') - return brotliCompressSync(buffer, { + return await brotliCompressAsync(buffer, { params: { [constants.BROTLI_PARAM_MODE]: constants.BROTLI_MODE_TEXT, [constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY, diff --git a/_scripts/build.js b/_scripts/build.js index 78b1ed622ee5a..035f986c97dbe 100644 --- a/_scripts/build.js +++ b/_scripts/build.js @@ -41,7 +41,7 @@ if (platform === 'darwin') { const config = { appId: `io.freetubeapp.${name}`, - copyright: 'Copyleft © 2020-2022 freetubeapp@protonmail.com', + copyright: 'Copyleft © 2020-2023 freetubeapp@protonmail.com', // asar: false, // compression: 'store', productName, @@ -62,13 +62,6 @@ const config = { './dist/**/*', '!dist/web/*', '!node_modules/**/*', - - // renderer - 'node_modules/{miniget,ytpl,ytsr}/**/*', - - '!**/README.md', - '!**/*.js.map', - '!**/*.d.ts', ], dmg: { contents: [ diff --git a/_scripts/cordova-build.js b/_scripts/cordova-build.js index 3b68defc6a471..042cae9ae91f5 100644 --- a/_scripts/cordova-build.js +++ b/_scripts/cordova-build.js @@ -1,500 +1,60 @@ - -const DIST_FOLDER_NAME = 'android-dist' +const { writeFile, copyFile, stat } = require('fs/promises') +const { move } = require('fs-extra') const path = require('path') -const fs = require('fs') -const fse = require('fs-extra') -const util = require('util') -const fsExists = function (path) { - return new Promise(function (resolve, reject) { - fs.access(path, function (err, stat) { - if (err) { - resolve(false) - } else { - resolve(true) - } - }) - }) -} -const fsMkdir = util.promisify(fs.mkdir) -const fsReadFile = util.promisify(fs.readFile) -const fsWriteFile = util.promisify(fs.writeFile) -const fsMove = util.promisify(fs.rename) -const fsRm = util.promisify(fs.rm) -const fsCopy = util.promisify(fse.cp) -const exec = util.promisify(require('child_process').exec) -const xml2js = require('xml2js') -const parseXMLString = util.promisify(xml2js.parseString) -const createXMLStringFromObject = function (obj) { - const builder = new xml2js.Builder() - return builder.buildObject(obj) -} -const archiver = require('archiver'); - -(async function () { +const pkg = require('../package.json') +const exec = require('./helpers').execWithLiveOutput +;(async () => { + const log = (message, level = 'INFO') => { + // 🤷‍♀️ idk if there is a better way to implement logging here + // eslint-disable-next-line + console.log(`(${new Date().toISOString()})[${level}]: ${message}`) + } + const distDirectory = 'dist/cordova' try { - const sourceDirectory = path.join(__dirname, '..') - console.log('Using source directory: ' + sourceDirectory) - // Remove the dist folder if it already exists - const buildDirectory = path.join(sourceDirectory, 'build') - console.log('Using build directory: ' + buildDirectory) - if (!await fsExists(buildDirectory)) { - await fsMkdir(buildDirectory) - } - const distDirectory = path.join(buildDirectory, DIST_FOLDER_NAME) - console.log('Using dist directory: ' + distDirectory) - if (await fsExists(distDirectory)) { - await fsRm(distDirectory, { recursive: true, force: true }) - } - - const wwwroot = path.join(distDirectory, 'www') - console.log('Using wwwroot: ' + wwwroot) - - // Create the outline of the cordova project - console.log('Creating cordova outline') - const cordovaTemplateDirectory = path.join(sourceDirectory, 'node_modules/cordova-template') - if (await fsExists(cordovaTemplateDirectory)) { - await fsCopy(cordovaTemplateDirectory, distDirectory, { recursive: true, force: true }) - console.log('Was able to recycle previously built outline') - } - if (!await fsExists(cordovaTemplateDirectory)) { - const addCordovaPlugin = async function (pluginName) { - await exec('cd ' + distDirectory + ' && npx cordova plugin add ' + pluginName) - console.log('Installed ' + pluginName) - } - const addNpmPackage = async function (packageName) { - await exec(' cd ' + distDirectory + ' && npm install ' + packageName) - console.log('Installed ' + packageName) - } - await exec('cd ' + buildDirectory + ' && npx cordova create ' + DIST_FOLDER_NAME) - await addCordovaPlugin('cordova-plugin-background-mode') - await addCordovaPlugin('cordova-plugin-theme-detection') - await addCordovaPlugin('cordova-plugin-advanced-background-mode') - await addCordovaPlugin('cordova-plugin-media') - await addCordovaPlugin('cordova-plugin-music-controls2@3.0.5') - await addCordovaPlugin('cordova-plugin-save-dialog') - await addCordovaPlugin('cordova-plugin-android-permissions') - await addCordovaPlugin('cordova-clipboard') - - await addNpmPackage('browserify') - - if (await fsExists(wwwroot)) { - await fsRm(wwwroot, { recursive: true, force: true }) - } - try { - await fsCopy(distDirectory, cordovaTemplateDirectory, { recursive: true, force: true }) - } catch (exception) { - console.log(exception) - } - } - const sourcePackageUri = path.join(sourceDirectory, 'package.json') - const sourcePackage = JSON.parse((await fsReadFile(sourcePackageUri)).toString()) - - const destinationPackageUri = path.join(distDirectory, 'package.json') - const destinationPackage = JSON.parse((await fsReadFile(destinationPackageUri)).toString()) - - destinationPackage.name = 'io.freetubeapp.' + sourcePackage.name - destinationPackage.displayName = sourcePackage.productName - destinationPackage.version = sourcePackage.version - destinationPackage.author = sourcePackage.author - destinationPackage.repository = sourcePackage.repository - destinationPackage.bugs = sourcePackage.bugs - destinationPackage.license = sourcePackage.license - destinationPackage.description = sourcePackage.description - destinationPackage.private = sourcePackage.private - - let apkName = sourcePackage.name + '-' + sourcePackage.version + '.apk' - let exportType = 'cordova' - let keystorePath = null//if null, don't sign the apk - let keystorePassphrase = null - if (process.argv.length > 2) { - apkName = process.argv[2] - } - if (process.argv.length > 3) { - exportType = process.argv[3] - } - if (process.argv.length > 4) { - keystorePath = process.argv[4] - } - if (process.argv.length > 5) { - keystorePassphrase = process.argv[5] - } - // Copy dist folder into cordova project; - console.log('Copying dist output to cordova outline') - await fsCopy(path.join(sourceDirectory, 'dist', 'web'), wwwroot, { recursive: true, force: true }) - - console.log('Writing package.json in cordova project') - await fsWriteFile(destinationPackageUri, JSON.stringify(destinationPackage, null, 2)) - - // Running browserify on the renderer to remove to allow app to run in browser frame - console.log('Running browserify on cordova project') - await exec('cd ' + distDirectory + '/' + ' && npx browserify www/renderer.js -o www/renderer.js') - - let rendererContent = (await fsReadFile(path.join(wwwroot, 'renderer.js'))).toString() - // These escaped characters need to be escaped - // because they are part of a regular expression - // and they do not refer to a group - // they refer to the literal characters '(' and ')' - /* eslint-disable no-useless-escape */ - // this is a POC, random changes to the codebase break these regex all the time - rendererContent = rendererContent.replace(/([^(){}?.;:=,`&]*?)\(\)(\.(readFile|readFileSync|readdirSync|writeFileSync|writeFile|existsSync)\((.[^\)]*)\))/g, 'fileSystem$2') - rendererContent = rendererContent.replace(/\)([^(){}?.;:=,`&]*?)\(\)(\.(readFile|readFileSync|readdirSync|writeFileSync|writeFile|existsSync)\((.[^\)]*)\))/g, ';fileSystem$2') - //rendererContent = rendererContent.replace(/(this.showSaveDialog)\(([^\(\)]*?)\)/g, 'showFileSaveDialog($2);') - rendererContent = rendererContent.replace(/([a-zA-Z]*)=([a-zA-Z]*\([1-9]*\))\.createInstance/g, '$1=window.dataStore=$2.createInstance') - if (exportType === 'cordova') { - rendererContent = rendererContent.replace(/this.invidiousGetVideoInformation\(this.videoId\).then\(/g, 'this.invidiousGetVideoInformation(this.videoId).then(updatePlayingVideo);this.invidiousGetVideoInformation\(this.videoId\).then(') - rendererContent = rendererContent.replace('systemTheme:function(){return window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}', 'systemTheme:function () { return window.isDarkMode }') - } else { - rendererContent = rendererContent.replaceAll("createNewWindow:function(){", "createNewWindow: window.createNewWindow, electronNewWindow:function(){") - } - /* eslint-enable no-useless-escape */ - console.log('Setting up the renderer') - await fsWriteFile(path.join(wwwroot, 'renderer.js'), `(async function () { - ` + ((exportType === 'cordova') - ? ` - (() => { - Object.defineProperties(Array.prototype, { - at: { - value(index) { - if (this.length > index) { - if (index >= 0) { - return this[index] - } else { - if (this.length + index >= 0) { - if (this.length + index < this.length) { - return this[this.length + index] - } - } - } - } - } - } - }) - })(); - var createControls = function (object = {}, success = function () {}) { - MusicControls.create(object, success); - var listeners = {}; - var addListener = function (type, funct) { - var listenerTypes = Object.keys(listeners); - if (listenerTypes.indexOf(type) === -1) { - listeners[type] = []; - } - listeners[type].push(funct); - } - var triggerListeners = function (type, value) { - var listenerTypes = Object.keys(listeners); - if (listenerTypes.indexOf(type) !== -1) { - for (var i = 0; i < listeners[type].length; i++) { - var listener = listeners[type][i]; - listener(value); - } - } - } - function events(action) { - const message = JSON.parse(action).message; - switch(message) { - case 'music-controls-next': - triggerListeners(message); - break; - case 'music-controls-previous': - triggerListeners(message); - // Do something - break; - case 'music-controls-pause': - triggerListeners(message); - // Do something - break; - case 'music-controls-play': - triggerListeners(message); - // Do something - break; - case 'music-controls-destroy': - triggerListeners(message); - // Do something - break; - case 'music-controls-toggle-play-pause' : - triggerListeners(message); - // Do something - break; - // Lockscreen seek controls (iOS only) - case 'music-controls-seek-to': - const seekToInSeconds = JSON.parse(action).position; - MusicControls.updateElapsed({ - elapsed: seekToInSeconds, - isPlaying: true - }); - triggerListeners(message); - // Do something - break; - - // Headset events (Android only) - // All media button events are listed below - case 'music-controls-media-button' : - // Do something - triggerListeners(message); - break; - case 'music-controls-headset-unplugged': - // Do something - triggerListeners(message); - break; - case 'music-controls-headset-plugged': - // Do something - triggerListeners(message); - break; - default: - triggerListeners(message); - break; - } - } - - MusicControls.subscribe(events); - MusicControls.listen(); - return { - addListener: addListener, - updateData: function (newObject) { - MusicControls.destroy(function () { - var newKeys = Object.keys(newObject); - for (var i = 0; i < newKeys.length; i++) { - object[newKeys[i]] = newObject[newKeys[i]]; - } - MusicControls.create(object); - }, function (error) { - console.log(error); - }) - } - } - } - var currentVideo = null; - window.currentControls = null; - var setupControlsListeners = function (controls) { - controls.addListener("music-controls-play", function () { - if (currentVideo !== null) { - if (currentVideo.paused) { - currentVideo.play(); - } - } - }); - var pauseListener = function () { - if (currentVideo !== null) { - if (!currentVideo.paused) { - currentVideo.pause(); - } - } - }; - controls.addListener("music-controls-pause", pauseListener); - controls.addListener("music-controls-headset-unplugged", pauseListener); - controls.addListener("music-controls-next", function () { - try { - window.location.href = document.querySelector(".playlistItem .watchPlaylistItem .router-link-active").parentNode.parentNode.parentNode.nextElementSibling.querySelector("a").href; - } catch { - try { - window.location.href = document.body.querySelector(".recommendation a").href; - } catch (exception) { - console.log(exception); - } - } - }); - controls.addListener("music-controls-previous", function () { - try { - window.location.href = document.querySelector(".playlistItem .watchPlaylistItem").parentNode.previousElementSibling.querySelector("a").href; - } catch { - try { - history.back(); - } catch (exception) { - console.log(exception); - } - } - }); - } - window.updatePlayingVideo = function (videoObject) { - var videoObject = { track: videoObject.title, artist: videoObject.author, cover: videoObject.videoThumbnails[videoObject.videoThumbnails.length - 4].url }; - currentControls.updateData(videoObject) - window.currentVideoData = videoObject; - } - try { - // trying to fix the issue where the first controls object created does not have any data - currentControls = createControls(window.currentVideoData, function () { - setupControlsListeners(currentControls); - // Destroy any rouge controls that have popped up - MusicControls.destroy(); - }); - } catch { - - } - window.currentVideoData = {}; - setInterval(function () { - // Check for current video - var video = document.querySelector('video'); - if (video !== currentVideo) { - currentVideo = video; - // setup media controls - if (video === null || video === undefined) { - MusicControls.destroy(); - } - if (video.getAttribute("data-music-controls-loaded") !== "true") { - if (currentControls === null) { - currentControls = createControls(window.currentVideoData); - setupControlsListeners(currentControls); - } - video.setAttribute("data-music-controls-loaded", "true"); - video.onplay = function () { - MusicControls.updateIsPlaying(true); - } - video.onpause = function () { - MusicControls.updateIsPlaying(false); - } - - } - } - - }, 500);` - : '') + - ` - window.play = function () { - if (currentVideo !== null) { - currentVideo.play(); - } - }; - Object.defineProperty(window, 'player', { - get: function () { - return currentVideo; - } - }); - ` + ((exportType === 'cordova') - ? ` - window.isDarkMode = "light"; - if (await new Promise(function (resolve, reject) { cordova.plugins.ThemeDetection.isAvailable(resolve, reject) }) ) { - var isDarkMode = await new Promise(function (resolve, reject) { cordova.plugins.ThemeDetection.isDarkModeEnabled(function (result) { resolve(result.value) },reject) }); - if (isDarkMode) { - window.isDarkMode = "dark"; - } - } - var removeNewWindowIconStyle = document.createElement('style'); - removeNewWindowIconStyle.innerHTML = ".navNewWindowIcon { display: none !important; }" - document.head.appendChild(removeNewWindowIconStyle); - ` - : ` - window.createNewWindow = function () { - window.open(window.location.pathname, "_blank") - }; - `) + ` - ` + rendererContent + ` - }()); - `) - // Commenting out the electron exports because they will not exist in cordova - let content = (await fsReadFile(path.join(wwwroot, 'renderer.js'))).toString() - content = content.toString().replace('module.exports = getElectronPath();', "// commenting out this line because it doesn't work in cordova\r\n// module.exports = getElectronPath();") - await fsWriteFile(path.join(wwwroot, 'renderer.js'), content) - - let indexContent = (await fsReadFile(path.join(wwwroot, 'index.html'))).toString() - indexContent = indexContent.replace('', '') - if (exportType === 'cordova') { - indexContent = indexContent.replace('', '') - } - if (exportType === 'browser') { - indexContent = indexContent.replace('', '') - } - await fsWriteFile(path.join(wwwroot, 'index.html'), indexContent) - // Copy the icons to the cordova directory - console.log('Copying icons into cordova project') - await fsMkdir(path.join(distDirectory, 'res')) - await fsMkdir(path.join(distDirectory, 'res/icon')) - await fsCopy(path.join(sourceDirectory, '_icons/.icon-set'), path.join(distDirectory, 'res/icon/android'), { recursive: true, force: true }) - await fsCopy(path.join(sourceDirectory, '_icons/icon.svg'), path.join(distDirectory, 'res/icon/android/background.xml')) - - // Copy the values from the package.json into the config.xml - const configAddon = ` - - - - - - - - - ` - const configXML = await parseXMLString(await fsReadFile(path.join(distDirectory, 'config.xml'))) - configXML.widget.$.id = `io.freetubeapp.${sourcePackage.name}` - configXML.widget.$.version = sourcePackage.version - const sourcePackageParts = sourcePackage.version.split('-') - const [major, minor, patch] = sourcePackageParts[0].split('.') - let build = 0 - if (sourcePackageParts.length > 1) { - // Seperate environments by ID instead of version code - configXML.widget.$.id += `.${sourcePackageParts[1]}` - } - // disable environment based package id - /* if (sourcePackageParts.length > 2) { - build = sourcePackageParts[2] - } */ - configXML.widget.$['android-versionCode'] = `${major * 10000000 + minor * 100000 + patch * 1000 + build}` - configXML.widget.author[0].$.email = sourcePackage.author.email - configXML.widget.author[0]._ = sourcePackage.author.name - configXML.widget.author[0].$.href = sourcePackage.author.url - configXML.widget.name[0] = sourcePackage.productName - configXML.widget.description[0] = sourcePackage.description - const xmlString = createXMLStringFromObject(configXML) - console.log('Writing config.xml to cordova project') - await fsWriteFile(path.join(distDirectory, 'config.xml'), xmlString.replace('', configAddon + '')) + await stat(distDirectory) + } catch { + log(`The dist directory \`${distDirectory}\` cannot be found. This build *will* fail. \`pack:cordova\` did not complete.`, 'WARN') + } + let apkName = `${pkg.name}-${pkg.version}.apk` + let keystorePath = null + let keystorePassphrase = null + let release = false + const args = Array.from(process.argv) + if (args.indexOf('--release') !== -1) { + release = true + args.splice(args.indexOf('--release'), 1) + } + if (args.length > 2) { + apkName = args[2] + } + if (args.length > 3) { + keystorePath = args[3] + } + if (args.length > 4) { + keystorePassphrase = args[4] + } - // Adding export platforms to cordova - console.log('Adding platforms to cordova project') - if (exportType === 'cordova') { - await exec('cd ' + distDirectory + ' && npx cordova platform add android') - } - await exec('cd ' + distDirectory + ' && npx cordova platform add browser') - if (exportType === 'cordova') { - let buildArguments = '' - if (keystorePassphrase !== null) { - // the apk needs to be signed - buildArguments = '--buildConfig --warning-mode-all' - await fsMove(keystorePath, path.join(distDirectory, 'freetubecordova.keystore')); - await fsWriteFile(path.join(distDirectory, 'build.json'), JSON.stringify({ - "android": { - "debug": { - "keystore": "./freetubecordova.keystore", - "storePassword": keystorePassphrase, - "alias": "freetubecordova", - "password" : keystorePassphrase, - "keystoreType": "jks" - }, - "release": { - "keystore": "./freetubecordova.keystore", - "storePassword": keystorePassphrase, - "alias": "freetubecordova", - "password" : keystorePassphrase, - "keystoreType": "jks" - } - } - }, null, 4)); - } - // Run the apk build - console.log('Building apk file') - await exec('cd ' + distDirectory + ' && npx cordova build android ' + buildArguments) - // Copy the apk to the build dir - console.log('Copying apk file to build directory') - await fsCopy(path.join(distDirectory, 'platforms/android/app/build/outputs/apk/debug/app-debug.apk'), path.join(buildDirectory, apkName)) - } else if (exportType === 'browser') { - console.log('Copying output directory to build directory') - await fsCopy(wwwroot, path.join(buildDirectory, apkName), { recursive: true, force: true }) - const manifest = { - background_color: 'white', - description: sourcePackage.description, - display: 'standalone', - icons: [ - { - src: '_icons/icon.ico', - sizes: '256x256', - type: 'image/ico' - } - ], - name: sourcePackage.productName, - short_name: sourcePackage.productName, - start_url: './index.html' - } - await fsWriteFile(path.join(buildDirectory, apkName, 'manifest.webmanifest'), JSON.stringify(manifest, null, 2)) - } - } catch (exception) { - const g = 'see not useless' - throw exception + let buildArguments = '' + if (keystorePassphrase !== null) { + // the apk needs to be signed + buildArguments = `--stacktrace --buildConfig --warning-mode-all ${release ? '--release' : ''}` + await move(keystorePath, path.join(distDirectory, 'freetubecordova.keystore')) + const buildJSON = { + android: {} + } + buildJSON.android[release ? 'release' : 'debug'] = { + keystore: './freetubecordova.keystore', + storePassword: keystorePassphrase, + alias: 'freetubecordova', + password: keystorePassphrase, + keystoreType: 'jks' + } + await writeFile(path.join(distDirectory, 'build.json'), JSON.stringify(buildJSON, null, 4)) } -}()) + // 🏃‍♀️ Run the apk build + log(`Building apk file for ${release ? 'release' : 'development'}`) + await exec(`cd ${distDirectory} && npx cordova build android ${buildArguments}`) + // 📋 Copy the apk to the build dir + log('Copying apk file to build directory') + await copyFile(path.join(distDirectory, 'platforms/android/app/build/outputs/apk/debug/app-debug.apk'), path.join(distDirectory, '..', apkName)) +})() diff --git a/_scripts/cordova-run.js b/_scripts/cordova-run.js deleted file mode 100644 index fb1dab2998a2b..0000000000000 --- a/_scripts/cordova-run.js +++ /dev/null @@ -1,35 +0,0 @@ - - -const DIST_FOLDER_NAME = "android-dist"; -const fs = require('fs'); -const fse = require('fs-extra'); -const util = require('util'); -const fsExists = util.promisify(fs.exists); -const fsMkdir = util.promisify(fs.mkdir); -const fsReadFile = util.promisify(fs.readFile); -const fsWriteFile = util.promisify(fs.writeFile); -const fsRm = util.promisify(fs.rm); -const fsCopy = util.promisify(fse.cp); -const exec = util.promisify(require('child_process').exec); - -(async function () { - try { - // Remove the dist folder if it already exists - if (!await fsExists(__dirname + "/../build")) { - await fsMkdir(__dirname + "/../build"); - } - if (await fsExists(__dirname + "/../build/" + DIST_FOLDER_NAME)) { - var type = "browser"; - if (process.argv.length > 2) { - type = process.argv[2]; - } - await exec("npx cordova run " + type, { cwd: __dirname + "/../build/" + DIST_FOLDER_NAME }); - } else { - console.log("No cordova build found."); - console.log("Run: "); - console.log(" npm run build:cordova"); - } - } catch (exception) { - console.log(exception); - } -}()); \ No newline at end of file diff --git a/_scripts/helpers.js b/_scripts/helpers.js new file mode 100644 index 0000000000000..8b70fea38bd62 --- /dev/null +++ b/_scripts/helpers.js @@ -0,0 +1,32 @@ + +const exec = require('child_process').exec + +/** + * Calls `child_process`.exec, but it outputs + * all of the stdout live and can be awaited + * @param {string} command The command to be executed + * @returns + */ +function execWithLiveOutput (command) { + return new Promise((resolve, reject) => { + const execCall = exec(command, (error, stdout, stderr) => { + if (error) { + reject(error) + } + resolve() + }) + execCall.stdout.on('data', (data) => { + process.stdout.write(data) + }) + execCall.stderr.on('data', (data) => { + console.error(data) + }) + execCall.on('close', () => { + resolve() + }) + }) +} + +module.exports = { + execWithLiveOutput +} diff --git a/_scripts/webpack.cordova.config.js b/_scripts/webpack.cordova.config.js new file mode 100644 index 0000000000000..96da473f9807c --- /dev/null +++ b/_scripts/webpack.cordova.config.js @@ -0,0 +1,195 @@ +const path = require('path') +const fs = require('fs') +const webpack = require('webpack') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const VueLoaderPlugin = require('vue-loader/lib/plugin') +const CopyWebpackPlugin = require('copy-webpack-plugin') +const MiniCssExtractPlugin = require('mini-css-extract-plugin') +const JsonMinimizerPlugin = require('json-minimizer-webpack-plugin') +const CssMinimizerPlugin = require('css-minimizer-webpack-plugin') +const ProcessLocalesPlugin = require('./ProcessLocalesPlugin') +const CordovaPlugin = require('./CordovaPlugin') + +const isDevMode = process.env.NODE_ENV === 'development' + +const config = { + name: 'cordova', + mode: process.env.NODE_ENV, + devtool: isDevMode ? 'eval-cheap-module-source-map' : false, + entry: { + web: path.join(__dirname, '../src/renderer/main.js'), + }, + output: { + path: path.join(__dirname, '../dist/cordova/www'), + filename: '[name].js', + }, + externals: [ + { + electron: '{}', + cordova: 'cordova', + 'music-controls': 'MusicControls' + }, + ({ request }, callback) => { + if (request.startsWith('youtubei.js')) { + return callback(null, '{}') + } + callback() + } + ], + module: { + rules: [ + { + test: /\.js$/, + use: 'babel-loader', + exclude: /node_modules/, + }, + { + test: /\.vue$/, + loader: 'vue-loader' + }, + { + test: /\.scss$/, + use: [ + { + loader: MiniCssExtractPlugin.loader, + }, + { + loader: 'css-loader', + options: { + esModule: false + } + }, + { + loader: 'sass-loader', + options: { + implementation: require('sass') + } + }, + ], + }, + { + test: /\.css$/, + use: [ + { + loader: MiniCssExtractPlugin.loader + }, + { + loader: 'css-loader', + options: { + esModule: false + } + } + ], + }, + { + test: /\.html$/, + use: 'vue-html-loader', + }, + { + test: /\.(png|jpe?g|gif|tif?f|bmp|webp|svg)(\?.*)?$/, + type: 'asset/resource', + generator: { + filename: 'imgs/[name][ext]' + } + }, + { + test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, + type: 'asset/resource', + generator: { + filename: 'fonts/[name][ext]' + } + }, + ], + }, + // webpack defaults to only optimising the production builds, so having this here is fine + optimization: { + minimizer: [ + '...', // extend webpack's list instead of overwriting it + new JsonMinimizerPlugin({ + exclude: /\/locales\/.*\.json/ + }), + new CssMinimizerPlugin() + ] + }, + node: { + __dirname: true, + __filename: isDevMode, + }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env.IS_ELECTRON': false, + 'process.env.IS_ELECTRON_MAIN': false, + 'process.env.IS_CORDOVA': true + }), + new webpack.ProvidePlugin({ + process: 'process/browser', + Buffer: ['buffer', 'Buffer'], + }), + new HtmlWebpackPlugin({ + excludeChunks: ['processTaskWorker'], + filename: 'index.html', + template: path.resolve(__dirname, '../src/index.ejs'), + nodeModules: false, + }), + new VueLoaderPlugin(), + new MiniCssExtractPlugin({ + filename: isDevMode ? '[name].css' : '[name].[contenthash].css', + chunkFilename: isDevMode ? '[id].css' : '[id].[contenthash].css', + }) + ], + resolve: { + alias: { + vue$: 'vue/dist/vue.esm.js' + }, + fallback: { + buffer: require.resolve('buffer/'), + dns: require.resolve('browserify/lib/_empty.js'), + 'fs/promises': require.resolve('browserify/lib/_empty.js'), + http: require.resolve('stream-http'), + https: require.resolve('https-browserify'), + net: require.resolve('browserify/lib/_empty.js'), + os: require.resolve('os-browserify/browser.js'), + path: require.resolve('path-browserify'), + stream: require.resolve('stream-browserify'), + timers: require.resolve('timers-browserify'), + tls: require.resolve('browserify/lib/_empty.js'), + vm: require.resolve('vm-browserify'), + zlib: require.resolve('browserify-zlib') + }, + extensions: ['.js', '.vue'] + }, + target: 'web', +} + +const processLocalesPlugin = new ProcessLocalesPlugin({ + compress: false, + inputDir: path.join(__dirname, '../static/locales'), + outputDir: 'static/locales', +}) + +config.plugins.push( + new CordovaPlugin(), + processLocalesPlugin, + new webpack.DefinePlugin({ + 'process.env.LOCALE_NAMES': JSON.stringify(processLocalesPlugin.localeNames), + '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/cordova/www/pwabuilder-sw.js'), + }, + { + from: path.join(__dirname, '../static'), + to: path.join(__dirname, '../dist/cordova/www/static'), + globOptions: { + dot: true, + ignore: ['**/.*', '**/locales/**', '**/pwabuilder-sw.js', '**/dashFiles/**', '**/storyboards/**'], + }, + }, + ] + }) +) + +module.exports = config diff --git a/_scripts/webpack.main.config.js b/_scripts/webpack.main.config.js index f48ceca476616..52b357afe9bd5 100644 --- a/_scripts/webpack.main.config.js +++ b/_scripts/webpack.main.config.js @@ -3,8 +3,6 @@ const webpack = require('webpack') const CopyWebpackPlugin = require('copy-webpack-plugin') const JsonMinimizerPlugin = require('json-minimizer-webpack-plugin') -const { productName } = require('../package.json') - const isDevMode = process.env.NODE_ENV === 'development' const config = { @@ -38,8 +36,8 @@ const config = { }, plugins: [ new webpack.DefinePlugin({ - 'process.env.PRODUCT_NAME': JSON.stringify(productName), - }), + 'process.env.IS_ELECTRON_MAIN': true + }) ], output: { filename: '[name].js', diff --git a/_scripts/webpack.renderer.browser.config.js b/_scripts/webpack.renderer.browser.config.js deleted file mode 100644 index 41fc48f35af5d..0000000000000 --- a/_scripts/webpack.renderer.browser.config.js +++ /dev/null @@ -1,199 +0,0 @@ -const path = require('path') -const fs = require('fs') -const webpack = require('webpack') -const HtmlWebpackPlugin = require('html-webpack-plugin') -const VueLoaderPlugin = require('vue-loader/lib/plugin') -const MiniCssExtractPlugin = require('mini-css-extract-plugin') -const CssMinimizerPlugin = require('css-minimizer-webpack-plugin') -const CopyWebpackPlugin = require('copy-webpack-plugin') -const JsonMinimizerPlugin = require('json-minimizer-webpack-plugin') -const ProcessLocalesPlugin = require('./ProcessLocalesPlugin') -const { productName } = require('../package.json') - -const isDevMode = process.env.NODE_ENV === 'development' - -const config = { - name: 'renderer', - mode: process.env.NODE_ENV, - devtool: isDevMode ? 'eval-cheap-module-source-map' : false, - entry: { - renderer: path.join(__dirname, '../src/renderer/main.js'), - }, - infrastructureLogging: { - // Only warnings and errors - // level: 'none' disable logging - // Please read https://webpack.js.org/configuration/other-options/#infrastructurelogginglevel - level: isDevMode ? 'info' : 'none' - }, - output: { - publicPath: '', - libraryTarget: 'commonjs2', - path: path.join(__dirname, '../dist/web'), - filename: '[name].js', - }, - // webpack spits out errors while inlining ytpl and ytsr as - // they dynamically import their package.json file to extract the bug report URL - // the error: "Critical dependency: the request of a dependency is an expression" - externals: ['ytpl', 'ytsr'], - module: { - rules: [ - { - test: /\.js$/, - use: 'babel-loader', - exclude: /node_modules/, - }, - { - test: /\.vue$/, - loader: 'vue-loader', - }, - { - test: /\.s(c|a)ss$/, - use: [ - { - loader: MiniCssExtractPlugin.loader, - }, - { - loader: 'css-loader', - options: { - esModule: false - } - }, - { - loader: 'sass-loader', - options: { - // eslint-disable-next-line - implementation: require('sass'), - sassOptions: { - indentedSyntax: true - } - } - }, - ], - }, - { - test: /\.css$/, - use: [ - { - loader: MiniCssExtractPlugin.loader - }, - { - loader: 'css-loader', - options: { - esModule: false - } - } - ], - }, - { - test: /\.(png|jpe?g|gif|tif?f|bmp|webp|svg)(\?.*)?$/, - type: 'asset/resource', - generator: { - filename: 'imgs/[name][ext]' - } - }, - { - test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, - type: 'asset/resource', - generator: { - filename: 'fonts/[name][ext]' - } - }, - ], - }, - // webpack defaults to only optimising the production builds, so having this here is fine - optimization: { - minimizer: [ - '...', // extend webpack's list instead of overwriting it - new JsonMinimizerPlugin({ - exclude: /\/locales\/.*\.json/ - }), - new CssMinimizerPlugin() - ] - }, - node: { - __dirname: isDevMode, - __filename: isDevMode, - global: isDevMode, - }, - plugins: [ - new webpack.DefinePlugin({ - 'process.env.PRODUCT_NAME': JSON.stringify(productName), - 'process.env.IS_ELECTRON': false - }), - new HtmlWebpackPlugin({ - excludeChunks: ['processTaskWorker'], - filename: 'index.html', - template: path.resolve(__dirname, '../src/index.ejs'), - nodeModules: isDevMode - ? path.resolve(__dirname, '../node_modules') - : false, - }), - new VueLoaderPlugin(), - new MiniCssExtractPlugin({ - filename: isDevMode ? '[name].css' : '[name].[contenthash].css', - chunkFilename: isDevMode ? '[id].css' : '[id].[contenthash].css', - }), - ], - resolve: { - alias: { - vue$: 'vue/dist/vue.common.js', - '@': path.join(__dirname, '../src/'), - src: path.join(__dirname, '../src/'), - icons: path.join(__dirname, '../_icons/'), - images: path.join(__dirname, '../src/renderer/assets/img/'), - static: path.join(__dirname, '../static/'), - }, - extensions: ['.js', '.vue', '.json'], - }, - target: 'electron-renderer', -} - -/** - * Adjust rendererConfig for production settings - */ -if (isDevMode) { - // any dev only config - config.plugins.push( - new webpack.DefinePlugin({ - __static: `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"`, - }) - ) -} else { - const processLocalesPlugin = new ProcessLocalesPlugin({ - compress: false, - inputDir: path.join(__dirname, '../static/locales'), - outputDir: 'static/locales', - }) - - config.plugins.push( - processLocalesPlugin, - new webpack.DefinePlugin({ - 'process.env.LOCALE_NAMES': JSON.stringify(processLocalesPlugin.localeNames), - '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'), - }, - { - from: path.join(__dirname, '../static'), - to: path.join(__dirname, '../dist/web/static'), - globOptions: { - dot: true, - ignore: ['**/.*', '**/locales/**', '**/pwabuilder-sw.js', '**/dashFiles/**', '**/storyboards/**'], - }, - }, - ] - }), - // webpack doesn't get rid of js-yaml even though it isn't used in the production builds - // so we need to manually tell it to ignore any imports for `js-yaml` - new webpack.IgnorePlugin({ - resourceRegExp: /^js-yaml$/, - contextRegExp: /i18n$/ - }) - ) -} - -module.exports = config diff --git a/_scripts/webpack.renderer.config.js b/_scripts/webpack.renderer.config.js index e994fb626aa82..7325a7e8bb91a 100644 --- a/_scripts/webpack.renderer.config.js +++ b/_scripts/webpack.renderer.config.js @@ -6,10 +6,14 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin') const CssMinimizerPlugin = require('css-minimizer-webpack-plugin') const ProcessLocalesPlugin = require('./ProcessLocalesPlugin') -const { productName } = require('../package.json') - const isDevMode = process.env.NODE_ENV === 'development' +const processLocalesPlugin = new ProcessLocalesPlugin({ + compress: !isDevMode, + inputDir: path.join(__dirname, '../static/locales'), + outputDir: 'static/locales', +}) + const config = { name: 'renderer', mode: process.env.NODE_ENV, @@ -29,10 +33,12 @@ const config = { path: path.join(__dirname, '../dist'), filename: '[name].js', }, - // webpack spits out errors while inlining ytpl and ytsr as - // they dynamically import their package.json file to extract the bug report URL - // the error: "Critical dependency: the request of a dependency is an expression" - externals: ['ytpl', 'ytsr'], + externals: { + // ignore linkedom's unnecessary broken canvas import, as youtubei.js only uses linkedom to generate DASH manifests + canvas: '{}', + 'cordova': 'browserify/lib/_empty.js', + 'music-controls': 'browserify/lib/_empty.js' + }, module: { rules: [ { @@ -45,7 +51,7 @@ const config = { loader: 'vue-loader', }, { - test: /\.s(c|a)ss$/, + test: /\.scss$/, use: [ { loader: MiniCssExtractPlugin.loader, @@ -59,11 +65,7 @@ const config = { { loader: 'sass-loader', options: { - // eslint-disable-next-line - implementation: require('sass'), - sassOptions: { - indentedSyntax: true - } + implementation: require('sass') } }, ], @@ -110,9 +112,11 @@ const config = { __filename: isDevMode }, plugins: [ + processLocalesPlugin, new webpack.DefinePlugin({ - 'process.env.PRODUCT_NAME': JSON.stringify(productName), - 'process.env.IS_ELECTRON': true + 'process.env.IS_ELECTRON': true, + 'process.env.IS_ELECTRON_MAIN': false, + 'process.env.LOCALE_NAMES': JSON.stringify(processLocalesPlugin.localeNames) }), new HtmlWebpackPlugin({ excludeChunks: ['processTaskWorker'], @@ -126,33 +130,20 @@ const config = { new MiniCssExtractPlugin({ filename: isDevMode ? '[name].css' : '[name].[contenthash].css', chunkFilename: isDevMode ? '[id].css' : '[id].[contenthash].css', - }), + }) ], resolve: { alias: { - vue$: 'vue/dist/vue.common.js' + 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', }, extensions: ['.js', '.vue'] }, target: 'electron-renderer', } -/** - * Adjust rendererConfig for production settings - */ -if (!isDevMode) { - const processLocalesPlugin = new ProcessLocalesPlugin({ - compress: true, - inputDir: path.join(__dirname, '../static/locales'), - outputDir: 'static/locales', - }) - - config.plugins.push( - processLocalesPlugin, - new webpack.DefinePlugin({ - 'process.env.LOCALE_NAMES': JSON.stringify(processLocalesPlugin.localeNames) - }) - ) -} - module.exports = config diff --git a/_scripts/webpack.web.config.js b/_scripts/webpack.web.config.js index cf3a7ee1840b8..8f0bdd92fa820 100644 --- a/_scripts/webpack.web.config.js +++ b/_scripts/webpack.web.config.js @@ -9,8 +9,6 @@ const JsonMinimizerPlugin = require('json-minimizer-webpack-plugin') const CssMinimizerPlugin = require('css-minimizer-webpack-plugin') const ProcessLocalesPlugin = require('./ProcessLocalesPlugin') -const { productName } = require('../package.json') - const isDevMode = process.env.NODE_ENV === 'development' const config = { @@ -25,11 +23,19 @@ const config = { path: path.join(__dirname, '../dist/web'), filename: '[name].js', }, - externals: { - electron: '{}', - ytpl: '{}', - ytsr: '{}' - }, + externals: [ + { + electron: '{}', + cordova: '{}', + 'music-controls': '{}' + }, + ({ request }, callback) => { + if (request.startsWith('youtubei.js')) { + return callback(null, '{}') + } + callback() + } + ], module: { rules: [ { @@ -42,7 +48,7 @@ const config = { loader: 'vue-loader' }, { - test: /\.s(c|a)ss$/, + test: /\.scss$/, use: [ { loader: MiniCssExtractPlugin.loader, @@ -56,11 +62,7 @@ const config = { { loader: 'sass-loader', options: { - // eslint-disable-next-line - implementation: require('sass'), - sassOptions: { - indentedSyntax: true - } + implementation: require('sass') } }, ], @@ -115,8 +117,8 @@ const config = { }, plugins: [ new webpack.DefinePlugin({ - 'process.env.PRODUCT_NAME': JSON.stringify(productName), - 'process.env.IS_ELECTRON': false + 'process.env.IS_ELECTRON': false, + 'process.env.IS_ELECTRON_MAIN': false }), new webpack.ProvidePlugin({ process: 'process/browser', @@ -132,7 +134,7 @@ const config = { new MiniCssExtractPlugin({ filename: isDevMode ? '[name].css' : '[name].[contenthash].css', chunkFilename: isDevMode ? '[id].css' : '[id].[contenthash].css', - }), + }) ], resolve: { alias: { @@ -141,7 +143,7 @@ const config = { fallback: { buffer: require.resolve('buffer/'), dns: require.resolve('browserify/lib/_empty.js'), - fs: require.resolve('browserify/lib/_empty.js'), + 'fs/promises': require.resolve('browserify/lib/_empty.js'), http: require.resolve('stream-http'), https: require.resolve('https-browserify'), net: require.resolve('browserify/lib/_empty.js'), diff --git a/_scripts/webpack.workers.config.js b/_scripts/webpack.workers.config.js deleted file mode 100644 index 65412b94d282d..0000000000000 --- a/_scripts/webpack.workers.config.js +++ /dev/null @@ -1,66 +0,0 @@ -const path = require('path') -const webpack = require('webpack') - -const { - dependencies, - devDependencies, - productName, -} = require('../package.json') - -const externals = Object.keys(dependencies).concat(Object.keys(devDependencies)) -const isDevMode = process.env.NODE_ENV === 'development' - -const config = { - name: 'workers', - mode: process.env.NODE_ENV, - devtool: isDevMode ? 'eval-cheap-module-source-map' : false, - entry: { - workerSample: path.join(__dirname, '../src/utilities/workerSample.js'), - }, - output: { - publicPath: '', - libraryTarget: 'commonjs2', - path: path.join(__dirname, '../dist'), - filename: '[name].js', - }, - externals: externals, - module: { - rules: [ - { - test: /\.js$/, - use: 'babel-loader', - exclude: /node_modules/, - }, - ], - }, - node: { - __dirname: isDevMode, - __filename: isDevMode, - global: isDevMode, - }, - plugins: [ - // new WriteFilePlugin(), - new webpack.DefinePlugin({ - 'process.env.PRODUCT_NAME': JSON.stringify(productName), - }), - ], - resolve: { - alias: { - '@': path.join(__dirname, '../src/'), - src: path.join(__dirname, '../src/'), - }, - extensions: ['.js', '.json'], - }, - target: 'node', -} - -/** - * Adjust rendererConfig for production settings - */ -if (isDevMode) { - // any dev only config -} else { - // any producation only config -} - -module.exports = config diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000000000..8f5ea8a228e3c --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,5 @@ +{ + "vueCompilerOptions": { + "target": 2.7 + } +} diff --git a/lefthook-local.yml.example b/lefthook-local.yml.example new file mode 100644 index 0000000000000..db5a35b2f9a9a --- /dev/null +++ b/lefthook-local.yml.example @@ -0,0 +1,7 @@ +# See following doc for details +# https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md#rc + +# You can choose whatever name/path you want for `~/.lefthookrc`. +# You can share it between projects where you use lefthook. +# Make sure the path is absolute. +rc: ~/.lefthookrc diff --git a/lefthook.yml b/lefthook.yml index 7f6f4e3d68548..6a09c9530afc2 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -1,4 +1,3 @@ - # Refer for explanation to following link: # https://github.com/evilmartians/lefthook/blob/master/docs/full_guide.md pre-commit: @@ -12,8 +11,6 @@ pre-commit: skip: - rebase - - # EXAMPLE USAGE # # pre-push: diff --git a/package.json b/package.json index c8279957ed289..4f4a114163b55 100644 --- a/package.json +++ b/package.json @@ -19,11 +19,10 @@ "url": "https://github.com/FreeTubeApp/FreeTube/issues" }, "scripts": { - "build:cordova": "node _scripts/cordova-build.js", - "run:cordova": "node _scripts/cordova-run.js", "build": "run-s rebuild:electron pack build-release", "build:arm64": "run-s rebuild:electron pack build-release:arm64", "build:arm32": "run-s rebuild:electron pack build-release:arm32", + "build:cordova": "node _scripts/cordova-build.js", "build-release": "node _scripts/build.js", "build-release:arm64": "node _scripts/build.js arm64", "build-release:arm32": "node _scripts/build.js arm32", @@ -33,95 +32,103 @@ "dev": "run-s rebuild:electron dev-runner", "dev:web": "node _scripts/dev-runner.js --web", "dev-runner": "node _scripts/dev-runner.js", + "lint-all": "run-p lint lint-json lint-style", "lint-fix": "eslint --fix --ext .js,.vue ./", "lint": "eslint --ext .js,.vue ./", + "lint-json": "eslint --ext .json ./", + "lint-style": "run-p lint-style:scss lint-style:css", + "lint-style:scss": "stylelint \"**/*.scss\"", + "lint-style:css": "stylelint \"**/*.css\"", + "lint-style-fix:scss": "stylelint --fix \"**/*.scss\"", + "lint-style-fix:css": "stylelint --fix \"**/*.css\"", "pack": "run-p pack:main pack:renderer", - "pack:browser": "webpack --mode=production --node-env=production --config _scripts/webpack.renderer.browser.config.js", "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": "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", "prettier": "prettier --write \"{src,_scripts}/**/*.{js,vue}\"", "rebuild:electron": "electron-builder install-app-deps", "release": "run-s test build", + "run:cordova": "cd dist/cordova && npx cordova run android", "ci": "yarn install --silent --frozen-lockfile" }, "dependencies": { - "@fortawesome/fontawesome-svg-core": "^6.2.0", - "@fortawesome/free-brands-svg-icons": "^6.2.0", - "@fortawesome/free-solid-svg-icons": "^6.2.0", - "@fortawesome/vue-fontawesome": "^2.0.8", - "@freetube/youtube-chat": "^1.1.2", + "@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", - "@freetube/yt-trending-scraper": "^3.1.1", "@silvermine/videojs-quality-selector": "^1.2.5", "autolinker": "^4.0.0", "browserify": "^17.0.0", "browserify-zlib": "^0.2.0", - "electron-context-menu": "^3.5.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", + "electron-context-menu": "^3.6.1", "lodash.debounce": "^4.0.8", - "marked": "^4.1.1", + "marked": "^4.2.12", "nedb-promises": "^6.2.1", "process": "^0.11.10", - "socks-proxy-agent": "^7.0.0", - "video.js": "7.18.1", - "videojs-contrib-quality-levels": "^2.1.0", + "video.js": "7.20.3", + "videojs-contrib-quality-levels": "^3.0.0", "videojs-http-source-selector": "^1.1.6", "videojs-mobile-ui": "^0.8.0", - "videojs-overlay": "^2.1.4", + "videojs-overlay": "^3.0.0", "videojs-vtt-thumbnails-freetube": "0.0.15", - "vue": "^2.7.13", - "vue-i18n": "^8.28.1", + "vue": "^2.7.14", + "vue-i18n": "^8.28.2", "vue-observe-visibility": "^1.0.0", "vue-router": "^3.6.5", "vuex": "^3.6.2", - "youtube-suggest": "^1.2.0", - "yt-channel-info": "^3.2.1", - "yt-dash-manifest-generator": "1.1.0", - "ytdl-core": "https://github.com/absidue/node-ytdl-core#fix-likes-extraction", - "ytpl": "^2.3.0", - "ytsr": "^3.8.0" + "youtubei.js": "^2.9.0", + "yt-channel-info": "^3.2.1" }, "devDependencies": { - "@babel/core": "^7.20.2", + "@babel/core": "^7.20.12", + "@babel/eslint-parser": "^7.19.1", "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/preset-env": "^7.18.10", - "archiver": "^5.3.1", - "babel-eslint": "^10.1.0", - "babel-loader": "^9.1.0", + "@babel/preset-env": "^7.20.2", + "@double-great/stylelint-a11y": "^2.0.2", + "babel-loader": "^9.1.2", "copy-webpack-plugin": "^11.0.0", "cordova": "^11.0.0", - "css-loader": "^6.7.2", + "css-loader": "^6.7.3", "css-minimizer-webpack-plugin": "^4.2.2", - "electron": "^21.2.3", + "electron": "^22.0.2", "electron-builder": "^23.6.0", - "eslint": "^7.32.0", - "eslint-config-prettier": "^8.3.0", - "eslint-config-standard": "^16.0.3", - "eslint-plugin-import": "^2.24.2", - "eslint-plugin-node": "^11.1.0", + "eslint": "^8.32.0", + "eslint-config-prettier": "^8.6.0", + "eslint-config-standard": "^17.0.0", + "eslint-plugin-import": "^2.27.4", + "eslint-plugin-jsonc": "^2.6.0", + "eslint-plugin-n": "^15.6.1", "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-promise": "^5.1.0", - "eslint-plugin-standard": "^5.0.0", - "eslint-plugin-vue": "^9.7.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-unicorn": "^45.0.2", + "eslint-plugin-vue": "^9.9.0", + "eslint-plugin-vuejs-accessibility": "^2.1.0", + "fs-extra": "^11.1.0", "html-webpack-plugin": "^5.3.2", "js-yaml": "^4.1.0", "json-minimizer-webpack-plugin": "^4.0.0", - "lefthook": "^1.1.4", - "mini-css-extract-plugin": "^2.7.0", + "lefthook": "^1.2.7", + "mini-css-extract-plugin": "^2.7.2", "npm-run-all": "^4.1.5", - "prettier": "^2.7.1", - "rimraf": "^3.0.2", - "sass": "^1.56.0", + "postcss": "^8.4.21", + "postcss-scss": "^4.0.6", + "prettier": "^2.8.3", + "rimraf": "^4.1.0", + "sass": "^1.57.1", "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", "tree-kill": "1.2.2", "vue-devtools": "^5.1.4", "vue-eslint-parser": "^9.1.0", "vue-loader": "^15.10.0", "webpack": "^5.75.0", - "webpack-cli": "^4.10.0", + "webpack-cli": "^5.0.1", "webpack-dev-server": "^4.11.1", "xml2js": "^0.4.23" } diff --git a/src/cordova/config.xml b/src/cordova/config.xml new file mode 100644 index 0000000000000..2419a575c2413 --- /dev/null +++ b/src/cordova/config.xml @@ -0,0 +1,22 @@ + + + FreeTube + A private YouTube client + PrestonN + + + + + + + + + + + + + + + + + diff --git a/src/cordova/config.xml.js b/src/cordova/config.xml.js new file mode 100644 index 0000000000000..bf80fe1c79e45 --- /dev/null +++ b/src/cordova/config.xml.js @@ -0,0 +1,30 @@ + +const pkg = require('../../package.json') +const fs = require('fs') +const util = require('util') +const xml2js = require('xml2js') +const parseXMLString = util.promisify(xml2js.parseString) +const createXMLStringFromObject = (obj) => { + const builder = new xml2js.Builder() + return builder.buildObject(obj) +} + +module.exports = (async () => { + const configXML = await parseXMLString((await util.promisify(fs.readFile)('./src/cordova/config.xml')).toString()) + configXML.widget.$.id = `io.freetubeapp.${pkg.name}` + const versionParts = pkg.version.split('-') + const [major, minor, patch] = versionParts[0].split('.') + let build = 0 + if (versionParts.length > 2) { + build = versionParts[2] + } + configXML.widget.$['android-versionCode'] = `${major * 10000000 + minor * 100000 + patch * 1000 + build}` + configXML.widget.$.version = pkg.version + configXML.widget.author[0].$.email = pkg.author.email + configXML.widget.author[0]._ = pkg.author.name + configXML.widget.author[0].$.href = pkg.author.url + configXML.widget.name[0] = pkg.productName + configXML.widget.description[0] = pkg.description + const configXMLString = createXMLStringFromObject(configXML) + return { string: configXMLString, object: configXML } +})() diff --git a/src/cordova/package.js b/src/cordova/package.js new file mode 100644 index 0000000000000..90ec05a73559b --- /dev/null +++ b/src/cordova/package.js @@ -0,0 +1,40 @@ + +const pkg = require('../../package.json') + +module.exports = { + name: pkg.name, + displayName: pkg.productName, + version: pkg.version, + author: pkg.author, + repository: pkg.repository, + bugs: pkg.bugs, + license: pkg.license, + description: pkg.description, + private: pkg.private, + scripts: { + restore: 'npx cordova platform add android' + }, + devDependencies: { + 'cordova-android': '^11.0.0', + 'cordova-clipboard': '^1.3.0', + 'cordova-plugin-advanced-background-mode': '^1.1.1', + 'cordova-plugin-android-permissions': '^1.1.4', + 'cordova-plugin-music-controls2': '3.0.5', + 'cordova-plugin-save-dialog': '^1.1.1', + 'cordova-plugin-theme-detection': '^1.3.0', + 'cordova-plugin-device': '^2.1.0' + }, + cordova: { + platforms: [ + 'android' + ], + plugins: { + 'cordova-plugin-android-permissions': {}, + 'cordova-plugin-music-controls2': {}, + 'cordova-clipboard': {}, + 'cordova-plugin-advanced-background-mode': {}, + 'cordova-plugin-theme-detection': {}, + 'cordova-plugin-save-dialog': {} + } + } +} diff --git a/src/datastores/handlers/base.js b/src/datastores/handlers/base.js index 1250052510cb1..285307017abf9 100644 --- a/src/datastores/handlers/base.js +++ b/src/datastores/handlers/base.js @@ -31,6 +31,14 @@ class Settings { return db.settings.findOne({ _id: 'baseTheme' }) } + static _findSidenavSettings() { + return { + hideTrendingVideos: db.settings.findOne({ _id: 'hideTrendingVideos' }), + hidePopularVideos: db.settings.findOne({ _id: 'hidePopularVideos' }), + hidePlaylists: db.settings.findOne({ _id: 'hidePlaylists' }), + } + } + static _updateBounds(value) { return db.settings.update({ _id: 'bounds' }, { _id: 'bounds', value }, { upsert: true }) } diff --git a/src/datastores/index.js b/src/datastores/index.js index 713a0ea304641..a54fd0b4ad3c0 100644 --- a/src/datastores/index.js +++ b/src/datastores/index.js @@ -2,8 +2,7 @@ import Datastore from 'nedb-promises' let dbPath = null -const isElectronMain = !!process?.versions?.electron -if (isElectronMain) { +if (process.env.IS_ELECTRON_MAIN) { const { app } = require('electron') const { join } = require('path') const userDataPath = app.getPath('userData') // This is based on the user's OS diff --git a/src/index.ejs b/src/index.ejs index 9fd3136e3ffae..312ee4b4177ad 100644 --- a/src/index.ejs +++ b/src/index.ejs @@ -5,7 +5,10 @@ - + <% if (!process.env.IS_ELECTRON) { %> + + + <% } %> <% if (htmlWebpackPlugin.options.nodeModules) { %> + <% } %> + <% if (process.env.IS_CORDOVA) { %> + + <% } %> diff --git a/src/main/ImageCache.js b/src/main/ImageCache.js index 4a635b7d92e9f..8228f38dc63ed 100644 --- a/src/main/ImageCache.js +++ b/src/main/ImageCache.js @@ -51,7 +51,7 @@ export class ImageCache { * @returns a timestamp in seconds */ export function extractExpiryTimestamp(headers) { - const maxAgeRegex = /max-age=([0-9]+)/ + const maxAgeRegex = /max-age=(\d+)/ const cacheControl = headers['cache-control'] if (cacheControl && maxAgeRegex.test(cacheControl)) { diff --git a/src/main/index.js b/src/main/index.js index 15a38879d6b21..6bdff568c4709 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -1,6 +1,7 @@ import { app, BrowserWindow, dialog, Menu, ipcMain, - powerSaveBlocker, screen, session, shell, nativeTheme, net, protocol + powerSaveBlocker, screen, session, shell, + nativeTheme, net, protocol, clipboard } from 'electron' import path from 'path' import cp from 'child_process' @@ -21,6 +22,8 @@ function runApp() { showSearchWithGoogle: false, showSaveImageAs: true, showCopyImageAddress: true, + showSelectAll: false, + showCopyLink: false, prepend: (defaultActions, parameters, browserWindow) => [ { label: 'Show / Hide Video Statistics', @@ -36,8 +39,122 @@ function runApp() { click: () => { createWindow({ replaceMainWindow: false, windowStartupUrl: parameters.linkURL, showWindowNow: true }) } + }, + // Only show select all in text fields + { + label: 'Select All', + enabled: parameters.editFlags.canSelectAll, + visible: parameters.isEditable, + click: () => { + browserWindow.webContents.selectAll() + } } - ] + ], + // only show the copy link entry for external links and the /playlist, /channel and /watch in-app URLs + // the /playlist, /channel and /watch in-app URLs get transformed to their equivalent YouTube or Invidious URLs + append: (defaultActions, parameters, browserWindow) => { + let visible = false + const urlParts = parameters.linkURL.split('#') + const isInAppUrl = urlParts[0] === browserWindow.webContents.getURL().split('#')[0] + + if (parameters.linkURL.length > 0) { + if (isInAppUrl) { + const path = urlParts[1] + + if (path) { + visible = ['/playlist', '/channel', '/watch'].some(p => path.startsWith(p)) + } + } else { + visible = true + } + } + + const copy = (url) => { + if (parameters.linkText) { + clipboard.write({ + bookmark: parameters.linkText, + text: url + }) + } else { + clipboard.writeText(url) + } + } + + const transformURL = (toYouTube) => { + let origin + + if (toYouTube) { + origin = 'https://www.youtube.com' + } else { + origin = 'https://redirect.invidious.io' + } + + const [path, query] = urlParts[1].split('?') + const [route, id] = path.split('/').filter(p => p) + + switch (route) { + case 'playlist': + return `${origin}/playlist?list=${id}` + case 'channel': + return `${origin}/channel/${id}` + case 'watch': { + let url + + if (toYouTube) { + url = `https://youtu.be/${id}` + } else { + url = `https://redirect.invidious.io/watch?v=${id}` + } + + if (query) { + const params = new URLSearchParams(query) + const newParams = new URLSearchParams() + let hasParams = false + + if (params.has('playlistId')) { + newParams.set('list', params.get('playlistId')) + hasParams = true + } + + if (params.has('timestamp')) { + newParams.set('t', params.get('timestamp')) + hasParams = true + } + + if (hasParams) { + url += '?' + newParams.toString() + } + } + + return url + } + } + } + + return [ + { + label: 'Copy Lin&k', + visible: visible && !isInAppUrl, + click: () => { + copy(parameters.linkURL) + } + }, + { + label: 'Copy YouTube Link', + visible: visible && isInAppUrl, + click: () => { + copy(transformURL(true)) + } + }, + { + label: 'Copy Invidious Link', + visible: visible && isInAppUrl, + click: () => { + copy(transformURL(false)) + } + } + ] + } }) // disable electron warning @@ -148,9 +265,7 @@ function runApp() { // Set CONSENT cookie on reasonable domains const consentCookieDomains = [ - 'http://www.youtube.com', 'https://www.youtube.com', - 'http://youtube.com', 'https://youtube.com' ] consentCookieDomains.forEach(url => { @@ -162,6 +277,16 @@ function runApp() { }) }) + // make InnerTube requests work with the fetch function + // InnerTube rejects requests if the referer isn't YouTube or empty + const innertubeRequestFilter = { urls: ['https://www.youtube.com/youtubei/*'] } + + session.defaultSession.webRequest.onBeforeSendHeaders(innertubeRequestFilter, ({ requestHeaders }, callback) => { + requestHeaders.referer = 'https://www.youtube.com' + // eslint-disable-next-line n/no-callback-literal + callback({ requestHeaders }) + }) + if (replaceHttpCache) { // in-memory image cache @@ -173,7 +298,7 @@ function runApp() { if (imageCache.has(url)) { const cached = imageCache.get(url) - // eslint-disable-next-line node/no-callback-literal + // eslint-disable-next-line n/no-callback-literal callback({ mimeType: cached.mimeType, data: cached.data @@ -211,7 +336,7 @@ function runApp() { imageCache.add(url, mimeType, data, expiryTimestamp) - // eslint-disable-next-line node/no-callback-literal + // eslint-disable-next-line n/no-callback-literal callback({ mimeType, data: data @@ -239,7 +364,7 @@ function runApp() { return value }) - // eslint-disable-next-line node/no-callback-literal + // eslint-disable-next-line n/no-callback-literal callback({ statusCode: response.statusCode ?? 400, mimeType: 'application/json', @@ -248,6 +373,10 @@ function runApp() { }) }) + newRequest.on('error', (err) => { + console.error(err) + }) + newRequest.end() }) @@ -256,12 +385,12 @@ function runApp() { // the requests made by the imagecache:// handler to fetch the image, // are allowed through, as their resourceType is 'other' if (details.resourceType === 'image') { - // eslint-disable-next-line node/no-callback-literal + // eslint-disable-next-line n/no-callback-literal callback({ redirectURL: `imagecache://${encodeURIComponent(details.url)}` }) } else { - // eslint-disable-next-line node/no-callback-literal + // eslint-disable-next-line n/no-callback-literal callback({}) } }) @@ -328,7 +457,7 @@ function runApp() { darkTheme: nativeTheme.shouldUseDarkColors, icon: process.env.NODE_ENV === 'development' ? path.join(__dirname, '../../_icons/iconColor.png') - /* eslint-disable-next-line */ + /* eslint-disable-next-line n/no-path-concat */ : `${__dirname}/_icons/iconColor.png`, autoHideMenuBar: true, // useContentSize: true, @@ -419,7 +548,7 @@ function runApp() { if (windowStartupUrl != null) { newWindow.loadURL(windowStartupUrl) } else { - /* eslint-disable-next-line */ + /* eslint-disable-next-line n/no-path-concat */ newWindow.loadFile(`${__dirname}/index.html`) } } @@ -611,6 +740,17 @@ function runApp() { event, { event: SyncEvents.GENERAL.UPSERT, data } ) + switch (data._id) { + // Update app menu on related setting update + case 'hideTrendingVideos': + case 'hidePopularVideos': + case 'hidePlaylists': + await setMenu() + break + + default: + // Do nothing for unmatched settings + } return null default: @@ -888,7 +1028,7 @@ function runApp() { } } - /** + /* * Auto Updater * * Uncomment the following code below and install `electron-updater` to @@ -907,12 +1047,23 @@ function runApp() { }) */ - /* eslint-disable-next-line */ - const sendMenuEvent = async data => { - mainWindow.webContents.send('change-view', data) + function navigateTo(path, browserWindow) { + if (browserWindow == null) { + return + } + + browserWindow.webContents.send( + 'change-view', + { route: path } + ) } - function setMenu() { + async function setMenu() { + const sidenavSettings = baseHandlers.settings._findSidenavSettings() + const hideTrendingVideos = (await sidenavSettings.hideTrendingVideos)?.value + const hidePopularVideos = (await sidenavSettings.hidePopularVideos)?.value + const hidePlaylists = (await sidenavSettings.hidePlaylists)?.value + const template = [ { label: 'File', @@ -933,12 +1084,7 @@ function runApp() { label: 'Preferences', accelerator: 'CmdOrCtrl+,', click: (_menuItem, browserWindow, _event) => { - if (browserWindow == null) { return } - - browserWindow.webContents.send( - 'change-view', - { route: '/settings' } - ) + navigateTo('/settings', browserWindow) }, type: 'normal' }, @@ -1001,22 +1147,81 @@ function runApp() { { role: 'togglefullscreen' }, { type: 'separator' }, { - label: 'History', - // MacOS: Command + Y - // Other OS: Ctrl + H - accelerator: process.platform === 'darwin' ? 'Cmd+Y' : 'Ctrl+H', + label: 'Back', + accelerator: 'Alt+Left', click: (_menuItem, browserWindow, _event) => { if (browserWindow == null) { return } browserWindow.webContents.send( - 'change-view', - { route: '/history' } + 'history-back', ) }, - type: 'normal' + type: 'normal', + }, + { + label: 'Forward', + accelerator: 'Alt+Right', + click: (_menuItem, browserWindow, _event) => { + if (browserWindow == null) { return } + + browserWindow.webContents.send( + 'history-forward', + ) + }, + type: 'normal', }, ] }, + { + label: 'Navigate', + submenu: [ + { + label: 'Subscriptions', + click: (_menuItem, browserWindow, _event) => { + navigateTo('/subscriptions', browserWindow) + }, + type: 'normal' + }, + { + label: 'Channels', + click: (_menuItem, browserWindow, _event) => { + navigateTo('/subscribedchannels', browserWindow) + }, + type: 'normal' + }, + !hideTrendingVideos && { + label: 'Trending', + click: (_menuItem, browserWindow, _event) => { + navigateTo('/trending', browserWindow) + }, + type: 'normal' + }, + !hidePopularVideos && { + label: 'Most Popular', + click: (_menuItem, browserWindow, _event) => { + navigateTo('/popular', browserWindow) + }, + type: 'normal' + }, + !hidePlaylists && { + label: 'Playlists', + click: (_menuItem, browserWindow, _event) => { + navigateTo('/userplaylists', browserWindow) + }, + type: 'normal' + }, + { + label: 'History', + // MacOS: Command + Y + // Other OS: Ctrl + H + accelerator: process.platform === 'darwin' ? 'Cmd+Y' : 'Ctrl+H', + click: (_menuItem, browserWindow, _event) => { + navigateTo('/history', browserWindow) + }, + type: 'normal' + }, + ].filter((v) => v !== false), + }, { role: 'window', submenu: [ diff --git a/src/renderer/App.js b/src/renderer/App.js index da24cab6e4ac4..c05bf89cef89a 100644 --- a/src/renderer/App.js +++ b/src/renderer/App.js @@ -1,4 +1,4 @@ -import Vue from 'vue' +import Vue, { defineComponent } from 'vue' import { mapActions, mapMutations } from 'vuex' import { ObserveVisibility } from 'vue-observe-visibility' import FtFlexBox from './components/ft-flex-box/ft-flex-box.vue' @@ -13,12 +13,13 @@ import { marked } from 'marked' import { IpcChannels } from '../constants' import packageDetails from '../../package.json' import { openExternalLink, openInternalPath, showToast } from './helpers/utils' +import cordova from 'cordova' let ipcRenderer = null Vue.directive('observe-visibility', ObserveVisibility) -export default Vue.extend({ +export default defineComponent({ name: 'App', components: { FtFlexBox, @@ -76,10 +77,10 @@ export default Vue.extend({ if (this.$route.meta.title !== 'Channel' && this.$route.meta.title !== 'Watch') { let title = this.$route.meta.path === '/home' - ? process.env.PRODUCT_NAME - : `${this.$t(this.$route.meta.title)} - ${process.env.PRODUCT_NAME}` + ? packageDetails.productName + : `${this.$t(this.$route.meta.title)} - ${packageDetails.productName}` if (!title) { - title = process.env.PRODUCT_NAME + title = packageDetails.productName } return title } else { @@ -143,6 +144,17 @@ export default Vue.extend({ this.setWindowTitle() }, 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() + }) + } this.grabUserSettings().then(async () => { this.checkThemeSettings() @@ -154,20 +166,30 @@ export default Vue.extend({ 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 + this.watchSystemTheme() + } + }) if (process.env.IS_ELECTRON) { ipcRenderer = require('electron').ipcRenderer this.setupListenersToSyncWindows() this.activateKeyboardShortcuts() + this.activateIPCListeners() this.openAllLinksExternally() this.enableSetSearchQueryText() this.enableOpenUrl() - this.watchSystemTheme() await this.checkExternalPlayer() } this.dataReady = true - + setTimeout(() => { + if (process.env.IS_CORDOVA) { + // hide the splashscreen + navigator.splashscreen.hide() + } + }, 250) setTimeout(() => { this.checkForNewUpdates() this.checkForNewBlogPosts() @@ -204,13 +226,22 @@ export default Vue.extend({ .then((response) => response.json()) .then((json) => { const tagName = json[0].tag_name - const versionNumber = tagName.split('-nightly-')[1] + const tagNameParts = tagName.split('.') + let versionNumber = tagNameParts[tagNameParts.length - 1] + // if the tag is a nightly release + if (tagName.indexOf('-nightly-') !== -1) { + versionNumber = tagName.split('-nightly-')[1] + } this.updateChangelog = marked.parse(json[0].body) this.changeLogTitle = json[0].name const message = this.$t('Version $ is now available! Click for more details') this.updateBannerMessage = message.replace('$', versionNumber) - const appVersion = packageDetails.version.split('-nightly-')[1] + const versionParts = packageDetails.version.split('.') + let appVersion = versionParts[versionParts.length - 1] + if (appVersion.indexOf('-nightly-') !== -1) { + appVersion = appVersion.split('-nightly-')[1] + } if (parseInt(versionNumber) > parseInt(appVersion)) { this.showUpdatesBanner = true } @@ -288,15 +319,19 @@ export default Vue.extend({ }) }, + 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) { - case 'ArrowRight': - this.$refs.topNav.historyForward() - break - case 'ArrowLeft': - this.$refs.topNav.historyBack() - break case 'D': case 'd': this.$refs.topNav.focusSearch() @@ -453,9 +488,41 @@ export default Vue.extend({ * all systems running the electron app. */ watchSystemTheme: function () { - ipcRenderer.on(IpcChannels.NATIVE_THEME_UPDATE, (event, shouldUseDarkColors) => { - document.body.dataset.systemTheme = shouldUseDarkColors ? 'dark' : 'light' - }) + if (process.env.IS_ELECTRON) { + ipcRenderer.on(IpcChannels.NATIVE_THEME_UPDATE, (event, shouldUseDarkColors) => { + 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) + } + }, + + openInternalPath: function({ path, doCreateNewWindow, query = {}, searchQueryText = null }) { + if (process.env.IS_ELECTRON && doCreateNewWindow) { + const { ipcRenderer } = require('electron') + + // Combine current document path and new "hash" as new window startup URL + const newWindowStartupURL = [ + window.location.href.split('#')[0], + `#${path}?${(new URLSearchParams(query)).toString()}` + ].join('') + ipcRenderer.send(IpcChannels.CREATE_NEW_WINDOW, { + windowStartupUrl: newWindowStartupURL, + searchQueryText + }) + } else { + // Web + this.$router.push({ + path, + query + }) + } }, enableSetSearchQueryText: function () { diff --git a/src/renderer/App.vue b/src/renderer/App.vue index a1a3f925fb38c..b4d7faa4ef45d 100644 --- a/src/renderer/App.vue +++ b/src/renderer/App.vue @@ -20,12 +20,14 @@ v-if="showUpdatesBanner" class="banner" :message="updateBannerMessage" + role="link" @click="handleUpdateBannerClick" />