diff --git a/.babelrc b/.babelrc index c13cff889347b..5b381b9ea33a8 100644 --- a/.babelrc +++ b/.babelrc @@ -4,7 +4,7 @@ "@babel/env", { "targets": { - "chrome": "122", + "chrome": "130", "node": "20.9.0" } } diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 65a5c0f0ce04e..9819ec7de35f8 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,10 +1,10 @@ blank_issues_enabled: false contact_links: - name: Discussions - url: https://github.com/FreeTubeApp/FreeTube/discussions/categories/general + url: https://github.com/FreeTubeApp/FreeTube/discussions/categories/general about: View discussions or start one yourself - name: Questions - url: https://github.com/FreeTubeApp/FreeTube/discussions/categories/q-a + url: https://github.com/FreeTubeApp/FreeTube/discussions/categories/q-a about: Ask and answer questions - name: Matrix Community url: https://matrix.to/#/+freetube:matrix.org diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 6b83338919247..0bbf74c851074 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -34,7 +34,7 @@ body: - type: textarea attributes: label: Alternatives Considered - description: A clear and concise description of any alternative solutions or features you've considered. + description: A clear and concise description of any alternative solutions or features you've considered. validations: required: true - type: dropdown diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 34f5ce3ee5439..248241a93065d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -20,6 +20,7 @@ updates: - "@eslint/*" - "yaml-eslint-parser" - "vue-eslint-parser" + - "neostandard" stylelint: patterns: - "stylelint" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f726d5aaff36a..5d34902f607ac 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,9 +20,7 @@ jobs: - win-x64 - win-arm64 - osx-x64 - # `osx-arm64` disabled due to "macOS gatekeeper" - # See details in https://github.com/FreeTubeApp/FreeTube/pull/2113 - # - osx-arm64 + - osx-arm64 include: - runtime: linux-x64 os: ubuntu-latest @@ -36,8 +34,8 @@ jobs: - runtime: osx-x64 os: macOS-latest -# - runtime: osx-arm64 -# os: macOS-latest + - runtime: osx-arm64 + os: macOS-latest - runtime: win-x64 os: windows-latest @@ -86,10 +84,9 @@ jobs: with: version: ${{ steps.versionNumber.outputs.result }} - - name: Install libarchive-tools if: startsWith(matrix.os, 'ubuntu') - run: sudo apt -y install libarchive-tools; echo "Version Number ${{ toJson(job) }} ${{ toJson(needs) }}" + run: sudo apt update; sudo apt -y install libarchive-tools; echo "Version Number ${{ toJson(job) }} ${{ toJson(needs) }}" - name: Build x64 with Node.js ${{ matrix.node-version}} if: contains(matrix.runtime, 'x64') @@ -103,6 +100,19 @@ jobs: if: contains(matrix.runtime, 'arm64') run: yarn run build:arm64 + - name: Convert X64 AppImage to static runtime + if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64') + run: | + sudo apt install desktop-file-utils + cd build + appimage="FreeTube-${{ steps.versionNumber.outputs.result }}.AppImage" + wget "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage" -O ./appimagetool.AppImage + chmod +x ./"$appimage" ./appimagetool.AppImage + ./"$appimage" --appimage-extract && rm -f ./"$appimage" + ./appimagetool.AppImage --comp zstd --mksquashfs-opt -Xcompression-level --mksquashfs-opt 20 \ + -n ./squashfs-root ./"$appimage" + rm -rf ./squashfs-root ./appimagetool.AppImage + - name: Upload Linux .zip x64 Artifact uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64') @@ -301,12 +311,12 @@ jobs: name: freetube-${{ steps.versionNumber.outputs.result }}-mac-x64.dmg path: build/freetube-${{ steps.versionNumber.outputs.result }}.dmg -# - name: Upload Mac arm64 .dmg Artifact -# uses: actions/upload-artifact@v4 -# if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-arm64') -# with: -# name: freetube-${{ steps.versionNumber.outputs.result }}-mac-arm64.dmg -# path: build/freetube-${{ steps.versionNumber.outputs.result }}-arm64.dmg + - name: Upload Mac arm64 .dmg Artifact + uses: actions/upload-artifact@v4 + if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-arm64') + with: + name: freetube-${{ steps.versionNumber.outputs.result }}-mac-arm64.dmg + path: build/freetube-${{ steps.versionNumber.outputs.result }}-arm64.dmg - name: Upload Mac x64 .zip Artifact uses: actions/upload-artifact@v4 @@ -322,9 +332,16 @@ jobs: name: freetube-${{ steps.versionNumber.outputs.result }}-mac-x64.7z path: build/freetube-${{ steps.versionNumber.outputs.result }}-mac.7z -# - name: Upload Mac arm64 .zip Artifact -# uses: actions/upload-artifact@v4 -# if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-arm64') -# with: -# name: freetube-${{ steps.versionNumber.outputs.result }}-mac-arm64.zip -# path: build/freetube-${{ steps.versionNumber.outputs.result }}-arm64-mac.zip + - name: Upload Mac arm64 .zip Artifact + uses: actions/upload-artifact@v4 + if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-arm64') + with: + name: freetube-${{ steps.versionNumber.outputs.result }}-mac-arm64.zip + path: build/freetube-${{ steps.versionNumber.outputs.result }}-arm64-mac.zip + + - name: Upload Mac arm64 .7z Artifact + uses: actions/upload-artifact@v4 + if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-arm64') + with: + name: freetube-${{ steps.versionNumber.outputs.result }}-mac-arm64.7z + path: build/freetube-${{ steps.versionNumber.outputs.result }}-arm64-mac.7z diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 521082d630464..9e9ce6803c6dd 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -37,11 +37,10 @@ jobs: # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. - + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild @@ -50,7 +49,7 @@ jobs: # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - # If the Autobuild fails above, remove it and uncomment the following three lines. + # If the Autobuild fails above, remove it and uncomment the following three lines. # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. # - run: | diff --git a/.github/workflows/conflicts.yml b/.github/workflows/conflicts.yml index b65794cbb1647..3cb403dc42ed9 100644 --- a/.github/workflows/conflicts.yml +++ b/.github/workflows/conflicts.yml @@ -20,4 +20,3 @@ 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/flatpak.yml b/.github/workflows/flatpak.yml index abee92197ac42..217f4c926cc43 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -77,22 +77,22 @@ jobs: date +"%Y-%m-%d" >> $GITHUB_ENV echo 'EOF' >> $GITHUB_ENV - name: Update x64 File Location in yml File - uses: mikefarah/yq@v4.44.3 + uses: mikefarah/yq@v4.44.6 with: # The Command which should be run cmd: yq -i '.modules[0].sources[0].url = "https://github.com/FreeTubeApp/FreeTube/releases/download/v${{ steps.sub.outputs.result }}-beta/freetube-${{ steps.sub.outputs.result }}-linux-portable-x64.zip"' io.freetubeapp.FreeTube.yml - name: Update x64 Hash in yml File - uses: mikefarah/yq@v4.44.3 + uses: mikefarah/yq@v4.44.6 with: # The Command which should be run cmd: yq -i '.modules[0].sources[0].sha256 = "${{ env.HASH_X64 }}"' io.freetubeapp.FreeTube.yml - name: Update ARM File Location in yml File - uses: mikefarah/yq@v4.44.3 + uses: mikefarah/yq@v4.44.6 with: # The Command which should be run cmd: yq -i '.modules[0].sources[1].url = "https://github.com/FreeTubeApp/FreeTube/releases/download/v${{ steps.sub.outputs.result }}-beta/freetube-${{ steps.sub.outputs.result }}-linux-portable-arm64.zip"' io.freetubeapp.FreeTube.yml - name: Update ARM Hash in yml File - uses: mikefarah/yq@v4.44.3 + uses: mikefarah/yq@v4.44.6 with: # The Command which should be run cmd: yq -i '.modules[0].sources[1].sha256 = "${{ env.HASH_ARM64 }}"' io.freetubeapp.FreeTube.yml diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 962a5f55de4c6..e0ec42ea8ed80 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -6,7 +6,7 @@ name: Linter # events but only for the master branch on: pull_request: - branches: [ master, development ] + branches: [ master, development, '**-RC' ] # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: diff --git a/.github/workflows/no-response.yml b/.github/workflows/no-response.yml index a0a8bf8fc13a8..b430c13ed588d 100644 --- a/.github/workflows/no-response.yml +++ b/.github/workflows/no-response.yml @@ -1,24 +1,24 @@ -name: No Response - -# Both `issue_comment` and `scheduled` event types are required for this Action -# to work properly. -on: - issue_comment: - types: [created] - schedule: - # Run daily at midnight. - - cron: '0 0 * * *' - -jobs: - noResponse: - runs-on: ubuntu-latest - steps: - - uses: lee-dohm/no-response@v0.5.0 - with: - token: ${{ github.token }} - closeComment: > - This issue has been automatically closed because there has been no response to our request for more information from the original author. - With only the information that is currently in the issue, we don't have enough information to take action. - Please reach out if you have or find the answers we need so that we can investigate further. - daysUntilClose: 14 - responseRequiredLabel: "U: Waiting for Response from Author" +name: No Response + +# Both `issue_comment` and `scheduled` event types are required for this Action +# to work properly. +on: + issue_comment: + types: [created] + schedule: + # Run daily at midnight. + - cron: '0 0 * * *' + +jobs: + noResponse: + runs-on: ubuntu-latest + steps: + - uses: lee-dohm/no-response@v0.5.0 + with: + token: ${{ github.token }} + closeComment: > + This issue has been automatically closed because there has been no response to our request for more information from the original author. + With only the information that is currently in the issue, we don't have enough information to take action. + Please reach out if you have or find the answers we need so that we can investigate further. + daysUntilClose: 7 + responseRequiredLabel: "U: Waiting for Response from Author" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eee0f8a462700..9ec4bdab83db0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,9 +20,7 @@ jobs: - win-x64 - win-arm64 - osx-x64 - # `osx-arm64` disabled due to "macOS gatekeeper" - # See details in https://github.com/FreeTubeApp/FreeTube/pull/2113 - # - osx-arm64 + - osx-arm64 include: - runtime: linux-x64 os: ubuntu-latest @@ -36,8 +34,8 @@ jobs: - runtime: osx-x64 os: macOS-latest -# - runtime: osx-arm64 -# os: macOS-latest + - runtime: osx-arm64 + os: macOS-latest - runtime: win-x64 os: windows-latest @@ -73,6 +71,19 @@ jobs: if: contains(matrix.runtime, 'arm64') run: yarn run build:arm64 + - name: Convert X64 AppImage to static runtime + if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64') + run: | + sudo apt install desktop-file-utils + cd build + appimage="FreeTube-${{ steps.versionNumber.outputs.result }}.AppImage" + wget "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage" -O ./appimagetool.AppImage + chmod +x ./"$appimage" ./appimagetool.AppImage + ./"$appimage" --appimage-extract && rm -f ./"$appimage" + ./appimagetool.AppImage --comp zstd --mksquashfs-opt -Xcompression-level --mksquashfs-opt 20 \ + -n ./squashfs-root ./"$appimage" + rm -rf ./squashfs-root ./appimagetool.AppImage + - name: Upload AppImage x64 Release uses: actions/upload-release-asset@v1 if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64') @@ -84,6 +95,28 @@ jobs: asset_path: build/FreeTube-${{ steps.getPackageInfo.outputs.version }}.AppImage asset_content_type: application/vnd.appimage + - name: Upload AppImage ARMv7l Release + uses: actions/upload-release-asset@v1 + if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l') + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ secrets.UPLOAD_ID }}/assets{?name,label} + asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-armv7l.AppImage + asset_path: build/FreeTube-${{ steps.getPackageInfo.outputs.version }}-armv7l.AppImage + asset_content_type: application/vnd.appimage + + - name: Upload AppImage ARM64 Release + uses: actions/upload-release-asset@v1 + if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64') + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ secrets.UPLOAD_ID }}/assets{?name,label} + asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-arm64.AppImage + asset_path: build/FreeTube-${{ steps.getPackageInfo.outputs.version }}-arm64.AppImage + asset_content_type: application/vnd.appimage + - name: Upload Linux .zip x64 Release uses: actions/upload-release-asset@v1 if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64') @@ -306,16 +339,16 @@ jobs: asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}.dmg asset_content_type: application/x-apple-diskimage -# - name: Upload Mac arm64 .dmg Release -# uses: actions/upload-release-asset@v1 -# if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-arm64') -# env: -# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -# with: -# upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ secrets.UPLOAD_ID }}/assets{?name,label} -# asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-mac-arm64.dmg -# asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}-arm64.dmg -# asset_content_type: application/x-apple-diskimage + - name: Upload Mac arm64 .dmg Release + uses: actions/upload-release-asset@v1 + if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-arm64') + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ secrets.UPLOAD_ID }}/assets{?name,label} + asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-mac-arm64.dmg + asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}-arm64.dmg + asset_content_type: application/x-apple-diskimage - name: Upload Mac x64 .zip Release uses: actions/upload-release-asset@v1 @@ -339,14 +372,68 @@ jobs: asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}-mac.7z asset_content_type: application/x-7z-compressed -# - name: Upload Mac arm64 .zip Release -# uses: actions/upload-release-asset@v1 -# if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-arm64') -# env: -# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -# with: -# upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ secrets.UPLOAD_ID }}/assets{?name,label} -# asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-mac-arm64.zip -# asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}-arm64-mac.zip -# asset_content_type: application/x-apple-diskimage + - name: Upload Mac arm64 .zip Release + uses: actions/upload-release-asset@v1 + if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-arm64') + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ secrets.UPLOAD_ID }}/assets{?name,label} + asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-mac-arm64.zip + asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}-arm64-mac.zip + asset_content_type: application/x-apple-diskimage + + - name: Upload Mac arm64 .7z Release + uses: actions/upload-release-asset@v1 + if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-arm64') + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ secrets.UPLOAD_ID }}/assets{?name,label} + asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-mac-arm64.7z + asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}-arm64-mac.7z + asset_content_type: application/x-7z-compressed + + - name: Upload Alpine .apk x64 Release + uses: actions/upload-release-asset@v1 + if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64') + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ secrets.UPLOAD_ID }}/assets{?name,label} + asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-alpine-amd64.apk + asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}.apk + asset_content_type: application/octet-stream + + - name: Upload Alpine .apk ARMv7l Release + uses: actions/upload-release-asset@v1 + if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l') + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ secrets.UPLOAD_ID }}/assets{?name,label} + asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-alpine-armv7l.apk + asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}-armv7l.apk + asset_content_type: application/octet-stream + + - name: Upload Alpine .apk ARM64 Release + uses: actions/upload-release-asset@v1 + if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64') + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ secrets.UPLOAD_ID }}/assets{?name,label} + asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-alpine-arm64.apk + asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}-arm64.apk + asset_content_type: application/octet-stream + - name: Upload Pacman .pacman x64 Release + uses: actions/upload-release-asset@v1 + if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64') + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ secrets.UPLOAD_ID }}/assets{?name,label} + asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-amd64.pacman + asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}.pacman + asset_content_type: application/x-zstd-compressed-tar diff --git a/.github/workflows/remove-outdated-labels.yml b/.github/workflows/remove-outdated-labels.yml index 8ea7ee1d48d40..1341ad028821b 100644 --- a/.github/workflows/remove-outdated-labels.yml +++ b/.github/workflows/remove-outdated-labels.yml @@ -1,61 +1,61 @@ -name: Remove outdated labels -on: - pull_request_target: - types: - - closed - - converted_to_draft - - ready_for_review -jobs: - remove-merged-pr-labels: - name: Remove merged pull request labels - if: github.event.pull_request.merged - runs-on: ubuntu-latest - steps: - - uses: mondeja/remove-labels-gh-action@v2.0.0 - with: - token: ${{ secrets.GITHUB_TOKEN }} - labels: | - PR: waiting for review - PR: WIP - PR: changes requested - PR: merge conflicts / rebase needed - PR/Issue: dependent - PR: stale - - remove-closed-pr-labels: - name: Remove closed pull request labels - if: github.event_name == 'pull_request_target' && (! github.event.pull_request.merged) && (github.event.action != 'converted_to_draft') && (github.event.action != 'ready_for_review') - runs-on: ubuntu-latest - steps: - - uses: mondeja/remove-labels-gh-action@v2.0.0 - with: - token: ${{ secrets.GITHUB_TOKEN }} - labels: | - PR: waiting for review - PR: WIP - PR: changes requested - PR: merge conflicts / rebase needed - PR/Issue: dependent - PR: stale - - remove-draft-pr-labels: - name: Remove labels from draft pull requests - if: github.event_name == 'pull_request_target' && github.event.action == 'converted_to_draft' - runs-on: ubuntu-latest - steps: - - uses: mondeja/remove-labels-gh-action@v2.0.0 - with: - token: ${{ secrets.GITHUB_TOKEN }} - labels: | - PR: waiting for review - - remove-ready-pr-labels: - name: Remove labels when draft pr is marked ready for review - if: github.event_name == 'pull_request_target' && github.event.action == 'ready_for_review' - runs-on: ubuntu-latest - steps: - - uses: mondeja/remove-labels-gh-action@v2.0.0 - with: - token: ${{ secrets.GITHUB_TOKEN }} - labels: | - PR: WIP +name: Remove outdated labels +on: + pull_request_target: + types: + - closed + - converted_to_draft + - ready_for_review +jobs: + remove-merged-pr-labels: + name: Remove merged pull request labels + if: github.event.pull_request.merged + runs-on: ubuntu-latest + steps: + - uses: mondeja/remove-labels-gh-action@v2.0.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} + labels: | + PR: waiting for review + PR: WIP + PR: changes requested + PR: merge conflicts / rebase needed + PR/Issue: dependent + PR: stale + + remove-closed-pr-labels: + name: Remove closed pull request labels + if: github.event_name == 'pull_request_target' && (! github.event.pull_request.merged) && (github.event.action != 'converted_to_draft') && (github.event.action != 'ready_for_review') + runs-on: ubuntu-latest + steps: + - uses: mondeja/remove-labels-gh-action@v2.0.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} + labels: | + PR: waiting for review + PR: WIP + PR: changes requested + PR: merge conflicts / rebase needed + PR/Issue: dependent + PR: stale + + remove-draft-pr-labels: + name: Remove labels from draft pull requests + if: github.event_name == 'pull_request_target' && github.event.action == 'converted_to_draft' + runs-on: ubuntu-latest + steps: + - uses: mondeja/remove-labels-gh-action@v2.0.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} + labels: | + PR: waiting for review + + remove-ready-pr-labels: + name: Remove labels when draft pr is marked ready for review + if: github.event_name == 'pull_request_target' && github.event.action == 'ready_for_review' + runs-on: ubuntu-latest + steps: + - uses: mondeja/remove-labels-gh-action@v2.0.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} + labels: | + PR: WIP diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 6945419318d9a..354408edee5ef 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -14,14 +14,14 @@ jobs: - uses: actions/stale@v9 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.' + stale-pr-message: 'This PR is stale because it has been open 14 days with no activity. Remove stale label or comment or this will be closed in 14 days.' close-issue-message: 'This issue was closed because it has been stalled for 7 days with no activity.' close-pr-message: 'This PR was closed because it has been stalled for 14 days with no activity.' days-before-issue-stale: 28 - days-before-pr-stale: 28 + days-before-pr-stale: 14 days-before-issue-close: 7 days-before-pr-close: 14 stale-issue-label: 'U: stale' stale-pr-label: 'PR: stale' exempt-pr-labels: 'PR: WIP' - exempt-issue-labels: 'enhancement' + exempt-issue-labels: 'enhancement, U: reproduced' diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index b96994ddd09ab..0000000000000 --- a/.prettierrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "semi": false, - "singleQuote": true, - "tabWidth": 2, - "trailingComma": false, - "useTabs": false -} diff --git a/README.md b/README.md index 5a42b6c49d877..839d78b3fba4f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ FreeTube is an open source desktop YouTube player built with privacy in mind. Use YouTube without advertisements and prevent Google from tracking you with their cookies and JavaScript. -Available for Windows (10 and later), Mac (macOS 10.15 and later) & Linux thanks to Electron. +Available for Windows (10 and later), Mac (macOS 11 and later) & Linux thanks to Electron.

