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 = `