Merge branch 'development'
4
.babelrc
|
@ -4,8 +4,8 @@
|
|||
"@babel/env",
|
||||
{
|
||||
"targets": {
|
||||
"chrome": "96",
|
||||
"node": 16
|
||||
"chrome": "106",
|
||||
"node": "16.16.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
12
.eslintrc.js
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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. -->
|
||||
|
|
|
@ -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"
|
|
@ -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'
|
|
@ -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 }}"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }}
|
|
@ -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}}"
|
|
@ -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."
|
|
@ -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"
|
|
@ -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."
|
||||
|
|
|
@ -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 }}"
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
@ -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> • <a href="#how-does-it-work">How does it work?</a> • <a href="#features">Features</a> • <a href="#download-links">Download Links</a> • <a href="#contributing">Contributing</a> • <a href="#localization">Localization</a> • <a href="#contact">Contact</a> • <a href="#donate">Donate</a> • <a href="#license">License</a></p>
|
||||
<p align="center"><a href="https://freetubeapp.io/">Website</a> • <a href="https://blog.freetubeapp.io/">Blog</a> • <a href="https://docs.freetubeapp.io/">Documentation</a> • <a href="https://docs.freetubeapp.io/faq/">FAQ</a> • <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)
|
||||
|
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 526 B After Width: | Height: | Size: 489 B |
Before Width: | Height: | Size: 785 B After Width: | Height: | Size: 668 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 828 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 498 B After Width: | Height: | Size: 352 B |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 425 B |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 426 B |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 579 B After Width: | Height: | Size: 433 B |
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 356 B |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 353 B |
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 1.6 KiB |
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -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}`)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
})
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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$/
|
||||
})
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
97
package.json
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 },
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
<RouterView
|
||||
ref="router"
|
||||
class="routerView"
|
||||
@showOutlines="hideOutlines = false"
|
||||
/>
|
||||
<!-- </keep-alive> -->
|
||||
</transition>
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
@use "../../sass-partials/settings"
|
|
@ -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" />
|
||||
|
|
|
@ -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'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
@use "../../sass-partials/settings"
|
|
@ -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" />
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,8 +1,2 @@
|
|||
@use "../../sass-partials/settings"
|
||||
|
||||
@media only screen and (max-width: 500px)
|
||||
.downloadSettingsFlexBox
|
||||
justify-content: flex-start
|
||||
|
||||
.folderDisplay
|
||||
width: 50vh
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
.experimental-warning {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
padding-left: 4%;
|
||||
padding-right: 4%
|
||||
}
|
|
@ -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'
|
||||
])
|
||||
}
|
||||
})
|
|
@ -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" />
|
|
@ -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}`
|
||||
}
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
@use "../../sass-partials/settings"
|
|
@ -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" />
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<template>
|
||||
<div class="ft-card">
|
||||
<div
|
||||
class="ft-card"
|
||||
@focusout="$emit('focusout')"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
class="bubble selected"
|
||||
>
|
||||
<font-awesome-icon
|
||||
icon="check"
|
||||
:icon="['fas', 'check']"
|
||||
class="icon"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
.ft-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 240px);
|
||||
justify-content: space-evenly;
|
||||
grid-gap: 5px;
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
import Vue from 'vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'FtGrid'
|
||||
})
|
|
@ -1,8 +0,0 @@
|
|||
<template>
|
||||
<div class="ft-grid">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./ft-grid.js" />
|
||||
<style scoped src="./ft-grid.css" />
|
|
@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
])
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
</label>
|
||||
<font-awesome-icon
|
||||
v-if="showClearTextButton"
|
||||
icon="times-circle"
|
||||
:icon="['fas', 'times-circle']"
|
||||
class="clearInputTextButton"
|
||||
:class="{
|
||||
visible: inputDataPresent
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
})
|
|
@ -1,5 +0,0 @@
|
|||
<template>
|
||||
<div ref="elem" />
|
||||
</template>
|
||||
|
||||
<script src="./ft-intersection-observer.js" />
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{{ title }}
|
||||
<font-awesome-icon
|
||||
class="angleDownIcon"
|
||||
icon="angle-down"
|
||||
:icon="['fas', 'angle-down']"
|
||||
/>
|
||||
</div>
|
||||
<ul>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
</div>
|
||||
<font-awesome-icon
|
||||
class="bannerIcon"
|
||||
icon="times"
|
||||
:icon="['fas', 'times']"
|
||||
@click="handleClose"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -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'
|
||||
])
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
])
|
||||
}
|
||||
|
|