Download FreeTube

@@ -59,20 +59,25 @@ longer track you using cookies or JavaScript. Your subscriptions and history are * Option to show only family friendly content * Show/hide functionality or elements within the app using the distraction free settings * View channel community posts -* View most age restricted videos -### Browser Extension -FreeTube is supported by the [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect) and [LibRedirect](https://github.com/libredirect/libredirect) extensions, which will allow you to open YouTube links into FreeTube. +### Browser Extensions +The following extensions open YouTube links directly in FreeTube: -> [!IMPORTANT] -> You must enable the option within the advanced settings of the extension for it to work. +- [LibRedirect](https://libredirect.github.io/) +- [RedirectTube](https://github.com/MStankiewiczOfficial/RedirectTube) + +LibRedirect automatically redirect YouTube links to FreeTube. +> [!IMPORTANT] +> To ensure proper functionality, select FreeTube as Frontend in the Services settings of the extension. + +RedirectTube, doesn’t automatically open YouTube links in FreeTube. Instead, it adds buttons to the toolbar and context menu, which you can click to open videos in FreeTube manually. -* Download Privacy Redirect for [Firefox](https://addons.mozilla.org/en-US/firefox/addon/privacy-redirect/) or [Google Chrome](https://chrome.google.com/webstore/detail/privacy-redirect/pmcmeagblkinmogikoikkdjiligflglb). +- Download LibRedirect from [Mozilla Add-ons](https://addons.mozilla.org/firefox/addon/libredirect/) (for Firefox based-browsers) or [developer's website](https://libredirect.github.io/download_chromium.html) (for Chrome and Chromium-based browsers). -* Download LibRedirect for [Firefox](https://addons.mozilla.org/firefox/addon/libredirect/) or [Google Chrome](https://libredirect.github.io/download_chromium.html). +- Download RedirectTube from [Mozilla Add-ons](https://addons.mozilla.org/firefox/addon/redirecttube/) (for Firefox based-browsers). > [!NOTE] -> This extension does not work on Linux portable builds! +> These extensions do not work on Linux portable builds! > > If you have issues with the extension working with FreeTube, please create an issue in this repository instead of the extension repository. @@ -80,7 +85,7 @@ FreeTube is supported by the [Privacy Redirect](https://github.com/SimonBrazell/ ### Official Downloads > [!CAUTION] -> FreeTube is only supported on Windows 10 and later, macOS 10.15 and above, and various Linux distributions. Installing it on unsupported systems may result in unexpected issues. +> FreeTube is only supported on Windows 10 and later, macOS 11 and above, and various Linux distributions. Installing it on unsupported systems may result in unexpected issues. * [GitHub Releases](https://github.com/FreeTubeApp/FreeTube/releases) diff --git a/_icons/iconCatppuccinFrappeDarkSmall.svg b/_icons/iconCatppuccinFrappeDarkSmall.svg new file mode 100644 index 0000000000000..c27e4b485d982 --- /dev/null +++ b/_icons/iconCatppuccinFrappeDarkSmall.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/_icons/iconCatppuccinFrappeLightSmall.svg b/_icons/iconCatppuccinFrappeLightSmall.svg new file mode 100644 index 0000000000000..d0a38927553b6 --- /dev/null +++ b/_icons/iconCatppuccinFrappeLightSmall.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/_icons/iconGruvboxDarkSmall.svg b/_icons/iconGruvboxDarkSmall.svg new file mode 100644 index 0000000000000..e6dd6ad1595cf --- /dev/null +++ b/_icons/iconGruvboxDarkSmall.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/_icons/iconGruvboxLightSmall.svg b/_icons/iconGruvboxLightSmall.svg new file mode 100644 index 0000000000000..a08e5c34f9830 --- /dev/null +++ b/_icons/iconGruvboxLightSmall.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/_icons/textCatppuccinFrappeDarkSmall.svg b/_icons/textCatppuccinFrappeDarkSmall.svg new file mode 100644 index 0000000000000..b67b41aba5f83 --- /dev/null +++ b/_icons/textCatppuccinFrappeDarkSmall.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/_icons/textCatppuccinFrappeLightSmall.svg b/_icons/textCatppuccinFrappeLightSmall.svg new file mode 100644 index 0000000000000..47d0d38a6fc95 --- /dev/null +++ b/_icons/textCatppuccinFrappeLightSmall.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/_icons/textGruvboxDarkSmall.svg b/_icons/textGruvboxDarkSmall.svg new file mode 100644 index 0000000000000..00e3080e7268f --- /dev/null +++ b/_icons/textGruvboxDarkSmall.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/_icons/textGruvboxLightSmall.svg b/_icons/textGruvboxLightSmall.svg new file mode 100644 index 0000000000000..434c116ddd900 --- /dev/null +++ b/_icons/textGruvboxLightSmall.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/_scripts/ProcessLocalesPlugin.js b/_scripts/ProcessLocalesPlugin.js index 2b6abeae8df09..d08bb3734f833 100644 --- a/_scripts/ProcessLocalesPlugin.js +++ b/_scripts/ProcessLocalesPlugin.js @@ -26,11 +26,11 @@ class ProcessLocalesPlugin { } this.outputDir = options.outputDir - /** @type {Map} */ + /** @type {Map} */ this.locales = new Map() this.localeNames = [] - /** @type {Map} */ + /** @type {Map} */ this.cache = new Map() this.filePaths = [] @@ -45,7 +45,7 @@ class ProcessLocalesPlugin { /** @param {import('webpack').Compiler} compiler */ apply(compiler) { - const { CachedSource, RawSource } = compiler.webpack.sources; + const { CachedSource, RawSource } = compiler.webpack.sources const { Compilation, DefinePlugin } = compiler.webpack new DefinePlugin({ @@ -59,7 +59,6 @@ class ProcessLocalesPlugin { name: PLUGIN_NAME, stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL }, async (_assets) => { - // While running in the webpack dev server, this hook gets called for every incremental build. // For incremental builds we can return the already processed versions, which saves time // and makes webpack treat them as cached diff --git a/_scripts/build.js b/_scripts/build.js index bee33667fbeae..de43f6f7e681b 100644 --- a/_scripts/build.js +++ b/_scripts/build.js @@ -16,7 +16,7 @@ if (platform === 'darwin') { arch = Arch.arm64 } - targets = Platform.MAC.createTarget(['DMG','zip', '7z'], arch) + targets = Platform.MAC.createTarget(['DMG', 'zip', '7z'], arch) } else if (platform === 'win32') { let arch = Arch.x64 diff --git a/_scripts/dev-runner.js b/_scripts/dev-runner.js index 6b083cb6cc9ed..15d9b356b842e 100644 --- a/_scripts/dev-runner.js +++ b/_scripts/dev-runner.js @@ -18,12 +18,14 @@ const web = process.argv.indexOf('--web') !== -1 let mainConfig let rendererConfig +let botGuardScriptConfig let webConfig let SHAKA_LOCALES_TO_BE_BUNDLED if (!web) { mainConfig = require('./webpack.main.config') rendererConfig = require('./webpack.renderer.config') + botGuardScriptConfig = require('./webpack.botGuardScript.config') SHAKA_LOCALES_TO_BE_BUNDLED = rendererConfig.SHAKA_LOCALES_TO_BE_BUNDLED delete rendererConfig.SHAKA_LOCALES_TO_BE_BUNDLED @@ -88,7 +90,7 @@ async function restartElectron() { */ function setupNotifyLocaleUpdate(compiler, devServer) { const notifyLocaleChange = (updatedLocales) => { - devServer.sendMessage(devServer.webSocketServer.clients, "freetube-locale-update", updatedLocales) + devServer.sendMessage(devServer.webSocketServer.clients, 'freetube-locale-update', updatedLocales) } compiler.options.plugins @@ -98,6 +100,14 @@ function setupNotifyLocaleUpdate(compiler, devServer) { }) } +function startBotGuardScript() { + webpack(botGuardScriptConfig, (err) => { + if (err) console.error(err) + + console.log(`\nCompiled ${botGuardScriptConfig.name} script!`) + }) +} + function startMain() { const compiler = webpack(mainConfig) const { name } = compiler @@ -196,7 +206,10 @@ function startWeb () { }) } if (!web) { - startRenderer(startMain) + startRenderer(() => { + startBotGuardScript() + startMain() + }) } else { startWeb() } diff --git a/_scripts/ebuilder.config.js b/_scripts/ebuilder.config.js index 5b79d96185685..51e2afaf7b9c2 100644 --- a/_scripts/ebuilder.config.js +++ b/_scripts/ebuilder.config.js @@ -11,9 +11,9 @@ const config = { }, protocols: [ { - name: "FreeTube", + name: 'FreeTube', schemes: [ - "freetube" + 'freetube' ] } ], @@ -52,19 +52,19 @@ const config = { // https://github.com/jordansissel/fpm/issues/1503 // https://github.com/jgraph/drawio-desktop/issues/259 rpm: { - fpm: [`--rpm-rpmbuild-define=_build_id_links none`] + fpm: ['--rpm-rpmbuild-define=_build_id_links none'] }, deb: { depends: [ - "libgtk-3-0", - "libnotify4", - "libnss3", - "libxss1", - "libxtst6", - "xdg-utils", - "libatspi2.0-0", - "libuuid1", - "libsecret-1-0" + 'libgtk-3-0', + 'libnotify4', + 'libnss3', + 'libxss1', + 'libxtst6', + 'xdg-utils', + 'libatspi2.0-0', + 'libuuid1', + 'libsecret-1-0' ] }, mac: { diff --git a/_scripts/eslint-rules/plugin.mjs b/_scripts/eslint-rules/plugin.mjs new file mode 100644 index 0000000000000..451cd50836ac7 --- /dev/null +++ b/_scripts/eslint-rules/plugin.mjs @@ -0,0 +1,11 @@ +import preferUseI18nPolyfillRule from './prefer-use-i18n-polyfill-rule.mjs' + +export default { + meta: { + name: 'eslint-plugin-freetube', + version: '1.0' + }, + rules: { + 'prefer-use-i18n-polyfill': preferUseI18nPolyfillRule + } +} diff --git a/_scripts/eslint-rules/prefer-use-i18n-polyfill-rule.mjs b/_scripts/eslint-rules/prefer-use-i18n-polyfill-rule.mjs new file mode 100644 index 0000000000000..1d27f74f0f9d1 --- /dev/null +++ b/_scripts/eslint-rules/prefer-use-i18n-polyfill-rule.mjs @@ -0,0 +1,62 @@ +import { dirname, relative, resolve } from 'path' + +const polyfillPath = resolve(import.meta.dirname, '../../src/renderer/composables/use-i18n-polyfill') + +function getRelativePolyfillPath(filePath) { + const relativePath = relative(dirname(filePath), polyfillPath).replaceAll('\\', '/') + + if (relativePath[0] !== '.') { + return `./${relativePath}` + } + + return relativePath +} + +/** @type {import('eslint').Rule.RuleModule} */ +export default { + meta: { + type: 'problem', + fixable: 'code' + }, + create(context) { + return { + 'ImportDeclaration[source.value="vue-i18n"]'(node) { + const specifierIndex = node.specifiers.findIndex(specifier => specifier.type === 'ImportSpecifier' && specifier.imported.name === 'useI18n') + + if (specifierIndex !== -1) { + context.report({ + node: node.specifiers.length === 1 ? node : node.specifiers[specifierIndex], + message: "Please use FreeTube's useI18n polyfill, as vue-i18n's useI18n composable does not work when the vue-i18n is in legacy mode, which is needed for components using the Options API.", + fix: context.physicalFilename === '' + ? undefined + : (fixer) => { + const relativePath = getRelativePolyfillPath(context.physicalFilename) + + // If the import only imports `useI18n`, we can just update the source/from text + // Else we need to create a new import for `useI18n` and remove useI18n from the original one + if (node.specifiers.length === 1) { + return fixer.replaceText(node.source, `'${relativePath}'`) + } else { + const specifier = node.specifiers[specifierIndex] + + let specifierText = 'useI18n' + + if (specifier.imported.name !== specifier.local.name) { + specifierText += ` as ${specifier.local.name}` + } + + return [ + fixer.removeRange([ + specifierIndex === 0 ? specifier.start : node.specifiers[specifierIndex - 1].end, + specifierIndex === node.specifiers.length - 1 ? specifier.end : node.specifiers[specifierIndex + 1].start + ]), + fixer.insertTextAfter(node, `\nimport { ${specifierText} } from '${relativePath}'`) + ] + } + } + }) + } + } + } + } +} diff --git a/_scripts/getRegions.mjs b/_scripts/getRegions.mjs index 842bd7cb16efa..5b36a05fbcd81 100644 --- a/_scripts/getRegions.mjs +++ b/_scripts/getRegions.mjs @@ -12,11 +12,9 @@ */ import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs' -import { dirname } from 'path' -import { fileURLToPath } from 'url' import { Innertube, Misc } from 'youtubei.js' -const STATIC_DIRECTORY = `${dirname(fileURLToPath(import.meta.url))}/../static` +const STATIC_DIRECTORY = `${import.meta.dirname}/../static` const activeLanguagesPath = `${STATIC_DIRECTORY}/locales/activeLocales.json` /** @type {string[]} */ @@ -46,6 +44,7 @@ for (const language of youTubeLanguages) { youTube: language, freeTube: language }) + // eslint-disable-next-line @stylistic/brace-style } // special cases else if (language === 'de') { @@ -120,8 +119,6 @@ for (const { youTube, freeTube } of languagesToScrape) { processGeolocations(freeTube, youTube, response) } - - async function scrapeLanguage(youTubeLanguageCode) { const session = await Innertube.create({ retrieve_player: false, diff --git a/_scripts/getShakaLocales.js b/_scripts/getShakaLocales.js index 76ed42b2633c1..644a13a8ef6a1 100644 --- a/_scripts/getShakaLocales.js +++ b/_scripts/getShakaLocales.js @@ -32,7 +32,7 @@ function getMappings(shakaLocales, freeTubeLocales) { * @type {[string, string][]} * Using this structure as it gets passed to `new Map()` in the player component * The first element is the FreeTube locale, the second one is the shaka-player one - **/ + */ const mappings = [] for (const locale of freeTubeLocales) { diff --git a/_scripts/injectAllowedPaths.mjs b/_scripts/injectAllowedPaths.mjs index bf1eae9962410..e5c1ce2293232 100644 --- a/_scripts/injectAllowedPaths.mjs +++ b/_scripts/injectAllowedPaths.mjs @@ -6,12 +6,9 @@ * to ensure that it cannot access other files on the disk, without the users permission (e.g. file picker). */ import { closeSync, ftruncateSync, openSync, readFileSync, readdirSync, writeSync } from 'fs' -import { dirname, join, relative, resolve } from 'path' -import { fileURLToPath } from 'url' +import { join, relative, resolve } from 'path' -const __dirname = dirname(fileURLToPath(import.meta.url)); - -const distDirectory = resolve(__dirname, '..', 'dist') +const distDirectory = resolve(import.meta.dirname, '..', 'dist') const webDirectory = join(distDirectory, 'web') const paths = readdirSync(distDirectory, { @@ -24,15 +21,16 @@ const paths = readdirSync(distDirectory, { // disallow the renderer process/browser windows to read the main.js file dirent.name !== 'main.js' && dirent.name !== 'main.js.LICENSE.txt' && + // disallow the renderer process/browser windows to read the botGuardScript.js file + dirent.name !== 'botGuardScript.js' && // filter out any web build files, in case the dist directory contains a web build - !dirent.path.startsWith(webDirectory); + !dirent.parentPath.startsWith(webDirectory) }) .map(dirent => { - const joined = join(dirent.path, dirent.name) + const joined = join(dirent.parentPath, dirent.name) return '/' + relative(distDirectory, joined).replaceAll('\\', '/') }) - let fileHandle try { fileHandle = openSync(join(distDirectory, 'main.js'), 'r+') diff --git a/_scripts/mime-db-shrinking-loader.js b/_scripts/mime-db-shrinking-loader.js index 547858c648edc..c3aba020875a8 100644 --- a/_scripts/mime-db-shrinking-loader.js +++ b/_scripts/mime-db-shrinking-loader.js @@ -14,7 +14,6 @@ module.exports = function (source) { if (mimeType.startsWith('image/') && original[mimeType].extensions && (!mimeType.startsWith('image/x-') || mimeType === 'image/x-icon' || mimeType === 'image/x-ms-bmp') && (!mimeType.startsWith('image/vnd.') || mimeType === 'image/vnd.microsoft.icon')) { - // Only the extensions field is needed, see: https://github.com/kevva/ext-list/blob/v2.2.2/index.js reduced[mimeType] = { extensions: original[mimeType].extensions diff --git a/_scripts/patchShaka.mjs b/_scripts/patchShaka.mjs index a064ce9b59758..c5582880d4c60 100644 --- a/_scripts/patchShaka.mjs +++ b/_scripts/patchShaka.mjs @@ -99,7 +99,6 @@ async function replaceAndDownloadMaterialIconsFont() { let newFontCSS = text.match(/(@font-face\s*{[^}]+})/)[1].replaceAll('\n', '') - const urlMatch = newFontCSS.match(/https:\/\/fonts\.gstatic\.com\/s\/materialiconsround\/(?[^/]+)\/[^.]+\.(?\w+)/) const url = urlMatch[0] diff --git a/_scripts/webpack.botGuardScript.config.js b/_scripts/webpack.botGuardScript.config.js new file mode 100644 index 0000000000000..9b66ffc370f21 --- /dev/null +++ b/_scripts/webpack.botGuardScript.config.js @@ -0,0 +1,23 @@ +const path = require('path') + +/** @type {import('webpack').Configuration} */ +module.exports = { + name: 'botGuardScript', + // Always use production mode, as we use the output as a function body and the debug output doesn't work for that + mode: 'production', + devtool: false, + target: 'web', + entry: { + botGuardScript: path.join(__dirname, '../src/botGuardScript.js'), + }, + output: { + filename: '[name].js', + path: path.join(__dirname, '../dist'), + library: { + type: 'modern-module' + } + }, + experiments: { + outputModule: true + } +} diff --git a/_scripts/webpack.main.config.js b/_scripts/webpack.main.config.js index 1bb102105840c..d0c9effe02219 100644 --- a/_scripts/webpack.main.config.js +++ b/_scripts/webpack.main.config.js @@ -54,15 +54,15 @@ const config = { if (!isDevMode) { config.plugins.push( new CopyWebpackPlugin({ - patterns: [ - { - from: path.join(__dirname, '../static'), - to: path.join(__dirname, '../dist/static'), - globOptions: { - dot: true, - ignore: ['**/.*', '**/locales/**', '**/pwabuilder-sw.js', '**/manifest.json', '**/dashFiles/**', '**/storyboards/**'], - }, + patterns: [ + { + from: path.join(__dirname, '../static'), + to: path.join(__dirname, '../dist/static'), + globOptions: { + dot: true, + ignore: ['**/.*', '**/locales/**', '**/pwabuilder-sw.js', '**/manifest.json', '**/dashFiles/**', '**/storyboards/**'], }, + }, ] }) ) diff --git a/_scripts/webpack.renderer.config.js b/_scripts/webpack.renderer.config.js index c1b813816b867..11f1cf828711c 100644 --- a/_scripts/webpack.renderer.config.js +++ b/_scripts/webpack.renderer.config.js @@ -153,18 +153,20 @@ const config = { }, // Don't need to copy them in dev mode, // as we configure WebpackDevServer to serve them - ...(isDevMode ? [] : [ - { - from: path.join(__dirname, '../node_modules/shaka-player/ui/locales', `{${SHAKA_LOCALES_TO_BE_BUNDLED.join(',')}}.json`).replaceAll('\\', '/'), - to: path.join(__dirname, '../dist/static/shaka-player-locales'), - context: path.join(__dirname, '../node_modules/shaka-player/ui/locales'), - transform: { - transformer: (input) => { - return JSON.stringify(JSON.parse(input.toString('utf-8'))) + ...(isDevMode + ? [] + : [ + { + from: path.join(__dirname, '../node_modules/shaka-player/ui/locales', `{${SHAKA_LOCALES_TO_BE_BUNDLED.join(',')}}.json`).replaceAll('\\', '/'), + to: path.join(__dirname, '../dist/static/shaka-player-locales'), + context: path.join(__dirname, '../node_modules/shaka-player/ui/locales'), + transform: { + transformer: (input) => { + return JSON.stringify(JSON.parse(input.toString('utf-8'))) + } + } } - } - } - ]) + ]) ] }) ], diff --git a/_scripts/webpack.web.config.js b/_scripts/webpack.web.config.js index 75f2ccd846c8b..0df7ff510f550 100644 --- a/_scripts/webpack.web.config.js +++ b/_scripts/webpack.web.config.js @@ -184,18 +184,18 @@ config.plugins.push( 'process.env.SHAKA_LOCALES_PREBUNDLED': JSON.stringify(SHAKA_LOCALES_PREBUNDLED) }), new CopyWebpackPlugin({ - patterns: [ - { - from: path.join(__dirname, '../static/pwabuilder-sw.js'), - to: path.join(__dirname, '../dist/web/pwabuilder-sw.js'), + 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/**'], }, - { - from: path.join(__dirname, '../static'), - to: path.join(__dirname, '../dist/web/static'), - globOptions: { - dot: true, - ignore: ['**/.*', '**/locales/**', '**/pwabuilder-sw.js', '**/dashFiles/**', '**/storyboards/**'], - }, }, { from: path.join(__dirname, '../node_modules/shaka-player/ui/locales', `{${SHAKA_LOCALES_TO_BE_BUNDLED.join(',')}}.json`).replaceAll('\\', '/'), @@ -206,5 +206,4 @@ config.plugins.push( }) ) - module.exports = config diff --git a/eslint.config.mjs b/eslint.config.mjs index 0105059384b8a..3e05e0731dc91 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,40 +1,38 @@ import eslintPluginVue from 'eslint-plugin-vue' import vuejsAccessibility from 'eslint-plugin-vuejs-accessibility' import eslintPluginUnicorn from 'eslint-plugin-unicorn' -import eslintConfigPrettier from 'eslint-config-prettier' import intlifyVueI18N from '@intlify/eslint-plugin-vue-i18n' import globals from 'globals' import vueEslintParser from 'vue-eslint-parser' import js from '@eslint/js' -import { FlatCompat } from '@eslint/eslintrc' -import { fixupConfigRules } from '@eslint/compat' import jsoncEslintParser from 'jsonc-eslint-parser' import eslintPluginJsonc from 'eslint-plugin-jsonc' import eslintPluginYml from 'eslint-plugin-yml' import yamlEslintParser from 'yaml-eslint-parser' +// Faster than importing the default import, +// because the default import imports a lot of other dependencies +// for the `resolveIgnoresFromGitignore` function that we don't use +import { neostandard } from 'neostandard/lib/main.js' +import jsdoc from 'eslint-plugin-jsdoc' +import freetube from './_scripts/eslint-rules/plugin.mjs' import activeLocales from './static/locales/activeLocales.json' with { type: 'json' } -const compat = new FlatCompat({ - baseDirectory: import.meta.dirname, - recommendedConfig: js.configs.recommended, - allConfig: js.configs.all -}) - export default [ { ignores: [ + 'build/', 'dist/', - 'eslint.config.mjs' + 'eslint.config.mjs', + // The JSON files inside this directory are auto-generated, so they don't follow the code style rules + 'static/geolocations/' ] }, - ...fixupConfigRules( - compat.config({ - extends: ['standard'] - }) - ), + ...neostandard({ + noJsx: true, + ts: false, + }), js.configs.recommended, - eslintConfigPrettier, ...eslintPluginVue.configs['flat/vue2-recommended'], ...vuejsAccessibility.configs["flat/recommended"], ...intlifyVueI18N.configs['flat/recommended'], @@ -47,6 +45,8 @@ export default [ ], plugins: { unicorn: eslintPluginUnicorn, + jsdoc, + freetube, }, languageOptions: { @@ -68,8 +68,8 @@ export default [ }, rules: { - 'space-before-function-paren': 'off', - 'comma-dangle': ['error', 'only-multiline'], + '@stylistic/space-before-function-paren': 'off', + '@stylistic/comma-dangle': ['error', 'only-multiline'], 'vue/no-v-html': 'off', 'no-console': ['error', { @@ -81,7 +81,6 @@ export default [ 'object-shorthand': 'off', 'vue/no-template-key': 'warn', 'vue/multi-word-component-names': 'off', - 'vuejs-accessibility/no-onchange': 'off', 'vuejs-accessibility/label-has-for': ['error', { required: { @@ -97,6 +96,9 @@ export default [ 'unicorn/prefer-keyboard-event-key': 'error', 'unicorn/prefer-regexp-test': 'error', 'unicorn/prefer-string-replace-all': 'error', + 'unicorn/prefer-optional-catch-binding': 'error', + 'unicorn/prefer-date-now': 'error', + 'unicorn/prefer-array-index-of': 'error', '@intlify/vue-i18n/no-dynamic-keys': 'error', '@intlify/vue-i18n/no-duplicate-keys-in-locale': 'error', @@ -122,6 +124,17 @@ export default [ '@intlify/vue-i18n/no-deprecated-tc': 'off', 'vue/require-explicit-emits': 'error', 'vue/no-unused-emit-declarations': 'error', + + 'jsdoc/check-alignment': 'error', + 'jsdoc/check-property-names': 'error', + 'jsdoc/check-param-names': 'error', + 'jsdoc/check-syntax': 'error', + 'jsdoc/check-template-names': 'error', + 'jsdoc/check-types': 'error', + 'jsdoc/no-bad-blocks': 'error', + 'jsdoc/no-multi-asterisks': 'error', + + 'freetube/prefer-use-i18n-polyfill': 'error', }, }, @@ -137,8 +150,9 @@ export default [ }, rules: { - 'no-tabs': 'off', - 'comma-spacing': 'off', + '@stylistic/no-tabs': 'off', + '@stylistic/comma-spacing': 'off', + '@stylistic/eol-last': 'off', 'no-irregular-whitespace': 'off', }, @@ -164,6 +178,7 @@ export default [ rules: { 'yml/no-irregular-whitespace': 'off', + '@stylistic/spaced-comment': 'off', }, settings: { @@ -195,10 +210,9 @@ export default [ { files: ['_scripts/*.js'], languageOptions: { - globals: { - ...globals.node - }, + globals: globals.node, ecmaVersion: 'latest', + sourceType: 'commonjs' }, plugins: { @@ -206,17 +220,20 @@ export default [ }, rules: { + '@stylistic/space-before-function-paren': 'off', + '@stylistic/comma-dangle': ['error', 'only-multiline'], 'no-console': 'off', 'n/no-path-concat': 'off', 'unicorn/better-regex': 'error', + 'unicorn/prefer-optional-catch-binding': 'error', + 'unicorn/prefer-date-now': 'error', + 'unicorn/prefer-array-index-of': 'error', } }, { - files: ['_scripts/*.mjs'], + files: ['_scripts/**/*.mjs'], languageOptions: { - globals: { - ...globals.node, - }, + globals: globals.node, ecmaVersion: 'latest', sourceType: 'module', }, @@ -227,8 +244,13 @@ export default [ rules: { 'no-console': 'off', + '@stylistic/space-before-function-paren': 'off', + '@stylistic/comma-dangle': ['error', 'only-multiline'], 'n/no-path-concat': 'off', 'unicorn/better-regex': 'error', + 'unicorn/prefer-optional-catch-binding': 'error', + 'unicorn/prefer-date-now': 'error', + 'unicorn/prefer-array-index-of': 'error', } } ] diff --git a/package.json b/package.json index d4dedf61bbcc9..6b9313784d29d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "freetube", "productName": "FreeTube", "description": "A private YouTube client", - "version": "0.21.3", + "version": "0.22.1", "license": "AGPL-3.0-or-later", "main": "./dist/main.js", "private": true, @@ -37,16 +37,17 @@ "lint-all": "run-p lint lint-json", "lint": "run-p eslint-lint lint-style", "lint-fix": "run-p eslint-lint-fix lint-style-fix", - "eslint-lint": "eslint --config eslint.config.mjs \"./src/**/*.js\" \"./src/**/*.vue\" \"./static/**/*.js\" \"./_scripts/*.js\" \"./_scripts/*.mjs\"", - "eslint-lint-fix": "eslint --config eslint.config.mjs --fix \"./src/**/*.js\" \"./src/**/*.vue\" \"./static/**/*.js\" \"./_scripts/*.js\" \"./_scripts/*.mjs\"", - "lint-json": "eslint --config eslint.config.mjs \"./static/**/*.json\"", + "eslint-lint": "eslint --config eslint.config.mjs \"src/**/*.js\" \"src/renderer/**/*.vue\" \"static/*.js\" \"_scripts/*.js\" \"_scripts/**/*.mjs\"", + "eslint-lint-fix": "eslint --config eslint.config.mjs --fix \"src/**/*.js\" \"src/renderer/**/*.vue\" \"static/*.js\" \"_scripts/*.js\" \"_scripts/**/*.mjs\"", + "lint-json": "eslint --config eslint.config.mjs \"static/**/*.json\"", "lint-style": "stylelint \"**/*.{css,scss}\"", "lint-style-fix": "stylelint --fix \"**/*.{css,scss}\"", - "lint-yml": "eslint --config eslint.config.mjs \"./**/*.yml\" \"./**/*.yaml\"", - "pack": "run-p pack:main pack:renderer && node _scripts/injectAllowedPaths.mjs", + "lint-yml": "eslint --config eslint.config.mjs \"**/*.yml\" \"**/*.yaml\"", + "pack": "run-p pack:main pack:renderer pack:botGuardScript && node _scripts/injectAllowedPaths.mjs", "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:web": "webpack --mode=production --node-env=production --config _scripts/webpack.web.config.js", + "pack:botGuardScript": "webpack --config _scripts/webpack.botGuardScript.config.js", "postinstall": "run-s --silent rebuild:electron patch-shaka", "prettier": "prettier --write \"{src,_scripts}/**/*.{js,vue}\"", "rebuild:electron": "electron-builder install-app-deps", @@ -54,70 +55,63 @@ "ci": "yarn install --silent --frozen-lockfile" }, "dependencies": { - "@fortawesome/fontawesome-svg-core": "^6.6.0", - "@fortawesome/free-brands-svg-icons": "^6.6.0", - "@fortawesome/free-regular-svg-icons": "^6.6.0", - "@fortawesome/free-solid-svg-icons": "^6.6.0", + "@fortawesome/fontawesome-svg-core": "^6.7.2", + "@fortawesome/free-brands-svg-icons": "^6.7.2", + "@fortawesome/free-regular-svg-icons": "^6.7.2", + "@fortawesome/free-solid-svg-icons": "^6.7.2", "@fortawesome/vue-fontawesome": "^2.0.10", "@seald-io/nedb": "^4.0.4", - "autolinker": "^4.0.0", + "autolinker": "^4.0.1", + "bgutils-js": "^3.1.0", "electron-context-menu": "^4.0.4", "lodash.debounce": "^4.0.8", - "marked": "^14.1.2", + "marked": "^15.0.4", "path-browserify": "^1.0.1", "portal-vue": "^2.1.7", "process": "^0.11.10", - "shaka-player": "^4.11.7", - "swiper": "^11.1.14", + "shaka-player": "^4.12.5", + "swiper": "^11.1.15", "vue": "^2.7.16", "vue-i18n": "^8.28.2", "vue-observe-visibility": "^1.0.0", "vue-router": "^3.6.5", "vuex": "^3.6.2", - "youtubei.js": "^10.5.0" + "youtubei.js": "^12.2.0" }, "devDependencies": { - "@babel/core": "^7.25.7", - "@babel/eslint-parser": "^7.25.7", - "@babel/plugin-transform-class-properties": "^7.25.7", - "@babel/preset-env": "^7.25.7", + "@babel/core": "^7.26.0", + "@babel/plugin-transform-class-properties": "^7.25.9", + "@babel/preset-env": "^7.26.0", "@double-great/stylelint-a11y": "^3.0.2", - "@eslint/compat": "^1.2.0", - "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "^9.12.0", - "@intlify/eslint-plugin-vue-i18n": "^3.0.0", + "@eslint/js": "^9.17.0", + "@intlify/eslint-plugin-vue-i18n": "^3.2.0", "babel-loader": "^9.2.1", "copy-webpack-plugin": "^12.0.2", "css-loader": "^7.1.2", "css-minimizer-webpack-plugin": "^7.0.0", - "electron": "^32.1.2", - "electron-builder": "^25.1.7", - "eslint": "^9.11.1", - "eslint-config-prettier": "^9.1.0", - "eslint-config-standard": "^17.1.0", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jsonc": "^2.16.0", - "eslint-plugin-n": "^17.10.3", - "eslint-plugin-prettier": "^5.2.1", - "eslint-plugin-promise": "^7.1.0", - "eslint-plugin-unicorn": "^56.0.0", - "eslint-plugin-vue": "^9.28.0", + "electron": "^33.2.1", + "electron-builder": "^25.1.8", + "eslint": "^9.17.0", + "eslint-plugin-jsdoc": "^50.6.1", + "eslint-plugin-jsonc": "^2.18.2", + "eslint-plugin-unicorn": "^56.0.1", + "eslint-plugin-vue": "^9.32.0", "eslint-plugin-vuejs-accessibility": "^2.4.1", - "eslint-plugin-yml": "^1.14.0", - "globals": "^15.10.0", - "html-webpack-plugin": "^5.6.0", + "eslint-plugin-yml": "^1.16.0", + "globals": "^15.14.0", + "html-webpack-plugin": "^5.6.3", "js-yaml": "^4.1.0", "json-minimizer-webpack-plugin": "^5.0.0", - "lefthook": "^1.7.18", - "mini-css-extract-plugin": "^2.9.1", - "npm-run-all2": "^6.2.3", - "postcss": "^8.4.47", + "lefthook": "^1.10.0", + "mini-css-extract-plugin": "^2.9.2", + "neostandard": "^0.12.0", + "npm-run-all2": "^7.0.2", + "postcss": "^8.4.49", "postcss-scss": "^4.0.9", - "prettier": "^2.8.8", "rimraf": "^6.0.1", - "sass": "^1.79.4", - "sass-loader": "^16.0.2", - "stylelint": "^16.9.0", + "sass": "^1.83.0", + "sass-loader": "^16.0.4", + "stylelint": "^16.12.0", "stylelint-config-sass-guidelines": "^12.1.0", "stylelint-config-standard": "^36.0.1", "stylelint-high-performance-animation": "^1.10.0", @@ -126,9 +120,9 @@ "vue-devtools": "^5.1.4", "vue-eslint-parser": "^9.4.3", "vue-loader": "^15.10.0", - "webpack": "^5.95.0", - "webpack-cli": "^5.1.4", - "webpack-dev-server": "^5.1.0", + "webpack": "^5.97.1", + "webpack-cli": "^6.0.1", + "webpack-dev-server": "^5.2.0", "yaml-eslint-parser": "^1.2.3" } } diff --git a/src/botGuardScript.js b/src/botGuardScript.js new file mode 100644 index 0000000000000..11da8855a6472 --- /dev/null +++ b/src/botGuardScript.js @@ -0,0 +1,37 @@ +import { BG } from 'bgutils-js' + +// This script has it's own webpack config, as it gets passed as a string to Electron's evaluateJavaScript function +// in src/main/poTokenGenerator.js +export default async function(visitorData) { + const requestKey = 'O43z0dpjhgX20SCx4KAo' + + const bgConfig = { + fetch: (input, init) => fetch(input, init), + requestKey, + globalObj: window, + identifier: visitorData + } + + const challenge = await BG.Challenge.create(bgConfig) + + if (!challenge) { + throw new Error('Could not get challenge') + } + + const interpreterJavascript = challenge.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue + + if (interpreterJavascript) { + // eslint-disable-next-line no-new-func + new Function(interpreterJavascript)() + } else { + console.warn('Unable to load VM.') + } + + const poTokenResult = await BG.PoToken.generate({ + program: challenge.program, + globalName: challenge.globalName, + bgConfig + }) + + return poTokenResult.poToken +} diff --git a/src/constants.js b/src/constants.js index f45e30fb8211d..c21ace4448a8c 100644 --- a/src/constants.js +++ b/src/constants.js @@ -5,6 +5,7 @@ const IpcChannels = { OPEN_EXTERNAL_LINK: 'open-external-link', GET_SYSTEM_LOCALE: 'get-system-locale', GET_PICTURES_PATH: 'get-pictures-path', + GET_NAVIGATION_HISTORY: 'get-navigation-history', SHOW_OPEN_DIALOG: 'show-open-dialog', SHOW_SAVE_DIALOG: 'show-save-dialog', STOP_POWER_SAVE_BLOCKER: 'stop-power-save-blocker', @@ -39,7 +40,9 @@ const IpcChannels = { PLAYER_CACHE_GET: 'player-cache-get', PLAYER_CACHE_SET: 'player-cache-set', - SET_INVIDIOUS_AUTHORIZATION: 'set-invidious-authorization' + SET_INVIDIOUS_AUTHORIZATION: 'set-invidious-authorization', + + GENERATE_PO_TOKEN: 'generate-po-token', } const DBActions = { @@ -113,6 +116,69 @@ const SyncEvents = { }, } +/* + DEV NOTE: Duplicate any and all changes made here to our [official documentation site here](https://github.com/FreeTubeApp/FreeTube-Docs/blob/master/usage/keyboard-shortcuts.md) + to have them reflect on the [keyboard shortcut reference webpage](https://docs.freetubeapp.io/usage/keyboard-shortcuts). + Please also update the [keyboard shortcut modal](src/renderer/components/FtKeyboardShortcutPrompt/FtKeyboardShortcutPrompt.vue) +*/ +const KeyboardShortcuts = { + APP: { + GENERAL: { + SHOW_SHORTCUTS: 'shift+?', + HISTORY_BACKWARD: 'alt+arrowleft', + HISTORY_FORWARD: 'alt+arrowright', + FULLSCREEN: 'f11', + NAVIGATE_TO_SETTINGS: 'ctrl+,', + NAVIGATE_TO_HISTORY: 'ctrl+H', + NAVIGATE_TO_HISTORY_MAC: 'cmd+Y', + NEW_WINDOW: 'ctrl+N', + MINIMIZE_WINDOW: 'ctrl+M', + CLOSE_WINDOW: 'ctrl+W', + RESTART_WINDOW: 'ctrl+R', + FORCE_RESTART_WINDOW: 'ctrl+shift+R', + TOGGLE_DEVTOOLS: 'ctrl+shift+I', + FOCUS_SEARCH: 'alt+D', + SEARCH_IN_NEW_WINDOW: 'shift+enter', + RESET_ZOOM: 'ctrl+0', + ZOOM_IN: 'ctrl+plus', + ZOOM_OUT: 'ctrl+-' + + }, + SITUATIONAL: { + REFRESH: 'r', + FOCUS_SECONDARY_SEARCH: 'ctrl+F' + }, + }, + VIDEO_PLAYER: { + GENERAL: { + CAPTIONS: 'c', + THEATRE_MODE: 't', + FULLSCREEN: 'f', + FULLWINDOW: 's', + PICTURE_IN_PICTURE: 'i', + MUTE: 'm', + VOLUME_UP: 'arrowup', + VOLUME_DOWN: 'arrowdown', + STATS: 'd', + TAKE_SCREENSHOT: 'u', + }, + PLAYBACK: { + PLAY: 'k', + LARGE_REWIND: 'j', + LARGE_FAST_FORWARD: 'l', + SMALL_REWIND: 'arrowleft', + SMALL_FAST_FORWARD: 'arrowright', + DECREASE_VIDEO_SPEED: 'o', + INCREASE_VIDEO_SPEED: 'p', + SKIP_N_TENTHS: '0..9', + LAST_CHAPTER: 'ctrl+arrowleft', + NEXT_CHAPTER: 'ctrl+arrowright', + LAST_FRAME: ',', + NEXT_FRAME: '.', + } + }, +} + // Utils const MAIN_PROFILE_ID = 'allChannels' @@ -132,6 +198,7 @@ export { IpcChannels, DBActions, SyncEvents, + KeyboardShortcuts, MAIN_PROFILE_ID, MOBILE_WIDTH_THRESHOLD, PLAYLIST_HEIGHT_FORCE_LIST_THRESHOLD, diff --git a/src/datastores/handlers/base.js b/src/datastores/handlers/base.js index 614b21cd290e6..a0f620925b6a9 100644 --- a/src/datastores/handlers/base.js +++ b/src/datastores/handlers/base.js @@ -10,6 +10,21 @@ class Settings { await this.upsert('currentLocale', currentLocale.value.replace('_', '-')) } + // In FreeTube 0.22.0 and earlier the external player arguments were displayed in a text box, + // with the user manually entering `;` to separate the different arguments. + // This is a one time migration that converts the old string to a JSON array + const externalPlayerCustomArgs = await db.settings.findOneAsync({ _id: 'externalPlayerCustomArgs' }) + + if (externalPlayerCustomArgs && !externalPlayerCustomArgs.value.startsWith('[')) { + let newValue = '[]' + + if (externalPlayerCustomArgs.value.length > 0) { + newValue = JSON.stringify(externalPlayerCustomArgs.value.split(';')) + } + + await this.upsert('externalPlayerCustomArgs', newValue) + } + return db.settings.findAsync({ _id: { $ne: 'bounds' } }) } @@ -168,7 +183,7 @@ class Playlists { return db.playlists.removeAsync({ _id, protected: { $ne: true } }) } - static deleteVideoIdByPlaylistId({ _id, videoId, playlistItemId }) { + static deleteVideoIdByPlaylistId(_id, videoId, playlistItemId) { if (playlistItemId != null) { return db.playlists.updateAsync( { _id }, @@ -216,7 +231,7 @@ class SubscriptionCache { return db.subscriptionCache.findAsync({}) } - static updateVideosByChannelId({ channelId, entries, timestamp }) { + static updateVideosByChannelId(channelId, entries, timestamp) { return db.subscriptionCache.updateAsync( { _id: channelId }, { $set: { videos: entries, videosTimestamp: timestamp } }, @@ -224,7 +239,7 @@ class SubscriptionCache { ) } - static updateLiveStreamsByChannelId({ channelId, entries, timestamp }) { + static updateLiveStreamsByChannelId(channelId, entries, timestamp) { return db.subscriptionCache.updateAsync( { _id: channelId }, { $set: { liveStreams: entries, liveStreamsTimestamp: timestamp } }, @@ -232,7 +247,7 @@ class SubscriptionCache { ) } - static updateShortsByChannelId({ channelId, entries, timestamp }) { + static updateShortsByChannelId(channelId, entries, timestamp) { return db.subscriptionCache.updateAsync( { _id: channelId }, { $set: { shorts: entries, shortsTimestamp: timestamp } }, @@ -240,7 +255,7 @@ class SubscriptionCache { ) } - static updateShortsWithChannelPageShortsByChannelId({ channelId, entries }) { + static updateShortsWithChannelPageShortsByChannelId(channelId, entries) { return db.subscriptionCache.findOneAsync({ _id: channelId }, { shorts: 1 }).then((doc) => { if (doc == null) { return } @@ -273,7 +288,7 @@ class SubscriptionCache { }) } - static updateCommunityPostsByChannelId({ channelId, entries, timestamp }) { + static updateCommunityPostsByChannelId(channelId, entries, timestamp) { return db.subscriptionCache.updateAsync( { _id: channelId }, { $set: { communityPosts: entries, communityPostsTimestamp: timestamp } }, diff --git a/src/datastores/handlers/electron.js b/src/datastores/handlers/electron.js index 889c91d4f060c..b18706ec8ee5d 100644 --- a/src/datastores/handlers/electron.js +++ b/src/datastores/handlers/electron.js @@ -173,7 +173,7 @@ class Playlists { ) } - static deleteVideoIdByPlaylistId({ _id, videoId, playlistItemId }) { + static deleteVideoIdByPlaylistId(_id, videoId, playlistItemId) { return ipcRenderer.invoke( IpcChannels.DB_PLAYLISTS, { @@ -226,7 +226,7 @@ class SubscriptionCache { ) } - static updateVideosByChannelId({ channelId, entries, timestamp }) { + static updateVideosByChannelId(channelId, entries, timestamp) { return ipcRenderer.invoke( IpcChannels.DB_SUBSCRIPTION_CACHE, { @@ -236,7 +236,7 @@ class SubscriptionCache { ) } - static updateLiveStreamsByChannelId({ channelId, entries, timestamp }) { + static updateLiveStreamsByChannelId(channelId, entries, timestamp) { return ipcRenderer.invoke( IpcChannels.DB_SUBSCRIPTION_CACHE, { @@ -246,7 +246,7 @@ class SubscriptionCache { ) } - static updateShortsByChannelId({ channelId, entries, timestamp }) { + static updateShortsByChannelId(channelId, entries, timestamp) { return ipcRenderer.invoke( IpcChannels.DB_SUBSCRIPTION_CACHE, { @@ -256,7 +256,7 @@ class SubscriptionCache { ) } - static updateShortsWithChannelPageShortsByChannelId({ channelId, entries }) { + static updateShortsWithChannelPageShortsByChannelId(channelId, entries) { return ipcRenderer.invoke( IpcChannels.DB_SUBSCRIPTION_CACHE, { @@ -266,7 +266,7 @@ class SubscriptionCache { ) } - static updateCommunityPostsByChannelId({ channelId, entries, timestamp }) { + static updateCommunityPostsByChannelId(channelId, entries, timestamp) { return ipcRenderer.invoke( IpcChannels.DB_SUBSCRIPTION_CACHE, { diff --git a/src/datastores/handlers/web.js b/src/datastores/handlers/web.js index d68e24f042615..dfddc6a0c31ba 100644 --- a/src/datastores/handlers/web.js +++ b/src/datastores/handlers/web.js @@ -20,165 +20,8 @@ class Settings { } } -class History { - static find() { - return baseHandlers.history.find() - } - - static upsert(record) { - return baseHandlers.history.upsert(record) - } - - static overwrite(records) { - return baseHandlers.history.overwrite(records) - } - - static updateWatchProgress(videoId, watchProgress) { - return baseHandlers.history.updateWatchProgress(videoId, watchProgress) - } - - static updateLastViewedPlaylist(videoId, lastViewedPlaylistId, lastViewedPlaylistType, lastViewedPlaylistItemId) { - return baseHandlers.history.updateLastViewedPlaylist(videoId, lastViewedPlaylistId, lastViewedPlaylistType, lastViewedPlaylistItemId) - } - - static delete(videoId) { - return baseHandlers.history.delete(videoId) - } - - static deleteAll() { - return baseHandlers.history.deleteAll() - } -} - -class Profiles { - static create(profile) { - return baseHandlers.profiles.create(profile) - } - - static find() { - return baseHandlers.profiles.find() - } - - static upsert(profile) { - return baseHandlers.profiles.upsert(profile) - } - - static addChannelToProfiles(channel, profileIds) { - return baseHandlers.profiles.addChannelToProfiles(channel, profileIds) - } - - static removeChannelFromProfiles(channelId, profileIds) { - return baseHandlers.profiles.removeChannelFromProfiles(channelId, profileIds) - } - - static delete(id) { - return baseHandlers.profiles.delete(id) - } -} - -class Playlists { - static create(playlists) { - return baseHandlers.playlists.create(playlists) - } - - static find() { - return baseHandlers.playlists.find() - } - - static upsert(playlist) { - return baseHandlers.playlists.upsert(playlist) - } +// For the settings we use the wrapper class to hide some methods only needed in the Electron main process +export { Settings as settings } - static upsertVideoByPlaylistId(_id, videoData) { - return baseHandlers.playlists.upsertVideoByPlaylistId(_id, videoData) - } - - static upsertVideosByPlaylistId(_id, videoData) { - return baseHandlers.playlists.upsertVideosByPlaylistId(_id, videoData) - } - - static delete(_id) { - return baseHandlers.playlists.delete(_id) - } - - static deleteVideoIdByPlaylistId({ _id, videoId, playlistItemId }) { - return baseHandlers.playlists.deleteVideoIdByPlaylistId({ _id, videoId, playlistItemId }) - } - - static deleteVideoIdsByPlaylistId(_id, videoIds) { - return baseHandlers.playlists.deleteVideoIdsByPlaylistId(_id, videoIds) - } - - static deleteAllVideosByPlaylistId(_id) { - return baseHandlers.playlists.deleteAllVideosByPlaylistId(_id) - } - - static deleteMultiple(ids) { - return baseHandlers.playlists.deleteMultiple(ids) - } - - static deleteAll() { - return baseHandlers.playlists.deleteAll() - } -} - -class SubscriptionCache { - static find() { - return baseHandlers.subscriptionCache.find() - } - - static updateVideosByChannelId({ channelId, entries, timestamp }) { - return baseHandlers.subscriptionCache.updateVideosByChannelId({ - channelId, - entries, - timestamp, - }) - } - - static updateLiveStreamsByChannelId({ channelId, entries, timestamp }) { - return baseHandlers.subscriptionCache.updateLiveStreamsByChannelId({ - channelId, - entries, - timestamp, - }) - } - - static updateShortsByChannelId({ channelId, entries, timestamp }) { - return baseHandlers.subscriptionCache.updateShortsByChannelId({ - channelId, - entries, - timestamp, - }) - } - - static updateShortsWithChannelPageShortsByChannelId({ channelId, entries }) { - return baseHandlers.subscriptionCache.updateShortsWithChannelPageShortsByChannelId({ - channelId, - entries, - }) - } - - static updateCommunityPostsByChannelId({ channelId, entries, timestamp }) { - return baseHandlers.subscriptionCache.updateCommunityPostsByChannelId({ - channelId, - entries, - timestamp, - }) - } - - static deleteMultipleChannels(channelIds) { - return baseHandlers.subscriptionCache.deleteMultipleChannels(channelIds) - } - - static deleteAll() { - return baseHandlers.subscriptionCache.deleteAll() - } -} - -export { - Settings as settings, - History as history, - Profiles as profiles, - Playlists as playlists, - SubscriptionCache as subscriptionCache, -} +// These classes don't require any changes from the base classes, so can be exported as-is. +export { history, profiles, playlists, subscriptionCache } from './base' diff --git a/src/main/index.js b/src/main/index.js index 10ca46343829e..d361bf19dfa98 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -22,6 +22,7 @@ import { brotliDecompress } from 'zlib' import contextMenu from 'electron-context-menu' import packageDetails from '../../package.json' +import { generatePoToken } from './poTokenGenerator' const brotliDecompressAsync = promisify(brotliDecompress) @@ -254,13 +255,16 @@ function runApp() { app.on('second-instance', (_, commandLine, __) => { // Someone tried to run a second instance, we should focus our window - if (mainWindow && typeof commandLine !== 'undefined') { - if (mainWindow.isMinimized()) mainWindow.restore() - mainWindow.focus() - + if (typeof commandLine !== 'undefined') { const url = getLinkUrl(commandLine) - if (url) { - mainWindow.webContents.send(IpcChannels.OPEN_URL, url) + if (mainWindow && mainWindow.webContents) { + if (mainWindow.isMinimized()) mainWindow.restore() + mainWindow.focus() + + if (url) mainWindow.webContents.send(IpcChannels.OPEN_URL, url) + } else { + if (url) startupUrl = url + createWindow() } } }) @@ -424,24 +428,9 @@ function runApp() { requestHeaders.Referer = 'https://www.youtube.com/' requestHeaders.Origin = 'https://www.youtube.com' - // Make iOS requests work and look more realistic - if (requestHeaders['x-youtube-client-name'] === '5') { - delete requestHeaders.Referer - delete requestHeaders.Origin - delete requestHeaders['Sec-Fetch-Site'] - delete requestHeaders['Sec-Fetch-Mode'] - delete requestHeaders['Sec-Fetch-Dest'] - delete requestHeaders['sec-ch-ua'] - delete requestHeaders['sec-ch-ua-mobile'] - delete requestHeaders['sec-ch-ua-platform'] - - requestHeaders['User-Agent'] = requestHeaders['x-user-agent'] - delete requestHeaders['x-user-agent'] - } else { - requestHeaders['Sec-Fetch-Site'] = 'same-origin' - requestHeaders['Sec-Fetch-Mode'] = 'same-origin' - requestHeaders['X-Youtube-Bootstrap-Logged-In'] = 'false' - } + requestHeaders['Sec-Fetch-Site'] = 'same-origin' + requestHeaders['Sec-Fetch-Mode'] = 'same-origin' + requestHeaders['X-Youtube-Bootstrap-Logged-In'] = 'false' } else if (urlObj.origin.endsWith('.googlevideo.com') && urlObj.pathname === '/videoplayback') { requestHeaders.Referer = 'https://www.youtube.com/' requestHeaders.Origin = 'https://www.youtube.com' @@ -455,7 +444,7 @@ function runApp() { requestHeaders.Authorization = invidiousAuthorization.authorization } } - + callback({ requestHeaders }) }) @@ -466,7 +455,7 @@ function runApp() { if (responseHeaders) { delete responseHeaders['set-cookie'] } - + callback({ responseHeaders }) }) @@ -662,6 +651,12 @@ function runApp() { return '#002B36' case 'solarized-light': return '#fdf6e3' + case 'gruvbox-dark': + return '#282828' + case 'gruvbox-light': + return '#fbf1c7' + case 'catppuccin-frappe': + return '#303446' case 'system': default: return nativeTheme.shouldUseDarkColors ? '#212121' : '#f1f1f1' @@ -690,7 +685,9 @@ function runApp() { webSecurity: false, backgroundThrottling: false, contextIsolation: false - } + }, + minWidth: 340, + minHeight: 380 } const newWindow = new BrowserWindow( @@ -757,7 +754,6 @@ function runApp() { // If called multiple times // Duplicate menu items will be added if (replaceMainWindow) { - setMenu() } @@ -824,10 +820,11 @@ function runApp() { }) } - ipcMain.once(IpcChannels.APP_READY, () => { + ipcMain.on(IpcChannels.APP_READY, () => { if (startupUrl) { - mainWindow.webContents.send(IpcChannels.OPEN_URL, startupUrl) + mainWindow.webContents.send(IpcChannels.OPEN_URL, startupUrl, { isLaunchLink: true }) } + startupUrl = null }) function relaunch() { @@ -875,6 +872,10 @@ function runApp() { }) }) + ipcMain.handle(IpcChannels.GENERATE_PO_TOKEN, (_, visitorData) => { + return generatePoToken(visitorData) + }) + ipcMain.on(IpcChannels.ENABLE_PROXY, (_, url) => { session.defaultSession.setProxy({ proxyRules: url @@ -887,6 +888,43 @@ function runApp() { session.defaultSession.closeAllConnections() }) + // #region navigation history + + const NAV_HISTORY_DISPLAY_LIMIT = 15 + // Math.trunc but with a bitwise OR so that it can be calcuated at build time and the number inlined + const HALF_OF_NAV_HISTORY_DISPLAY_LIMIT = (NAV_HISTORY_DISPLAY_LIMIT / 2) | 0 + + ipcMain.handle(IpcChannels.GET_NAVIGATION_HISTORY, ({ sender }) => { + const activeIndex = sender.navigationHistory.getActiveIndex() + const length = sender.navigationHistory.length() + + let end + + if (activeIndex < HALF_OF_NAV_HISTORY_DISPLAY_LIMIT) { + end = Math.min(length - 1, NAV_HISTORY_DISPLAY_LIMIT - 1) + } else if (length - activeIndex < HALF_OF_NAV_HISTORY_DISPLAY_LIMIT + 1) { + end = length - 1 + } else { + end = activeIndex + HALF_OF_NAV_HISTORY_DISPLAY_LIMIT + } + + const dropdownOptions = [] + + for (let index = end; index >= Math.max(0, end + 1 - NAV_HISTORY_DISPLAY_LIMIT); --index) { + const routeLabel = sender.navigationHistory.getEntryAtIndex(index)?.title + + dropdownOptions.push({ + label: routeLabel, + value: index - activeIndex, + active: index === activeIndex + }) + } + + return dropdownOptions + }) + + // #endregion navigation history + ipcMain.handle(IpcChannels.OPEN_EXTERNAL_LINK, (_, url) => { if (typeof url === 'string') { let parsedURL @@ -1269,11 +1307,7 @@ function runApp() { return null case DBActions.PLAYLISTS.DELETE_VIDEO_ID: - await baseHandlers.playlists.deleteVideoIdByPlaylistId({ - _id: data._id, - videoId: data.videoId, - playlistItemId: data.playlistItemId, - }) + await baseHandlers.playlists.deleteVideoIdByPlaylistId(data._id, data.videoId, data.playlistItemId) syncOtherWindows( IpcChannels.SYNC_PLAYLISTS, event, @@ -1326,7 +1360,7 @@ function runApp() { return await baseHandlers.subscriptionCache.find() case DBActions.SUBSCRIPTION_CACHE.UPDATE_VIDEOS_BY_CHANNEL: - await baseHandlers.subscriptionCache.updateVideosByChannelId(data) + await baseHandlers.subscriptionCache.updateVideosByChannelId(data.channelId, data.entries, data.timestamp) syncOtherWindows( IpcChannels.SYNC_SUBSCRIPTION_CACHE, event, @@ -1335,7 +1369,7 @@ function runApp() { return null case DBActions.SUBSCRIPTION_CACHE.UPDATE_LIVE_STREAMS_BY_CHANNEL: - await baseHandlers.subscriptionCache.updateLiveStreamsByChannelId(data) + await baseHandlers.subscriptionCache.updateLiveStreamsByChannelId(data.channelId, data.entries, data.timestamp) syncOtherWindows( IpcChannels.SYNC_SUBSCRIPTION_CACHE, event, @@ -1344,7 +1378,7 @@ function runApp() { return null case DBActions.SUBSCRIPTION_CACHE.UPDATE_SHORTS_BY_CHANNEL: - await baseHandlers.subscriptionCache.updateShortsByChannelId(data) + await baseHandlers.subscriptionCache.updateShortsByChannelId(data.channelId, data.entries, data.timestamp) syncOtherWindows( IpcChannels.SYNC_SUBSCRIPTION_CACHE, event, @@ -1353,7 +1387,7 @@ function runApp() { return null case DBActions.SUBSCRIPTION_CACHE.UPDATE_SHORTS_WITH_CHANNEL_PAGE_SHORTS_BY_CHANNEL: - await baseHandlers.subscriptionCache.updateShortsWithChannelPageShortsByChannelId(data) + await baseHandlers.subscriptionCache.updateShortsWithChannelPageShortsByChannelId(data.channelId, data.entries) syncOtherWindows( IpcChannels.SYNC_SUBSCRIPTION_CACHE, event, @@ -1362,7 +1396,7 @@ function runApp() { return null case DBActions.SUBSCRIPTION_CACHE.UPDATE_COMMUNITY_POSTS_BY_CHANNEL: - await baseHandlers.subscriptionCache.updateCommunityPostsByChannelId(data) + await baseHandlers.subscriptionCache.updateCommunityPostsByChannelId(data.channelId, data.entries, data.timestamp) syncOtherWindows( IpcChannels.SYNC_SUBSCRIPTION_CACHE, event, @@ -1417,6 +1451,7 @@ function runApp() { app.on('window-all-closed', () => { // Clean up resources (datastores' compaction + Electron cache and storage data clearing) cleanUpResources().finally(() => { + mainWindow = null if (process.platform !== 'darwin') { app.quit() } @@ -1486,6 +1521,7 @@ function runApp() { mainWindow.webContents.send(IpcChannels.OPEN_URL, baseUrl(url)) } else { startupUrl = baseUrl(url) + if (app.isReady()) createWindow() } }) diff --git a/src/main/poTokenGenerator.js b/src/main/poTokenGenerator.js new file mode 100644 index 0000000000000..5871d7483975d --- /dev/null +++ b/src/main/poTokenGenerator.js @@ -0,0 +1,140 @@ +import { session, WebContentsView } from 'electron' +import { readFile } from 'fs/promises' +import { join } from 'path' + +/** + * Generates a poToken (proof of origin token) using `bgutils-js`. + * The script to generate it is `src/botGuardScript.js` + * + * This is intentionally split out into it's own thing, with it's own temporary in-memory session, + * as the BotGuard stuff accesses the global `document` and `window` objects and also requires making some requests. + * So we definitely don't want it running in the same places as the rest of the FreeTube code with the user data. + * @param {string} visitorData + * @returns {Promise} + */ +export async function generatePoToken(visitorData) { + const sessionUuid = crypto.randomUUID() + + const theSession = session.fromPartition(`potoken-${sessionUuid}`, { cache: false }) + + theSession.setPermissionCheckHandler(() => false) + // eslint-disable-next-line n/no-callback-literal + theSession.setPermissionRequestHandler((webContents, permission, callback) => callback(false)) + + theSession.setUserAgent( + theSession.getUserAgent() + .split(' ') + .filter(part => !part.includes('Electron')) + .join(' ') + ) + + const webContentsView = new WebContentsView({ + webPreferences: { + backgroundThrottling: false, + safeDialogs: true, + sandbox: true, + v8CacheOptions: 'none', + session: theSession, + offscreen: true + } + }) + + webContentsView.webContents.setWindowOpenHandler(() => ({ action: 'deny' })) + + webContentsView.webContents.setAudioMuted(true) + webContentsView.setBounds({ + x: 0, + y: 0, + width: 1920, + height: 1080 + }) + + webContentsView.webContents.debugger.attach() + + await webContentsView.webContents.loadURL('data:text/html,', { + baseURLForDataURL: 'https://www.youtube.com' + }) + + await webContentsView.webContents.debugger.sendCommand('Emulation.setUserAgentOverride', { + userAgent: theSession.getUserAgent(), + acceptLanguage: 'en-US', + platform: 'Win32', + userAgentMetadata: { + brands: [ + { + brand: 'Not/A)Brand', + version: '99' + }, + { + brand: 'Chromium', + version: process.versions.chrome.split('.')[0] + } + ], + fullVersionList: [ + { + brand: 'Not/A)Brand', + version: '99.0.0.0' + }, + { + brand: 'Chromium', + version: process.versions.chrome + } + ], + platform: 'Windows', + platformVersion: '10.0.0', + architecture: 'x86', + model: '', + mobile: false, + bitness: '64', + wow64: false + } + }) + + await webContentsView.webContents.debugger.sendCommand('Emulation.setDeviceMetricsOverride', { + width: 1920, + height: 1080, + deviceScaleFactor: 1, + mobile: false, + screenWidth: 1920, + screenHeight: 1080, + positionX: 0, + positionY: 0, + screenOrientation: { + type: 'landscapePrimary', + angle: 0 + } + }) + + const script = await getScript(visitorData) + + const response = await webContentsView.webContents.executeJavaScript(script) + + webContentsView.webContents.close({ waitForBeforeUnload: false }) + await theSession.closeAllConnections() + + return response +} + +let cachedScript + +/** + * @param {string} visitorData + */ +async function getScript(visitorData) { + if (!cachedScript) { + const pathToScript = process.env.NODE_ENV === 'development' + ? join(__dirname, '../../dist/botGuardScript.js') + /* eslint-disable-next-line n/no-path-concat */ + : `${__dirname}/botGuardScript.js` + + const content = await readFile(pathToScript, 'utf-8') + + const match = content.match(/export{(\w+) as default};/) + + const functionName = match[1] + + cachedScript = content.replace(match[0], `;${functionName}("FT_VISITOR_DATA")`) + } + + return cachedScript.replace('FT_VISITOR_DATA', visitorData) +} diff --git a/src/renderer/App.js b/src/renderer/App.js index 6f29f4e2a7c42..e7ebffa07ca9b 100644 --- a/src/renderer/App.js +++ b/src/renderer/App.js @@ -1,16 +1,17 @@ import { defineComponent } from 'vue' -import { mapActions } from 'vuex' +import { mapActions, mapMutations } from 'vuex' import FtFlexBox from './components/ft-flex-box/ft-flex-box.vue' import TopNav from './components/top-nav/top-nav.vue' -import SideNav from './components/side-nav/side-nav.vue' +import SideNav from './components/SideNav/SideNav.vue' import FtNotificationBanner from './components/ft-notification-banner/ft-notification-banner.vue' import FtPrompt from './components/ft-prompt/ft-prompt.vue' import FtButton from './components/ft-button/ft-button.vue' import FtToast from './components/ft-toast/ft-toast.vue' -import FtProgressBar from './components/ft-progress-bar/ft-progress-bar.vue' +import FtProgressBar from './components/FtProgressBar/FtProgressBar.vue' import FtPlaylistAddVideoPrompt from './components/ft-playlist-add-video-prompt/ft-playlist-add-video-prompt.vue' import FtCreatePlaylistPrompt from './components/ft-create-playlist-prompt/ft-create-playlist-prompt.vue' -import FtSearchFilters from './components/ft-search-filters/ft-search-filters.vue' +import FtKeyboardShortcutPrompt from './components/FtKeyboardShortcutPrompt/FtKeyboardShortcutPrompt.vue' +import FtSearchFilters from './components/FtSearchFilters/FtSearchFilters.vue' import { marked } from 'marked' import { IpcChannels } from '../constants' import packageDetails from '../../package.json' @@ -32,7 +33,8 @@ export default defineComponent({ FtProgressBar, FtPlaylistAddVideoPrompt, FtCreatePlaylistPrompt, - FtSearchFilters + FtSearchFilters, + FtKeyboardShortcutPrompt, }, data: function () { return { @@ -71,6 +73,9 @@ export default defineComponent({ checkForBlogPosts: function () { return this.$store.getters.getCheckForBlogPosts }, + isKeyboardShortcutPromptShown: function () { + return this.$store.getters.getIsKeyboardShortcutPromptShown + }, showAddToPlaylistPrompt: function () { return this.$store.getters.getShowAddToPlaylistPrompt }, @@ -82,7 +87,7 @@ export default defineComponent({ }, windowTitle: function () { const routePath = this.$route.path - if (!routePath.startsWith('/channel/') && !routePath.startsWith('/watch/') && !routePath.startsWith('/hashtag/') && !routePath.startsWith('/playlist/')) { + if (!routePath.startsWith('/channel/') && !routePath.startsWith('/watch/') && !routePath.startsWith('/hashtag/') && !routePath.startsWith('/playlist/') && !routePath.startsWith('/search/')) { let title = translateWindowTitle(this.$route.meta.title) if (!title) { title = packageDetails.productName @@ -97,6 +102,7 @@ export default defineComponent({ externalPlayer: function () { return this.$store.getters.getExternalPlayer }, + defaultInvidiousInstance: function () { return this.$store.getters.getDefaultInvidiousInstance }, @@ -142,6 +148,14 @@ export default defineComponent({ externalLinkHandling: function () { return this.$store.getters.getExternalLinkHandling + }, + + appTitle: function () { + return this.$store.getters.getAppTitle + }, + + openDeepLinksInNewWindow: function () { + return this.$store.getters.getOpenDeepLinksInNewWindow } }, watch: { @@ -154,10 +168,11 @@ export default defineComponent({ secColor: 'checkThemeSettings', locale: 'setLocale', + + appTitle: 'setDocumentTitle' }, created () { this.checkThemeSettings() - this.setWindowTitle() this.setLocale() }, mounted: function () { @@ -203,10 +218,16 @@ export default defineComponent({ if (this.$router.currentRoute.path === '/') { this.$router.replace({ path: this.landingPage }) } + + this.setWindowTitle() }) }) }, methods: { + setDocumentTitle: function(value) { + document.title = value + this.$nextTick(() => this.$refs.topNav?.setActiveNavigationHistoryEntryTitle(value)) + }, checkThemeSettings: function () { const theme = { baseTheme: this.baseTheme || 'dark', @@ -331,6 +352,10 @@ export default defineComponent({ }, handleKeyboardShortcuts: function (event) { + if (event.shiftKey && event.key === '?') { + this.$store.commit('setIsKeyboardShortcutPromptShown', !this.isKeyboardShortcutPromptShown) + } + if (event.altKey) { switch (event.key) { case 'D': @@ -512,9 +537,9 @@ export default defineComponent({ }, enableOpenUrl: function () { - ipcRenderer.on(IpcChannels.OPEN_URL, (event, url) => { + ipcRenderer.on(IpcChannels.OPEN_URL, (event, url, { isLaunchLink = false } = { }) => { if (url) { - this.handleYoutubeLink(url) + this.handleYoutubeLink(url, { doCreateNewWindow: this.openDeepLinksInNewWindow && !isLaunchLink }) } }) @@ -535,7 +560,7 @@ export default defineComponent({ setWindowTitle: function() { if (this.windowTitle !== null) { - document.title = this.windowTitle + this.setAppTitle(this.windowTitle) } }, @@ -560,11 +585,17 @@ export default defineComponent({ 'fetchInvidiousInstancesFromFile', 'setRandomCurrentInvidiousInstance', 'setupListenersToSyncWindows', + 'hideKeyboardShortcutPrompt', + 'showKeyboardShortcutPrompt', 'updateBaseTheme', 'updateMainColor', 'updateSecColor', 'showOutlines', 'hideOutlines', + ]), + + ...mapMutations([ + 'setAppTitle' ]) } }) diff --git a/src/renderer/App.vue b/src/renderer/App.vue index a0235a5854898..f4ac948e85e41 100644 --- a/src/renderer/App.vue +++ b/src/renderer/App.vue @@ -50,6 +50,9 @@ + diff --git a/src/renderer/components/channel-about/channel-about.css b/src/renderer/components/ChannelAbout/ChannelAbout.css similarity index 100% rename from src/renderer/components/channel-about/channel-about.css rename to src/renderer/components/ChannelAbout/ChannelAbout.css diff --git a/src/renderer/components/channel-about/channel-about.vue b/src/renderer/components/ChannelAbout/ChannelAbout.vue similarity index 57% rename from src/renderer/components/channel-about/channel-about.vue rename to src/renderer/components/ChannelAbout/ChannelAbout.vue index 5829f8bb58293..5cd8bf4d950f2 100644 --- a/src/renderer/components/channel-about/channel-about.vue +++ b/src/renderer/components/ChannelAbout/ChannelAbout.vue @@ -96,18 +96,84 @@ v-if="!hideFeaturedChannels && relatedChannels.length > 0" >

{{ $t("Channel.About.Featured Channels") }}

- - + - + - + +