Merge branch 'development'

This commit is contained in:
PrestonN 2022-11-01 22:44:24 -04:00
commit 4fc5b47416
273 changed files with 12398 additions and 7417 deletions

View File

@ -4,8 +4,8 @@
"@babel/env",
{
"targets": {
"chrome": "96",
"node": 16
"chrome": "106",
"node": "16.16.0"
}
}
]

View File

@ -32,12 +32,14 @@ module.exports = {
plugins: ['vue'],
rules: {
'space-before-function-paren': 0,
'space-before-function-paren': 'off',
'comma-dangle': ['error', 'never'],
'vue/no-v-html': 'off',
'no-console': 0,
'no-unused-vars': 1,
'no-undef': 1,
'vue/no-template-key': 1
'no-console': ['error', { allow: ['warn', 'error'] }],
'no-unused-vars': 'warn',
'no-undef': 'warn',
'vue/no-template-key': 'warn',
'vue/no-useless-template-attributes': 'off',
'vue/multi-word-component-names': 'off'
}
}

View File

@ -15,7 +15,7 @@ body:
options:
- label: I have encountered this bug in the [latest release of FreeTube](https://github.com/FreeTubeApp/FreeTube/releases).
required: true
- label: I have searched the [issue tracker](https://github.com/FreeTubeApp/FreeTube/issues) for a bug report that matches the one I want to file, without success.
- label: I have searched the [issue tracker](https://github.com/FreeTubeApp/FreeTube/issues) for open and closed issues that are similar to the bug report I want to file, without success.
required: true
- label: I have searched the [documentation](https://docs.freetubeapp.io/) for information that matches the description of the bug I want to file, without success.
required: true
@ -85,10 +85,12 @@ body:
- .dmg
- .exe
- Flathub
- MPR
- .pacman
- Portable
- PortableApps
- .rpm
- Scoop
- winget
- .zip
- other

View File

@ -13,7 +13,7 @@ body:
label: Guidelines
description: Please ensure you've completed all of the following.
options:
- label: I have searched the [issue tracker](https://github.com/FreeTubeApp/FreeTube/issues) for a feature request that matches the one I want to file, without success.
- label: I have searched the [issue tracker](https://github.com/FreeTubeApp/FreeTube/issues) for open and closed issues that are similar to the feature request I want to file, without success.
required: true
- label: I have searched the [documentation](https://docs.freetubeapp.io/) for information that matches the description of the feature request I want to file, without success.
required: true

View File

@ -1,34 +1,36 @@
---
Title
---
# Title
**Important note**
We may remove your pull request if you do not use this provided PR template correctly.
<!-- Thanks for sending a pull request! Make sure to follow the contributing guidelines. -->
<!-- Important note, we may remove your pull request if you do not use this provided PR template correctly. -->
**Pull Request Type**
Please select what type of pull request this is:
## Pull Request Type
<!-- Please select what type of pull request this is: [x] -->
- [ ] Bugfix
- [ ] Feature Implementation
- [ ] Documentation
- [ ] Other
**Related issue**
Please link the issue your pull request is referring to. If this pull request fully resolves the relevant issue, put "closes" before the issue number. Example: "closes #123456".
## Related issue
<!-- Please link the issue your pull request is referring to. -->
<!-- If this pull request fully resolves the relevant issue, put "closes" before the issue number. -->
<!-- Example: "closes #123456". -->
**Description**
Please write a clear and concise description of what the pull request does.
## Description
<!-- Please write a clear and concise description of what the pull request does. -->
**Screenshots (if appropriate)**
Please add before and after screenshots if there is a visible change.
## Screenshots <!-- If appropriate -->
<!-- Please add before and after screenshots if there is a visible change. -->
**Testing (for code that is not small enough to be easily understandable)**
Has this pull request been tested?
Please describe shortly how you tested it and whether there are any ramifications remaining.
## Testing <!-- for code that is not small enough to be easily understandable -->
<!-- Has this pull request been tested? -->
<!-- Please describe shortly how you tested it. -->
<!-- Are there any ramifications remaining? -->
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- OS Version: [e.g. 22]
- FreeTube version: [e.g. 0.8]
## Desktop
<!-- Please complete the following information-->
- **OS:**
- **OS Version:**
- **FreeTube version:**
**Additional context**
Add any other context about the problem here.
## Additional context
<!-- Add any other context about the pull request here. -->

16
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,16 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
labels:
- "PR: waiting for review"
- "PR: dependencies"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
labels:
- "PR: waiting for review"
- "PR: dependencies"

19
.github/labeler.yml vendored Normal file
View File

@ -0,0 +1,19 @@
'PR: waiting for review':
- '*'
- '.babelrc'
- '.editorconfig'
- '.eslintignore'
- '.eslintrc.js'
- '.gitignore'
- '.prettierrc'
- '.whitesource'
- '.github/**/*'
- '.vscode/**/*'
- '_icons/**/*'
- '_scripts/**/*'
- 'src/**/*'
- 'static/**/*'
'PR: dependencies':
- 'yarn.lock'
- 'package.json'

View File

@ -10,5 +10,95 @@ jobs:
- uses: Naturalclar/issue-action@v2.0.2
with:
body: "both"
parameters: '[ {"keywords": ["visual bug"], "labels": ["B: visual"]}, {"keywords": ["AUR", "Chocolatey", "PortableApps", "winget"], "labels": ["B: Unofficial Download"]}, {"keywords": ["keyboard control not working"], "labels": ["B: keyboard control"]}, {"keywords": ["text/string issue"], "labels": ["B: text/string"]}, {"keywords": ["content not loading"], "labels": ["B: content not loading"]}, {"keywords": ["accessibility issue"], "labels": ["B: accessibility"]}, {"keywords": ["usability issue"], "labels": ["B: usability"]}, {"keywords": ["causes crash"], "labels": ["B: crash"]}, {"keywords": ["feature stopped working"], "labels": ["B: feature stopped working"]}, {"keywords": ["inconsistent behavior"], "labels": ["B: inconsistent behavior"]}, {"keywords": ["data loss"], "labels": ["B: data loss"]}, {"keywords": ["race condition"], "labels": ["B: race condition"]}, {"keywords": ["API issue"], "labels": ["B: API issue"]}, {"keywords": ["only happens in developer mode"], "labels": ["B: developer mode"]}, {"keywords": ["improvement to existing feature"], "labels": ["E: improvement existing feature"]}, {"keywords": ["new optional setting"], "labels": ["E: new optional setting"]}, {"keywords": ["visual improvement"], "labels": ["E: visual improvement"]}, {"keywords": ["display more information to user"], "labels": ["E: display more information"]}, {"keywords": ["ease of use improvement"], "labels": ["E: ease of use improvement"]}, {"keywords": ["support for external software"], "labels": ["E: support external software"]}, {"keywords": ["new feature"], "labels": ["E: new feature"]}, {"keywords": ["new keyboard shortcut"], "labels": ["E: keyboard shortcut"]}]'
parameters: >-
[
{
"keywords": ["visual bug"],
"labels": ["B: visual"]
},
{
"keywords": ["AUR", "Chocolatey", "PortableApps", "winget", "Scoop", "MPR"],
"labels": ["B: Unofficial Download"]
},
{
"keywords": ["keyboard control not working"],
"labels": ["B: keyboard control"]
},
{
"keywords": ["text/string issue"],
"labels": ["B: text/string"]
},
{
"keywords": ["content not loading"],
"labels": ["B: content not loading"]
},
{
"keywords": ["accessibility issue"],
"labels": ["B: accessibility"]
},
{
"keywords": ["usability issue"],
"labels": ["B: usability"]
},
{
"keywords": ["causes crash"],
"labels": ["B: crash"]
},
{
"keywords": ["feature stopped working"],
"labels": ["B: feature stopped working"]
},
{
"keywords": ["inconsistent behavior"],
"labels": ["B: inconsistent behavior"]
},
{
"keywords": ["data loss"],
"labels": ["B: data loss"]
},
{
"keywords": ["race condition"],
"labels": ["B: race condition"]
},
{
"keywords": ["API issue"],
"labels": ["B: API issue"]
},
{
"keywords": ["only happens in developer mode"],
"labels": ["B: developer mode"]
},
{
"keywords": ["improvement to existing feature"],
"labels": ["E: improvement existing feature"]
},
{
"keywords": ["new optional setting"],
"labels": ["E: new optional setting"]
},
{
"keywords": ["visual improvement"],
"labels": ["E: visual improvement"]
},
{
"keywords": ["display more information to user"],
"labels": ["E: display more information"]
},
{
"keywords": ["ease of use improvement"],
"labels": ["E: ease of use improvement"]
},
{
"keywords": ["support for external software"],
"labels": ["E: support external software"]
},
{
"keywords": ["new feature"],
"labels": ["E: new feature"]
},
{
"keywords": ["new keyboard shortcut"],
"labels": ["E: keyboard shortcut"]
}
]
github-token: "${{ secrets.GITHUB_TOKEN }}"

View File

@ -6,37 +6,56 @@ name: Build
on:
push:
branches: [ master, development, '**-RC' ]
workflow_dispatch:
jobs:
build:
strategy:
matrix:
node-version: [16.x]
runtime: [ linux-x64, linux-arm64, win-x64, osx-x64 ]
runtime:
- linux-x64
- linux-armv7l
- linux-arm64
- 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
include:
- runtime: linux-x64
os: ubuntu-latest
- runtime: linux-armv7l
os: ubuntu-latest
- runtime: linux-arm64
os: ubuntu-latest
- runtime: osx-x64
os: macOS-latest
# - runtime: osx-arm64
# os: macOS-latest
- runtime: win-x64
os: windows-latest
- runtime: win-arm64
os: windows-latest
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: "yarn"
- run: npm run ci
- run: npm run lint
- run: yarn run ci
- run: yarn run lint
- name: Get Version Number
uses: nyaayaya/package-version@v1
with:
@ -45,7 +64,7 @@ jobs:
- name: Set Version Number Variable
id: versionNumber
uses: actions/github-script@v3
uses: actions/github-script@v6
env:
IS_DEV: ${{ contains(github.ref, 'development') }}
IS_RC: ${{ contains(github.ref, 'RC') }}
@ -65,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@v1
uses: jossef/action-set-json-field@v2
with:
file: package.json
field: version
@ -73,125 +92,243 @@ jobs:
- name: Install libarchive-tools
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
if: startsWith(matrix.os, 'ubuntu')
run: 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')
run: npm run build --if-present
run: yarn run build
- name: Build ARMv7l with Node.js ${{ matrix.node-version}}
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
run: yarn run build:arm32
- name: Build ARM64 with Node.js ${{ matrix.node-version}}
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
run: npm run build:arm64 --if-present
if: contains(matrix.runtime, 'arm64')
run: yarn run build:arm64
- name: Upload Linux .zip x64 Artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
with:
name: freetube_${{ steps.versionNumber.outputs.result }}_linux_portable_x64
path: build/freetube-${{ steps.versionNumber.outputs.result }}.zip
- name: Upload Linux .zip ARM Artifact
uses: actions/upload-artifact@v2
- name: Upload Linux .7z x64 Artifact
uses: actions/upload-artifact@v3
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
with:
name: freetube_${{ steps.versionNumber.outputs.result }}_linux_portable_x64.7z
path: build/freetube-${{ steps.versionNumber.outputs.result }}.7z
- name: Upload Linux .zip ARMv7l Artifact
uses: actions/upload-artifact@v3
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
with:
name: freetube_${{ steps.versionNumber.outputs.result }}_linux_portable_armv7l
path: build/freetube-${{ steps.versionNumber.outputs.result }}-armv7l.zip
- name: Upload Linux .7z ARMv7l Artifact
uses: actions/upload-artifact@v3
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
with:
name: freetube_${{ steps.versionNumber.outputs.result }}_linux_portable_armv7l.7z
path: build/freetube-${{ steps.versionNumber.outputs.result }}-armv7l.7z
- name: Upload Linux .zip ARM64 Artifact
uses: actions/upload-artifact@v3
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
with:
name: freetube_${{ steps.versionNumber.outputs.result }}_linux_portable_arm64
path: build/freetube-${{ steps.versionNumber.outputs.result }}-arm64.zip
- name: Upload Linux .7z ARM64 Artifact
uses: actions/upload-artifact@v3
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
with:
name: freetube_${{ steps.versionNumber.outputs.result }}_linux_portable_arm64.7z
path: build/freetube-${{ steps.versionNumber.outputs.result }}-arm64.7z
- name: Upload .deb x64 Artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
with:
name: freetube_${{ steps.versionNumber.outputs.result }}_amd64.deb
path: build/freetube_${{ steps.versionNumber.outputs.result }}_amd64.deb
- name: Upload .deb ARM Artifact
uses: actions/upload-artifact@v2
- name: Upload .deb ARMv7l Artifact
uses: actions/upload-artifact@v3
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
with:
name: freetube_${{ steps.versionNumber.outputs.result }}_armv7l.deb
path: build/freetube_${{ steps.versionNumber.outputs.result }}_armv7l.deb
- name: Upload .deb ARM64 Artifact
uses: actions/upload-artifact@v3
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
with:
name: freetube_${{ steps.versionNumber.outputs.result }}_arm64.deb
path: build/freetube_${{ steps.versionNumber.outputs.result }}_arm64.deb
- name: Upload AppImage x64 Artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
with:
name: freetube_${{ steps.versionNumber.outputs.result }}_amd64.AppImage
path: build/FreeTube-${{ steps.versionNumber.outputs.result }}.AppImage
- name: Upload AppImage ARM Artifact
uses: actions/upload-artifact@v2
- name: Upload AppImage ARMv7l Artifact
uses: actions/upload-artifact@v3
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
with:
name: freetube_${{ steps.versionNumber.outputs.result }}_armv7l.AppImage
path: build/FreeTube-${{ steps.versionNumber.outputs.result }}-armv7l.AppImage
- name: Upload AppImage ARM64 Artifact
uses: actions/upload-artifact@v3
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
with:
name: freetube_${{ steps.versionNumber.outputs.result }}_arm64.AppImage
path: build/FreeTube-${{ steps.versionNumber.outputs.result }}-arm64.AppImage
- name: Upload .rpm x64 Artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
with:
name: freetube_${{ steps.versionNumber.outputs.result }}_amd64.rpm
path: build/freetube-${{ steps.versionNumber.outputs.result }}.x86_64.rpm
- name: Upload .rpm ARM Artifact
uses: actions/upload-artifact@v2
# rpm are not built for armv7l
- name: Upload .rpm ARM64 Artifact
uses: actions/upload-artifact@v3
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
with:
name: freetube_${{ steps.versionNumber.outputs.result }}_arm64.rpm
path: build/freetube-${{ steps.versionNumber.outputs.result }}.aarch64.rpm
- name: Upload Alpine .apk x64 Artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
with:
name: freetube_${{ steps.versionNumber.outputs.result }}_alpine_amd64.apk
path: build/freetube-${{ steps.versionNumber.outputs.result }}.apk
- name: Upload Alpine .apk ARM Artifact
uses: actions/upload-artifact@v2
- name: Upload Alpine .apk ARMv7l Artifact
uses: actions/upload-artifact@v3
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
with:
name: freetube_${{ steps.versionNumber.outputs.result }}_alpine_armv7l.apk
path: build/freetube-${{ steps.versionNumber.outputs.result }}-armv7l.apk
- name: Upload Alpine .apk ARM64 Artifact
uses: actions/upload-artifact@v3
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
with:
name: freetube_${{ steps.versionNumber.outputs.result }}_alpine_arm64.apk
path: build/freetube-${{ steps.versionNumber.outputs.result }}-arm64.apk
- name: Upload Pacman .pacman x64 Artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
with:
name: freetube_${{ steps.versionNumber.outputs.result }}_amd64.pacman
path: build/freetube-${{ steps.versionNumber.outputs.result }}.pacman
# - name: Upload Web Build
# uses: actions/upload-artifact@v2
# uses: actions/upload-artifact@v3
# if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
# with:
# name: freetube_${{ steps.versionNumber.outputs.result }}_static_web
# path: dist/web
- name: Upload Windows .exe Artifact
uses: actions/upload-artifact@v2
if: startsWith(matrix.os, 'windows')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-win-x64-portable
path: build/freetube-${{ steps.versionNumber.outputs.result }}-win.zip
- name: Upload Windows .zip Artifact
uses: actions/upload-artifact@v2
if: startsWith(matrix.os, 'windows')
- name: Upload Windows x64 .exe Artifact
uses: actions/upload-artifact@v3
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-x64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-setup-x64.exe
path: build/freetube Setup ${{ steps.versionNumber.outputs.result }}.exe
- name: Upload Windows Portable Artifact
uses: actions/upload-artifact@v2
if: startsWith(matrix.os, 'windows')
- name: Upload Windows arm64 .exe Artifact
uses: actions/upload-artifact@v3
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-arm64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-setup-arm64.exe
path: build/freetube Setup ${{ steps.versionNumber.outputs.result }}.exe
- name: Upload Windows x64 .zip Artifact
uses: actions/upload-artifact@v3
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-x64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-win-x64-portable
path: build/freetube-${{ steps.versionNumber.outputs.result }}-win.zip
- name: Upload Windows x64 .7z Artifact
uses: actions/upload-artifact@v3
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-x64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-win-x64-portable.7z
path: build/freetube-${{ steps.versionNumber.outputs.result }}-win.7z
- name: Upload Windows arm64 .zip Artifact
uses: actions/upload-artifact@v3
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-arm64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-win-arm64-portable
path: build/freetube-${{ steps.versionNumber.outputs.result }}-arm64-win.zip
- name: Upload Windows arm64 .7z Artifact
uses: actions/upload-artifact@v3
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-arm64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-win-arm64-portable.7z
path: build/freetube-${{ steps.versionNumber.outputs.result }}-arm64-win.7z
- name: Upload Windows x64 Portable Artifact
uses: actions/upload-artifact@v3
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-x64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-portable-x64.exe
path: build/freetube ${{ steps.versionNumber.outputs.result }}.exe
- name: Upload Mac .dmg Artifact
uses: actions/upload-artifact@v2
if: startsWith(matrix.os, 'macos')
- name: Upload Windows arm64 Portable Artifact
uses: actions/upload-artifact@v3
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-arm64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-mac.dmg
name: freetube-${{ steps.versionNumber.outputs.result }}-portable-arm64.exe
path: build/freetube ${{ steps.versionNumber.outputs.result }}.exe
- name: Upload Mac x64 .dmg Artifact
uses: actions/upload-artifact@v3
if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-x64')
with:
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@v3
# 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@v3
if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-x64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-mac-x64.zip
path: build/freetube-${{ steps.versionNumber.outputs.result }}-mac.zip
- name: Upload Mac x64 .7z Artifact
uses: actions/upload-artifact@v3
if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-x64')
with:
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@v3
# 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

View File

@ -0,0 +1,28 @@
# Compress images on demand (workflow_dispatch), and at 12am every Sunday (schedule).
# Open a Pull Request if any images can be compressed.
name: Compress Images
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * 0'
jobs:
build:
name: calibreapp/image-actions
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v3
- name: Compress Images
id: calibre
uses: calibreapp/image-actions@main
with:
githubToken: ${{ secrets.GITHUB_TOKEN }}
compressOnly: true
- name: Create New Pull Request If Needed
if: steps.calibre.outputs.markdown != ''
uses: peter-evans/create-pull-request@v4
with:
title: Compressed Images Nightly
branch-suffix: timestamp
commit-message: Compressed Images
body: ${{ steps.calibre.outputs.markdown }}

63
.github/workflows/codeql.yml vendored Normal file
View File

@ -0,0 +1,63 @@
name: "CodeQL"
on:
push:
branches: [ "development" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "development" ]
schedule:
- cron: '36 3 * * 5'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# 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
uses: github/codeql-action/autobuild@v2
# 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.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"

25
.github/workflows/conflicts.yml vendored Normal file
View File

@ -0,0 +1,25 @@
name: "Conflicts"
on:
# So that PRs touching the same files as the push are updated
push:
# So that the `dirtyLabel` is removed if conflicts are resolve
# We recommend `pull_request_target` so that github secrets are available.
# 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:
runs-on: ubuntu-latest
steps:
- name: check if prs are dirty
uses: eps1lon/actions-label-merge-conflict@releases/2.x
with:
dirtyLabel: "PR: merge conflicts / rebase needed"
removeOnDirtyLabel: "PR: waiting for review"
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."

9
.github/workflows/dummy-conflicts.yml vendored Normal file
View File

@ -0,0 +1,9 @@
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"

View File

@ -15,10 +15,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
repository: flathub/io.freetubeapp.FreeTube
token: ${{ secrets.PUSH_TOKEN }}
token: ${{ secrets.FLATHUB_TOKEN }}
- name: GitHub API exec action
uses: moustacheful/github-api-exec-action@v0
id: api_results
@ -36,7 +36,7 @@ jobs:
- name: Install xmlstarlet
run: sudo apt -y install xmlstarlet
- name: Create Version Variable
uses: bluwy/substitute-string-action@v1
uses: bluwy/substitute-string-action@v2
id: sub
with:
_input-text: ${{ fromJson(steps.api_results.outputs.result).tag_name }}
@ -77,25 +77,13 @@ jobs:
date +"%Y-%m-%d" >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
- name: Update x64 File Location in yml File
uses: mikefarah/yq@4.0.0-beta1
with:
# The Command which should be run
cmd: yq w -i io.freetubeapp.FreeTube.yml 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'
run: yq w -i io.freetubeapp.FreeTube.yml 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'
- name: Update x64 Hash in yml File
uses: mikefarah/yq@4.0.0-beta1
with:
# The Command which should be run
cmd: yq w -i io.freetubeapp.FreeTube.yml modules[0].sources[0].sha256 ${{ env.HASH_X64 }}
run: yq w -i io.freetubeapp.FreeTube.yml modules[0].sources[0].sha256 ${{ env.HASH_X64 }}
- name: Update ARM File Location in yml File
uses: mikefarah/yq@4.0.0-beta1
with:
# The Command which should be run
cmd: yq w -i io.freetubeapp.FreeTube.yml 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'
run: yq w -i io.freetubeapp.FreeTube.yml 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'
- name: Update ARM Hash in yml File
uses: mikefarah/yq@4.0.0-beta1
with:
# The Command which should be run
cmd: yq w -i io.freetubeapp.FreeTube.yml modules[0].sources[1].sha256 ${{ env.HASH_ARM64 }}
run: yq w -i io.freetubeapp.FreeTube.yml modules[0].sources[1].sha256 ${{ env.HASH_ARM64 }}
- name: Add Patch Notes to XML File
run: xmlstarlet ed -L -i /application/releases/release[1] -t elem -n releaseTMP -v "" -i //releaseTMP -t attr -n version -v "${{ steps.sub.outputs.result }} Beta" -i //releaseTMP -t attr -n date -v "${{ env.CURRENT_DATE }}" -s //releaseTMP -t elem -n url -v "" -s //releaseTMP/url -t text -n "" -v "https://github.com/FreeTubeApp/FreeTube/releases/tag/v${{ steps.sub.outputs.result }}-beta" -r //releaseTMP -v "release" io.freetubeapp.FreeTube.metainfo.xml
- name: Remove Release Files
@ -108,7 +96,7 @@ jobs:
# Optional but recommended
# Defaults to "Apply automatic changes"
commit_message: Update files for v${{ steps.sub.outputs.result }}
token: ${{ secrets.PUSH_TOKEN }}
token: ${{ secrets.FLATHUB_TOKEN }}
# Optional options appended to `git-commit`
# See https://git-scm.com/docs/git-commit for a list of available options
@ -118,7 +106,7 @@ jobs:
skip_dirty_check: true
- name: Create PR
run: |
echo ${{ secrets.PUSH_TOKEN }} >> auth.txt
echo ${{ secrets.FLATHUB_TOKEN }} >> auth.txt
gh auth login --with-token < auth.txt
rm auth.txt
gh pr create --title "Release v${{ steps.sub.outputs.result }}" --body "This is an automated PR for the v${{ steps.sub.outputs.result }} release. This PR will be updated and merged once testing is complete."

15
.github/workflows/label-pr.yml vendored Normal file
View File

@ -0,0 +1,15 @@
name: "Pull Request Labeler"
on:
pull_request_target:
types: [opened, reopened]
jobs:
triage:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v4
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"

View File

@ -17,11 +17,11 @@ jobs:
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Use Node.js 16.x
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: "yarn"
- run: npm run ci
- run: npm run lint
- run: yarn run ci
- run: yarn run lint

View File

@ -20,5 +20,5 @@ jobs:
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: 21
daysUntilClose: 14
responseRequiredLabel: "U: Waiting for Response from Author"

View File

@ -13,31 +13,49 @@ jobs:
strategy:
matrix:
node-version: [16.x]
runtime: [ linux-x64, linux-arm64, win-x64, osx-x64 ]
runtime:
- linux-x64
- linux-armv7l
- linux-arm64
- 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
include:
- runtime: linux-x64
os: ubuntu-latest
- runtime: linux-armv7l
os: ubuntu-latest
- runtime: linux-arm64
os: ubuntu-latest
- runtime: osx-x64
os: macOS-latest
# - runtime: osx-arm64
# os: macOS-latest
- runtime: win-x64
os: windows-latest
- runtime: win-arm64
os: windows-latest
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: "yarn"
- run: npm run ci
- run: npm run lint
- run: yarn run ci
- run: yarn run lint
- name: Get Version Number
uses: nyaayaya/package-version@v1
@ -47,11 +65,15 @@ jobs:
- name: Build x64 with Node.js ${{ matrix.node-version}}
if: contains(matrix.runtime, 'x64')
run: npm run build --if-present
run: yarn run build
- name: Build ARMv7l with Node.js ${{ matrix.node-version}}
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
run: yarn run build:arm32
- name: Build ARM64 with Node.js ${{ matrix.node-version}}
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
run: npm run build:arm64 --if-present
if: contains(matrix.runtime, 'arm64')
run: yarn run build:arm64
- name: Upload AppImage x64 Release
uses: actions/upload-release-asset@v1
@ -75,7 +97,40 @@ jobs:
asset_path: build/freetube-${{ env.PACKAGE_VERSION }}.zip
asset_content_type: application/zip
- name: Upload Linux .zip ARM Release
- name: Upload Linux .7z 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-${{ env.PACKAGE_VERSION }}-linux-portable-x64.7z
asset_path: build/freetube-${{ env.PACKAGE_VERSION }}.7z
asset_content_type: application/x-7z-compressed
- name: Upload Linux .zip 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-${{ env.PACKAGE_VERSION }}-linux-portable-armv7l.zip
asset_path: build/freetube-${{ env.PACKAGE_VERSION }}-armv7l.zip
asset_content_type: application/zip
- name: Upload Linux .7z 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-${{ env.PACKAGE_VERSION }}-linux-portable-armv7l.7z
asset_path: build/freetube-${{ env.PACKAGE_VERSION }}-armv7l.7z
asset_content_type: application/x-7z-compressed
- name: Upload Linux .zip ARM64 Release
uses: actions/upload-release-asset@v1
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
env:
@ -86,6 +141,17 @@ jobs:
asset_path: build/freetube-${{ env.PACKAGE_VERSION }}-arm64.zip
asset_content_type: application/zip
- name: Upload Linux .7z 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-${{ env.PACKAGE_VERSION }}-linux-portable-arm64.7z
asset_path: build/freetube-${{ env.PACKAGE_VERSION }}-arm64.7z
asset_content_type: application/x-7z-compressed
- name: Upload Linux .deb x64 Release
uses: actions/upload-release-asset@v1
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
@ -97,7 +163,18 @@ jobs:
asset_path: build/freetube_${{ env.PACKAGE_VERSION }}_amd64.deb
asset_content_type: application/vnd.debian.binary-package
- name: Upload Linux .deb ARM Release
- name: Upload Linux .deb 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_${{ env.PACKAGE_VERSION }}_armv7l.deb
asset_path: build/freetube_${{ env.PACKAGE_VERSION }}_armv7l.deb
asset_content_type: application/vnd.debian.binary-package
- name: Upload Linux .deb ARM64 Release
uses: actions/upload-release-asset@v1
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
env:
@ -119,7 +196,9 @@ jobs:
asset_path: build/freetube-${{ env.PACKAGE_VERSION }}.x86_64.rpm
asset_content_type: application/x-rpm
- name: Upload Linux .rpm ARM Release
# rpm are not built for armv7l
- name: Upload Linux .rpm ARM64 Release
uses: actions/upload-release-asset@v1
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
env:
@ -130,9 +209,9 @@ jobs:
asset_path: build/freetube-${{ env.PACKAGE_VERSION }}.aarch64.rpm
asset_content_type: application/x-rpm
- name: Upload Windows .exe Release
- name: Upload Windows x64 .exe Release
uses: actions/upload-release-asset@v1
if: startsWith(matrix.os, 'windows')
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-x64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@ -141,9 +220,20 @@ jobs:
asset_path: build/freetube Setup ${{ env.PACKAGE_VERSION }}.exe
asset_content_type: application/x-ms-dos-executable
- name: Upload Windows .zip Release
- name: Upload Windows arm64 .exe Release
uses: actions/upload-release-asset@v1
if: startsWith(matrix.os, 'windows')
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-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-${{ env.PACKAGE_VERSION }}-setup-arm64.exe
asset_path: build/freetube Setup ${{ env.PACKAGE_VERSION }}.exe
asset_content_type: application/x-ms-dos-executable
- name: Upload Windows x64 .zip Release
uses: actions/upload-release-asset@v1
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-x64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@ -152,13 +242,113 @@ jobs:
asset_path: build/freetube-${{ env.PACKAGE_VERSION }}-win.zip
asset_content_type: application/zip
- name: Upload Mac .dmg Release
- name: Upload Windows x64 .7z Release
uses: actions/upload-release-asset@v1
if: startsWith(matrix.os, 'macos')
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-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-${{ env.PACKAGE_VERSION }}-mac.dmg
asset_name: freetube-${{ env.PACKAGE_VERSION }}-win-x64-portable.7z
asset_path: build/freetube-${{ env.PACKAGE_VERSION }}-win.7z
asset_content_type: application/x-7z-compressed
- name: Upload Windows arm64 .zip Release
uses: actions/upload-release-asset@v1
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-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-${{ env.PACKAGE_VERSION }}-win-arm64-portable.zip
asset_path: build/freetube-${{ env.PACKAGE_VERSION }}-arm64-win.zip
asset_content_type: application/zip
- name: Upload Windows arm64 .7z Release
uses: actions/upload-release-asset@v1
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-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-${{ env.PACKAGE_VERSION }}-win-arm64-portable.7z
asset_path: build/freetube-${{ env.PACKAGE_VERSION }}-arm64-win.7z
asset_content_type: application/x-7z-compressed
- name: Upload Windows x64 portable Release
uses: actions/upload-release-asset@v1
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-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-${{ env.PACKAGE_VERSION }}-win-x64-portable.exe
asset_path: build/FreeTube ${{ env.PACKAGE_VERSION }}.exe
asset_content_type: application/x-ms-dos-executable
- name: Upload Windows arm64 portable Release
uses: actions/upload-release-asset@v1
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-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-${{ env.PACKAGE_VERSION }}-win-arm64-portable.exe
asset_path: build/FreeTube ${{ env.PACKAGE_VERSION }}.exe
asset_content_type: application/x-ms-dos-executable
- name: Upload Mac x64 .dmg Release
uses: actions/upload-release-asset@v1
if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-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-${{ env.PACKAGE_VERSION }}-mac-x64.dmg
asset_path: build/freetube-${{ env.PACKAGE_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-${{ env.PACKAGE_VERSION }}-mac-arm64.dmg
# asset_path: build/freetube-${{ env.PACKAGE_VERSION }}-arm64.dmg
# asset_content_type: application/x-apple-diskimage
- name: Upload Mac x64 .zip Release
uses: actions/upload-release-asset@v1
if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-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-${{ env.PACKAGE_VERSION }}-mac-x64.zip
asset_path: build/freetube-${{ env.PACKAGE_VERSION }}-mac.zip
asset_content_type: application/zip
- name: Upload Mac x64 .7z Release
uses: actions/upload-release-asset@v1
if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-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-${{ env.PACKAGE_VERSION }}-mac-x64.7z
asset_path: build/freetube-${{ env.PACKAGE_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-${{ env.PACKAGE_VERSION }}-mac-arm64.zip
# asset_path: build/freetube-${{ env.PACKAGE_VERSION }}-arm64-mac.zip
# asset_content_type: application/x-apple-diskimage

View File

@ -4,7 +4,7 @@ name: Project Board Automation
on:
issues:
types: [labeled, unlabeled, closed, deleted]
types: [closed, deleted, reopened, opened]
jobs:
assign-issues-to-projects:
@ -13,38 +13,58 @@ jobs:
# For bug reports
- name: New bug issue
uses: alex-page/github-project-automation-plus@v0.5.1
if: github.event.action == 'labeled' && contains(github.event.issue.labels.*.name, 'bug')
uses: alex-page/github-project-automation-plus@v0.8.2
if: contains(github.event.issue.labels.*.name, 'bug') && github.event.action == 'opened'
with:
project: Bug Reports
column: To assign
repo-token: ${{ secrets.PUSH_TOKEN }}
action: update
- name: Bug label removed
uses: alex-page/github-project-automation-plus@v0.5.1
if: github.event.action == 'unlabeled' || github.event.action == 'closed' || github.event.action == 'deleted'
- name: Bug issue closed
uses: alex-page/github-project-automation-plus@v0.8.2
if: github.event.action == 'closed' || github.event.action == 'deleted'
with:
action: delete
project: Bug Reports
column: To assign
repo-token: ${{ secrets.PUSH_TOKEN }}
- name: Bug issue reopened
uses: alex-page/github-project-automation-plus@v0.8.2
if: contains(github.event.issue.labels.*.name, 'bug') && github.event.action == 'reopened'
with:
project: Bug Reports
column: To assign
repo-token: ${{ secrets.PUSH_TOKEN }}
action: update
# For feature requests
- name: New feature issue
uses: alex-page/github-project-automation-plus@v0.5.1
if: github.event.action == 'labeled' && contains(github.event.issue.labels.*.name, 'enhancement')
uses: alex-page/github-project-automation-plus@v0.8.2
if: contains(github.event.issue.labels.*.name, 'enhancement') && github.event.action == 'opened'
with:
project: Feature Requests
column: To assign
repo-token: ${{ secrets.PUSH_TOKEN }}
action: update
- name: Feature request label removed
uses: alex-page/github-project-automation-plus@v0.5.1
if: github.event.action == 'unlabeled' || github.event.action == 'closed' || github.event.action == 'deleted'
- name: Feature request issue closed
uses: alex-page/github-project-automation-plus@v0.8.2
if: github.event.action == 'closed' || github.event.action == 'deleted'
with:
action: delete
project: Feature Requests
column: To assign
repo-token: ${{ secrets.PUSH_TOKEN }}
- name: Feature request issue reopened
uses: alex-page/github-project-automation-plus@v0.8.2
if: contains(github.event.issue.labels.*.name, 'enhancement') && github.event.action == 'reopened'
with:
project: Feature Requests
column: To assign
repo-token: ${{ secrets.PUSH_TOKEN }}
action: update

21
.github/workflows/stale.yml vendored Normal file
View File

@ -0,0 +1,21 @@
name: 'Close stale issues and PRs'
on:
schedule:
- cron: '30 1 * * *'
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v6
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.'
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-issue-close: 7
days-before-pr-close: 14
stale-issue-label: 'U: stale'
stale-pr-label: 'PR: stale'

111
README.md
View File

@ -6,79 +6,86 @@ FreeTube is an open source desktop YouTube player built with privacy in mind.
Use YouTube without advertisements and prevent Google from tracking you with their cookies and JavaScript.
Available for Windows, Mac & Linux thanks to Electron.
Please note that FreeTube is currently in Beta. While it should work well for
most users, there are still bugs and missing features that need to be
addressed.
<p align="center"><a href="https://github.com/FreeTubeApp/FreeTube/releases">Download FreeTube</a></p>
[Download FreeTube](https://github.com/FreeTubeApp/FreeTube/releases)
<hr>
<p align="center"><a href="#screenshots">Screenshots</a> &bull; <a href="#how-does-it-work">How does it work?</a> &bull; <a href="#features">Features</a> &bull; <a href="#download-links">Download Links</a> &bull; <a href="#contributing">Contributing</a> &bull; <a href="#localization">Localization</a> &bull; <a href="#contact">Contact</a> &bull; <a href="#donate">Donate</a> &bull; <a href="#license">License</a></p>
<p align="center"><a href="https://freetubeapp.io/">Website</a> &bull; <a href="https://blog.freetubeapp.io/">Blog</a> &bull; <a href="https://docs.freetubeapp.io/">Documentation</a> &bull; <a href="https://docs.freetubeapp.io/faq/">FAQ</a> &bull; <a href="https://github.com/FreeTubeApp/FreeTube/discussions">Discussions</a></p>
<hr>
### Browser Extension
<b>Please note that FreeTube is currently in Beta. While it should work well for most users, there are still bugs and missing features that need to be addressed. If you have an idea or if you found a bug, please submit a [GitHub issue](https://github.com/FreeTubeApp/FreeTube/issues/new/choose) so that
we can track it. Please search [the existing issues](https://github.com/FreeTubeApp/FreeTube/issues) before submitting to
prevent duplicates!</b>
FreeTube is supported by the [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect) and [LibRedirect](https://github.com/libredirect/libredirect) extension, which will allow you to open YouTube links into FreeTube. You must enable the option within the advanced settings for it to work.
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 for [Firefox](https://addons.mozilla.org/firefox/addon/libredirect/) or [Google Chrome](https://github.com/libredirect/libredirect/blob/master/chromium.md).
Disclaimer: Learn more about why a browser extension is bad for your [privacy](https://www.privacyguides.org/browsers/#extensions).
If you have issues with the extension working with FreeTube, please create an issue in this repository instead of the extension repository.
## Screenshots
<img src="https://i.imgur.com/zFgZUUV.png" width=300> <img src="https://i.imgur.com/9evYHgN.png" width=300> <img src="https://i.imgur.com/yT2UzPa.png" width=300> <img src="https://i.imgur.com/47zIEt4.png" width=300> <img src="https://i.imgur.com/hFB2fKC.png" width=300>
## How does it work?
FreeTube uses a built in extractor to grab and serve data / videos. The [Invidious API](https://github.com/iv-org/invidious) can also optionally be used. FreeTube does not use any official APIs to obtain data. While YouTube can still see your video requests, it can no
longer track you using cookies or JavaScript. Your subscriptions and history are stored locally on your computer and never sent out. Using a VPN or Tor is highly recommended
to hide your IP while using FreeTube.
Go to [FreeTube's Documentation](https://docs.freetubeapp.io/) if you'd like to know more about how to operate FreeTube and its features.
## Screenshots
<img src="https://i.imgur.com/zFgZUUV.png" width=300> <img src="https://i.imgur.com/9evYHgN.png" width=300> <img src="https://i.imgur.com/yT2UzPa.png" width=300> <img src="https://i.imgur.com/47zIEt4.png" width=300> <img src="https://i.imgur.com/hFB2fKC.png" width=300>
## Features
* Watch videos without ads
* Use YouTube without Google tracking you using cookies and JavaScript
* Two extractor APIs to choose from (Built in or Invidious)
* Subscribe to channels without an account
* Local subscriptions, history, and saved videos
* Connect to an externally setup proxy such as Tor
* View and search your local subscriptions, history, and saved videos
* Organize your subscriptions into "Profiles" to create a more focused feed
* Export & import subscriptions
* Youtube Trending
* Youtube Chapters
* Most popular videos page based on the set Invidious instance
* SponsorBlock
* Open videos from your browser directly into FreeTube (with extension)
* Mini Player
* Watch videos using an external player
* Full Theme support
* Make a screenshot of a video
* Multiple windows
* Mini Player (Picture-in-Picture)
* Keyboard shortcuts
* Option to show only family friendly content
* Show/hide functionality or elements within the app using the distraction free settings
### 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. You must enable the option within the advanced settings of the extension for it to work.
* 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 for [Firefox](https://addons.mozilla.org/firefox/addon/libredirect/) or [Google Chrome](https://github.com/libredirect/libredirect/blob/master/chromium.md).
If you have issues with the extension working with FreeTube, please create an issue in this repository instead of the extension repository. This extension does not work on Linux portable builds!
## Download Links
### Official Downloads
* [GitHub Releases](https://github.com/FreeTubeApp/FreeTube/releases)
[GitHub Releases](https://github.com/FreeTubeApp/FreeTube/releases)
* [FreeTube Website](https://freetubeapp.io/#download)
[FreeTube Website](https://freetubeapp.io/#download)
Flatpak on Flathub: [Download](https://flathub.org/apps/details/io.freetubeapp.FreeTube) [Source](https://github.com/flathub/io.freetubeapp.FreeTube)
### Unofficial Downloads
These builds are maintained by the community. While they should be safe, download at your own risk. There may be issues with using these versus the official builds. Any issues specific with these builds should be sent to their respective maintainer.
Arch User Repository (AUR): [Download](https://aur.archlinux.org/packages/freetube-bin/)
Chocolatey: [Download](https://chocolatey.org/packages/freetube/)
PortableApps (Windows Only): [Download](https://github.com/rddim/FreeTubePortable/releases) [Source](https://github.com/rddim/FreeTubePortable)
Windows Package Manager (winget): [Usage](https://docs.microsoft.com/en-us/windows/package-manager/winget/)
### Automated Builds (Nightly / Weekly)
* Flatpak on Flathub: [Download](https://flathub.org/apps/details/io.freetubeapp.FreeTube) [Source](https://github.com/flathub/io.freetubeapp.FreeTube)
#### Automated Builds (Nightly / Weekly)
Builds are automatically created from changes to our development branch via [GitHub Actions](https://github.com/FreeTubeApp/FreeTube/actions?query=workflow%3ABuild).
The first build with a green check mark is the latest build. You will need to have a GitHub account to download these builds.
## Contributing
If you have an idea or if you found a bug, please submit a GitHub issue so that
we can track it. Please search the existing issues before submitting to
prevent duplicates.
### Unofficial Downloads
These builds are maintained by the community. While they should be safe, download at your own risk. There may be issues with using these versus the official builds. Any issues specific with these builds should be sent to their respective maintainer.
* Arch User Repository (AUR): [Download](https://aur.archlinux.org/packages/freetube-bin/)
* Chocolatey: [Download](https://chocolatey.org/packages/freetube/)
* makedeb Package Repository (MPR): [Download](https://mpr.makedeb.org/packages/freetube-bin)
* PortableApps (Windows Only): [Download](https://github.com/rddim/FreeTubePortable/releases) [Source](https://github.com/rddim/FreeTubePortable)
* Scoop (Windows Only): [Usage](https://github.com/ScoopInstaller/Scoop)
* Windows Package Manager (winget): [Usage](https://docs.microsoft.com/en-us/windows/package-manager/winget/)
## Contributing
If you like to get your hands dirty and want to contribute, we would love to
have your help. Send a pull request and someone will review your code. Please
follow the [Contribution
@ -89,7 +96,7 @@ Thank you very much to the [People and Projects](https://docs.freetubeapp.io/cre
## Localization
<a href="https://hosted.weblate.org/engage/free-tube/">
<img src="https://hosted.weblate.org/widgets/free-tube/-/translations/287x66-grey.png" alt="Translation status" />
<img src="https://hosted.weblate.org/widgets/free-tube/-/287x66-grey.png" alt="Translation status" />
</a>
We are actively looking for translations! We use [Weblate](https://hosted.weblate.org/engage/free-tube/) to make it easy for translators to get involved. Click on the badge above to learn how to get involved.
@ -97,20 +104,18 @@ We are actively looking for translations! We use [Weblate](https://hosted.webla
For the Linux Flatpak, the desktop entry comment string can be translated at our [Flatpak repository](https://github.com/flathub/io.freetubeapp.FreeTube/blob/master/io.freetubeapp.FreeTube.desktop).
## Contact
If you ever have any questions, feel free to make an issue here on GitHub. Alternatively, you can email me at FreeTubeApp@protonmail.com or you can join our [Matrix Community](https://matrix.to/#/+freetube:matrix.org). Don't forget to check out the [rules](https://docs.freetubeapp.io/community/matrix/) before joining.
You can also stay up to date by reading the [FreeTube Blog](https://write.as/freetube/). [View the welcome blog](https://write.as/freetube/welcome-to-freetube-blogs).
If you ever have any questions, feel free to ask it on our [Discussions](https://github.com/FreeTubeApp/FreeTube/discussions) page. Alternatively, you can email us at FreeTubeApp@protonmail.com or you can join our [Matrix Community](https://matrix.to/#/+freetube:matrix.org). Don't forget to check out the [rules](https://docs.freetubeapp.io/community/matrix/) before joining.
## Donate
If you enjoy using FreeTube, you're welcome to leave a donation using the following methods.
[FreeTube on Liberapay](https://liberapay.com/FreeTube)
* [FreeTube on Liberapay](https://liberapay.com/FreeTube)
Bitcoin Address: 1Lih7Ho5gnxb1CwPD4o59ss78pwo2T91eS
* Bitcoin Address: `1Lih7Ho5gnxb1CwPD4o59ss78pwo2T91eS`
Monero Address: 48WyAPdjwc6VokeXACxSZCFeKEXBiYPV6GjfvBsfg4CrUJ95LLCQSfpM9pvNKy5GE5H4hNaw99P8RZyzmaU9kb1pD7kzhCB
* Monero Address: `48WyAPdjwc6VokeXACxSZCFeKEXBiYPV6GjfvBsfg4CrUJ95LLCQSfpM9pvNKy5GE5H4hNaw99P8RZyzmaU9kb1pD7kzhCB`
If you enjoy using FreeTube, you're welcome to leave a donation using the following methods. While your donations are much appreciated, only donate if you really want to. Donations are used for keeping the website up and running and eventual code signing costs.
While your donations are much appreciated, only donate if you really want to. Donations are used for keeping the website up and running and eventual code signing costs.
## License
[![GNU AGPLv3 Image](https://www.gnu.org/graphics/agplv3-155x51.png)](https://www.gnu.org/licenses/agpl-3.0.html)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 526 B

After

Width:  |  Height:  |  Size: 489 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 785 B

After

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 828 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 498 B

After

Width:  |  Height:  |  Size: 352 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 425 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 426 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 579 B

After

Width:  |  Height:  |  Size: 433 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 356 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,94 @@
const { existsSync, readFileSync } = require('fs')
const { brotliCompressSync, constants } = require('zlib')
const { load: loadYaml } = require('js-yaml')
class ProcessLocalesPlugin {
constructor(options = {}) {
this.compress = !!options.compress
if (typeof options.inputDir !== 'string') {
throw new Error('ProcessLocalesPlugin: no input directory `inputDir` specified.')
} else if (!existsSync(options.inputDir)) {
throw new Error('ProcessLocalesPlugin: the specified input directory does not exist.')
}
this.inputDir = options.inputDir
if (typeof options.outputDir !== 'string') {
throw new Error('ProcessLocalesPlugin: no output directory `outputDir` specified.')
}
this.outputDir = options.outputDir
this.localeNames = []
this.loadLocales()
}
apply(compiler) {
compiler.hooks.thisCompilation.tap('ProcessLocalesPlugin', (compilation) => {
const { RawSource } = compiler.webpack.sources;
compilation.hooks.processAssets.tapPromise({
name: 'process-locales-plugin',
stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL
},
async (_assets) => {
const promises = []
for (const { locale, data } of this.locales) {
promises.push(new Promise((resolve) => {
if (Object.prototype.hasOwnProperty.call(data, 'Locale Name')) {
delete data['Locale Name']
}
let filename = `${this.outputDir}/${locale}.json`
let output = JSON.stringify(data)
if (this.compress) {
filename += '.br'
output = this.compressLocale(output)
}
compilation.emitAsset(
filename,
new RawSource(output),
{ minimized: true }
)
resolve()
}))
}
await Promise.all(promises)
})
})
}
loadLocales() {
this.locales = []
const activeLocales = JSON.parse(readFileSync(`${this.inputDir}/activeLocales.json`))
for (const locale of activeLocales) {
const contents = readFileSync(`${this.inputDir}/${locale}.yaml`, 'utf-8')
const data = loadYaml(contents)
this.localeNames.push(data['Locale Name'] ?? locale)
this.locales.push({ locale, data })
}
}
compressLocale(data) {
const buffer = Buffer.from(data, 'utf-8')
return brotliCompressSync(buffer, {
params: {
[constants.BROTLI_PARAM_MODE]: constants.BROTLI_MODE_TEXT,
[constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY,
[constants.BROTLI_PARAM_SIZE_HINT]: buffer.byteLength
}
})
}
}
module.exports = ProcessLocalesPlugin

View File

@ -8,20 +8,23 @@ const args = process.argv
let targets
const platform = os.platform()
const cpus = os.cpus()
if (platform === 'darwin') {
let arch = Arch.x64
// Macbook Air 2020 with M1 = 'Apple M1'
// Macbook Pro 2021 with M1 Pro = 'Apple M1 Pro'
if (cpus[0].model.startsWith('Apple')) {
if (args[2] === 'arm64') {
arch = Arch.arm64
}
targets = Platform.MAC.createTarget(['dmg'], arch)
targets = Platform.MAC.createTarget(['DMG','zip', '7z'], arch)
} else if (platform === 'win32') {
targets = Platform.WINDOWS.createTarget()
let arch = Arch.x64
if (args[2] === 'arm64') {
arch = Arch.arm64
}
targets = Platform.WINDOWS.createTarget(['nsis', 'zip', '7z', 'portable'], arch)
} else if (platform === 'linux') {
let arch = Arch.x64
@ -33,7 +36,7 @@ if (platform === 'darwin') {
arch = Arch.armv7l
}
targets = Platform.LINUX.createTarget(['deb', 'zip', 'apk', 'rpm', 'AppImage', 'pacman'], arch)
targets = Platform.LINUX.createTarget(['deb', 'zip', '7z', 'apk', 'rpm', 'AppImage', 'pacman'], arch)
}
const config = {
@ -58,22 +61,14 @@ const config = {
'icon.svg',
'./dist/**/*',
'!dist/web/*',
'!**/node_modules/**/.*',
'!**/node_modules/**/index.html',
'!**/{.github,Jenkinsfile}',
'!**/{CHANGES.md,CODE_OF_CONDUCT.md,CONTRIBUTING.md,CONTRIBUTION.md,DEVELOPMENT.md,docs,docs.md,docs.mli,examples,History.md,HISTORY.md,README.md,TODO.md,UPGRADE_GUIDE.md,UPGRADING.md}',
'!**/{commitlint.config.js,.editorconfig,.eslintignore,.eslintrc.{js,yml},.gitmodules,.huskyrc,.lintstagedrc,.nvmrc,.nycrc{,.json},.prettierrc{,.yaml},tslint.json}',
'!**/{.babelrc,bower.json,Gruntfile.js,Makefile,.npmrc.proregistry,rollup.config.js,.tm_properties,.tool-versions,tsconfig.json,webpack.config.js}',
'!**/*.{{,c,m}js,min,ts}.map',
'!**/*.d.ts',
'!node_modules/**/*',
// only exclude the src directory for specific packages
// as some of them have their dist code in there and we don't want to exclude those
'!**/node_modules/{@fortawesome/vue-fontawesome,agent-base,jquery,localforage,m3u8-parser,marked,mpd-parser,performance-now,video.js,vue,vue-i18n,vue-router}/src/*',
'!**/node_modules/**/{bin,man,scripts}/*',
'!**/node_modules/jquery/dist/jquery.slim*.js',
'!**/node_modules/video.js/dist/{alt/*,video.js}',
'!**/node_modules/@videojs/*/src'
// renderer
'node_modules/{miniget,ytpl,ytsr}/**/*',
'!**/README.md',
'!**/*.js.map',
'!**/*.d.ts',
],
dmg: {
contents: [
@ -97,7 +92,7 @@ const config = {
linux: {
category: 'Network',
icon: '_icons/icon.svg',
target: ['deb', 'zip', 'apk', 'rpm', 'AppImage', 'pacman'],
target: ['deb', 'zip', '7z', 'apk', 'rpm', 'AppImage', 'pacman'],
},
// See the following issues for more information
// https://github.com/jordansissel/fpm/issues/1503
@ -121,7 +116,7 @@ const config = {
mac: {
category: 'public.app-category.utilities',
icon: '_icons/iconMac.icns',
target: ['dmg', 'zip'],
target: ['dmg', 'zip', '7z'],
type: 'distribution',
extendInfo: {
CFBundleURLTypes: [
@ -134,7 +129,7 @@ const config = {
},
win: {
icon: '_icons/icon.ico',
target: ['nsis', 'zip', 'portable', 'squirrel'],
target: ['nsis', 'zip', '7z', 'portable'],
},
nsis: {
allowToChangeInstallationDirectory: true,

View File

@ -1,5 +1,6 @@
process.env.NODE_ENV = 'development'
const open = require('open')
const electron = require('electron')
const webpack = require('webpack')
const WebpackDevServer = require('webpack-dev-server')
@ -10,13 +11,14 @@ const { spawn } = require('child_process')
const mainConfig = require('./webpack.main.config')
const rendererConfig = require('./webpack.renderer.config')
const webConfig = require('./webpack.web.config')
const workersConfig = require('./webpack.workers.config')
let electronProcess = null
let manualRestart = null
const remoteDebugging = !!(
process.argv[2] && process.argv[2] === '--remote-debug'
)
const remoteDebugging = process.argv.indexOf('--remote-debug') !== -1
const web = process.argv.indexOf('--web') !== -1
if (remoteDebugging) {
// disable dvtools open in electron
@ -51,10 +53,12 @@ async function restartElectron() {
electronProcess = spawn(electron, [
path.join(__dirname, '../dist/main.js'),
// '--enable-logging', Enable to show logs from all electron processes
// '--enable-logging', // Enable to show logs from all electron processes
remoteDebugging ? '--inspect=9222' : '',
remoteDebugging ? '--remote-debugging-port=9223' : '',
])
remoteDebugging ? '--remote-debugging-port=9223' : ''
],
// { stdio: 'inherit' } // required for logs to actually appear in the stdout
)
electronProcess.on('exit', (code, _) => {
if (code === relaunchExitCode) {
@ -87,7 +91,6 @@ function startMain() {
manualRestart = true
await restartElectron()
setTimeout(() => {
manualRestart = false
}, 2500)
@ -135,4 +138,38 @@ function startRenderer(callback) {
})
}
startRenderer(startMain)
function startWeb (callback) {
const compiler = webpack(webConfig)
const { name } = compiler
compiler.hooks.afterEmit.tap('afterEmit', () => {
console.log(`\nCompiled ${name} script!`)
console.log(`\nWatching file changes for ${name} script...`)
})
const server = new WebpackDevServer({
static: {
directory: path.join(process.cwd(), 'dist/web/static'),
watch: {
ignored: [
/(dashFiles|storyboards)\/*/,
'/**/.DS_Store',
]
}
},
port
}, compiler)
server.startCallback(err => {
if (err) console.error(err)
callback({ port: server.options.port })
})
}
if (!web) {
startRenderer(startMain)
} else {
startWeb(({ port }) => {
open(`http://localhost:${port}`)
})
}

View File

@ -1,16 +1,11 @@
const path = require('path')
const webpack = require('webpack')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const JsonMinimizerPlugin = require('json-minimizer-webpack-plugin')
const {
dependencies,
devDependencies,
productName,
} = require('../package.json')
const { productName } = require('../package.json')
const externals = Object.keys(dependencies).concat(Object.keys(devDependencies))
const isDevMode = process.env.NODE_ENV === 'development'
const whiteListedModules = []
const config = {
name: 'main',
@ -19,7 +14,6 @@ const config = {
entry: {
main: path.join(__dirname, '../src/main/index.js'),
},
externals: externals.filter(d => !whiteListedModules.includes(d)),
module: {
rules: [
{
@ -27,12 +21,17 @@ const config = {
use: 'babel-loader',
exclude: /node_modules/,
},
{
test: /\.node$/,
loader: 'node-loader',
},
],
},
// webpack defaults to only optimising the production builds, so having this here is fine
optimization: {
minimizer: [
'...', // extend webpack's list instead of overwriting it
new JsonMinimizerPlugin({
exclude: /\/locales\/.*\.json/
})
]
},
node: {
__dirname: isDevMode,
__filename: isDevMode,
@ -58,49 +57,19 @@ const config = {
target: 'electron-main',
}
if (isDevMode) {
config.plugins.push(
new webpack.DefinePlugin({
__static: `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"`,
})
)
} else {
if (!isDevMode) {
config.plugins.push(
new CopyWebpackPlugin({
patterns: [
{
from: path.join(__dirname, '../static/pwabuilder-sw.js'),
to: path.join(__dirname, '../dist/web/pwabuilder-sw.js'),
},
{
from: path.join(__dirname, '../static'),
to: path.join(__dirname, '../dist/web/static'),
to: path.join(__dirname, '../dist/static'),
globOptions: {
dot: true,
ignore: ['**/.*', '**/pwabuilder-sw.js', '**/dashFiles/**', '**/storyboards/**'],
ignore: ['**/.*', '**/locales/**', '**/pwabuilder-sw.js', '**/dashFiles/**', '**/storyboards/**'],
},
},
{
from: path.join(__dirname, '../_icons'),
to: path.join(__dirname, '../dist/_icons'),
globOptions: {
dot: true,
ignore: ['**/.*'],
},
},
{
from: path.join(__dirname, '../src/renderer/assets/img'),
to: path.join(__dirname, '../dist/images'),
globOptions: {
dot: true,
ignore: ['**/.*'],
},
},
]
}
),
new webpack.LoaderOptionsPlugin({
minimize: true,
]
})
)
}

View File

@ -2,18 +2,13 @@ const path = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const ProcessLocalesPlugin = require('./ProcessLocalesPlugin')
const {
dependencies,
devDependencies,
productName,
} = require('../package.json')
const { productName } = require('../package.json')
const externals = Object.keys(dependencies).concat(Object.keys(devDependencies))
const isDevMode = process.env.NODE_ENV === 'development'
const whiteListedModules = ['vue']
const config = {
name: 'renderer',
@ -33,7 +28,10 @@ const config = {
path: path.join(__dirname, '../dist'),
filename: '[name].js',
},
externals: externals.filter(d => !whiteListedModules.includes(d)),
// webpack spits out errors while inlining ytpl and ytsr as
// they dynamically import their package.json file to extract the bug report URL
// the error: "Critical dependency: the request of a dependency is an expression"
externals: ['ytpl', 'ytsr'],
module: {
rules: [
{
@ -41,10 +39,6 @@ const config = {
use: 'babel-loader',
exclude: /node_modules/,
},
{
test: /\.node$/,
loader: 'node-loader',
},
{
test: /\.vue$/,
loader: 'vue-loader',
@ -54,10 +48,12 @@ const config = {
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {},
},
{
loader: 'css-loader',
options: {
esModule: false
}
},
{
loader: 'sass-loader',
@ -75,43 +71,49 @@ const config = {
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {},
loader: MiniCssExtractPlugin.loader
},
'css-loader',
{
loader: 'css-loader',
options: {
esModule: false
}
}
],
},
{
test: /\.(png|jpe?g|gif|tif?f|bmp|webp|svg)(\?.*)?$/,
use: {
loader: 'url-loader',
options: {
esModule: false,
limit: 10000,
name: 'imgs/[name]--[folder].[ext]',
},
},
type: 'asset/resource',
generator: {
filename: 'imgs/[name][ext]'
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
use: {
loader: 'url-loader',
options: {
esModule: false,
limit: 10000,
name: 'fonts/[name]--[folder].[ext]',
},
},
type: 'asset/resource',
generator: {
filename: 'fonts/[name][ext]'
}
},
],
},
// webpack defaults to only optimising the production builds, so having this here is fine
optimization: {
minimizer: [
'...', // extend webpack's list instead of overwriting it
new CssMinimizerPlugin()
]
},
node: {
__dirname: isDevMode,
__filename: isDevMode,
global: isDevMode,
},
plugins: [
// new WriteFilePlugin(),
new webpack.DefinePlugin({
'process.env.PRODUCT_NAME': JSON.stringify(productName),
'process.env.IS_ELECTRON': true
}),
new HtmlWebpackPlugin({
excludeChunks: ['processTaskWorker'],
filename: 'index.html',
@ -121,9 +123,6 @@ const config = {
: false,
}),
new VueLoaderPlugin(),
new webpack.DefinePlugin({
'process.env.PRODUCT_NAME': JSON.stringify(productName),
}),
new MiniCssExtractPlugin({
filename: isDevMode ? '[name].css' : '[name].[contenthash].css',
chunkFilename: isDevMode ? '[id].css' : '[id].[contenthash].css',
@ -146,58 +145,23 @@ const config = {
/**
* Adjust rendererConfig for production settings
*/
if (isDevMode) {
// any dev only config
if (!isDevMode) {
const processLocalesPlugin = new ProcessLocalesPlugin({
compress: true,
inputDir: path.join(__dirname, '../static/locales'),
outputDir: 'static/locales',
})
config.plugins.push(
processLocalesPlugin,
new webpack.DefinePlugin({
__static: `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"`,
})
)
} else {
config.plugins.push(
new CopyWebpackPlugin({
patterns: [
{
from: path.join(__dirname, '../static/pwabuilder-sw.js'),
to: path.join(__dirname, '../dist/web/pwabuilder-sw.js'),
},
{
from: path.join(__dirname, '../static'),
to: path.join(__dirname, '../dist/web/static'),
globOptions: {
dot: true,
ignore: ['**/.*', '**/pwabuilder-sw.js', '**/dashFiles/**', '**/storyboards/**'],
},
},
{
from: path.join(__dirname, '../static'),
to: path.join(__dirname, '../dist/static'),
globOptions: {
dot: true,
ignore: ['**/.*', '**/pwabuilder-sw.js', '**/dashFiles/**', '**/storyboards/**'],
},
},
{
from: path.join(__dirname, '../_icons'),
to: path.join(__dirname, '../dist/web/_icons'),
globOptions: {
dot: true,
ignore: ['**/.*'],
},
},
{
from: path.join(__dirname, '../src/renderer/assets/img'),
to: path.join(__dirname, '../dist/web/images'),
globOptions: {
dot: true,
ignore: ['**/.*'],
},
},
]
}
),
new webpack.LoaderOptionsPlugin({
minimize: true,
'process.env.LOCALE_NAMES': JSON.stringify(processLocalesPlugin.localeNames)
}),
// webpack doesn't get rid of js-yaml even though it isn't used in the production builds
// so we need to manually tell it to ignore any imports for `js-yaml`
new webpack.IgnorePlugin({
resourceRegExp: /^js-yaml$/,
contextRegExp: /i18n$/
})
)
}

View File

@ -1,9 +1,13 @@
const path = require('path')
const fs = require('fs')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const JsonMinimizerPlugin = require('json-minimizer-webpack-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const ProcessLocalesPlugin = require('./ProcessLocalesPlugin')
const { productName } = require('../package.json')
@ -12,7 +16,7 @@ const isDevMode = process.env.NODE_ENV === 'development'
const config = {
name: 'web',
mode: process.env.NODE_ENV,
devtool: isDevMode ? '#cheap-module-eval-source-map' : false,
devtool: isDevMode ? 'eval-cheap-module-source-map' : false,
entry: {
web: path.join(__dirname, '../src/renderer/main.js'),
},
@ -20,6 +24,11 @@ const config = {
path: path.join(__dirname, '../dist/web'),
filename: '[name].js',
},
externals: {
electron: '{}',
ytpl: '{}',
ytsr: '{}'
},
module: {
rules: [
{
@ -29,27 +38,19 @@ const config = {
},
{
test: /\.vue$/,
use: {
loader: 'vue-loader',
options: {
extractCSS: true,
loaders: {
sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1',
scss: 'vue-style-loader!css-loader!sass-loader',
less: 'vue-style-loader!css-loader!less-loader',
},
},
},
loader: 'vue-loader'
},
{
test: /\.s(c|a)ss$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {},
},
{
loader: 'css-loader',
options: {
esModule: false
}
},
{
loader: 'sass-loader',
@ -67,10 +68,14 @@ const config = {
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {},
loader: MiniCssExtractPlugin.loader
},
'css-loader',
{
loader: 'css-loader',
options: {
esModule: false
}
}
],
},
{
@ -79,39 +84,43 @@ const config = {
},
{
test: /\.(png|jpe?g|gif|tif?f|bmp|webp|svg)(\?.*)?$/,
use: {
loader: 'url-loader',
options: {
esModule: false,
limit: 10000,
name: 'imgs/[name]--[folder].[ext]',
},
},
type: 'asset/resource',
generator: {
filename: 'imgs/[name][ext]'
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
use: {
loader: 'url-loader',
options: {
esModule: false,
limit: 10000,
name: 'fonts/[name]--[folder].[ext]',
},
},
type: 'asset/resource',
generator: {
filename: 'fonts/[name][ext]'
}
},
],
},
// webpack defaults to only optimising the production builds, so having this here is fine
optimization: {
minimizer: [
'...', // extend webpack's list instead of overwriting it
new JsonMinimizerPlugin({
exclude: /\/locales\/.*\.json/
}),
new CssMinimizerPlugin()
]
},
node: {
__dirname: isDevMode,
__dirname: true,
__filename: isDevMode,
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty',
dns: 'empty'
},
plugins: [
// new WriteFilePlugin(),
new webpack.DefinePlugin({
'process.env.PRODUCT_NAME': JSON.stringify(productName),
'process.env.IS_ELECTRON': false
}),
new webpack.ProvidePlugin({
process: 'process/browser',
Buffer: ['buffer', 'Buffer'],
}),
new HtmlWebpackPlugin({
excludeChunks: ['processTaskWorker'],
filename: 'index.html',
@ -119,9 +128,6 @@ const config = {
nodeModules: false,
}),
new VueLoaderPlugin(),
new webpack.DefinePlugin({
'process.env.PRODUCT_NAME': JSON.stringify(productName),
}),
new MiniCssExtractPlugin({
filename: isDevMode ? '[name].css' : '[name].[contenthash].css',
chunkFilename: isDevMode ? '[id].css' : '[id].[contenthash].css',
@ -136,60 +142,61 @@ const config = {
images: path.join(__dirname, '../src/renderer/assets/img/'),
static: path.join(__dirname, '../static/'),
},
fallback: {
buffer: require.resolve('buffer/'),
dns: require.resolve('browserify/lib/_empty.js'),
fs: require.resolve('browserify/lib/_empty.js'),
http: require.resolve('stream-http'),
https: require.resolve('https-browserify'),
net: require.resolve('browserify/lib/_empty.js'),
os: require.resolve('os-browserify/browser.js'),
path: require.resolve('path-browserify'),
stream: require.resolve('stream-browserify'),
timers: require.resolve('timers-browserify'),
tls: require.resolve('browserify/lib/_empty.js'),
vm: require.resolve('vm-browserify'),
zlib: require.resolve('browserify-zlib')
},
extensions: ['.js', '.vue', '.json', '.css'],
},
target: 'web',
}
/**
* Adjust web for production settings
*/
if (isDevMode) {
// any dev only config
config.plugins.push(
new webpack.DefinePlugin({
__static: `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"`,
})
)
} else {
config.plugins.push(
new CopyWebpackPlugin({
patterns: [
{
from: path.join(__dirname, '../static/pwabuilder-sw.js'),
to: path.join(__dirname, '../dist/web/pwabuilder-sw.js'),
const processLocalesPlugin = new ProcessLocalesPlugin({
compress: false,
inputDir: path.join(__dirname, '../static/locales'),
outputDir: 'static/locales',
})
config.plugins.push(
processLocalesPlugin,
new webpack.DefinePlugin({
'process.env.LOCALE_NAMES': JSON.stringify(processLocalesPlugin.localeNames),
'process.env.GEOLOCATION_NAMES': JSON.stringify(fs.readdirSync(path.join(__dirname, '..', 'static', 'geolocations')))
}),
new CopyWebpackPlugin({
patterns: [
{
from: path.join(__dirname, '../static/pwabuilder-sw.js'),
to: path.join(__dirname, '../dist/web/pwabuilder-sw.js'),
},
{
from: path.join(__dirname, '../static'),
to: path.join(__dirname, '../dist/web/static'),
globOptions: {
dot: true,
ignore: ['**/.*', '**/locales/**', '**/pwabuilder-sw.js', '**/dashFiles/**', '**/storyboards/**'],
},
{
from: path.join(__dirname, '../static'),
to: path.join(__dirname, '../dist/web/static'),
globOptions: {
dot: true,
ignore: ['**/.*', '**/pwabuilder-sw.js', '**/dashFiles/**', '**/storyboards/**'],
},
},
{
from: path.join(__dirname, '../_icons'),
to: path.join(__dirname, '../dist/web/_icons'),
globOptions: {
dot: true,
ignore: ['**/.*'],
},
},
{
from: path.join(__dirname, '../src/renderer/assets/img'),
to: path.join(__dirname, '../dist/web/images'),
globOptions: {
dot: true,
ignore: ['**/.*'],
},
},
]
}
),
new webpack.LoaderOptionsPlugin({
minimize: true,
})
)
}
},
]
}),
// webpack doesn't get rid of js-yaml even though it isn't used in the production builds
// so we need to manually tell it to ignore any imports for `js-yaml`
new webpack.IgnorePlugin({
resourceRegExp: /^js-yaml$/,
contextRegExp: /i18n$/
})
)
module.exports = config

View File

@ -30,10 +30,6 @@ const config = {
use: 'babel-loader',
exclude: /node_modules/,
},
{
test: /\.node$/,
loader: 'node-loader',
},
],
},
node: {
@ -63,11 +59,7 @@ const config = {
if (isDevMode) {
// any dev only config
} else {
config.plugins.push(
new webpack.LoaderOptionsPlugin({
minimize: true,
})
)
// any producation only config
}
module.exports = config

48
lefthook.yml Normal file
View File

@ -0,0 +1,48 @@
# Refer for explanation to following link:
# https://github.com/evilmartians/lefthook/blob/master/docs/full_guide.md
pre-commit:
parallel: true
commands:
lint:
# Only runs when any file with filename
# matching the glob is being committed
glob: "*.{js,vue}"
run: yarn run eslint --no-color {staged_files}
skip:
- rebase
# EXAMPLE USAGE
#
# pre-push:
# commands:
# packages-audit:
# tags: frontend security
# run: yarn audit
# gems-audit:
# tags: backend security
# run: bundle audit
#
# pre-commit:
# parallel: true
# commands:
# eslint:
# glob: "*.{js,ts}"
# run: yarn eslint {staged_files}
# rubocop:
# tags: backend style
# glob: "*.rb"
# exclude: "application.rb|routes.rb"
# run: bundle exec rubocop --force-exclusion {all_files}
# govet:
# tags: backend style
# files: git ls-files -m
# glob: "*.go"
# run: go vet {files}
# scripts:
# "hello.js":
# runner: node
# "any.go":
# runner: go run

View File

@ -2,7 +2,7 @@
"name": "freetube",
"productName": "FreeTube",
"description": "A private YouTube client",
"version": "0.17.1",
"version": "0.18.0",
"license": "AGPL-3.0-or-later",
"main": "./dist/main.js",
"private": true,
@ -29,6 +29,7 @@
"debug": "run-s rebuild:electron debug-runner",
"debug-runner": "node _scripts/dev-runner.js --remote-debug",
"dev": "run-s rebuild:electron dev-runner",
"dev:web": "node _scripts/dev-runner.js --web",
"dev-runner": "node _scripts/dev-runner.js",
"lint-fix": "eslint --fix --ext .js,.vue ./",
"lint": "eslint --ext .js,.vue ./",
@ -37,92 +38,88 @@
"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:workers": "webpack --mode=production --node-env=production --config _scripts/webpack.workers.config.js",
"postinstall": "npm run rebuild:electron",
"postinstall": "yarn run --silent rebuild:electron",
"prettier": "prettier --write \"{src,_scripts}/**/*.{js,vue}\"",
"rebuild:electron": "electron-builder install-app-deps",
"rebuild:node": "npm rebuild",
"release": "run-s test build",
"ci": "yarn install --frozen-lockfile"
"ci": "yarn install --silent --frozen-lockfile"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/vue-fontawesome": "^2.0.2",
"@fortawesome/fontawesome-svg-core": "^6.2.0",
"@fortawesome/free-brands-svg-icons": "^6.2.0",
"@fortawesome/free-solid-svg-icons": "^6.2.0",
"@fortawesome/vue-fontawesome": "^2.0.8",
"@freetube/youtube-chat": "^1.1.2",
"@freetube/yt-comment-scraper": "^6.1.0",
"@freetube/yt-comment-scraper": "^6.2.0",
"@freetube/yt-trending-scraper": "^3.1.1",
"@silvermine/videojs-quality-selector": "^1.2.5",
"autolinker": "^3.15.0",
"electron-context-menu": "^3.1.2",
"http-proxy-agent": "^4.0.1",
"autolinker": "^4.0.0",
"browserify": "^17.0.0",
"browserify-zlib": "^0.2.0",
"electron-context-menu": "^3.5.0",
"http-proxy-agent": "^5.0.0",
"https-proxy-agent": "^5.0.0",
"jquery": "^3.6.0",
"js-yaml": "^4.1.0",
"lodash.debounce": "^4.0.8",
"lodash.isequal": "^4.5.0",
"marked": "^4.0.17",
"nedb-promises": "^5.0.1",
"marked": "^4.1.1",
"nedb-promises": "^6.2.1",
"opml-to-json": "^1.0.1",
"rss-parser": "^3.12.0",
"process": "^0.11.10",
"socks-proxy-agent": "^6.0.0",
"video.js": "7.18.1",
"videojs-contrib-quality-levels": "^2.1.0",
"videojs-http-source-selector": "^1.1.6",
"videojs-mobile-ui": "^0.8.0",
"videojs-overlay": "^2.1.4",
"videojs-vtt-thumbnails-freetube": "0.0.15",
"vue": "^2.6.14",
"vue-i18n": "^8.25.0",
"vue": "^2.7.13",
"vue-i18n": "^8.28.1",
"vue-observe-visibility": "^1.0.0",
"vue-router": "^3.5.2",
"vue-router": "^3.6.5",
"vuex": "^3.6.2",
"youtube-suggest": "^1.1.2",
"yt-channel-info": "^3.0.4",
"youtube-suggest": "^1.2.0",
"yt-channel-info": "^3.2.1",
"yt-dash-manifest-generator": "1.1.0",
"yt-trending-scraper": "^2.0.1",
"ytdl-core": "git+https://github.com/absidue/node-ytdl-core#temp-fix-11-08-2022",
"ytdl-core": "^4.11.2",
"ytpl": "^2.3.0",
"ytsr": "^3.8.0"
},
"devDependencies": {
"@babel/core": "^7.17.10",
"@babel/plugin-proposal-class-properties": "^7.16.7",
"@babel/preset-env": "^7.17.10",
"@babel/core": "^7.18.13",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/preset-env": "^7.18.10",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.2.5",
"copy-webpack-plugin": "^9.0.1",
"css-loader": "5.2.6",
"electron": "^16.2.7",
"electron-builder": "^23.0.3",
"electron-builder-squirrel-windows": "^22.13.1",
"electron-debug": "^3.2.0",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.7.1",
"css-minimizer-webpack-plugin": "^4.2.2",
"electron": "^21.2.0",
"electron-builder": "^23.6.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-standard": "^5.0.0",
"eslint-plugin-vue": "^7.17.0",
"file-loader": "^6.2.0",
"eslint-plugin-vue": "^9.6.0",
"html-webpack-plugin": "^5.3.2",
"mini-css-extract-plugin": "^2.2.2",
"node-loader": "^2.0.0",
"js-yaml": "^4.1.0",
"json-minimizer-webpack-plugin": "^4.0.0",
"lefthook": "^1.1.3",
"mini-css-extract-plugin": "^2.6.1",
"npm-run-all": "^4.1.5",
"prettier": "^2.3.2",
"prettier": "^2.7.1",
"rimraf": "^3.0.2",
"sass": "^1.38.2",
"sass-loader": "^12.1.0",
"style-loader": "^3.2.1",
"sass": "^1.54.9",
"sass-loader": "^13.0.2",
"tree-kill": "1.2.2",
"url-loader": "^4.1.1",
"vue-devtools": "^5.1.4",
"vue-eslint-parser": "^7.10.0",
"vue-loader": "^15.9.8",
"vue-style-loader": "^4.1.3",
"vue-template-compiler": "^2.6.14",
"webpack": "^5.51.1",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^4.1.0"
"vue-eslint-parser": "^9.1.0",
"vue-loader": "^15.10.0",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.11.1"
}
}

View File

@ -1,6 +1,5 @@
let handlers
const usingElectron = window?.process?.type === 'renderer'
if (usingElectron) {
if (process.env.IS_ELECTRON) {
handlers = require('./electron').default
} else {
handlers = require('./web').default

View File

@ -18,23 +18,13 @@
<body>
<div id="app"></div>
<!-- Set `__static` path to static files in production -->
<script>
try {
if (process.env.NODE_ENV !== 'development')
window.__static = require('path')
.join(__dirname, '/static')
.replace(/\\/g, '\\\\')
} catch {}
</script>
<script>
// This is the service worker with the Advanced caching
// Add this below content to your HTML page, or add the js file to your page at the very top to register service worker
// Check compatibility for the browser we're running this in
if ("serviceWorker" in navigator && (window && window.process && window.process.type !== 'renderer')) {
if ("serviceWorker" in navigator && !<%= process.env.IS_ELECTRON %>) {
if (navigator.serviceWorker.controller) {
console.log("[PWA Builder] active service worker found, no need to register");
} else {

73
src/main/ImageCache.js Normal file
View File

@ -0,0 +1,73 @@
// cleanup expired images once every 5 mins
const CLEANUP_INTERVAL = 300_000
// images expire after 2 hours if no expiry information is found in the http headers
const FALLBACK_MAX_AGE = 7200
export class ImageCache {
constructor() {
this._cache = new Map()
setInterval(this._cleanup.bind(this), CLEANUP_INTERVAL)
}
add(url, mimeType, data, expiry) {
this._cache.set(url, { mimeType, data, expiry })
}
has(url) {
return this._cache.has(url)
}
get(url) {
const entry = this._cache.get(url)
if (!entry) {
// this should never happen as the `has` method should be used to check for the existence first
throw new Error(`No image cache entry for ${url}`)
}
return {
data: entry.data,
mimeType: entry.mimeType
}
}
_cleanup() {
// seconds since 1970-01-01 00:00:00
const now = Math.trunc(Date.now() / 1000)
for (const [key, entry] of this._cache.entries()) {
if (entry.expiry <= now) {
this._cache.delete(key)
}
}
}
}
/**
* Extracts the cache expiry timestamp of image from HTTP headers
* @param {Record<string, string>} headers
* @returns a timestamp in seconds
*/
export function extractExpiryTimestamp(headers) {
const maxAgeRegex = /max-age=([0-9]+)/
const cacheControl = headers['cache-control']
if (cacheControl && maxAgeRegex.test(cacheControl)) {
let maxAge = parseInt(cacheControl.match(maxAgeRegex)[1])
if (headers.age) {
maxAge -= parseInt(headers.age)
}
// we don't need millisecond precision, so we can store it as seconds to use less memory
return Math.trunc(Date.now() / 1000) + maxAge
} else if (headers.expires) {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expires
return Math.trunc(Date.parse(headers.expires) / 1000)
} else {
return Math.trunc(Date.now() / 1000) + FALLBACK_MAX_AGE
}
}

View File

@ -1,15 +1,16 @@
import {
app, BrowserWindow, dialog, Menu, ipcMain,
powerSaveBlocker, screen, session, shell, nativeTheme
powerSaveBlocker, screen, session, shell, nativeTheme, net, protocol
} from 'electron'
import path from 'path'
import cp from 'child_process'
import { IpcChannels, DBActions, SyncEvents } from '../constants'
import baseHandlers from '../datastores/handlers/base'
import { extractExpiryTimestamp, ImageCache } from './ImageCache'
import { existsSync } from 'fs'
if (process.argv.includes('--version')) {
console.log(`v${app.getVersion()}`)
app.exit()
} else {
runApp()
@ -22,11 +23,18 @@ function runApp() {
showCopyImageAddress: true,
prepend: (defaultActions, parameters, browserWindow) => [
{
label: 'Show Video Statistics',
label: 'Show / Hide Video Statistics',
visible: parameters.mediaType === 'video',
click: () => {
browserWindow.webContents.send('showVideoStatistics')
}
},
{
label: 'Open in a New Window',
visible: parameters.linkURL.includes((new URL(browserWindow.webContents.getURL())).origin),
click: () => {
createWindow({ replaceMainWindow: false, windowStartupUrl: parameters.linkURL, showWindowNow: true })
}
}
]
})
@ -39,15 +47,21 @@ function runApp() {
let mainWindow
let startupUrl
// CORS somehow gets re-enabled in Electron v9.0.4
// This line disables it.
// This line can possible be removed if the issue is fixed upstream
app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors')
app.commandLine.appendSwitch('enable-accelerated-video-decode')
app.commandLine.appendSwitch('enable-file-cookies')
app.commandLine.appendSwitch('ignore-gpu-blacklist')
// command line switches need to be added before the app ready event first
// that means we can't use the normal settings system as that is asynchonous,
// doing it synchronously ensures that we add it before the event fires
const replaceHttpCache = existsSync(`${app.getPath('userData')}/experiment-replace-http-cache`)
if (replaceHttpCache) {
// the http cache causes excessive disk usage during video playback
// we've got a custom image cache to make up for disabling the http cache
// experimental as it increases RAM use in favour of reduced disk use
app.commandLine.appendSwitch('disable-http-cache')
}
// See: https://stackoverflow.com/questions/45570589/electron-protocol-handler-not-working-on-windows
// remove so we can register each time as we run the app.
app.removeAsDefaultProtocolClient('freetube')
@ -80,10 +94,6 @@ function runApp() {
}
}
})
} else {
require('electron-debug')({
showDevTools: !(process.env.RENDERER_REMOTE_DEBUGGING === 'true')
})
}
app.on('ready', async (_, __) => {
@ -152,6 +162,113 @@ function runApp() {
})
})
if (replaceHttpCache) {
// in-memory image cache
const imageCache = new ImageCache()
protocol.registerBufferProtocol('imagecache', (request, callback) => {
// Remove `imagecache://` prefix
const url = decodeURIComponent(request.url.substring(13))
if (imageCache.has(url)) {
const cached = imageCache.get(url)
// eslint-disable-next-line node/no-callback-literal
callback({
mimeType: cached.mimeType,
data: cached.data
})
return
}
const newRequest = net.request({
method: request.method,
url
})
// Electron doesn't allow certain headers to be set:
// https://www.electronjs.org/docs/latest/api/client-request#requestsetheadername-value
// also blacklist Origin and Referrer as we don't want to let YouTube know about them
const blacklistedHeaders = ['content-length', 'host', 'trailer', 'te', 'upgrade', 'cookie2', 'keep-alive', 'transfer-encoding', 'origin', 'referrer']
for (const header of Object.keys(request.headers)) {
if (!blacklistedHeaders.includes(header.toLowerCase())) {
newRequest.setHeader(header, request.headers[header])
}
}
newRequest.on('response', (response) => {
const chunks = []
response.on('data', (chunk) => {
chunks.push(chunk)
})
response.on('end', () => {
const data = Buffer.concat(chunks)
const expiryTimestamp = extractExpiryTimestamp(response.headers)
const mimeType = response.headers['content-type']
imageCache.add(url, mimeType, data, expiryTimestamp)
// eslint-disable-next-line node/no-callback-literal
callback({
mimeType,
data: data
})
})
response.on('error', (error) => {
console.error('image cache error', error)
// error objects don't get serialised properly
// https://stackoverflow.com/a/53624454
const errorJson = JSON.stringify(error, (key, value) => {
if (value instanceof Error) {
return {
// Pull all enumerable properties, supporting properties on custom Errors
...value,
// Explicitly pull Error's non-enumerable properties
name: value.name,
message: value.message,
stack: value.stack
}
}
return value
})
// eslint-disable-next-line node/no-callback-literal
callback({
statusCode: response.statusCode ?? 400,
mimeType: 'application/json',
data: Buffer.from(errorJson)
})
})
})
newRequest.end()
})
const imageRequestFilter = { urls: ['https://*/*', 'http://*/*'] }
session.defaultSession.webRequest.onBeforeRequest(imageRequestFilter, (details, callback) => {
// the requests made by the imagecache:// handler to fetch the image,
// are allowed through, as their resourceType is 'other'
if (details.resourceType === 'image') {
// eslint-disable-next-line node/no-callback-literal
callback({
redirectURL: `imagecache://${encodeURIComponent(details.url)}`
})
} else {
// eslint-disable-next-line node/no-callback-literal
callback({})
}
})
// --- end of `if experimentsDisableDiskCache` ---
}
await createWindow()
if (isDev) {
@ -169,11 +286,17 @@ function runApp() {
require('vue-devtools').install()
/* eslint-enable */
} catch (err) {
console.log(err)
console.error(err)
}
}
async function createWindow({ replaceMainWindow = true, windowStartupUrl = null, showWindowNow = false } = { }) {
async function createWindow(
{
replaceMainWindow = true,
windowStartupUrl = null,
showWindowNow = false,
searchQueryText = null
} = { }) {
// Syncing new window background to theme choice.
const windowBackground = await baseHandlers.settings._findTheme().then(({ value }) => {
switch (value) {
@ -192,7 +315,7 @@ function runApp() {
return nativeTheme.shouldUseDarkColors ? '#212121' : '#f1f1f1'
}
}).catch((error) => {
console.log(error)
console.error(error)
// Default to nativeTheme settings if nothing is found.
return nativeTheme.shouldUseDarkColors ? '#212121' : '#f1f1f1'
})
@ -255,7 +378,7 @@ function runApp() {
const boundsDoc = await baseHandlers.settings._findBounds()
if (typeof boundsDoc?.value === 'object') {
const { maximized, ...bounds } = boundsDoc.value
const { maximized, fullScreen, ...bounds } = boundsDoc.value
const allDisplaysSummaryWidth = screen
.getAllDisplays()
.reduce((accumulator, { size: { width } }) => accumulator + width, 0)
@ -272,6 +395,10 @@ function runApp() {
if (maximized) {
newWindow.maximize()
}
if (fullScreen) {
newWindow.setFullScreen(true)
}
}
// If called multiple times
@ -295,18 +422,30 @@ function runApp() {
/* eslint-disable-next-line */
newWindow.loadFile(`${__dirname}/index.html`)
}
}
global.__static = path
.join(__dirname, '/static')
.replace(/\\/g, '\\\\')
if (typeof searchQueryText === 'string' && searchQueryText.length > 0) {
ipcMain.once('searchInputHandlingReady', () => {
newWindow.webContents.send('updateSearchInputText', searchQueryText)
})
}
// Show when loaded
newWindow.once('ready-to-show', () => {
if (newWindow.isVisible()) { return }
if (newWindow.isVisible()) {
// only open the dev tools if they aren't already open
if (isDev && !newWindow.webContents.isDevToolsOpened()) {
newWindow.webContents.openDevTools({ activate: false })
}
return
}
newWindow.show()
newWindow.focus()
if (isDev) {
newWindow.webContents.openDevTools({ activate: false })
}
})
newWindow.once('close', async () => {
@ -316,7 +455,8 @@ function runApp() {
const value = {
...newWindow.getNormalBounds(),
maximized: newWindow.isMaximized()
maximized: newWindow.isMaximized(),
fullScreen: newWindow.isFullScreen()
}
await baseHandlers.settings._updateBounds(value)
@ -329,8 +469,6 @@ function runApp() {
// Which raises "Object has been destroyed" error
mainWindow = allWindows[0]
}
console.log('closed')
})
}
@ -382,7 +520,6 @@ function runApp() {
})
ipcMain.on(IpcChannels.ENABLE_PROXY, (_, url) => {
console.log(url)
session.defaultSession.setProxy({
proxyRules: url
})
@ -412,22 +549,28 @@ function runApp() {
return app.getPath('pictures')
})
ipcMain.handle(IpcChannels.SHOW_OPEN_DIALOG, async (_, options) => {
ipcMain.handle(IpcChannels.SHOW_OPEN_DIALOG, async ({ sender }, options) => {
const senderWindow = findSenderWindow(sender)
if (senderWindow) {
return await dialog.showOpenDialog(senderWindow, options)
}
return await dialog.showOpenDialog(options)
})
ipcMain.handle(IpcChannels.SHOW_SAVE_DIALOG, async (event, { options, useModal }) => {
if (useModal) {
const senderWindow = BrowserWindow.getAllWindows().find((window) => {
return window.webContents.id === event.sender.id
})
if (senderWindow) {
return await dialog.showSaveDialog(senderWindow, options)
}
ipcMain.handle(IpcChannels.SHOW_SAVE_DIALOG, async ({ sender }, options) => {
const senderWindow = findSenderWindow(sender)
if (senderWindow) {
return await dialog.showSaveDialog(senderWindow, options)
}
return await dialog.showSaveDialog(options)
})
function findSenderWindow(sender) {
return BrowserWindow.getAllWindows().find((window) => {
return window.webContents.id === sender.id
})
}
ipcMain.on(IpcChannels.STOP_POWER_SAVE_BLOCKER, (_, id) => {
powerSaveBlocker.stop(id)
})
@ -436,11 +579,12 @@ function runApp() {
return powerSaveBlocker.start('prevent-display-sleep')
})
ipcMain.on(IpcChannels.CREATE_NEW_WINDOW, (_e, { windowStartupUrl = null } = { }) => {
ipcMain.on(IpcChannels.CREATE_NEW_WINDOW, (_e, { windowStartupUrl = null, searchQueryText = null } = { }) => {
createWindow({
replaceMainWindow: false,
showWindowNow: true,
windowStartupUrl: windowStartupUrl
windowStartupUrl: windowStartupUrl,
searchQueryText: searchQueryText
})
})
@ -785,6 +929,20 @@ function runApp() {
type: 'normal'
},
{ type: 'separator' },
{
label: 'Preferences',
accelerator: 'CmdOrCtrl+,',
click: (_menuItem, browserWindow, _event) => {
if (browserWindow == null) { return }
browserWindow.webContents.send(
'change-view',
{ route: '/settings' }
)
},
type: 'normal'
},
{ type: 'separator' },
{ role: 'quit' }
]
},
@ -816,6 +974,21 @@ function runApp() {
accelerator: 'CmdOrCtrl+Shift+R'
},
{ role: 'toggledevtools' },
{ role: 'toggledevtools', accelerator: 'f12', visible: false },
{
label: 'Enter Inspect Element Mode',
accelerator: 'CmdOrCtrl+Shift+C',
click: (_, window) => {
if (window.webContents.isDevToolsOpened()) {
window.devToolsWebContents.executeJavaScript('DevToolsAPI.enterInspectElementMode()')
} else {
window.webContents.once('devtools-opened', () => {
window.devToolsWebContents.executeJavaScript('DevToolsAPI.enterInspectElementMode()')
})
window.webContents.openDevTools()
}
}
},
{ type: 'separator' },
{ role: 'resetzoom' },
{ role: 'resetzoom', accelerator: 'CmdOrCtrl+num0', visible: false },

View File

@ -9,10 +9,10 @@ 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 $ from 'jquery'
import { marked } from 'marked'
import Parser from 'rss-parser'
import { IpcChannels } from '../constants'
import packageDetails from '../../package.json'
import { openExternalLink, showToast } from './helpers/utils'
let ipcRenderer = null
@ -51,15 +51,9 @@ export default Vue.extend({
}
},
computed: {
isDev: function () {
return process.env.NODE_ENV === 'development'
},
isOpen: function () {
return this.$store.getters.getIsSideNavOpen
},
usingElectron: function() {
return this.$store.getters.getUsingElectron
},
showProgressBar: function () {
return this.$store.getters.getShowProgressBar
},
@ -152,7 +146,7 @@ export default Vue.extend({
this.grabUserSettings().then(async () => {
this.checkThemeSettings()
await this.fetchInvidiousInstances({ isDev: this.isDev })
await this.fetchInvidiousInstances()
if (this.defaultInvidiousInstance === '') {
await this.setRandomCurrentInvidiousInstance()
}
@ -161,12 +155,12 @@ export default Vue.extend({
this.grabHistory()
this.grabAllPlaylists()
if (this.usingElectron) {
console.log('User is using Electron')
if (process.env.IS_ELECTRON) {
ipcRenderer = require('electron').ipcRenderer
this.setupListenersToSyncWindows()
this.activateKeyboardShortcuts()
this.openAllLinksExternally()
this.enableSetSearchQueryText()
this.enableOpenUrl()
this.watchSystemTheme()
await this.checkExternalPlayer()
@ -197,75 +191,72 @@ export default Vue.extend({
},
updateTheme: function (theme) {
console.group('updateTheme')
console.log('Theme: ', theme)
document.body.className = `${theme.baseTheme} main${theme.mainColor} sec${theme.secColor}`
document.body.dataset.systemTheme = this.systemTheme
console.groupEnd()
},
checkForNewUpdates: function () {
if (this.checkForUpdates) {
const { version } = require('../../package.json')
const requestUrl = 'https://api.github.com/repos/freetubeapp/freetube/releases?per_page=1'
$.getJSON(requestUrl, (response) => {
const tagName = response[0].tag_name
const versionNumber = tagName.replace('v', '').replace('-beta', '')
this.updateChangelog = marked.parse(response[0].body)
this.changeLogTitle = response[0].name
fetch(requestUrl)
.then((response) => response.json())
.then((json) => {
const tagName = json[0].tag_name
const versionNumber = tagName.replace('v', '').replace('-beta', '')
this.updateChangelog = marked.parse(json[0].body)
this.changeLogTitle = json[0].name
const message = this.$t('Version $ is now available! Click for more details')
this.updateBannerMessage = message.replace('$', versionNumber)
this.updateBannerMessage = this.$t('Version {versionNumber} is now available! Click for more details', { versionNumber })
const appVersion = version.split('.')
const latestVersion = versionNumber.split('.')
const appVersion = packageDetails.version.split('.')
const latestVersion = versionNumber.split('.')
if (parseInt(appVersion[0]) < parseInt(latestVersion[0])) {
this.showUpdatesBanner = true
} else if (parseInt(appVersion[1]) < parseInt(latestVersion[1])) {
this.showUpdatesBanner = true
} else if (parseInt(appVersion[2]) < parseInt(latestVersion[2]) && parseInt(appVersion[1]) <= parseInt(latestVersion[1])) {
this.showUpdatesBanner = true
}
}).fail((xhr, textStatus, error) => {
console.log(xhr)
console.log(textStatus)
console.log(requestUrl)
console.log(error)
})
if (parseInt(appVersion[0]) < parseInt(latestVersion[0])) {
this.showUpdatesBanner = true
} else if (parseInt(appVersion[1]) < parseInt(latestVersion[1])) {
this.showUpdatesBanner = true
} else if (parseInt(appVersion[2]) < parseInt(latestVersion[2]) && parseInt(appVersion[1]) <= parseInt(latestVersion[1])) {
this.showUpdatesBanner = true
}
})
.catch((error) => {
console.error('errored while checking for updates', requestUrl, error)
})
}
},
checkForNewBlogPosts: function () {
if (this.checkForBlogPosts) {
const parser = new Parser()
const feedUrl = 'https://write.as/freetube/feed/'
let lastAppWasRunning = localStorage.getItem('lastAppWasRunning')
if (lastAppWasRunning !== null) {
lastAppWasRunning = new Date(lastAppWasRunning)
}
parser.parseURL(feedUrl).then((response) => {
const latestBlog = response.items[0]
const latestPubDate = new Date(latestBlog.pubDate)
fetch('https://write.as/freetube/feed/')
.then(response => response.text())
.then(response => {
const xmlDom = new DOMParser().parseFromString(response, 'application/xml')
if (lastAppWasRunning === null || latestPubDate > lastAppWasRunning) {
const message = this.$t('A new blog is now available, $. Click to view more')
this.blogBannerMessage = message.replace('$', latestBlog.title)
this.latestBlogUrl = latestBlog.link
this.showBlogBanner = true
}
const latestBlog = xmlDom.querySelector('item')
const latestPubDate = new Date(latestBlog.querySelector('pubDate').textContent)
localStorage.setItem('lastAppWasRunning', new Date())
})
if (lastAppWasRunning === null || latestPubDate > lastAppWasRunning) {
const title = latestBlog.querySelector('title').textContent
this.blogBannerMessage = this.$t('A new blog is now available, {blogTitle}. Click to view more', { blogTitle: title })
this.latestBlogUrl = latestBlog.querySelector('link').textContent
this.showBlogBanner = true
}
localStorage.setItem('lastAppWasRunning', new Date())
})
}
},
checkExternalPlayer: async function () {
const payload = {
isDev: this.isDev,
externalPlayer: this.externalPlayer
}
this.getExternalPlayerCmdArgumentsData(payload)
@ -281,7 +272,7 @@ export default Vue.extend({
handleNewBlogBannerClick: function (response) {
if (response) {
this.openExternalLink(this.latestBlogUrl)
openExternalLink(this.latestBlogUrl)
}
this.showBlogBanner = false
@ -289,37 +280,39 @@ export default Vue.extend({
openDownloadsPage: function () {
const url = 'https://freetubeapp.io#download'
this.openExternalLink(url)
openExternalLink(url)
this.showReleaseNotes = false
this.showUpdatesBanner = false
},
activateKeyboardShortcuts: function () {
$(document).on('keydown', this.handleKeyboardShortcuts)
$(document).on('mousedown', () => {
document.addEventListener('keydown', this.handleKeyboardShortcuts)
document.addEventListener('mousedown', () => {
this.hideOutlines = true
})
},
handleKeyboardShortcuts: function (event) {
if (event.altKey) {
switch (event.code) {
switch (event.key) {
case 'ArrowRight':
this.$refs.topNav.historyForward()
break
case 'ArrowLeft':
this.$refs.topNav.historyBack()
break
case 'KeyD':
case 'D':
case 'd':
this.$refs.topNav.focusSearch()
break
}
}
switch (event.code) {
switch (event.key) {
case 'Tab':
this.hideOutlines = false
break
case 'KeyL':
case 'L':
case 'l':
if ((process.platform !== 'darwin' && event.ctrlKey) ||
(process.platform === 'darwin' && event.metaKey)) {
this.$refs.topNav.focusSearch()
@ -329,22 +322,26 @@ export default Vue.extend({
},
openAllLinksExternally: function () {
$(document).on('click', 'a[href^="http"]', (event) => {
this.handleLinkClick(event)
const isExternalLink = (event) => event.target.tagName === 'A' && !event.target.href.startsWith(window.location.origin)
document.addEventListener('click', (event) => {
if (isExternalLink(event)) {
this.handleLinkClick(event)
}
})
$(document).on('auxclick', 'a[href^="http"]', (event) => {
document.addEventListener('auxclick', (event) => {
// auxclick fires for all clicks not performed with the primary button
// only handle the link click if it was the middle button,
// otherwise the context menu breaks
if (event.button === 1) {
if (isExternalLink(event) && event.button === 1) {
this.handleLinkClick(event)
}
})
},
handleLinkClick: function (event) {
const el = event.currentTarget
const el = event.target
event.preventDefault()
// Check if it's a YouTube link
@ -359,9 +356,7 @@ export default Vue.extend({
})
} else if (this.externalLinkHandling === 'doNothing') {
// Let user know opening external link is disabled via setting
this.showToast({
message: this.$t('External link opening has been disabled in the general settings')
})
showToast(this.$t('External link opening has been disabled in the general settings'))
} else if (this.externalLinkHandling === 'openLinkAfterPrompt') {
// Storing the URL is necessary as
// there is no other way to pass the URL to click callback
@ -369,7 +364,7 @@ export default Vue.extend({
this.showExternalLinkOpeningPrompt = true
} else {
// Open links externally
this.openExternalLink(el.href)
openExternalLink(el.href)
}
},
@ -414,7 +409,8 @@ export default Vue.extend({
this.openInternalPath({
path,
query,
doCreateNewWindow
doCreateNewWindow,
searchQueryText: searchQuery
})
break
}
@ -426,9 +422,7 @@ export default Vue.extend({
message = this.$t(message)
}
this.showToast({
message: message
})
showToast(message)
break
}
@ -455,9 +449,7 @@ export default Vue.extend({
message = this.$t(message)
}
this.showToast({
message: message
})
showToast(message)
}
}
})
@ -473,8 +465,8 @@ export default Vue.extend({
})
},
openInternalPath: function({ path, doCreateNewWindow, query = {} }) {
if (this.usingElectron && doCreateNewWindow) {
openInternalPath: function({ path, doCreateNewWindow, query = {}, searchQueryText = null }) {
if (process.env.IS_ELECTRON && doCreateNewWindow) {
const { ipcRenderer } = require('electron')
// Combine current document path and new "hash" as new window startup URL
@ -483,7 +475,8 @@ export default Vue.extend({
`#${path}?${(new URLSearchParams(query)).toString()}`
].join('')
ipcRenderer.send(IpcChannels.CREATE_NEW_WINDOW, {
windowStartupUrl: newWindowStartupURL
windowStartupUrl: newWindowStartupURL,
searchQueryText
})
} else {
// Web
@ -494,6 +487,16 @@ export default Vue.extend({
}
},
enableSetSearchQueryText: function () {
ipcRenderer.on('updateSearchInputText', (event, searchQueryText) => {
if (searchQueryText) {
this.$refs.topNav.updateSearchInputText(searchQueryText)
}
})
ipcRenderer.send('searchInputHandlingReady')
},
enableOpenUrl: function () {
ipcRenderer.on('openUrl', (event, url) => {
if (url) {
@ -512,7 +515,7 @@ export default Vue.extend({
// if `lastExternalLinkToBeOpened` is empty
// Open links externally
this.openExternalLink(this.lastExternalLinkToBeOpened)
openExternalLink(this.lastExternalLinkToBeOpened)
}
},
@ -527,8 +530,6 @@ export default Vue.extend({
]),
...mapActions([
'showToast',
'openExternalLink',
'grabUserSettings',
'grabAllProfiles',
'grabHistory',

View File

@ -38,6 +38,7 @@
<RouterView
ref="router"
class="routerView"
@showOutlines="hideOutlines = false"
/>
<!-- </keep-alive> -->
</transition>

File diff suppressed because it is too large Load Diff

View File

@ -1 +0,0 @@
@use "../../sass-partials/settings"

View File

@ -1,19 +1,11 @@
<template>
<details>
<summary>
<h3>
{{ $t("Settings.Data Settings.Data Settings") }}
</h3>
</summary>
<hr>
<ft-settings-section
:title="$t('Settings.Data Settings.Data Settings')"
>
<ft-flex-box>
<ft-button
:label="$t('Settings.Data Settings.Import Subscriptions')"
@click="showImportSubscriptionsPrompt = true"
/>
<ft-button
:label="$t('Settings.Data Settings.Check for Legacy Subscriptions')"
@click="checkForLegacySubscriptions"
@click="importSubscriptions"
/>
<ft-button
:label="$t('Settings.Data Settings.Export Subscriptions')"
@ -54,13 +46,6 @@
@click="exportPlaylists"
/>
</ft-flex-box>
<ft-prompt
v-if="showImportSubscriptionsPrompt"
:label="$t('Settings.Data Settings.Select Import Type')"
:option-names="importSubscriptionsPromptNames"
:option-values="subscriptionsPromptValues"
@click="importSubscriptions"
/>
<ft-prompt
v-if="showExportSubscriptionsPrompt"
:label="$t('Settings.Data Settings.Select Export Type')"
@ -68,8 +53,7 @@
:option-values="subscriptionsPromptValues"
@click="exportSubscriptions"
/>
</details>
</ft-settings-section>
</template>
<script src="./data-settings.js" />
<style scoped lang="sass" src="./data-settings.sass" />

View File

@ -1,19 +1,13 @@
import Vue from 'vue'
import { mapActions } from 'vuex'
import FtCard from '../ft-card/ft-card.vue'
import FtSettingsSection from '../ft-settings-section/ft-settings-section.vue'
import FtToggleSwitch from '../ft-toggle-switch/ft-toggle-switch.vue'
import FtButton from '../ft-button/ft-button.vue'
import FtSelect from '../ft-select/ft-select.vue'
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
export default Vue.extend({
name: 'PlayerSettings',
components: {
'ft-card': FtCard,
'ft-toggle-switch': FtToggleSwitch,
'ft-button': FtButton,
'ft-select': FtSelect,
'ft-flex-box': FtFlexBox
'ft-settings-section': FtSettingsSection,
'ft-toggle-switch': FtToggleSwitch
},
computed: {
hideVideoViews: function () {
@ -55,8 +49,14 @@ export default Vue.extend({
hideLiveStreams: function() {
return this.$store.getters.getHideLiveStreams
},
hideSharingActions: function() {
hideSharingActions: function () {
return this.$store.getters.getHideSharingActions
},
backendPreference: function () {
return this.$store.getters.getBackendPreference
},
hideChapters: function () {
return this.$store.getters.getHideChapters
}
},
methods: {
@ -84,7 +84,8 @@ export default Vue.extend({
'updateHideVideoDescription',
'updateHideComments',
'updateHideLiveStreams',
'updateHideSharingActions'
'updateHideSharingActions',
'updateHideChapters'
])
}
})

View File

@ -1 +0,0 @@
@use "../../sass-partials/settings"

View File

@ -1,11 +1,7 @@
<template>
<details>
<summary>
<h3>
{{ $t("Settings.Distraction Free Settings.Distraction Free Settings") }}
</h3>
</summary>
<hr>
<ft-settings-section
:title="$t('Settings.Distraction Free Settings.Distraction Free Settings')"
>
<div class="switchColumnGrid">
<div class="switchColumn">
<ft-toggle-switch
@ -50,6 +46,12 @@
:default-value="hideSharingActions"
@change="updateHideSharingActions"
/>
<ft-toggle-switch
:label="$t('Settings.Distraction Free Settings.Hide Chapters')"
:compact="true"
:default-value="hideChapters"
@change="updateHideChapters"
/>
</div>
<div class="switchColumn">
<ft-toggle-switch
@ -96,25 +98,7 @@
/>
</div>
</div>
<br>
<ft-flex-box>
<ft-select
v-if="false"
placeholder="Distraction View Type"
:value="viewValues[0]"
:select-names="viewNames"
:select-values="viewValues"
/>
</ft-flex-box>
<br>
<ft-flex-box>
<ft-button
v-if="false"
label="Manage My Distractions"
/>
</ft-flex-box>
</details>
</ft-settings-section>
</template>
<script src="./distraction-settings.js" />
<style scoped lang="sass" src="./distraction-settings.sass" />

View File

@ -1,4 +1,5 @@
import Vue from 'vue'
import FtSettingsSection from '../ft-settings-section/ft-settings-section.vue'
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
import FtToggleSwitch from '../ft-toggle-switch/ft-toggle-switch.vue'
import FtSelect from '../ft-select/ft-select.vue'
@ -11,6 +12,7 @@ import { IpcChannels } from '../../../constants'
export default Vue.extend({
name: 'DownloadSettings',
components: {
'ft-settings-section': FtSettingsSection,
'ft-toggle-switch': FtToggleSwitch,
'ft-flex-box': FtFlexBox,
'ft-select': FtSelect,

View File

@ -1,8 +1,2 @@
@use "../../sass-partials/settings"
@media only screen and (max-width: 500px)
.downloadSettingsFlexBox
justify-content: flex-start
.folderDisplay
width: 50vh

View File

@ -1,11 +1,7 @@
<template>
<details>
<summary>
<h3>
{{ $t("Settings.Download Settings.Download Settings") }}
</h3>
</summary>
<hr>
<ft-settings-section
:title="$t('Settings.Download Settings.Download Settings')"
>
<ft-flex-box>
<ft-select
:placeholder="$t('Settings.Download Settings.Download Behavior')"
@ -17,7 +13,7 @@
</ft-flex-box>
<ft-flex-box
v-if="downloadBehavior === 'download'"
class="downloadSettingsFlexBox"
class="settingsFlexStart500px"
>
<ft-toggle-switch
:label="$t('Settings.Download Settings.Ask Download Path')"
@ -44,7 +40,7 @@
@click="chooseDownloadingFolder"
/>
</ft-flex-box>
</details>
</ft-settings-section>
</template>
<script src="./download-settings.js" />

View File

@ -0,0 +1,6 @@
.experimental-warning {
text-align: center;
font-weight: bold;
padding-left: 4%;
padding-right: 4%
}

View File

@ -0,0 +1,73 @@
import { closeSync, existsSync, openSync, rmSync } from 'fs'
import Vue from 'vue'
import { mapActions } from 'vuex'
import FtSettingsSection from '../ft-settings-section/ft-settings-section.vue'
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
import FtToggleSwitch from '../ft-toggle-switch/ft-toggle-switch.vue'
import FtPrompt from '../ft-prompt/ft-prompt.vue'
export default Vue.extend({
name: 'ExperimentalSettings',
components: {
'ft-settings-section': FtSettingsSection,
'ft-flex-box': FtFlexBox,
'ft-toggle-switch': FtToggleSwitch,
'ft-prompt': FtPrompt
},
data: function () {
return {
replaceHttpCacheLoading: true,
replaceHttpCache: false,
replaceHttpCachePath: '',
showRestartPrompt: false
}
},
mounted: function () {
this.getUserDataPath().then((userData) => {
this.replaceHttpCachePath = `${userData}/experiment-replace-http-cache`
this.replaceHttpCache = existsSync(this.replaceHttpCachePath)
this.replaceHttpCacheLoading = false
})
},
methods: {
updateReplaceHttpCache: function () {
this.replaceHttpCache = !this.replaceHttpCache
if (this.replaceHttpCache) {
// create an empty file
closeSync(openSync(this.replaceHttpCachePath, 'w'))
} else {
rmSync(this.replaceHttpCachePath)
}
},
handleRestartPrompt: function (value) {
this.replaceHttpCache = value
this.showRestartPrompt = true
},
handleReplaceHttpCache: function (value) {
this.showRestartPrompt = false
if (value === null || value === 'no') {
this.replaceHttpCache = !this.replaceHttpCache
return
}
if (this.replaceHttpCache) {
// create an empty file
closeSync(openSync(this.replaceHttpCachePath, 'w'))
} else {
rmSync(this.replaceHttpCachePath)
}
const { ipcRenderer } = require('electron')
ipcRenderer.send('relaunchRequest')
},
...mapActions([
'getUserDataPath'
])
}
})

View File

@ -0,0 +1,30 @@
<template>
<ft-settings-section
:title="$t('Settings.Experimental Settings.Experimental Settings')"
>
<p class="experimental-warning">
{{ $t('Settings.Experimental Settings.Warning') }}
</p>
<ft-flex-box>
<ft-toggle-switch
tooltip-position="top"
:label="$t('Settings.Experimental Settings.Replace HTTP Cache')"
:compact="true"
:default-value="replaceHttpCache"
:disabled="replaceHttpCacheLoading"
:tooltip="$t('Tooltips.Experimental Settings.Replace HTTP Cache')"
@change="handleRestartPrompt"
/>
</ft-flex-box>
<ft-prompt
v-if="showRestartPrompt"
:label="$t('Settings[\'The app needs to restart for changes to take effect. Restart and apply change?\']')"
:option-names="[$t('Yes'), $t('No')]"
:option-values="['yes', 'no']"
@click="handleReplaceHttpCache"
/>
</ft-settings-section>
</template>
<script src="./experimental-settings.js" />
<style scoped src="./experimental-settings.css" />

View File

@ -1,6 +1,6 @@
import Vue from 'vue'
import { mapActions } from 'vuex'
import FtCard from '../ft-card/ft-card.vue'
import FtSettingsSection from '../ft-settings-section/ft-settings-section.vue'
import FtSelect from '../ft-select/ft-select.vue'
import FtInput from '../ft-input/ft-input.vue'
import FtToggleSwitch from '../ft-toggle-switch/ft-toggle-switch.vue'
@ -9,7 +9,7 @@ import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
export default Vue.extend({
name: 'ExternalPlayerSettings',
components: {
'ft-card': FtCard,
'ft-settings-section': FtSettingsSection,
'ft-select': FtSelect,
'ft-input': FtInput,
'ft-toggle-switch': FtToggleSwitch,
@ -19,10 +19,6 @@ export default Vue.extend({
return {}
},
computed: {
isDev: function () {
return process.env.NODE_ENV === 'development'
},
externalPlayerNames: function () {
const fallbackNames = this.$store.getters.getExternalPlayerNames
const nameTranslationKeys = this.$store.getters.getExternalPlayerNameTranslationKeys
@ -49,8 +45,8 @@ export default Vue.extend({
const cmdArgs = this.$store.getters.getExternalPlayerCmdArguments[this.externalPlayer]
if (cmdArgs && typeof cmdArgs.defaultCustomArguments === 'string' && cmdArgs.defaultCustomArguments !== '') {
const defaultArgs = this.$t('Tooltips.External Player Settings.DefaultCustomArgumentsTemplate')
.replace('$', cmdArgs.defaultCustomArguments)
const defaultArgs = this.$t('Tooltips.External Player Settings.DefaultCustomArgumentsTemplate',
{ defaultCustomArguments: cmdArgs.defaultCustomArguments })
return `${tooltip} ${defaultArgs}`
}

View File

@ -1 +0,0 @@
@use "../../sass-partials/settings"

View File

@ -1,11 +1,7 @@
<template>
<details>
<summary>
<h3>
{{ $t("Settings.External Player Settings.External Player Settings") }}
</h3>
</summary>
<hr>
<ft-settings-section
:title="$t('Settings.External Player Settings.External Player Settings')"
>
<div class="switchColumnGrid">
<div class="switchColumn">
<ft-select
@ -30,7 +26,7 @@
</div>
<ft-flex-box
v-if="externalPlayer !== ''"
class="externalPlayerSettingsFlexBox"
class="settingsFlexStart460px"
>
<ft-input
:placeholder="$t('Settings.External Player Settings.Custom External Player Executable')"
@ -49,8 +45,7 @@
@input="updateExternalPlayerCustomArgs"
/>
</ft-flex-box>
</details>
</ft-settings-section>
</template>
<script src="./external-player-settings.js" />
<style scoped lang="sass" src="./external-player-settings.sass" />

View File

@ -16,7 +16,7 @@ export default Vue.extend({
restrictedMessage: function () {
const contentType = this.$t('Age Restricted.Type.' + this.contentTypeString)
return this.$t('Age Restricted.This $contentType is age restricted').replace('$contentType', contentType)
return this.$t('Age Restricted.This {videoOrPlaylist} is age restricted', { videoOrPlaylist: contentType })
}
}
})

View File

@ -1,17 +1,7 @@
import Vue from 'vue'
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
import FtListVideo from '../ft-list-video/ft-list-video.vue'
import FtListChannel from '../ft-list-channel/ft-list-channel.vue'
import FtListPlaylist from '../ft-list-playlist/ft-list-playlist.vue'
export default Vue.extend({
name: 'FtElementList',
components: {
'ft-flex-box': FtFlexBox,
'ft-list-video': FtListVideo,
'ft-list-channel': FtListChannel,
'ft-list-playlist': FtListPlaylist
},
name: 'FtButton',
props: {
label: {
type: String,

View File

@ -1,5 +1,8 @@
<template>
<div class="ft-card">
<div
class="ft-card"
@focusout="$emit('focusout')"
>
<slot />
</div>
</template>

View File

@ -12,7 +12,7 @@
class="bubble selected"
>
<font-awesome-icon
icon="check"
:icon="['fas', 'check']"
class="icon"
/>
</div>

View File

@ -1,12 +1,10 @@
import Vue from 'vue'
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
import FtAutoGrid from '../ft-auto-grid/ft-auto-grid.vue'
import FtListLazyWrapper from '../ft-list-lazy-wrapper/ft-list-lazy-wrapper.vue'
export default Vue.extend({
name: 'FtElementList',
components: {
'ft-flex-box': FtFlexBox,
'ft-auto-grid': FtAutoGrid,
'ft-list-lazy-wrapper': FtListLazyWrapper
},

View File

@ -1,6 +0,0 @@
.ft-grid {
display: grid;
grid-template-columns: repeat(auto-fill, 240px);
justify-content: space-evenly;
grid-gap: 5px;
}

View File

@ -1,5 +0,0 @@
import Vue from 'vue'
export default Vue.extend({
name: 'FtGrid'
})

View File

@ -1,8 +0,0 @@
<template>
<div class="ft-grid">
<slot />
</div>
</template>
<script src="./ft-grid.js" />
<style scoped src="./ft-grid.css" />

View File

@ -1,5 +1,4 @@
import Vue from 'vue'
import $ from 'jquery'
export default Vue.extend({
name: 'FtIconButton',
@ -9,8 +8,8 @@ export default Vue.extend({
default: ''
},
icon: {
type: String,
default: 'ellipsis-v'
type: Array,
default: () => ['fas', 'ellipsis-v']
},
theme: {
type: String,
@ -56,64 +55,45 @@ export default Vue.extend({
data: function () {
return {
dropdownShown: false,
id: ''
mouseDownOnIcon: false
}
},
mounted: function () {
this.id = `iconButton${this._uid}`
},
methods: {
toggleDropdown: function () {
const dropdownBox = $(`#${this.id}`)
if (this.dropdownShown) {
dropdownBox.get(0).style.display = 'none'
this.dropdownShown = false
} else {
dropdownBox.get(0).style.display = 'inline'
dropdownBox.get(0).focus()
this.dropdownShown = true
dropdownBox.focusout(() => {
const shareLinks = dropdownBox.find('.shareLinks')
if (shareLinks.length > 0) {
if (!shareLinks[0].parentNode.matches(':hover')) {
dropdownBox.get(0).style.display = 'none'
// When pressing the profile button
// It will make the menu reappear if we set `dropdownShown` immediately
setTimeout(() => {
this.dropdownShown = false
}, 100)
}
} else {
dropdownBox.get(0).style.display = 'none'
// When pressing the profile button
// It will make the menu reappear if we set `dropdownShown` immediately
setTimeout(() => {
this.dropdownShown = false
}, 100)
}
})
}
},
focusOut: function () {
const dropdownBox = $(`#${this.id}`)
dropdownBox.focusout()
dropdownBox.get(0).style.display = 'none'
// used by the share menu
hideDropdown: function () {
this.dropdownShown = false
},
handleIconClick: function () {
if (this.forceDropdown || (this.dropdownOptions.length > 0)) {
this.toggleDropdown()
this.dropdownShown = !this.dropdownShown
if (this.dropdownShown) {
// wait until the dropdown is visible
// then focus it so we can hide it automatically when it loses focus
setTimeout(() => {
this.$refs.dropdown.focus()
}, 0)
}
} else {
this.$emit('click')
}
},
handleIconMouseDown: function () {
if (this.dropdownShown) {
this.mouseDownOnIcon = true
}
},
handleDropdownFocusOut: function () {
if (this.mouseDownOnIcon) {
this.mouseDownOnIcon = false
} else if (!this.$refs.dropdown.matches(':focus-within')) {
this.dropdownShown = false
}
},
handleDropdownClick: function ({ url, index }) {
if (this.returnIndex) {
this.$emit('click', index)
@ -121,7 +101,7 @@ export default Vue.extend({
this.$emit('click', url)
}
this.focusOut()
this.dropdownShown = false
}
}
})

View File

@ -56,7 +56,7 @@
color: var(--favorite-icon-color)
.iconDropdown
display: none
display: inline
position: absolute
text-align: center
list-style-type: none
@ -68,9 +68,6 @@
color: var(--secondary-text-color)
user-select: none
&:focus
display: inline
&.left
right: calc(50% - 10px)

View File

@ -13,9 +13,11 @@
fontSize: size + 'px'
}"
@click="handleIconClick"
@mousedown="handleIconMouseDown"
/>
<div
:id="id"
v-show="dropdownShown"
ref="dropdown"
tabindex="-1"
class="iconDropdown"
:class="{
@ -25,6 +27,7 @@
bottom: dropdownPositionY === 'bottom',
top: dropdownPositionY === 'top'
}"
@focusout="handleDropdownFocusOut"
>
<slot>
<ul

View File

@ -67,7 +67,7 @@ export default Vue.extend({
// As the text input box should be empty
clearTextButtonExisting: false,
clearTextButtonVisible: false,
actionButtonIconName: 'search'
actionButtonIconName: ['fas', 'search']
}
},
computed: {
@ -87,6 +87,18 @@ export default Vue.extend({
return this.inputData.length > 0
}
},
watch: {
dataList(val, oldVal) {
if (val !== oldVal) {
this.updateVisibleDataList()
}
},
inputData(val, oldVal) {
if (val !== oldVal) {
this.updateVisibleDataList()
}
}
},
mounted: function () {
this.id = this._uid
this.inputData = this.value
@ -95,13 +107,13 @@ export default Vue.extend({
setTimeout(this.addListener, 200)
},
methods: {
handleClick: function () {
handleClick: function (e) {
// No action if no input text
if (!this.inputDataPresent) { return }
this.searchState.showOptions = false
this.$emit('input', this.inputData)
this.$emit('click', this.inputData)
this.$emit('click', this.inputData, { event: e })
},
handleInput: function (val) {
@ -109,7 +121,6 @@ export default Vue.extend({
this.searchState.selectedOption !== -1 &&
this.inputData === this.visibleDataList[this.searchState.selectedOption]) { return }
this.handleActionIconChange()
this.updateVisibleDataList()
this.$emit('input', val)
},
@ -136,7 +147,7 @@ export default Vue.extend({
if (!this.inputDataPresent) {
// Change back to default icon if text is blank
this.actionButtonIconName = 'search'
this.actionButtonIconName = ['fas', 'search']
return
}
@ -165,15 +176,15 @@ export default Vue.extend({
if (isYoutubeLink) {
// Go to URL (i.e. Video/Playlist/Channel
this.actionButtonIconName = 'arrow-right'
this.actionButtonIconName = ['fas', 'arrow-right']
} else {
// Search with text
this.actionButtonIconName = 'search'
this.actionButtonIconName = ['fas', 'search']
}
})
} catch (ex) {
// On exception, consider text as invalid URL
this.actionButtonIconName = 'search'
this.actionButtonIconName = ['fas', 'search']
// Rethrow exception
throw ex
}
@ -185,7 +196,7 @@ export default Vue.extend({
if (inputElement !== null) {
inputElement.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
this.handleClick()
this.handleClick(event)
}
})
}
@ -253,6 +264,10 @@ export default Vue.extend({
this.visibleDataList = visList
},
updateInputData: function(text) {
this.inputData = text
},
...mapActions([
'getYoutubeUrlInfo'
])

View File

@ -24,7 +24,7 @@
</label>
<font-awesome-icon
v-if="showClearTextButton"
icon="times-circle"
:icon="['fas', 'times-circle']"
class="clearInputTextButton"
:class="{
visible: inputDataPresent

View File

@ -1,54 +0,0 @@
import Vue from 'vue'
export default Vue.extend({
name: 'FtIntersectionObserver',
props: {
checkOnMount: {
type: Boolean,
default: false
},
margin: {
type: String,
default: '0px 0px 0px 0px'
},
observeParent: {
type: Boolean,
default: false
},
threshold: {
type: Number,
default: 1
}
},
data() {
const observer = new IntersectionObserver(this.handleIntersection, {
rootMargin: this.margin,
threshold: this.threshold
})
const runOnce = false
return {
observer,
runOnce
}
},
mounted() {
this.observer.observe(this.observeParent ? this.$refs.elem.parentElement : this.$refs.elem)
},
beforeDestroy() {
this.observer.disconnect()
},
methods: {
handleIntersection(entries) {
if (!this.runOnce) {
this.runOnce = true
if (!this.checkOnMount) {
return
}
}
this.$emit(entries[0].isIntersecting ? 'intersected' : 'unintersected')
}
}
})

View File

@ -1,5 +0,0 @@
<template>
<div ref="elem" />
</template>
<script src="./ft-intersection-observer.js" />

View File

@ -1,4 +1,5 @@
import Vue from 'vue'
import i18n from '../../i18n/index'
export default Vue.extend({
name: 'FtListChannel',
@ -32,6 +33,9 @@ export default Vue.extend({
},
hideChannelSubscriptions: function () {
return this.$store.getters.getHideChannelSubscriptions
},
currentLocale: function () {
return i18n.locale.replace('_', '-')
}
},
mounted: function () {
@ -59,7 +63,7 @@ export default Vue.extend({
if (this.data.videos === null) {
this.videoCount = 0
} else {
this.videoCount = this.data.videos.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
this.videoCount = Intl.NumberFormat(this.currentLocale).format(this.data.videos)
}
this.description = this.data.descriptionShort
@ -81,9 +85,9 @@ export default Vue.extend({
if (this.hideChannelSubscriptions) {
this.subscriberCount = null
} else {
this.subscriberCount = this.data.subCount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
this.subscriberCount = Intl.NumberFormat(this.currentLocale).format(this.data.subCount)
}
this.videoCount = this.data.videoCount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
this.videoCount = Intl.NumberFormat(this.currentLocale).format(this.data.videoCount)
this.description = this.data.description
}
}

View File

@ -31,12 +31,5 @@ export default Vue.extend({
listType: function () {
return this.$store.getters.getListType
}
},
mounted: function () {
},
methods: {
goToChannel: function () {
console.log('TODO: ft-list-channel method goToChannel')
}
}
})

View File

@ -4,7 +4,7 @@
{{ title }}
<font-awesome-icon
class="angleDownIcon"
icon="angle-down"
:icon="['fas', 'angle-down']"
/>
</div>
<ul>

View File

@ -65,7 +65,6 @@ export default Vue.extend({
methods: {
handleExternalPlayer: function () {
this.openInExternalPlayer({
strings: this.$t('Video.External Player'),
watchProgress: 0,
playbackRate: this.defaultPlayback,
videoId: null,

View File

@ -18,15 +18,15 @@
<div class="background" />
<div class="inner">
<div>{{ videoCount }}</div>
<div><font-awesome-icon icon="list" /></div>
<div><font-awesome-icon :icon="['fas','list']" /></div>
</div>
</div>
</router-link>
<div class="info">
<ft-icon-button
v-if="externalPlayer !== ''"
:title="$t('Video.External Player.OpenInTemplate').replace('$', externalPlayer)"
icon="external-link-alt"
:title="$t('Video.External Player.OpenInTemplate', { externalPlayer })"
:icon="['fas', 'external-link-alt']"
class="externalPlayerButton"
theme="base-no-default"
:size="16"

View File

@ -1,6 +1,13 @@
import Vue from 'vue'
import FtIconButton from '../ft-icon-button/ft-icon-button.vue'
import { mapActions } from 'vuex'
import i18n from '../../i18n/index'
import {
copyToClipboard,
openExternalLink,
showToast,
toLocalePublicationString
} from '../../helpers/utils'
export default Vue.extend({
name: 'FtListVideo',
@ -249,6 +256,10 @@ export default Vue.extend({
saveWatchedProgress: function () {
return this.$store.getters.getSaveWatchedProgress
},
currentLocale: function () {
return i18n.locale.replace('_', '-')
}
},
mounted: function () {
@ -260,7 +271,6 @@ export default Vue.extend({
this.$emit('pause-player')
this.openInExternalPlayer({
strings: this.$t('Video.External Player'),
watchProgress: this.watchProgress,
playbackRate: this.defaultPlayback,
videoId: this.id,
@ -286,9 +296,6 @@ export default Vue.extend({
},
handleOptionsClick: function (option) {
console.log('Handling share')
console.log(option)
switch (option) {
case 'history':
if (this.watched) {
@ -298,49 +305,34 @@ export default Vue.extend({
}
break
case 'copyYoutube':
navigator.clipboard.writeText(this.youtubeShareUrl)
this.showToast({
message: this.$t('Share.YouTube URL copied to clipboard')
})
copyToClipboard(this.youtubeShareUrl, { messageOnSuccess: this.$t('Share.YouTube URL copied to clipboard') })
break
case 'openYoutube':
this.openExternalLink(this.youtubeUrl)
openExternalLink(this.youtubeUrl)
break
case 'copyYoutubeEmbed':
navigator.clipboard.writeText(this.youtubeEmbedUrl)
this.showToast({
message: this.$t('Share.YouTube Embed URL copied to clipboard')
})
copyToClipboard(this.youtubeEmbedUrl, { messageOnSuccess: this.$t('Share.YouTube Embed URL copied to clipboard') })
break
case 'openYoutubeEmbed':
this.openExternalLink(this.youtubeEmbedUrl)
openExternalLink(this.youtubeEmbedUrl)
break
case 'copyInvidious':
navigator.clipboard.writeText(this.invidiousUrl)
this.showToast({
message: this.$t('Share.Invidious URL copied to clipboard')
})
copyToClipboard(this.invidiousUrl, { messageOnSuccess: this.$t('Share.Invidious URL copied to clipboard') })
break
case 'openInvidious':
this.openExternalLink(this.invidiousUrl)
openExternalLink(this.invidiousUrl)
break
case 'copyYoutubeChannel':
navigator.clipboard.writeText(this.youtubeChannelUrl)
this.showToast({
message: this.$t('Share.YouTube Channel URL copied to clipboard')
})
copyToClipboard(this.youtubeChannelUrl, { messageOnSuccess: this.$t('Share.YouTube Channel URL copied to clipboard') })
break
case 'openYoutubeChannel':
this.openExternalLink(this.youtubeChannelUrl)
openExternalLink(this.youtubeChannelUrl)
break
case 'copyInvidiousChannel':
navigator.clipboard.writeText(this.invidiousChannelUrl)
this.showToast({
message: this.$t('Share.Invidious Channel URL copied to clipboard')
})
copyToClipboard(this.invidiousChannelUrl, { messageOnSuccess: this.$t('Share.Invidious Channel URL copied to clipboard') })
break
case 'openInvidiousChannel':
this.openExternalLink(this.invidiousChannelUrl)
openExternalLink(this.invidiousChannelUrl)
break
}
},
@ -405,26 +397,18 @@ export default Vue.extend({
if (typeof (this.data.publishedText) !== 'undefined' && this.data.publishedText !== null && !this.isLive) {
// produces a string according to the template in the locales string
this.toLocalePublicationString({
this.uploadedTime = toLocalePublicationString({
publishText: this.publishedText,
templateString: this.$t('Video.Publicationtemplate'),
timeStrings: this.$t('Video.Published'),
liveStreamString: this.$t('Video.Watching'),
upcomingString: this.$t('Video.Published.Upcoming'),
isLive: this.isLive,
isUpcoming: this.isUpcoming,
isRSS: this.data.isRSS
}).then((data) => {
this.uploadedTime = data
}).catch((error) => {
console.error(error)
})
}
if (this.hideVideoViews) {
this.hideViews = true
} else if (typeof (this.data.viewCount) !== 'undefined' && this.data.viewCount !== null) {
this.parsedViewCount = this.data.viewCount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
this.parsedViewCount = Intl.NumberFormat(this.currentLocale).format(this.data.viewCount)
} else if (typeof (this.data.viewCountText) !== 'undefined') {
this.parsedViewCount = this.data.viewCountText.replace(' views', '')
} else {
@ -468,9 +452,7 @@ export default Vue.extend({
type: 'video'
}
this.updateHistory(videoData)
this.showToast({
message: this.$t('Video.Video has been marked as watched')
})
showToast(this.$t('Video.Video has been marked as watched'))
this.watched = true
},
@ -478,9 +460,7 @@ export default Vue.extend({
removeFromWatched: function () {
this.removeFromHistory(this.id)
this.showToast({
message: this.$t('Video.Video has been removed from your history')
})
showToast(this.$t('Video.Video has been removed from your history'))
this.watched = false
this.watchProgress = 0
@ -509,9 +489,7 @@ export default Vue.extend({
this.addVideo(payload)
this.showToast({
message: this.$t('Video.Video has been saved')
})
showToast(this.$t('Video.Video has been saved'))
},
removeFromPlaylist: function () {
@ -522,20 +500,15 @@ export default Vue.extend({
this.removeVideo(payload)
this.showToast({
message: this.$t('Video.Video has been removed from your saved list')
})
showToast(this.$t('Video.Video has been removed from your saved list'))
},
...mapActions([
'showToast',
'toLocalePublicationString',
'openInExternalPlayer',
'updateHistory',
'removeFromHistory',
'addVideo',
'removeVideo',
'openExternalLink'
'removeVideo'
])
}
})

View File

@ -33,8 +33,8 @@
</div>
<ft-icon-button
v-if="externalPlayer !== ''"
:title="$t('Video.External Player.OpenInTemplate').replace('$', externalPlayer)"
icon="external-link-alt"
:title="$t('Video.External Player.OpenInTemplate', { externalPlayer })"
:icon="['fas', 'external-link-alt']"
class="externalPlayerIcon"
theme="base"
:padding="appearance === `watchPlaylistItem` ? 6 : 7"
@ -44,7 +44,7 @@
<ft-icon-button
v-if="!isLive"
:title="$t('Video.Save Video')"
icon="star"
:icon="['fas', 'star']"
class="favoritesIcon"
:theme="favoriteIconTheme"
:padding="appearance === `watchPlaylistItem` ? 5 : 6"
@ -66,7 +66,7 @@
<div class="info">
<ft-icon-button
class="optionsButton"
icon="ellipsis-v"
:icon="['fas', 'ellipsis-v']"
title="More Options"
theme="base-no-default"
:size="16"

View File

@ -14,7 +14,7 @@
</div>
<font-awesome-icon
class="bannerIcon"
icon="times"
:icon="['fas', 'times']"
@click="handleClose"
/>
</div>

View File

@ -6,6 +6,7 @@ import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
import FtChannelBubble from '../../components/ft-channel-bubble/ft-channel-bubble.vue'
import FtButton from '../../components/ft-button/ft-button.vue'
import FtPrompt from '../../components/ft-prompt/ft-prompt.vue'
import { showToast } from '../../helpers/utils'
export default Vue.extend({
name: 'FtProfileChannelList',
@ -49,8 +50,7 @@ export default Vue.extend({
return this.$store.getters.getProfileList
},
selectedText: function () {
const localeText = this.$t('Profile.$ selected')
return localeText.replace('$', this.selectedLength)
return this.$t('Profile.{number} selected', { number: this.selectedLength })
},
deletePromptMessage: function () {
if (this.isMainProfile) {
@ -111,9 +111,7 @@ export default Vue.extend({
methods: {
displayDeletePrompt: function () {
if (this.selectedLength === 0) {
this.showToast({
message: this.$t('Profile.No channel(s) have been selected')
})
showToast(this.$t('Profile.No channel(s) have been selected'))
} else {
this.showDeletePrompt = true
}
@ -142,9 +140,7 @@ export default Vue.extend({
this.updateProfile(profile)
})
this.showToast({
message: this.$t('Profile.Profile has been updated')
})
showToast(this.$t('Profile.Profile has been updated'))
this.selectNone()
} else {
const profile = JSON.parse(JSON.stringify(this.profile))
@ -158,9 +154,7 @@ export default Vue.extend({
this.updateProfile(profile)
this.showToast({
message: this.$t('Profile.Profile has been updated')
})
showToast(this.$t('Profile.Profile has been updated'))
this.selectNone()
}
}
@ -209,7 +203,6 @@ export default Vue.extend({
},
...mapActions([
'showToast',
'updateProfile'
])
}

View File

@ -6,6 +6,7 @@ import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
import FtInput from '../../components/ft-input/ft-input.vue'
import FtButton from '../../components/ft-button/ft-button.vue'
import { MAIN_PROFILE_ID } from '../../../constants'
import { calculateColorLuminance, colors, showToast } from '../../helpers/utils'
export default Vue.extend({
name: 'FtProfileEdit',
@ -45,7 +46,7 @@ export default Vue.extend({
return this.profileId === MAIN_PROFILE_ID
},
colorValues: function () {
return this.$store.getters.getColorValues
return colors.map(color => color.value)
},
profileInitial: function () {
return this?.profileName?.length > 0 ? Array.from(this.profileName)[0].toUpperCase() : ''
@ -70,8 +71,8 @@ export default Vue.extend({
}
},
watch: {
profileBgColor: async function (val) {
this.profileTextColor = await this.calculateColorLuminance(val)
profileBgColor: function (val) {
this.profileTextColor = calculateColorLuminance(val)
}
},
created: function () {
@ -95,9 +96,7 @@ export default Vue.extend({
saveProfile: function () {
if (this.profileName === '') {
this.showToast({
message: this.$t('Profile.Your profile name cannot be empty')
})
showToast(this.$t('Profile.Your profile name cannot be empty'))
return
}
const profile = {
@ -111,30 +110,22 @@ export default Vue.extend({
profile._id = this.profileId
}
console.log(profile)
if (this.isNew) {
this.createProfile(profile)
this.showToast({
message: this.$t('Profile.Profile has been created')
})
showToast(this.$t('Profile.Profile has been created'))
this.$router.push({
path: '/settings/profile/'
})
} else {
this.updateProfile(profile)
this.showToast({
message: this.$t('Profile.Profile has been updated')
})
showToast(this.$t('Profile.Profile has been updated'))
}
},
setDefaultProfile: function () {
this.updateDefaultProfile(this.profileId)
const message = this.$t('Profile.Your default profile has been set to $').replace('$', this.profileName)
this.showToast({
message: message
})
const message = this.$t('Profile.Your default profile has been set to {profile}', { profile: this.profileName })
showToast(message)
},
deleteProfile: function () {
@ -144,14 +135,12 @@ export default Vue.extend({
this.removeProfile(this.profileId)
const message = this.$t('Profile.Removed $ from your profiles').replace('$', this.profileName)
this.showToast({ message })
const message = this.$t('Profile.Removed {profile} from your profiles', { profile: this.profileName })
showToast(message)
if (this.defaultProfile === this.profileId) {
this.updateDefaultProfile(MAIN_PROFILE_ID)
this.showToast({
message: this.$t('Profile.Your default profile has been changed to your primary profile')
})
showToast(this.$t('Profile.Your default profile has been changed to your primary profile'))
}
this.$router.push({
@ -160,13 +149,11 @@ export default Vue.extend({
},
...mapActions([
'showToast',
'createProfile',
'updateProfile',
'removeProfile',
'updateDefaultProfile',
'updateActiveProfile',
'calculateColorLuminance'
'updateActiveProfile'
])
}
})

View File

@ -8,10 +8,10 @@
text-align: center;
}
::v-deep .select-label {
:deep(.select-label) {
width: 95%;
}
::v-deep .select {
:deep(.select) {
text-align-last: center;
}

View File

@ -5,8 +5,8 @@ import FtCard from '../../components/ft-card/ft-card.vue'
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
import FtChannelBubble from '../../components/ft-channel-bubble/ft-channel-bubble.vue'
import FtButton from '../../components/ft-button/ft-button.vue'
import FtPrompt from '../../components/ft-prompt/ft-prompt.vue'
import FtSelect from '../ft-select/ft-select.vue'
import { showToast } from '../../helpers/utils'
export default Vue.extend({
name: 'FtProfileFilterChannelsList',
@ -15,7 +15,6 @@ export default Vue.extend({
'ft-flex-box': FtFlexBox,
'ft-channel-bubble': FtChannelBubble,
'ft-button': FtButton,
'ft-prompt': FtPrompt,
'ft-select': FtSelect
},
props: {
@ -45,8 +44,7 @@ export default Vue.extend({
return this.profileList.flatMap((profile) => profile.name !== this.profile.name ? [profile.name] : [])
},
selectedText: function () {
const localeText = this.$t('Profile.$ selected')
return localeText.replace('$', this.selectedLength)
return this.$t('Profile.{number} selected', { number: this.selectedLength })
}
},
watch: {
@ -120,9 +118,7 @@ export default Vue.extend({
addChannelToProfile: function () {
if (this.selectedLength === 0) {
this.showToast({
message: this.$t('Profile.No channel(s) have been selected')
})
showToast(this.$t('Profile.No channel(s) have been selected'))
} else {
const subscriptions = this.channels.filter((channel) => {
return channel.selected
@ -131,9 +127,7 @@ export default Vue.extend({
const profile = JSON.parse(JSON.stringify(this.profile))
profile.subscriptions = profile.subscriptions.concat(subscriptions)
this.updateProfile(profile)
this.showToast({
message: this.$t('Profile.Profile has been updated')
})
showToast(this.$t('Profile.Profile has been updated'))
this.selectNone()
}
},
@ -173,7 +167,6 @@ export default Vue.extend({
},
...mapActions([
'showToast',
'updateProfile'
])
}

Some files were not shown because too many files have changed in this diff Show More