Merge branch 'FreeTubeApp:development' into Open-'featured'-links

This commit is contained in:
ChunkyProgrammer 2021-10-03 16:02:30 -04:00 committed by GitHub
commit 5ac1eab36c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
156 changed files with 14383 additions and 17437 deletions

View File

@ -3,7 +3,7 @@ Title
---
**Important note**
Please note that only PrestoN is able to merge Pull Requests into master.
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:
@ -11,7 +11,7 @@ Please select what type of pull request this is:
- [ ] Feature Implementation
**Related issue**
Please link the issue your pull request is referring to.
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.

9
.github/auto-merge.yml vendored Normal file
View File

@ -0,0 +1,9 @@
minApprovals:
COLLABORATOR: 2
maxRequestedChanges:
COLLABORATOR: 0
mergeMethod: squash
requiredBaseBranches:
- development
- test
reportStatus: true

17
.github/workflows/autoMerge.yml vendored Normal file
View File

@ -0,0 +1,17 @@
name: Auto Merge PR
on:
pull_request_target:
types: [opened, synchronize, reopened, auto_merge_disabled]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Auto Merge PR
if: contains(${{ github.event.pull_request.base.ref }}, 'development') || contains(${{ github.event.pull_request.base.ref }}, 'RC')
run: |
echo ${{ secrets.PUSH_TOKEN }} >> auth.txt
gh auth login --with-token < auth.txt
rm auth.txt
gh pr merge https://github.com/FreeTubeApp/FreeTube/pull/${{ github.event.pull_request.number }} --auto --squash

View File

@ -1,37 +0,0 @@
name: Auto Merge PR
# Controls when the workflow will run
on:
# Triggers the workflow on push or pull request events but only for the development branch
push: {} # update PR when base branch is updated
status: {} # try to merge when other checks are completed
pull_request_review: # try to merge after review
types:
- submitted
- edited
- dismissed
pull_request: # try to merge if labels have changed (white/black list)
types:
- labeled
- unlabeled
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-20.04
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- name: Merge Pal
# You may pin to the exact commit or the version.
# uses: maxkomarychev/merge-pal-action@7a3bca37e260865d9e9a259212c1d13ef4da7f41
uses: maxkomarychev/merge-pal-action@v0.5.1
with:
# Token to perform api calls
token: ${{ secrets.PUSH_TOKEN }}
method: squash

View File

@ -5,7 +5,7 @@ name: Build
on:
push:
branches: [ master, development ]
branches: [ master, development, '**-RC' ]
jobs:
build:
@ -31,10 +31,11 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
cache: "yarn"
- run: npm run ci
- run: npm run lint
- name: Get Version Number
uses: nyaayaya/package-version@v1
@ -47,13 +48,17 @@ jobs:
uses: actions/github-script@v3
env:
IS_DEV: ${{ contains(github.ref, 'development') }}
IS_RC: ${{ contains(github.ref, 'RC') }}
VERSION_NUMBER_NIGHTLY: ${{ env.PACKAGE_VERSION }}-nightly-${{ github.run_number }}
VERSION_NUMBER_RC: ${{ env.PACKAGE_VERSION }}-RC-${{ github.run_number }}
VERSION_NUMBER: ${{ env.PACKAGE_VERSION }}
with:
result-encoding: string
script: |
if (${{ env.IS_DEV }}) {
return "${{ env.VERSION_NUMBER_NIGHTLY }}"
} else if (${{ env.IS_RC }}) {
return "${{ env.VERSION_NUMBER_RC }}"
} else {
return "${{env.VERSION_NUMBER }}"
}

View File

@ -19,8 +19,9 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Use Node.js 14.x
uses: actions/setup-node@v1
uses: actions/setup-node@v2
with:
node-version: 14.x
- run: npm ci
cache: "yarn"
- run: npm run ci
- run: npm run lint

View File

@ -32,10 +32,11 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
cache: "yarn"
- run: npm run ci
- run: npm run lint
- name: Get Version Number

View File

@ -24,4 +24,4 @@ Please follow these guidelines before sending your pull request and making contr
# Setting up Your Environment
Check out the [wiki](https://github.com/FreeTubeApp/FreeTube/wiki/Environment-Setup-and-Packaging) page to learn how to set up your environment and create packages.
Check out the [wiki](https://docs.freetubeapp.io/development/getting-started/) page to learn how to set up your environment and get started.

View File

@ -25,6 +25,8 @@ FreeTube uses a built in extractor to grab and serve data / videos. The [Invidi
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>
@ -77,18 +79,20 @@ follow the [Contribution
Guidelines](https://github.com/FreeTubeApp/FreeTube/blob/development/CONTRIBUTING.md)
before sending your pull request.
Thank you very much to the [People and Projects](https://github.com/FreeTubeApp/FreeTube/wiki/Credits) that make FreeTube possible!
Thank you very much to the [People and Projects](https://docs.freetubeapp.io/credits/) that make FreeTube possible!
## 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" />
</a>
We are actively looking for translations! We use Weblate to make it easy for translators to get involved. Click on the badge above to learn how to get involved.
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.
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://github.com/FreeTubeApp/FreeTube/wiki/Matrix-Server-Info-&-Rules) before joining.
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).

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@ -116,7 +116,12 @@ function startRenderer(callback) {
})
const server = new WebpackDevServer(compiler, {
static: path.join(process.cwd(), 'static'),
static: {
directory: path.join(process.cwd(), 'static'),
watch: {
ignored: /(dashFiles|storyboards)\/*/
}
},
port
})

View File

@ -149,7 +149,6 @@ const config = {
if (isDevMode) {
// any dev only config
config.plugins.push(
new webpack.HotModuleReplacementPlugin(),
new webpack.DefinePlugin({
__static: `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"`,
})

View File

@ -147,7 +147,6 @@ const config = {
if (isDevMode) {
// any dev only config
config.plugins.push(
new webpack.HotModuleReplacementPlugin(),
new webpack.DefinePlugin({
__static: `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"`,
})

View File

@ -62,7 +62,6 @@ const config = {
*/
if (isDevMode) {
// any dev only config
config.plugins.push(new webpack.HotModuleReplacementPlugin())
} else {
config.plugins.push(
new webpack.LoaderOptionsPlugin({

15836
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
"name": "freetube",
"productName": "FreeTube",
"description": "A private YouTube client",
"version": "0.13.2",
"version": "0.15.0",
"license": "AGPL-3.0-or-later",
"main": "./dist/main.js",
"private": true,
@ -46,14 +46,16 @@
"rebuild:node": "npm rebuild",
"release": "run-s test build",
"test": "run-s rebuild:node pack:workers jest",
"test:watch": "run-s rebuild:node pack:workers jest:watch"
"test:watch": "run-s rebuild:node pack:workers jest:watch",
"ci": "yarn install --frozen-lockfile"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-brands-svg-icons": "^5.15.3",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@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",
"@freetube/youtube-chat": "^1.1.1",
"@freetube/youtube-chat": "^1.1.2",
"@freetube/yt-comment-scraper": "^6.0.0",
"@silvermine/videojs-quality-selector": "^1.2.5",
"autolinker": "^3.14.3",
@ -67,9 +69,9 @@
"lodash.debounce": "^4.0.8",
"lodash.isequal": "^4.5.0",
"lodash.uniqwith": "^4.5.0",
"marked": "^2.1.3",
"marked": "^3.0.2",
"material-design-icons": "^3.0.1",
"nedb-promises": "^5.0.0",
"nedb-promises": "^5.0.1",
"node-forge": "^0.10.0",
"opml-to-json": "^1.0.1",
"rss-parser": "^3.12.0",
@ -91,58 +93,58 @@
"yt-channel-info": "^2.2.0",
"yt-dash-manifest-generator": "1.1.0",
"yt-trending-scraper": "^2.0.1",
"ytdl-core": "^4.9.0",
"ytpl": "^2.2.1",
"ytsr": "^3.5.0"
"ytdl-core": "^4.9.1",
"ytpl": "^2.2.3",
"ytsr": "^3.5.3"
},
"devDependencies": {
"@babel/core": "^7.14.8",
"@babel/core": "^7.15.0",
"@babel/plugin-proposal-class-properties": "^7.14.5",
"@babel/plugin-proposal-object-rest-spread": "^7.14.7",
"@babel/preset-env": "^7.14.9",
"@babel/preset-typescript": "^7.14.5",
"@typescript-eslint/eslint-plugin": "^4.29.0",
"@typescript-eslint/parser": "^4.29.0",
"@babel/preset-env": "^7.15.0",
"@babel/preset-typescript": "^7.15.0",
"@typescript-eslint/eslint-plugin": "^4.30.0",
"@typescript-eslint/parser": "^4.30.0",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.2.2",
"copy-webpack-plugin": "^9.0.1",
"css-loader": "5.2.6",
"electron": "^13.1.7",
"electron": "^13.5.1",
"electron-builder": "^22.11.7",
"electron-builder-squirrel-windows": "^22.11.11",
"electron-builder-squirrel-windows": "^22.13.1",
"electron-debug": "^3.2.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-standard": "^5.0.0",
"eslint-plugin-vue": "^7.15.1",
"eslint-plugin-vue": "^7.17.0",
"fast-glob": "^3.2.7",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.3.2",
"jest": "^27.0.6",
"mini-css-extract-plugin": "^2.1.0",
"node-abi": "^2.30.0",
"jest": "^27.1.0",
"mini-css-extract-plugin": "^2.2.2",
"node-abi": "^2.30.1",
"node-loader": "^2.0.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.3.2",
"rimraf": "^3.0.2",
"sass": "^1.37.2",
"sass": "^1.38.2",
"sass-loader": "^12.1.0",
"style-loader": "^3.2.1",
"tree-kill": "1.2.2",
"typescript": "^4.3.5",
"typescript": "^4.4.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.48.0",
"webpack-cli": "^4.7.2",
"webpack-dev-server": "^4.0.0-beta.3"
"webpack": "^5.51.1",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^4.1.0"
}
}

View File

@ -16,7 +16,7 @@
<% } %>
</head>
<body class="light mainRed secBlue">
<body class="dark mainRed secBlue">
<div id="app"></div>
<!-- Set `__static` path to static files in production -->
<script>

View File

@ -181,7 +181,7 @@ function runApp() {
* Initial window options
*/
const newWindow = new BrowserWindow({
backgroundColor: '#fff',
backgroundColor: '#212121',
icon: isDev
? path.join(__dirname, '../../_icons/iconColor.png')
/* eslint-disable-next-line */

View File

@ -32,6 +32,12 @@ body {
margin-bottom: -75px;
}
#changeLogText {
overflow-y: scroll;
height: 40vh;
display: block
}
.fade-enter-active, .fade-leave-active {
transition: opacity .15s;
}

View File

@ -40,7 +40,14 @@ export default Vue.extend({
blogBannerMessage: '',
latestBlogUrl: '',
updateChangelog: '',
changeLogTitle: ''
changeLogTitle: '',
lastExternalLinkToBeOpened: '',
showExternalLinkOpeningPrompt: false,
externalLinkOpeningPromptValues: [
'yes',
'no'
]
}
},
computed: {
@ -96,12 +103,29 @@ export default Vue.extend({
},
defaultInvidiousInstance: function () {
return this.$store.getters.getDefaultInvidiousInstance
},
externalLinkOpeningPromptNames: function () {
return [
this.$t('Yes'),
this.$t('No')
]
},
externalLinkHandling: function () {
return this.$store.getters.getExternalLinkHandling
}
},
watch: {
windowTitle: 'setWindowTitle'
windowTitle: 'setWindowTitle',
$route () {
// react to route changes...
// Hide top nav filter panel on page change
this.$refs.topNav.hideFilters()
}
},
created () {
this.checkThemeSettings()
this.setWindowTitle()
},
mounted: function () {
@ -114,7 +138,6 @@ export default Vue.extend({
this.grabAllProfiles(this.$t('Profile.All Channels')).then(async () => {
this.grabHistory()
this.grabAllPlaylists()
this.checkThemeSettings()
if (this.usingElectron) {
console.log('User is using Electron')
@ -133,6 +156,10 @@ export default Vue.extend({
this.checkForNewBlogPosts()
}, 500)
})
this.$router.afterEach((to, from) => {
this.$refs.topNav.navigateHistory()
})
})
},
methods: {
@ -142,7 +169,7 @@ export default Vue.extend({
let secColor = localStorage.getItem('secColor')
if (baseTheme === null) {
baseTheme = 'light'
baseTheme = 'dark'
}
if (mainColor === null) {
@ -193,7 +220,7 @@ export default Vue.extend({
this.showUpdatesBanner = true
} else if (parseInt(appVersion[1]) < parseInt(latestVersion[1])) {
this.showUpdatesBanner = true
} else if (parseInt(appVersion[2]) < parseInt(latestVersion[2])) {
} else if (parseInt(appVersion[2]) < parseInt(latestVersion[2]) && parseInt(appVersion[1]) <= parseInt(latestVersion[1])) {
this.showUpdatesBanner = true
}
}).fail((xhr, textStatus, error) => {
@ -273,10 +300,10 @@ export default Vue.extend({
if (event.altKey) {
switch (event.code) {
case 'ArrowRight':
window.history.forward()
this.$refs.topNav.historyForward()
break
case 'ArrowLeft':
window.history.back()
this.$refs.topNav.historyBack()
break
}
}
@ -300,8 +327,18 @@ export default Vue.extend({
if (isYoutubeLink) {
this.handleYoutubeLink(el.href)
} 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')
})
} else if (this.externalLinkHandling === 'openLinkAfterPrompt') {
// Storing the URL is necessary as
// there is no other way to pass the URL to click callback
this.lastExternalLinkToBeOpened = el.href
this.showExternalLinkOpeningPrompt = true
} else {
// Open links externally by default
// Open links externally
this.openExternalLink(el.href)
}
})
@ -399,6 +436,18 @@ export default Vue.extend({
ipcRenderer.send('appReady')
},
handleExternalLinkOpeningPromptAnswer: function (option) {
this.showExternalLinkOpeningPrompt = false
if (option === 'yes' && this.lastExternalLinkToBeOpened.length > 0) {
// Maybe user should be notified
// if `lastExternalLinkToBeOpened` is empty
// Open links externally
this.openExternalLink(this.lastExternalLinkToBeOpened)
}
},
...mapMutations([
'setInvidiousInstancesList'
]),

View File

@ -46,7 +46,10 @@
<h2>
{{ changeLogTitle }}
</h2>
<span v-html="updateChangelog" />
<span
id="changeLogText"
v-html="updateChangelog"
/>
<ft-flex-box>
<ft-button
:label="$t('Download From Site')"
@ -54,6 +57,14 @@
/>
</ft-flex-box>
</ft-prompt>
<ft-prompt
v-if="showExternalLinkOpeningPrompt"
:label="$t('Are you sure you want to open this link?')"
:extra-labels="[lastExternalLinkToBeOpened]"
:option-names="externalLinkOpeningPromptNames"
:option-values="externalLinkOpeningPromptValues"
@click="handleExternalLinkOpeningPromptAnswer"
/>
<ft-toast />
<ft-progress-bar
v-if="showProgressBar"

View File

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" fill="white" width="20px" height="19px" data-prefix="fas" data-icon="desktop" class="svg-inline--fa fa-desktop fa-w-18" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M528 0H48C21.5 0 0 21.5 0 48v320c0 26.5 21.5 48 48 48h192l-16 48h-72c-13.3 0-24 10.7-24 24s10.7 24 24 24h272c13.3 0 24-10.7 24-24s-10.7-24-24-24h-72l-16-48h192c26.5 0 48-21.5 48-48V48c0-26.5-21.5-48-48-48zm-16 352H64V64h448v288z"></path></svg>

After

Width:  |  Height:  |  Size: 483 B

View File

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" fill="white" width="20px" height="19px" data-icon="tv" class="svg-inline--fa fa-tv fa-w-20" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M592 0H48A48 48 0 0 0 0 48v320a48 48 0 0 0 48 48h240v32H112a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16H352v-32h240a48 48 0 0 0 48-48V48a48 48 0 0 0-48-48zm-16 352H64V64h512z"></path></svg>

After

Width:  |  Height:  |  Size: 458 B

View File

@ -27,6 +27,7 @@ export default Vue.extend({
showExportSubscriptionsPrompt: false,
subscriptionsPromptValues: [
'freetube',
'youtubenew',
'youtube',
'youtubeold',
'newpipe'
@ -58,6 +59,7 @@ export default Vue.extend({
const importNewPipe = this.$t('Settings.Data Settings.Import NewPipe')
return [
`${importFreeTube} (.db)`,
`${importYouTube} (.csv)`,
`${importYouTube} (.json)`,
`${importYouTube} (.opml)`,
`${importNewPipe} (.json)`
@ -69,6 +71,7 @@ export default Vue.extend({
const exportNewPipe = this.$t('Settings.Data Settings.Export NewPipe')
return [
`${exportFreeTube} (.db)`,
`${exportYouTube} (.csv)`,
`${exportYouTube} (.json)`,
`${exportYouTube} (.opml)`,
`${exportNewPipe} (.json)`
@ -93,6 +96,9 @@ export default Vue.extend({
case 'freetube':
this.importFreeTubeSubscriptions()
break
case 'youtubenew':
this.importCsvYouTubeSubscriptions()
break
case 'youtube':
this.importYouTubeSubscriptions()
break
@ -228,6 +234,75 @@ export default Vue.extend({
this.handleFreetubeImportFile(filePath)
},
handleYoutubeCsvImportFile: function(filePath) { // first row = header, last row = empty
fs.readFile(filePath, async (err, data) => {
if (err) {
const message = this.$t('Settings.Data Settings.Unable to read file')
this.showToast({
message: `${message}: ${err}`
})
return
}
const textDecode = new TextDecoder('utf-8').decode(data)
console.log(textDecode)
const youtubeSubscriptions = textDecode.split('\n')
const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0]))
const subscriptions = []
this.showToast({
message: this.$t('Settings.Data Settings.This might take a while, please wait')
})
this.updateShowProgressBar(true)
this.setProgressBarPercentage(0)
let count = 0
for (let i = 1; i < (youtubeSubscriptions.length - 1); i++) {
const channelId = youtubeSubscriptions[i].split(',')[0]
const subExists = primaryProfile.subscriptions.findIndex((sub) => {
return sub.id === channelId
})
if (subExists === -1) {
let channelInfo
if (this.backendPreference === 'invidious') { // only needed for thumbnail
channelInfo = await this.getChannelInfoInvidious(channelId)
} else {
channelInfo = await this.getChannelInfoLocal(channelId)
}
if (typeof channelInfo.author !== 'undefined') {
const subscription = {
id: channelId,
name: channelInfo.author,
thumbnail: channelInfo.authorThumbnails[1].url
}
subscriptions.push(subscription)
}
}
count++
const progressPercentage = (count / (youtubeSubscriptions.length - 1)) * 100
this.setProgressBarPercentage(progressPercentage)
if (count + 1 === (youtubeSubscriptions.length - 1)) {
primaryProfile.subscriptions = primaryProfile.subscriptions.concat(subscriptions)
this.updateProfile(primaryProfile)
if (subscriptions.length < count + 2) {
this.showToast({
message: this.$t('Settings.Data Settings.One or more subscriptions were unable to be imported')
})
} else {
this.showToast({
message: this.$t('Settings.Data Settings.All subscriptions have been successfully imported')
})
}
this.updateShowProgressBar(false)
}
}
})
},
handleYoutubeImportFile: function (filePath) {
fs.readFile(filePath, async (err, data) => {
if (err) {
@ -310,6 +385,25 @@ export default Vue.extend({
})
},
importCsvYouTubeSubscriptions: async function () {
const options = {
properties: ['openFile'],
filters: [
{
name: 'Database File',
extensions: ['csv']
}
]
}
const response = await this.showOpenDialog(options)
if (response.canceled || response.filePaths.length === 0) {
return
}
const filePath = response.filePaths[0]
this.handleYoutubeCsvImportFile(filePath)
},
importYouTubeSubscriptions: async function () {
const options = {
properties: ['openFile'],
@ -387,25 +481,23 @@ export default Vue.extend({
feedData.forEach(async (channel, index) => {
const channelId = channel.xmlurl.replace('https://www.youtube.com/feeds/videos.xml?channel_id=', '')
let channelInfo
if (this.backendPreference === 'invidious') {
channelInfo = await this.getChannelInfoInvidious(channelId)
} else {
channelInfo = await this.getChannelInfoLocal(channelId)
}
if (typeof channelInfo.author !== 'undefined') {
const subscription = {
id: channelId,
name: channelInfo.author,
thumbnail: channelInfo.authorThumbnails[1].url
const subExists = primaryProfile.subscriptions.findIndex((sub) => {
return sub.id === channelId
})
if (subExists === -1) {
let channelInfo
if (this.backendPreference === 'invidious') {
channelInfo = await this.getChannelInfoInvidious(channelId)
} else {
channelInfo = await this.getChannelInfoLocal(channelId)
}
const subExists = primaryProfile.subscriptions.findIndex((sub) => {
return sub.id === subscription.id || sub.name === subscription.name
})
if (subExists === -1) {
if (typeof channelInfo.author !== 'undefined') {
const subscription = {
id: channelId,
name: channelInfo.author,
thumbnail: channelInfo.authorThumbnails[1].url
}
subscriptions.push(subscription)
}
}
@ -498,25 +590,24 @@ export default Vue.extend({
newPipeSubscriptions.forEach(async (channel, index) => {
const channelId = channel.url.replace(/https:\/\/(www\.)?youtube\.com\/channel\//, '')
let channelInfo
if (this.backendPreference === 'invidious') {
channelInfo = await this.getChannelInfoInvidious(channelId)
} else {
channelInfo = await this.getChannelInfoLocal(channelId)
}
const subExists = primaryProfile.subscriptions.findIndex((sub) => {
return sub.id === channelId
})
if (typeof channelInfo.author !== 'undefined') {
const subscription = {
id: channelId,
name: channelInfo.author,
thumbnail: channelInfo.authorThumbnails[1].url
if (subExists === -1) {
let channelInfo
if (this.backendPreference === 'invidious') {
channelInfo = await this.getChannelInfoInvidious(channelId)
} else {
channelInfo = await this.getChannelInfoLocal(channelId)
}
const subExists = primaryProfile.subscriptions.findIndex((sub) => {
return sub.id === subscription.id || sub.name === subscription.name
})
if (subExists === -1) {
if (typeof channelInfo.author !== 'undefined') {
const subscription = {
id: channelId,
name: channelInfo.author,
thumbnail: channelInfo.authorThumbnails[1].url
}
subscriptions.push(subscription)
}
}
@ -557,6 +648,9 @@ export default Vue.extend({
case 'freetube':
this.exportFreeTubeSubscriptions()
break
case 'youtubenew':
this.exportCsvYouTubeSubscriptions()
break
case 'youtube':
this.exportYouTubeSubscriptions()
break
@ -573,21 +667,8 @@ export default Vue.extend({
await this.compactProfiles()
const userData = await this.getUserDataPath()
const subscriptionsDb = `${userData}/profiles.db`
const date = new Date()
let dateMonth = date.getMonth() + 1
if (dateMonth < 10) {
dateMonth = '0' + dateMonth
}
let dateDay = date.getDate()
if (dateDay < 10) {
dateDay = '0' + dateDay
}
const dateYear = date.getFullYear()
const exportFileName = 'freetube-subscriptions-' + dateYear + '-' + dateMonth + '-' + dateDay + '.db'
const date = new Date().toISOString().split('T')[0]
const exportFileName = 'freetube-subscriptions-' + date + '.db'
const options = {
defaultPath: exportFileName,
@ -633,21 +714,8 @@ export default Vue.extend({
},
exportYouTubeSubscriptions: async function () {
const date = new Date()
let dateMonth = date.getMonth() + 1
if (dateMonth < 10) {
dateMonth = '0' + dateMonth
}
let dateDay = date.getDate()
if (dateDay < 10) {
dateDay = '0' + dateDay
}
const dateYear = date.getFullYear()
const exportFileName = 'youtube-subscriptions-' + dateYear + '-' + dateMonth + '-' + dateDay + '.json'
const date = new Date().toISOString().split('T')[0]
const exportFileName = 'youtube-subscriptions-' + date + '.json'
const options = {
defaultPath: exportFileName,
@ -719,21 +787,8 @@ export default Vue.extend({
},
exportOpmlYouTubeSubscriptions: async function () {
const date = new Date()
let dateMonth = date.getMonth() + 1
if (dateMonth < 10) {
dateMonth = '0' + dateMonth
}
let dateDay = date.getDate()
if (dateDay < 10) {
dateDay = '0' + dateDay
}
const dateYear = date.getFullYear()
const exportFileName = 'youtube-subscriptions-' + dateYear + '-' + dateMonth + '-' + dateDay + '.opml'
const date = new Date().toISOString().split('T')[0]
const exportFileName = 'youtube-subscriptions-' + date + '.opml'
const options = {
defaultPath: exportFileName,
@ -783,22 +838,50 @@ export default Vue.extend({
})
},
exportCsvYouTubeSubscriptions: async function () {
const date = new Date().toISOString().split('T')[0]
const exportFileName = 'youtube-subscriptions-' + date + '.csv'
const options = {
defaultPath: exportFileName,
filters: [
{
name: 'Database File',
extensions: ['csv']
}
]
}
let exportText = 'Channel ID,Channel URL,Channel title\n'
this.profileList[0].subscriptions.forEach((channel) => {
const channelUrl = `https://www.youtube.com/channel/${channel.id}`
exportText += `${channel.id},${channelUrl},${channel.name}\n`
})
exportText += '\n'
const response = await this.showSaveDialog(options)
if (response.canceled || response.filePath === '') {
// User canceled the save dialog
return
}
const filePath = response.filePath
fs.writeFile(filePath, exportText, (writeErr) => {
if (writeErr) {
const message = this.$t('Settings.Data Settings.Unable to write file')
this.showToast({
message: `${message}: ${writeErr}`
})
return
}
this.showToast({
message: this.$t('Settings.Data Settings.Subscriptions have been successfully exported')
})
})
},
exportNewPipeSubscriptions: async function () {
const date = new Date()
let dateMonth = date.getMonth() + 1
if (dateMonth < 10) {
dateMonth = '0' + dateMonth
}
let dateDay = date.getDate()
if (dateDay < 10) {
dateDay = '0' + dateDay
}
const dateYear = date.getFullYear()
const exportFileName = 'newpipe-subscriptions-' + dateYear + '-' + dateMonth + '-' + dateDay + '.json'
const date = new Date().toISOString().split('T')[0]
const exportFileName = 'newpipe-subscriptions-' + date + '.json'
const options = {
defaultPath: exportFileName,
@ -945,21 +1028,8 @@ export default Vue.extend({
await this.compactHistory()
const userData = await this.getUserDataPath()
const historyDb = `${userData}/history.db`
const date = new Date()
let dateMonth = date.getMonth() + 1
if (dateMonth < 10) {
dateMonth = '0' + dateMonth
}
let dateDay = date.getDate()
if (dateDay < 10) {
dateDay = '0' + dateDay
}
const dateYear = date.getFullYear()
const exportFileName = 'freetube-history-' + dateYear + '-' + dateMonth + '-' + dateDay + '.db'
const date = new Date().toISOString().split('T')[0]
const exportFileName = 'freetube-history-' + date + '.db'
const options = {
defaultPath: exportFileName,

View File

@ -1,10 +1,11 @@
<template>
<ft-card
class="relative card"
>
<h3>
{{ $t("Settings.Data Settings.Data Settings") }}
</h3>
<details>
<summary>
<h3>
{{ $t("Settings.Data Settings.Data Settings") }}
</h3>
</summary>
<hr>
<ft-flex-box>
<ft-button
:label="$t('Settings.Data Settings.Import Subscriptions')"
@ -30,7 +31,7 @@
<ft-flex-box>
<a
class="center"
href="https://github.com/FreeTubeApp/FreeTube/wiki/Importing-Your-YouTube-Subscriptions"
href="https://docs.freetubeapp.io/usage/importing-subscriptions/"
>
<p>
{{ $t("Settings.Data Settings.How do I import my subscriptions?") }}
@ -57,7 +58,7 @@
:option-values="subscriptionsPromptValues"
@click="exportSubscriptions"
/>
</ft-card>
</details>
</template>
<script src="./data-settings.js" />

View File

@ -1,12 +1,11 @@
<template>
<ft-card
class="relative card"
>
<h3
class="videoTitle"
>
{{ $t("Settings.Distraction Free Settings.Distraction Free Settings") }}
</h3>
<details>
<summary>
<h3>
{{ $t("Settings.Distraction Free Settings.Distraction Free Settings") }}
</h3>
</summary>
<hr>
<div class="switchColumnGrid">
<div class="switchColumn">
<ft-toggle-switch
@ -90,7 +89,7 @@
label="Manage My Distractions"
/>
</ft-flex-box>
</ft-card>
</details>
</template>
<script src="./distraction-settings.js" />

View File

@ -1,12 +1,11 @@
<template>
<ft-card
class="relative card"
>
<h3
class="videoTitle"
>
{{ $t("Settings.External Player Settings.External Player Settings") }}
</h3>
<details>
<summary>
<h3>
{{ $t("Settings.External Player Settings.External Player Settings") }}
</h3>
</summary>
<hr>
<div class="switchColumnGrid">
<div class="switchColumn">
<ft-select
@ -34,7 +33,7 @@
>
<ft-input
:placeholder="$t('Settings.External Player Settings.Custom External Player Executable')"
:show-arrow="false"
:show-action-button="false"
:show-label="true"
:value="externalPlayerExecutable"
:tooltip="$t('Tooltips.External Player Settings.Custom External Player Executable')"
@ -42,14 +41,14 @@
/>
<ft-input
:placeholder="$t('Settings.External Player Settings.Custom External Player Arguments')"
:show-arrow="false"
:show-action-button="false"
:show-label="true"
:value="externalPlayerCustomArgs"
:tooltip="$t('Tooltips.External Player Settings.Custom External Player Arguments')"
@input="updateExternalPlayerCustomArgs"
/>
</ft-flex-box>
</ft-card>
</details>
</template>
<script src="./external-player-settings.js" />

View File

@ -1,7 +1,7 @@
.ft-auto-grid
&.grid
display: grid
grid-template-columns: repeat(auto-fill, 262px)
grid-template-columns: repeat(auto-fill, minmax(262px, 1fr) )
justify-content: space-evenly
grid-gap: 8px

View File

@ -51,7 +51,7 @@ export default Vue.extend({
},
data: function () {
return {
showDropdown: false,
dropdownShown: false,
id: ''
}
},
@ -60,25 +60,46 @@ export default Vue.extend({
},
methods: {
toggleDropdown: function () {
$(`#${this.id}`)[0].style.display = 'inline'
$(`#${this.id}`).focus()
const dropdownBox = $(`#${this.id}`)
$(`#${this.id}`).focusout(() => {
const shareLinks = $(`#${this.id}`).find('.shareLinks')
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
if (shareLinks.length > 0) {
if (!shareLinks[0].parentNode.matches(':hover')) {
$(`#${this.id}`)[0].style.display = 'none'
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)
}
} else {
$(`#${this.id}`)[0].style.display = 'none'
}
})
})
}
},
focusOut: function () {
$(`#${this.id}`).focusout()
$(`#${this.id}`)[0].style.display = 'none'
const dropdownBox = $(`#${this.id}`)
dropdownBox.focusout()
dropdownBox.get(0).style.display = 'none'
this.dropdownShown = false
},
handleIconClick: function () {

View File

@ -2,6 +2,53 @@
position: relative;
}
.clearInputTextButton {
position: absolute;
/* horizontal intentionally reduced to keep "I-beam pointer" visible */
padding: 10px 8px;
top: 5px;
left: 0;
cursor: pointer;
border-radius: 200px 200px 200px 200px;
color: var(--primary-text-color);
opacity: 0;
-moz-transition: background 0.2s ease-in, opacity 0.2s ease-in;
-o-transition: background 0.2s ease-in, opacity 0.2s ease-in;
transition: background 0.2s ease-in, opacity 0.2s ease-in;
}
.clearInputTextButton:hover {
background-color: var(--side-nav-hover-color);
}
.clearInputTextButton.visible {
opacity: 1;
}
.forceTextColor .clearInputTextButton:hover {
background-color: var(--primary-color-hover);
}
.clearInputTextButton:active {
background-color: var(--tertiary-text-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}
.search .clearInputTextButton {
top: 12px;
}
.forceTextColor .clearInputTextButton {
color: #EEEEEE;
}
.forceTextColor .clearInputTextButton:active {
background-color: var(--primary-color-active);
}
.ft-input {
box-sizing: border-box;
-webkit-box-sizing: border-box;
@ -41,12 +88,19 @@
.inputAction {
position: absolute;
padding: 10px;
padding: 10px 8px;
top: 5px;
right: 0px;
cursor: pointer;
right: 0;
border-radius: 200px 200px 200px 200px;
color: var(--primary-text-color);
/* this should look disabled by default */
opacity: 50%;
}
.inputAction.enabled {
opacity: 100%;
/* Only look respond to cursor when enabled */
cursor: pointer;
}
.search ::-webkit-calendar-picker-indicator {
@ -61,25 +115,33 @@
color: #EEEEEE;
}
.inputAction:hover {
.ft-input-component.showActionButton .ft-input {
/*
With arrow present means
the text might get under the arrow with normal padding
*/
padding-right: 2em;
}
.inputAction.enabled:hover {
background-color: var(--side-nav-hover-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}
.forceTextColor .inputAction:hover {
.forceTextColor .inputAction.enabled:hover {
background-color: var(--primary-color-hover);
}
.inputAction:active {
.inputAction.enabled:active {
background-color: var(--tertiary-text-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}
.forceTextColor .inputAction:active {
.forceTextColor .inputAction.enabled:active {
background-color: var(--primary-color-active);
}
@ -91,9 +153,8 @@
padding: 5px 0;
z-index: 10;
border-radius: 0 0 5px 5px;
border: 1px #ccc solid;
background-color: white;
color: black;
box-shadow: 0 0 10px var(--scrollbar-color-hover);
background-color: var(--search-bar-color);
}
.list li {
@ -103,6 +164,10 @@
}
.hover {
background-color: #ccc;
background-color: var(--scrollbar-color-hover);
/* color: white; */
}
.showClearTextButton .ft-input {
padding-left: 2em;
}

View File

@ -1,5 +1,6 @@
import Vue from 'vue'
import FtTooltip from '../ft-tooltip/ft-tooltip.vue'
import { mapActions } from 'vuex'
export default Vue.extend({
name: 'FtInput',
@ -15,10 +16,14 @@ export default Vue.extend({
type: String,
default: ''
},
showArrow: {
showActionButton: {
type: Boolean,
default: true
},
showClearTextButton: {
type: Boolean,
default: false
},
showLabel: {
type: Boolean,
default: false
@ -56,7 +61,12 @@ export default Vue.extend({
showOptions: false,
selectedOption: -1,
isPointerInList: false
}
},
// This button should be invisible on app start
// As the text input box should be empty
clearTextButtonExisting: false,
clearTextButtonVisible: false,
actionButtonIconName: 'search'
}
},
computed: {
@ -70,11 +80,35 @@ export default Vue.extend({
idDataList: function () {
return `${this.id}_datalist`
},
inputDataPresent: function () {
return this.inputData.length > 0
}
},
watch: {
value: function (val) {
this.inputData = val
},
inputDataPresent: function (newVal, oldVal) {
if (newVal) {
// The button needs to be visible **immediately**
// To allow user to see the transition
this.clearTextButtonExisting = true
// The transition is not rendered if this property is set right after
// It's visible
setTimeout(() => {
this.clearTextButtonVisible = true
}, 0)
} else {
// Hide the button with transition
this.clearTextButtonVisible = false
// Remove the button after the transition
// 0.2s in CSS = 200ms in JS
setTimeout(() => {
this.clearTextButtonExisting = false
}, 200)
}
}
},
mounted: function () {
@ -85,6 +119,9 @@ export default Vue.extend({
},
methods: {
handleClick: function () {
// No action if no input text
if (!this.inputDataPresent) { return }
this.searchState.showOptions = false
this.$emit('input', this.inputData)
this.$emit('click', this.inputData)
@ -94,9 +131,69 @@ export default Vue.extend({
if (this.isSearch &&
this.searchState.selectedOption !== -1 &&
this.inputData === this.dataList[this.searchState.selectedOption]) { return }
this.handleActionIconChange()
this.$emit('input', this.inputData)
},
handleClearTextClick: function () {
this.inputData = ''
this.handleActionIconChange()
this.$emit('input', this.inputData)
// Focus on input element after text is clear for better UX
const inputElement = document.getElementById(this.id)
inputElement.focus()
},
handleActionIconChange: function() {
// Only need to update icon if visible
if (!this.showActionButton) { return }
if (!this.inputDataPresent) {
// Change back to default icon if text is blank
this.actionButtonIconName = 'search'
return
}
// Update action button icon according to input
try {
this.getYoutubeUrlInfo(this.inputData).then((result) => {
let isYoutubeLink = false
switch (result.urlType) {
case 'video':
case 'playlist':
case 'search':
case 'channel':
isYoutubeLink = true
break
case 'hashtag':
// TODO: Implement a hashtag related view
// isYoutubeLink is already `false`
break
case 'invalid_url':
default: {
// isYoutubeLink is already `false`
}
}
if (isYoutubeLink) {
// Go to URL (i.e. Video/Playlist/Channel
this.actionButtonIconName = 'arrow-right'
} else {
// Search with text
this.actionButtonIconName = 'search'
}
})
} catch (ex) {
// On exception, consider text as invalid URL
this.actionButtonIconName = 'search'
// Rethrow exception
throw ex
}
},
addListener: function () {
const inputElement = document.getElementById(this.id)
@ -145,6 +242,10 @@ export default Vue.extend({
if (this.selectOnFocus) {
e.target.select()
}
}
},
...mapActions([
'getYoutubeUrlInfo'
])
}
})

View File

@ -3,7 +3,9 @@
class="ft-input-component"
:class="{
search: isSearch,
forceTextColor: forceTextColor
forceTextColor: forceTextColor,
showActionButton: showActionButton,
showClearTextButton: showClearTextButton
}"
>
<label
@ -18,6 +20,20 @@
:tooltip="tooltip"
/>
</label>
<font-awesome-icon
v-if="showClearTextButton && clearTextButtonExisting"
icon="times-circle"
class="clearInputTextButton"
:class="{
visible: clearTextButtonVisible
}"
tabindex="0"
role="button"
:title="$t('Search Bar.Clear Input')"
@click="handleClearTextClick"
@keydown.space.prevent="handleClearTextClick"
@keydown.enter.prevent="handleClearTextClick"
/>
<input
:id="id"
v-model="inputData"
@ -33,9 +49,12 @@
@keydown="e => handleKeyDown(e.keyCode)"
>
<font-awesome-icon
v-if="showArrow"
icon="arrow-right"
v-if="showActionButton"
:icon="actionButtonIconName"
class="inputAction"
:class="{
enabled: inputDataPresent
}"
@click="handleClick"
/>

View File

@ -61,7 +61,7 @@
}
.buttonOption:hover {
background-color: var(--card-bg-color);
background-color: var(--search-bar-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;

View File

@ -1,6 +1,5 @@
.grid {
min-height: 264px;
display: flex;
}
.list {

View File

@ -7,7 +7,7 @@
class="profileName"
placeholder="Profile Name"
:value="profileName"
:show-arrow="false"
:show-action-button="false"
@input="e => profileName = e"
/>
</ft-flex-box>
@ -40,7 +40,7 @@
class="profileName"
placeholder=""
:value="profileBgColor"
:show-arrow="false"
:show-action-button="false"
:disabled="true"
/>
</ft-flex-box>

View File

@ -23,7 +23,7 @@
height: 400px;
padding: 5px;
background-color: var(--card-bg-color);
box-shadow: 0 1px 2px rgba(0,0,0,.1);
box-shadow: 0 0 4px var(--scrollbar-color-hover);
}
#profileList:focus {

View File

@ -13,7 +13,7 @@ export default Vue.extend({
},
data: function () {
return {
showProfileList: false
profileListShown: false
}
},
computed: {
@ -38,12 +38,25 @@ export default Vue.extend({
mounted: function () {
$('#profileList').focusout(() => {
$('#profileList')[0].style.display = 'none'
// When pressing the profile button
// It will make the menu reappear if we set `profileListShown` immediately
setTimeout(() => {
this.profileListShown = false
}, 100)
})
},
methods: {
toggleProfileList: function () {
$('#profileList')[0].style.display = 'inline'
$('#profileList').focus()
const profileList = $('#profileList')
if (this.profileListShown) {
profileList.get(0).style.display = 'none'
this.profileListShown = false
} else {
profileList.get(0).style.display = 'inline'
profileList.get(0).focus()
this.profileListShown = true
}
},
openProfileSettings: function () {

View File

@ -15,6 +15,10 @@ export default Vue.extend({
type: String,
default: ''
},
extraLabels: {
type: Array,
default: () => { return [] }
},
optionNames: {
type: Array,
default: () => { return [] }

View File

@ -8,6 +8,15 @@
<h2 class="center">
{{ label }}
</h2>
<p
v-for="extraLabel in extraLabels"
:key="extraLabel"
class="center"
>
<strong>
{{ extraLabel }}
</strong>
</p>
<ft-flex-box>
<ft-button
v-for="(option, index) in optionNames"

View File

@ -41,7 +41,8 @@
}
.select option {
color: #000000;
color: var(--secondary-text-color);
background-color: var(--card-bg-color);
}
/* Remove focus */

View File

@ -112,6 +112,7 @@ export default Vue.extend({
'subsCapsButton',
'audioTrackButton',
'pictureInPictureToggle',
'toggleTheatreModeButton',
'fullWindowButton',
'qualitySelector',
'fullscreenToggle'
@ -139,6 +140,10 @@ export default Vue.extend({
return this.$store.getters.getUsingElectron
},
currentLocale: function () {
return this.$store.getters.getCurrentLocale
},
defaultPlayback: function () {
return this.$store.getters.getDefaultPlayback
},
@ -195,6 +200,7 @@ export default Vue.extend({
this.createFullWindowButton()
this.createLoopButton()
this.createToggleTheatreModeButton()
this.determineFormatType()
this.determineMaxFramerate()
},
@ -268,6 +274,11 @@ export default Vue.extend({
}, 200)
}
// Remove built-in progress bar mouse over current time display
// `MouseTimeDisplay` in
// https://github.com/videojs/video.js/blob/v7.13.3/docs/guides/components.md#default-component-tree
this.player.controlBar.progressControl.seekBar.playProgressBar.removeChild('timeTooltip')
if (this.useSponsorBlock) {
this.initializeSponsorBlock()
}
@ -459,7 +470,7 @@ export default Vue.extend({
},
mouseScrollVolume: function (event) {
if (event.target) {
if (event.target && !event.currentTarget.querySelector('.vjs-menu:hover')) {
event.preventDefault()
if (this.player.muted() && event.wheelDelta > 0) {
@ -966,6 +977,45 @@ export default Vue.extend({
videojs.registerComponent('fullWindowButton', fullWindowButton)
},
createToggleTheatreModeButton: function() {
if (!this.$parent.theatrePossible) {
return
}
const theatreModeActive = this.$parent.useTheatreMode ? ' vjs-icon-theatre-active' : ''
const VjsButton = videojs.getComponent('Button')
const toggleTheatreModeButton = videojs.extend(VjsButton, {
constructor: function(player, options) {
VjsButton.call(this, player, options)
},
handleClick: () => {
this.toggleTheatreMode()
},
createControlTextEl: function (button) {
return $(button)
.addClass('vjs-button-theatre')
.html($(`<div id="toggleTheatreModeButton" class="vjs-icon-theatre-inactive${theatreModeActive} vjs-button"></div>`))
.attr('title', 'Toggle Theatre Mode')
}
})
videojs.registerComponent('toggleTheatreModeButton', toggleTheatreModeButton)
},
toggleTheatreMode: function() {
if (!this.player.isFullscreen_) {
const toggleTheatreModeButton = $('#toggleTheatreModeButton')
if (!this.$parent.useTheatreMode) {
toggleTheatreModeButton.addClass('vjs-icon-theatre-active')
} else {
toggleTheatreModeButton.removeClass('vjs-icon-theatre-active')
}
}
this.$parent.toggleTheatreMode()
},
createDashQualitySelector: function (levels) {
if (levels.levels_.length === 0) {
setTimeout(() => {
@ -1054,6 +1104,47 @@ export default Vue.extend({
this.determineDefaultQualityDash()
},
sortCaptions: function (captionList) {
return captionList.sort((captionA, captionB) => {
const aCode = captionA.languageCode.split('-') // ex. [en,US]
const bCode = captionB.languageCode.split('-')
const aName = (captionA.label || captionA.name.simpleText) // ex: english (auto-generated)
const bName = (captionB.label || captionB.name.simpleText)
const userLocale = this.currentLocale.split(/-|_/) // ex. [en,US]
if (aCode[0] === userLocale[0]) { // caption a has same language as user's locale
if (bCode[0] === userLocale[0]) { // caption b has same language as user's locale
if (bName.search('auto') !== -1) {
// prefer caption a: b is auto-generated captions
return -1
} else if (aName.search('auto') !== -1) {
// prefer caption b: a is auto-generated captions
return 1
} else if (aCode[1] === userLocale[1]) {
// prefer caption a: caption a has same county code as user's locale
return -1
} else if (bCode[1] === userLocale[1]) {
// prefer caption b: caption b has same county code as user's locale
return 1
} else if (aCode[1] === undefined) {
// prefer caption a: no country code is better than wrong country code
return -1
} else if (bCode[1] === undefined) {
// prefer caption b: no country code is better than wrong country code
return 1
}
} else {
// prefer caption a: b does not match user's language
return -1
}
} else if (bCode[0] === userLocale[0]) {
// prefer caption b: a does not match user's language
return 1
}
// sort alphabetically
return aName.localeCompare(bName)
})
},
transformAndInsertCaptions: async function() {
let captionList
if (this.captionHybridList[0] instanceof Promise) {
@ -1063,7 +1154,7 @@ export default Vue.extend({
captionList = this.captionHybridList
}
for (const caption of captionList) {
for (const caption of this.sortCaptions(captionList)) {
this.player.addRemoteTextTrack({
kind: 'subtitles',
src: caption.baseUrl || caption.url,
@ -1373,6 +1464,11 @@ export default Vue.extend({
// Toggle Full Window Mode
this.toggleFullWindow()
break
case 84:
// T Key
// Toggle Theatre Mode
this.toggleTheatreMode()
break
}
}
},

View File

@ -50,6 +50,11 @@ export default Vue.extend({
'start',
'middle',
'end'
],
externalLinkHandlingValues: [
'',
'openLinkAfterPrompt',
'doNothing'
]
}
},
@ -150,6 +155,18 @@ export default Vue.extend({
this.$t('Settings.General Settings.Thumbnail Preference.Middle'),
this.$t('Settings.General Settings.Thumbnail Preference.End')
]
},
externalLinkHandling: function () {
return this.$store.getters.getExternalLinkHandling
},
externalLinkHandlingNames: function () {
return [
this.$t('Settings.General Settings.External Link Handling.Open Link'),
this.$t('Settings.General Settings.External Link Handling.Ask Before Opening Link'),
this.$t('Settings.General Settings.External Link Handling.No Action')
]
}
},
mounted: function () {
@ -218,7 +235,8 @@ export default Vue.extend({
'updateListType',
'updateThumbnailPreference',
'updateForceLocalBackendForLegacy',
'updateCurrentLocale'
'updateCurrentLocale',
'updateExternalLinkHandling'
])
}
})

View File

@ -1 +1,10 @@
@use "../../sass-partials/settings"
.select
min-width: 240px
width: auto
// https://vue-loader.vuejs.org/guide/scoped-css.html#deep-selectors
.select::v-deep .select-text
min-width: 240px
width: auto

View File

@ -1,12 +1,11 @@
<template>
<ft-card
class="card"
>
<h3
class="videoTitle"
>
{{ $t("Settings.General Settings.General Settings") }}
</h3>
<details>
<summary>
<h3>
{{ $t("Settings.General Settings.General Settings") }}
</h3>
</summary>
<hr>
<div class="switchColumnGrid">
<div class="switchColumn">
<ft-toggle-switch
@ -85,11 +84,19 @@
:tooltip="$t('Tooltips.General Settings.Region for Trending')"
@change="updateRegion"
/>
<ft-select
:placeholder="$t('Settings.General Settings.External Link Handling.External Link Handling')"
:value="externalLinkHandling"
:select-names="externalLinkHandlingNames"
:select-values="externalLinkHandlingValues"
:tooltip="$t('Tooltips.General Settings.External Link Handling')"
@change="updateExternalLinkHandling"
/>
</div>
<ft-flex-box class="generalSettingsFlexBox">
<ft-input
:placeholder="$t('Settings.General Settings.Current Invidious Instance')"
:show-arrow="false"
:show-action-button="false"
:show-label="true"
:value="currentInvidiousInstance"
:data-list="invidiousInstancesList"
@ -130,7 +137,7 @@
@click="handleClearDefaultInstanceClick"
/>
</ft-flex-box>
</ft-card>
</details>
</template>
<script src="./general-settings.js" />

View File

@ -1,12 +1,11 @@
<template>
<ft-card
class="relative card"
>
<h3
class="videoTitle"
>
{{ $t("Settings.Player Settings.Player Settings") }}
</h3>
<details>
<summary>
<h3>
{{ $t("Settings.Player Settings.Player Settings") }}
</h3>
</summary>
<hr>
<div class="switchColumnGrid">
<div class="switchColumn">
<ft-toggle-switch
@ -127,7 +126,7 @@
@change="updateDefaultQuality"
/>
</ft-flex-box>
</ft-card>
</details>
</template>
<script src="./player-settings.js" />

View File

@ -100,12 +100,18 @@ export default Vue.extend({
switch (method) {
case 'copyYoutube':
navigator.clipboard.writeText(youtubeUrl)
this.showToast({
message: this.$t('Share.YouTube URL copied to clipboard')
})
break
case 'openYoutube':
this.openExternalLink(youtubeUrl)
break
case 'copyInvidious':
navigator.clipboard.writeText(invidiousUrl)
this.showToast({
message: this.$t('Share.Invidious URL copied to clipboard')
})
break
case 'openInvidious':
this.openExternalLink(invidiousUrl)
@ -131,6 +137,7 @@ export default Vue.extend({
},
...mapActions([
'showToast',
'openExternalLink'
])
}

View File

@ -1,10 +1,11 @@
<template>
<ft-card
class="relative card"
>
<h3>
{{ $t("Settings.Privacy Settings.Privacy Settings") }}
</h3>
<details>
<summary>
<h3>
{{ $t("Settings.Privacy Settings.Privacy Settings") }}
</h3>
</summary>
<hr>
<div class="switchColumnGrid">
<div class="switchColumn">
<ft-toggle-switch
@ -75,7 +76,7 @@
:option-values="promptValues"
@click="handleRemoveSubscriptions"
/>
</ft-card>
</details>
</template>
<script src="./privacy-settings.js" />

View File

@ -1,25 +0,0 @@
.relative {
position: relative;
}
.card {
width: 85%;
margin: 0 auto;
margin-bottom: 10px;
}
.center {
text-align: center;
}
@media only screen and (max-width: 680px) {
.card {
width: 90%;
}
}
@media only screen and (max-width: 500px) {
.subscriptionSettingsFlexBox {
justify-content: flex-start;
}
}

View File

@ -0,0 +1,5 @@
@use "../../sass-partials/settings"
@media only screen and (max-width: 500px)
.subscriptionSettingsFlexBox
justify-content: flex-start

View File

@ -1,12 +1,11 @@
<template>
<ft-card
class="relative card"
>
<h3
class="videoTitle"
>
{{ $t("Settings.Proxy Settings.Proxy Settings") }}
</h3>
<details>
<summary>
<h3>
{{ $t("Settings.Proxy Settings.Proxy Settings") }}
</h3>
</summary>
<hr>
<ft-flex-box class="subscriptionSettingsFlexBox">
<ft-toggle-switch
:label="$t('Settings.Proxy Settings.Enable Tor / Proxy')"
@ -26,14 +25,14 @@
<ft-flex-box>
<ft-input
:placeholder="$t('Settings.Proxy Settings.Proxy Host')"
:show-arrow="false"
:show-action-button="false"
:show-label="true"
:value="proxyHostname"
@input="handleUpdateProxyHostname"
/>
<ft-input
:placeholder="$t('Settings.Proxy Settings.Proxy Port Number')"
:show-arrow="false"
:show-action-button="false"
:show-label="true"
:value="proxyPort"
@input="handleUpdateProxyPort"
@ -71,8 +70,8 @@
{{ $t('Settings.Proxy Settings.City') }}: {{ proxyCity }}
</p>
</div>
</ft-card>
</details>
</template>
<script src="./proxy-settings.js" />
<style scoped src="./proxy-settings.css" />
<style scoped lang="sass" src="./proxy-settings.sass" />

View File

@ -26,7 +26,7 @@
class="navIcon"
/>
<p class="navLabel">
{{ $t("Trending") }}
{{ $t("Trending.Trending") }}
</p>
</div>
<div

View File

@ -34,7 +34,7 @@
fixed-width
/>
<p class="navLabel">
{{ $t("Trending") }}
{{ $t("Trending.Trending") }}
</p>
</div>
<div

View File

@ -1,25 +0,0 @@
.relative {
position: relative;
}
.card {
width: 85%;
margin: 0 auto;
margin-bottom: 10px;
}
.center {
text-align: center;
}
@media only screen and (max-width: 680px) {
.card {
width: 90%;
}
}
@media only screen and (max-width: 500px) {
.sponsorBlockSettingsFlexBox {
justify-content: flex-start;
}
}

View File

@ -0,0 +1,5 @@
@use "../../sass-partials/settings"
@media only screen and (max-width: 500px)
.sponsorBlockSettingsFlexBox
justify-content: flex-start

View File

@ -1,12 +1,11 @@
<template>
<ft-card
class="relative card"
>
<h3
class="videoTitle"
>
{{ $t("Settings.SponsorBlock Settings.SponsorBlock Settings") }}
</h3>
<details>
<summary>
<h3>
{{ $t("Settings.SponsorBlock Settings.SponsorBlock Settings") }}
</h3>
</summary>
<hr>
<ft-flex-box class="sponsorBlockSettingsFlexBox">
<ft-toggle-switch
:label="$t('Settings.SponsorBlock Settings.Enable SponsorBlock')"
@ -24,14 +23,14 @@
<ft-flex-box>
<ft-input
:placeholder="$t('Settings.SponsorBlock Settings[\'SponsorBlock API Url (Default is https://sponsor.ajay.app)\']')"
:show-arrow="false"
:show-action-button="false"
:show-label="true"
:value="sponsorBlockUrl"
@input="handleUpdateSponsorBlockUrl"
/>
</ft-flex-box>
</ft-card>
</details>
</template>
<script src="./sponsor-block-settings.js" />
<style scoped src="./sponsor-block-settings.css" />
<style scoped lang="sass" src="./sponsor-block-settings.sass" />

View File

@ -1,21 +0,0 @@
.relative {
position: relative;
}
.card {
width: 85%;
margin: 0 auto;
margin-bottom: 10px;
}
@media only screen and (max-width: 680px) {
.card {
width: 90%;
}
}
@media only screen and (max-width: 500px) {
.subscriptionSettingsFlexBox {
justify-content: flex-start;
}
}

View File

@ -0,0 +1,6 @@
@use "../../sass-partials/settings"
@media only screen and (max-width: 500px)
.subscriptionSettingsFlexBox
justify-content: flex-start

View File

@ -1,12 +1,11 @@
<template>
<ft-card
class="relative card"
>
<h3
class="videoTitle"
>
{{ $t("Settings.Subscription Settings.Subscription Settings") }}
</h3>
<details>
<summary>
<h3>
{{ $t("Settings.Subscription Settings.Subscription Settings") }}
</h3>
</summary>
<hr>
<ft-flex-box class="subscriptionSettingsFlexBox">
<ft-toggle-switch
:label="$t('Settings.Subscription Settings.Hide Videos on Watch')"
@ -37,8 +36,8 @@
label="Manage My Subscriptions"
/>
</ft-flex-box>
</ft-card>
</details>
</template>
<script src="./subscription-settings.js" />
<style scoped src="./subscription-settings.css" />
<style scoped lang="sass" src="./subscription-settings.sass" />

View File

@ -1,15 +0,0 @@
.relative {
position: relative;
}
.card {
width: 85%;
margin: 0 auto;
margin-bottom: 10px;
}
@media only screen and (max-width: 680px) {
.card {
width: 90%;
}
}

View File

@ -35,7 +35,8 @@ export default Vue.extend({
baseThemeValues: [
'light',
'dark',
'black'
'black',
'dracula'
],
colorValues: [
'Red',
@ -53,7 +54,14 @@ export default Vue.extend({
'Yellow',
'Amber',
'Orange',
'DeepOrange'
'DeepOrange',
'DraculaCyan',
'DraculaGreen',
'DraculaOrange',
'DraculaPink',
'DraculaPurple',
'DraculaRed',
'DraculaYellow'
]
}
},
@ -89,7 +97,8 @@ export default Vue.extend({
return [
this.$t('Settings.Theme Settings.Base Theme.Light'),
this.$t('Settings.Theme Settings.Base Theme.Dark'),
this.$t('Settings.Theme Settings.Base Theme.Black')
this.$t('Settings.Theme Settings.Base Theme.Black'),
this.$t('Settings.Theme Settings.Base Theme.Dracula')
]
},
@ -110,7 +119,14 @@ export default Vue.extend({
this.$t('Settings.Theme Settings.Main Color Theme.Yellow'),
this.$t('Settings.Theme Settings.Main Color Theme.Amber'),
this.$t('Settings.Theme Settings.Main Color Theme.Orange'),
this.$t('Settings.Theme Settings.Main Color Theme.Deep Orange')
this.$t('Settings.Theme Settings.Main Color Theme.Deep Orange'),
this.$t('Settings.Theme Settings.Main Color Theme.Dracula Cyan'),
this.$t('Settings.Theme Settings.Main Color Theme.Dracula Green'),
this.$t('Settings.Theme Settings.Main Color Theme.Dracula Orange'),
this.$t('Settings.Theme Settings.Main Color Theme.Dracula Pink'),
this.$t('Settings.Theme Settings.Main Color Theme.Dracula Purple'),
this.$t('Settings.Theme Settings.Main Color Theme.Dracula Red'),
this.$t('Settings.Theme Settings.Main Color Theme.Dracula Yellow')
]
}
},

View File

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

View File

@ -1,10 +1,11 @@
<template>
<ft-card
class="relative card"
>
<h3>
{{ $t("Settings.Theme Settings.Theme Settings") }}
</h3>
<details>
<summary>
<h3>
{{ $t("Settings.Theme Settings.Theme Settings") }}
</h3>
</summary>
<hr>
<ft-flex-box>
<ft-toggle-switch
:label="$t('Settings.Theme Settings.Match Top Bar with Main Color')"
@ -64,8 +65,8 @@
:option-values="restartPromptValues"
@click="handleSmoothScrolling"
/>
</ft-card>
</details>
</template>
<script src="./theme-settings.js" />
<style scoped src="./theme-settings.css" />
<style scoped lang="sass" src="./theme-settings.sass" />

View File

@ -20,6 +20,8 @@ export default Vue.extend({
windowWidth: 0,
showFilters: false,
searchFilterValueChanged: false,
historyIndex: 1,
isForwardOrBack: false,
searchSuggestionsDataList: []
}
},
@ -257,12 +259,41 @@ export default Vue.extend({
this.searchFilterValueChanged = filterValueChanged
},
navigateHistory: function() {
if (!this.isForwardOrBack) {
this.historyIndex = window.history.length
$('#historyArrowBack').removeClass('fa-arrow-left')
$('#historyArrowForward').addClass('fa-arrow-right')
} else {
this.isForwardOrBack = false
}
},
historyBack: function () {
this.isForwardOrBack = true
window.history.back()
if (this.historyIndex > 1) {
this.historyIndex--
$('#historyArrowForward').removeClass('fa-arrow-right')
if (this.historyIndex === 1) {
$('#historyArrowBack').addClass('fa-arrow-left')
}
}
},
historyForward: function () {
this.isForwardOrBack = true
window.history.forward()
if (this.historyIndex < window.history.length) {
this.historyIndex++
$('#historyArrowBack').removeClass('fa-arrow-left')
if (this.historyIndex === window.history.length) {
$('#historyArrowForward').addClass('fa-arrow-right')
}
}
},
toggleSideNav: function () {
@ -277,7 +308,12 @@ export default Vue.extend({
// Web placeholder
}
},
navigate: function (route) {
this.$router.push('/' + route)
},
hideFilters: function () {
this.showFilters = false
},
...mapActions([
'showToast',
'getYoutubeUrlInfo',

View File

@ -38,6 +38,12 @@
width: 1em
height: 1em
&.fa-arrow-left, &.fa-arrow-right
color: gray
opacity: 0.5
pointer-events: none
user-select: none
@include top-nav-is-colored
color: var(--text-with-main-color)
@ -85,7 +91,13 @@
display: flex
align-items: center
padding: 0px 25px 0px 10px
cursor: pointer
&:active
background-color: var(--tertiary-text-color)
transition: background 0.2s ease-in
@include top-nav-is-colored
background-color: var(--primary-color-active)
.logoIcon
background-image: var(--logo-icon)
background-repeat: no-repeat

View File

@ -13,7 +13,8 @@
@keypress="toggleSideNav"
/>
<font-awesome-icon
class="navBackIcon navIcon"
id="historyArrowBack"
class="navBackIcon navIcon fa-arrow-left"
icon="arrow-left"
role="button"
tabindex="0"
@ -22,7 +23,8 @@
@keypress="historyBack"
/>
<font-awesome-icon
class="navForwardIcon navIcon"
id="historyArrowForward"
class="navForwardIcon navIcon fa-arrow-right"
icon="arrow-right"
role="button"
tabindex="0"
@ -44,7 +46,15 @@
:title="newWindowText"
@click="createNewWindow"
/>
<div class="logo">
<div
class="logo"
role="link"
tabindex="0"
:title="$t('Subscriptions.Subscriptions')"
@click="navigate('subscriptions')"
@keydown.space.prevent="navigate('subscriptions')"
@keydown.enter.prevent="navigate('subscriptions')"
>
<div
class="logoIcon"
/>
@ -62,6 +72,7 @@
:select-on-focus="true"
:data-list="searchSuggestionsDataList"
:spellcheck="false"
:show-clear-text-button="true"
@input="getSearchSuggestionsDebounce"
@click="goToSearch"
/>

View File

@ -50,6 +50,7 @@
}
.commentText {
white-space: pre-wrap;
font-size: 14px;
margin-top: -10px;
margin-left: 70px;

View File

@ -191,7 +191,14 @@ export default Vue.extend({
this.showToast({
message: this.$t('Falling back to Invidious API')
})
this.getCommentDataInvidious()
this.getCommentDataInvidious({
resource: 'comments',
id: this.id,
params: {
continuation: this.nextPageToken,
sort_by: this.sortNewest ? 'new' : 'top'
}
})
} else {
this.isLoading = false
}
@ -219,7 +226,14 @@ export default Vue.extend({
this.showToast({
message: this.$t('Falling back to Invidious API')
})
this.getCommentDataInvidious()
this.getCommentDataInvidious({
resource: 'comments',
id: this.id,
params: {
continuation: this.nextPageToken,
sort_by: this.sortNewest ? 'new' : 'top'
}
})
} else {
this.isLoading = false
}
@ -250,7 +264,7 @@ export default Vue.extend({
if (this.hideCommentLikes) {
comment.likes = null
}
comment.text = autolinker.link(comment.text.replace(/(<([^>]+)>)/ig, ''))
comment.text = autolinker.link(comment.text.replace(/(<(?!br>)([^>]+)>)/ig, ''))
return comment
})
@ -282,7 +296,7 @@ export default Vue.extend({
} else {
comment.likes = comment.likeCount
}
comment.text = autolinker.link(comment.content.replace(/(<([^>]+)>)/ig, ''))
comment.text = autolinker.link(comment.content.replace(/(<(?!br>)([^>]+)>)/ig, ''))
comment.dataType = 'invidious'
if (typeof (comment.replies) !== 'undefined' && typeof (comment.replies.replyCount) !== 'undefined') {
@ -348,7 +362,7 @@ export default Vue.extend({
} else {
comment.likes = comment.likeCount
}
comment.text = autolinker.link(comment.content.replace(/(<([^>]+)>)/ig, ''))
comment.text = autolinker.link(comment.content.replace(/(<(?!br>)([^>]+)>)/ig, ''))
comment.time = comment.publishedText
comment.dataType = 'invidious'
comment.numReplies = 0

View File

@ -146,7 +146,7 @@
class="showMoreReplies"
@click="getCommentReplies(index)"
>
<span>Show More Replies</span>
<span>{{ $t("Comments.Show More Replies") }}</span>
</div>
</div>
</div>

View File

@ -2,6 +2,7 @@ import Vue from 'vue'
import FtCard from '../ft-card/ft-card.vue'
import FtTimestampCatcher from '../ft-timestamp-catcher/ft-timestamp-catcher.vue'
import autolinker from 'autolinker'
import $ from 'jquery'
export default Vue.extend({
name: 'WatchVideoDescription',
@ -34,6 +35,10 @@ export default Vue.extend({
} else {
this.shownDescription = autolinker.link(this.description)
}
if (/^\s*$/.test(this.shownDescription)) {
$('.videoDescription')[0].style.display = 'none'
}
},
methods: {
onTimestamp: function(timestamp) {

View File

@ -130,6 +130,10 @@ export default Vue.extend({
return this.$store.getters.getCurrentInvidiousInstance
},
currentLocale: function () {
return this.$store.getters.getCurrentLocale
},
profileList: function () {
return this.$store.getters.getProfileList
},
@ -227,9 +231,9 @@ export default Vue.extend({
dateString() {
const date = new Date(this.published)
const dateSplit = date.toDateString().split(' ')
const localeDateString = `Video.Published.${dateSplit[1]}`
return `${this.$t(localeDateString)} ${dateSplit[2]}, ${dateSplit[3]}`
const locale = this.currentLocale.replace('_', '-')
const localeDateString = new Intl.DateTimeFormat([locale, 'en'], { dateStyle: 'medium' }).format(date)
return `${localeDateString}`
},
publishedString() {

View File

@ -86,7 +86,3 @@
::v-deep .iconDropdown
left: calc(50% - 20px)
right: auto
@media only screen and (max-width: 1350px)
.theatreModeButton
display: none

View File

@ -78,14 +78,6 @@
theme="secondary"
@click="handleExternalPlayer"
/>
<ft-icon-button
v-if="theatrePossible"
:title="$t('Toggle Theatre Mode')"
class="theatreModeButton option"
icon="tv"
theme="secondary"
@click="$emit('theatre-mode')"
/>
<ft-icon-button
v-if="!isUpcoming && downloadLinks.length > 0"
:title="$t('Video.Download Video')"

View File

@ -190,14 +190,14 @@ export default Vue.extend({
}
} else {
const videoIndex = this.playlistItems.findIndex((item) => {
return item.id === this.videoId
return (item.id ?? item.videoId) === this.videoId
})
if (videoIndex === this.playlistItems.length - 1) {
if (this.loopEnabled) {
this.$router.push(
{
path: `/watch/${this.playlistItems[0].id}`,
path: `/watch/${this.playlistItems[0].id ?? this.playlistItems[0].videoId}`,
query: playlistInfo
}
)
@ -211,7 +211,7 @@ export default Vue.extend({
} else {
this.$router.push(
{
path: `/watch/${this.playlistItems[videoIndex + 1].id}`,
path: `/watch/${this.playlistItems[videoIndex + 1].id ?? this.playlistItems[videoIndex + 1].videoId}`,
query: playlistInfo
}
)
@ -253,20 +253,20 @@ export default Vue.extend({
}
} else {
const videoIndex = this.playlistItems.findIndex((item) => {
return item.id === this.videoId
return (item.id ?? item.videoId) === this.videoId
})
if (videoIndex === 0) {
this.$router.push(
{
path: `/watch/${this.playlistItems[this.randomizedPlaylistItems.length - 1].id}`,
path: `/watch/${this.playlistItems[this.randomizedPlaylistItems.length - 1].id ?? this.playlistItems[this.randomizedPlaylistItems.length - 1].videoId}`,
query: playlistInfo
}
)
} else {
this.$router.push(
{
path: `/watch/${this.playlistItems[videoIndex - 1].id}`,
path: `/watch/${this.playlistItems[videoIndex - 1].id ?? this.playlistItems[videoIndex - 1].videoId}`,
query: playlistInfo
}
)
@ -388,8 +388,8 @@ export default Vue.extend({
this.playlistItems.forEach((item) => {
const randomInt = Math.floor(Math.random() * remainingItems.length)
if (remainingItems[randomInt].id !== this.videoId) {
items.push(remainingItems[randomInt].id)
if ((remainingItems[randomInt].id ?? remainingItems[randomInt].videoId) !== this.videoId) {
items.push(remainingItems[randomInt].id ?? remainingItems[randomInt].videoId)
}
remainingItems.splice(randomInt, 1)

View File

@ -8,8 +8,12 @@
}
.autoPlayToggle {
width: 120px;
position: absolute;
top: 10px;
right: 0px;
display: flex;
justify-content: flex-end;
align-items: center;
}
.VideoRecommendationsTopBar{
display:flex;
justify-content:space-between;
}

View File

@ -3,17 +3,19 @@
v-if="!hideRecommendedVideos"
class="relative watchVideoRecommendations"
>
<h3>
{{ $t("Up Next") }}
</h3>
<ft-toggle-switch
v-if="showAutoplay"
class="autoPlayToggle"
:label="$t('Video.Autoplay')"
:compact="true"
:default-value="playNextVideo"
@change="updatePlayNextVideo"
/>
<div class="VideoRecommendationsTopBar">
<h3>
{{ $t("Up Next") }}
</h3>
<ft-toggle-switch
v-if="showAutoplay"
class="autoPlayToggle"
:label="$t('Video.Autoplay')"
:compact="true"
:default-value="playNextVideo"
@change="updatePlayNextVideo"
/>
</div>
<ft-list-video
v-for="(video, index) in data"
:key="index"

View File

@ -63,7 +63,7 @@ const router = new Router({
{
path: '/trending',
meta: {
title: 'Trending',
title: 'Trending.Trending',
icon: 'fa-home'
},
component: Trending

View File

@ -44,14 +44,12 @@ $thumbnail-overlay-opacity: 0.85
opacity: 1
.videoThumbnail
display: flex
position: relative
.thumbnailLink
display: flex
.thumbnailImage
height: 130px
@include is-sidebar-item
height: 75px
@ -62,6 +60,7 @@ $thumbnail-overlay-opacity: 0.85
.videoWatched
position: absolute
top:0
padding: 2px
opacity: $thumbnail-overlay-opacity
color: var(--primary-text-color)

View File

@ -16,10 +16,25 @@
flex-direction: column
justify-items: start
.card
details
background-color: var(--card-bg-color)
width: 85%
margin: 0 auto
margin-bottom: 10px
hr
width: 100%
height: 2px
border: 0
margin-top: -1px
background-color: var(--primary-color)
summary
display: block
cursor: pointer
padding: 1px 1px 1px 1px
h3
margin-left: 2%
@media only screen and (max-width: 680px)
width: 90%

View File

@ -179,6 +179,7 @@ const state = {
displayVideoPlayButton: true,
enableSearchSuggestions: true,
enableSubtitles: true,
externalLinkHandling: '',
externalPlayer: '',
externalPlayerExecutable: '',
externalPlayerIgnoreWarnings: false,

View File

@ -5,7 +5,12 @@ const state = {
isSideNavOpen: false,
sessionSearchHistory: [],
popularCache: null,
trendingCache: null,
trendingCache: {
default: null,
music: null,
gaming: null,
movies: null
},
showProgressBar: false,
progressBarPercentage: 0,
regionNames: [],
@ -303,6 +308,13 @@ const actions = {
return paramsObject
}
},
// youtube.com/shorts
function() {
if (urlObject.pathname.match(/^\/shorts\/[A-Za-z0-9_-]+$/)) {
extractParams(urlObject.pathname.replace('/shorts/', ''))
return paramsObject
}
},
// cloudtube
function() {
if (urlObject.host.match(/^cadence\.(gq|moe)$/) && urlObject.pathname.match(/^\/cloudtube\/video\/[A-Za-z0-9_-]+$/)) {
@ -838,8 +850,8 @@ const mutations = {
state.popularCache = value
},
setTrendingCache (state, value) {
state.trendingCache = value
setTrendingCache (state, value, page) {
state.trendingCache[page] = value
},
setSearchSortBy (state, value) {

View File

@ -288,7 +288,9 @@ const actions = {
break
}
}
const locale = settings.currentLocale.replace('-', '_')
ytpl(playlistId, {
hl: locale,
limit: 'Infinity',
requestOptions: { agent }
}).then((result) => {

View File

@ -22,7 +22,6 @@
--logo-text: url("~../../_icons/textColorSmall.png");
}
.dark {
--primary-text-color: #EEEEEE;
--secondary-text-color: #ddd;
@ -89,6 +88,30 @@
--logo-text: url("~../../_icons/textColorSmall.png");
}
.dracula {
--primary-text-color: #F8F8F2;
--secondary-text-color: #c6cee6;
--tertiary-text-color: #e5e8f3;
--primary-input-color: rgba(0, 0, 0, 0.50);
--primary-shadow-color: rgba(0, 0, 0, 0.75);
--title-color: #BD93F9;
--bg-color: #282A36;
--link-color: var(--accent-color);
--link-visited-color: var(--accent-color-visited);
--favorite-icon-color: #F1FA8C;
--card-bg-color: #33353F;
--secondary-card-bg-color: #282A36;
--scrollbar-color: #44475A;
--scrollbar-color-hover: #3D4051;
--side-nav-color: #44475A;
--side-nav-hover-color: #57596B;
--side-nav-active-color: #3D4051;
--search-bar-color: #3E3F4A;
--instance-menu-color: var(--search-bar-color);
--logo-icon: url("~../../_icons/iconDraculaLightSmall.png");
--logo-text: url("~../../_icons/textDraculaLightSmall.png");
}
.mainRed {
--primary-color: #f44336;
--primary-color-hover: #e53935;
@ -233,6 +256,69 @@
--logo-text-bar-color: url("~../../_icons/textBlackSmall.png");
}
.mainDraculaCyan {
--primary-color: #8BE9FD;
--primary-color-hover: #97EBFD;
--primary-color-active: #7DD2E4;
--text-with-main-color: #282A36;
--logo-icon-bar-color: url("~../../_icons/iconDraculaDarkSmall.png");
--logo-text-bar-color: url("~../../_icons/textDraculaDarkSmall.png");
}
.mainDraculaGreen {
--primary-color: #50FA7B;
--primary-color-hover: #62FB88;
--primary-color-active: #48E16F;
--text-with-main-color: #282A36;
--logo-icon-bar-color: url("~../../_icons/iconDraculaDarkSmall.png");
--logo-text-bar-color: url("~../../_icons/textDraculaDarkSmall.png");
}
.mainDraculaOrange {
--primary-color: #FFB86C;
--primary-color-hover: #FFBF7B;
--primary-color-active: #E6A661;
--text-with-main-color: #282A36;
--logo-icon-bar-color: url("~../../_icons/iconDraculaDarkSmall.png");
--logo-text-bar-color: url("~../../_icons/textDraculaDarkSmall.png");
}
.mainDraculaPink {
--primary-color: #FF79C6;
--primary-color-hover: #FF86CC;
--primary-color-active: #E66DB2;
--text-with-main-color: #282A36;
--logo-icon-bar-color: url("~../../_icons/iconDraculaLightSmall.png");
--logo-text-bar-color: url("~../../_icons/textDraculaLightSmall.png");
}
.mainDraculaPurple {
--primary-color: #BD93F9;
--primary-color-hover: #C49EFA;
--primary-color-active: #AA84E0;
--text-with-main-color: #282A36;
--logo-icon-bar-color: url("~../../_icons/iconDraculaLightSmall.png");
--logo-text-bar-color: url("~../../_icons/textDraculaLightSmall.png");
}
.mainDraculaRed {
--primary-color: #FF5555;
--primary-color-hover: #FF6666;
--primary-color-active: #E64D4D;
--text-with-main-color: #282A36;
--logo-icon-bar-color: url("~../../_icons/iconDraculaLightSmall.png");
--logo-text-bar-color: url("~../../_icons/textDraculaLightSmall.png");
}
.mainDraculaYellow {
--primary-color: #F1FA8C;
--primary-color-hover: #F2FB98;
--primary-color-active: #D9E17E;
--text-with-main-color: #282A36;
--logo-icon-bar-color: url("~../../_icons/iconDraculaDarkSmall.png");
--logo-text-bar-color: url("~../../_icons/textDraculaDarkSmall.png");
}
.secRed {
--accent-color: #f44336;
--accent-color-hover: #e53935;
@ -441,6 +527,97 @@
--accent-color-opacity4: rgba(255,87,34,0.24);
}
.secDraculaCyan {
--accent-color: #8BE9FD;
--accent-color-hover: #97EBFD;
--accent-color-active: #7DD2E4;
--accent-color-light: #A2EDFD;
--accent-color-visited: #6FBACA;
--text-with-accent-color: #212121;
--accent-color-opacity1: rgba(98,114,164,0.04);
--accent-color-opacity2: rgba(98,114,164,0.12);
--accent-color-opacity3: rgba(98,114,164,0.16);
--accent-color-opacity4: rgba(98,114,164,0.24);
}
.secDraculaGreen {
--accent-color: #50FA7B;
--accent-color-hover: #62FB88;
--accent-color-active: #48E16F;
--accent-color-light: #73FB95;
--accent-color-visited: #40C862;
--text-with-accent-color: #212121;
--accent-color-opacity1: rgba(98,114,164,0.04);
--accent-color-opacity2: rgba(98,114,164,0.12);
--accent-color-opacity3: rgba(98,114,164,0.16);
--accent-color-opacity4: rgba(98,114,164,0.24);
}
.secDraculaOrange {
--accent-color: #FFB86C;
--accent-color-hover: #FFBF7B;
--accent-color-active: #E6A661;
--accent-color-light: #FFC689;
--accent-color-visited: #CC9356;
--text-with-accent-color: #212121;
--accent-color-opacity1: rgba(98,114,164,0.04);
--accent-color-opacity2: rgba(98,114,164,0.12);
--accent-color-opacity3: rgba(98,114,164,0.16);
--accent-color-opacity4: rgba(98,114,164,0.24);
}
.secDraculaPink {
--accent-color: #FF79C6;
--accent-color-hover: #FF86CC;
--accent-color-active: #E66DB2;
--accent-color-light: #FF94D1;
--accent-color-visited: #CC619E;
--text-with-accent-color: #212121;
--accent-color-opacity1: rgba(98,114,164,0.04);
--accent-color-opacity2: rgba(98,114,164,0.12);
--accent-color-opacity3: rgba(98,114,164,0.16);
--accent-color-opacity4: rgba(98,114,164,0.24);
}
.secDraculaPurple {
--accent-color: #BD93F9;
--accent-color-hover: #C49EFA;
--accent-color-active: #AA84E0;
--accent-color-light: #CAA9FA;
--accent-color-visited: #9776C7;
--text-with-accent-color: #212121;
--accent-color-opacity1: rgba(98,114,164,0.04);
--accent-color-opacity2: rgba(98,114,164,0.12);
--accent-color-opacity3: rgba(98,114,164,0.16);
--accent-color-opacity4: rgba(98,114,164,0.24);
}
.secDraculaRed {
--accent-color: #FF5555;
--accent-color-hover: #FF6666;
--accent-color-active: #E64D4D;
--accent-color-light: #FF7777;
--accent-color-visited: #CC4444;
--text-with-accent-color: #212121;
--accent-color-opacity1: rgba(98,114,164,0.04);
--accent-color-opacity2: rgba(98,114,164,0.12);
--accent-color-opacity3: rgba(98,114,164,0.16);
--accent-color-opacity4: rgba(98,114,164,0.24);
}
.secDraculaYellow {
--accent-color: #F1FA8C;
--accent-color-hover: #F2FB98;
--accent-color-active: #D9E17E;
--accent-color-light: #F4FBA3;
--accent-color-visited: #C1C870;
--text-with-accent-color: #212121;
--accent-color-opacity1: rgba(98,114,164,0.04);
--accent-color-opacity2: rgba(98,114,164,0.12);
--accent-color-opacity3: rgba(98,114,164,0.16);
--accent-color-opacity4: rgba(98,114,164,0.24);
}
body {
color: var(--primary-text-color);
background-color: var(--bg-color);
@ -456,7 +633,7 @@ body {
outline: none;
}
.rightAligned: {
.rightAligned {
text-align: right;
}

View File

@ -437,7 +437,7 @@ body.vjs-full-window {
cursor: none;
}
.vjs-icon-fullwindow-enter, .video-js .vjs-fullwindow-control .vjs-icon-placeholder {
.vjs-icon-fullwindow-enter, .vjs-icon-theatre-inactive, .video-js .vjs-fullwindow-control .vjs-icon-placeholder {
color: white !important;
margin-top: 10px !important;
cursor:pointer;
@ -449,11 +449,11 @@ body.vjs-full-window {
content: url(assets/img/open_fullwindow.svg);
}
/* Hide button in full screen mode */
.vjs--full-screen-enabled .vjs-button-fullwindow {
.vjs--full-screen-enabled .vjs-button-fullwindow, .vjs--full-screen-enabled .vjs-button-theatre, .vjs-full-window .vjs-button-theatre {
display: none;
}
.vjs-icon-fullwindow-exit, .video-js.vjs-fullwindow .vjs-fullwindow-control .vjs-icon-placeholder {
.vjs-icon-fullwindow-exit, .vjs-icon-theatre-active, .video-js.vjs-fullwindow .vjs-fullwindow-control .vjs-icon-placeholder {
font-family: VideoJS;
font-weight: normal;
font-style: normal;
@ -469,6 +469,10 @@ body.vjs-full-window {
cursor: pointer;
}
.vjs-icon-theatre-inactive, .vjs-icon-theatre-active {
margin-top: 10px !important;
}
.vjs-icon-loop-active {
background-color: var(--primary-color);
}
@ -478,6 +482,20 @@ body.vjs-full-window {
/* filter: invert(1) drop-shadow(1px 0px 0px var(--primary-color)); */
}
.vjs-icon-theatre-inactive:before {
content: url(assets/img/open_theatre.svg)
}
.vjs-icon-theatre-active:before {
content: url(assets/img/close_theatre.svg)
}
@media only screen and (max-width: 1350px) {
.videoPlayer .vjs-button-theatre {
display: none
}
}
.loop-black:before {
filter: brightness(0%);
}

View File

@ -59,7 +59,7 @@ export default Vue.extend({
{
icon: 'comment-dots',
title: this.$t('About.Chat on Matrix'),
content: `<a href="https://matrix.to/#/#freetube:matrix.org?via=matrix.org&via=privacytools.io&via=tchncs.de">#freetube:matrix.org</a><br>${this.$t('About.Please read the')} <a href="https://github.com/FreeTubeApp/FreeTube/wiki/Matrix-Channel-Info-&-Rules">${this.$t('About.room rules')}</a>`
content: `<a href="https://matrix.to/#/#freetube:matrix.org?via=matrix.org&via=privacytools.io&via=tchncs.de">#freetube:matrix.org</a><br>${this.$t('About.Please read the')} <a href="https://docs.freetubeapp.io/community/matrix/">${this.$t('About.room rules')}</a>`
},
{
icon: 'language',

View File

@ -1,42 +1,34 @@
.card {
position: relative;
width: 85%;
margin: 0 auto;
margin-bottom: 20px;
margin: 0 auto 20px;
}
.channelBanner {
width: 100%;
position: absolute;
top: 0px;
left: 0px;
max-height: 300px;
max-height: 200px;
}
.defaultChannelBanner {
width: 100%;
position: absolute;
top: 0px;
left: 0px;
height: 200px;
max-height: 200px;
height:200px;
background-color: black;
background-image: url("~images/defaultBanner.png");
background-image: url("images/defaultBanner.png");
}
.channelInfoContainer {
width: 100%;
height: 200px;
margin-top: 300px;
position: relative;
background-color: var(--card-bg-color);
margin-top: 10px;
}
.channelInfo {
height: 100px;
width: 85%;
position: absolute;
top: 30px;
left: 30px;
display: flex;
flex-flow: row wrap;
width: 100%;
justify-content: space-between;
}
.channelThumbnail {
@ -50,24 +42,19 @@
font-weight: bold;
width: 100%;
font-size: 25px;
position: absolute;
top: 20px;
left: 120px;
}
.channelSubCount {
position: absolute;
color: var(--tertiary-text-color);
top: 50px;
left: 120px;
}
.subscribeButton {
position: absolute;
top: 50px;
right: 20px;
height: 50px;
min-width: 150px;
align-self: center;
margin-bottom: 10px;
}
.channelSearch {
@ -80,28 +67,30 @@
}
.channelInfoTabs {
position: absolute;
bottom: -16px;
position: relative;
width: 100%;
margin-top: -16px;
margin-bottom: -13px;
}
.tab {
padding: 15px;
font-size: 15px;
cursor: pointer;
text-decoration: underline;
align-self: flex-end;
-webkit-transition: background 0.2s ease-out;
-moz-transition: background 0.2s ease-out;
-o-transition: background 0.2s ease-out;
transition: background 0.2s ease-out;
color: var(--tertiary-text-color);
}
.selectedTab {
color: var(--primary-text-color);
border-bottom: 3px solid var(--primary-color);
margin-bottom: -3px;
font-weight: bold;
box-sizing: border-box;
}
.tab:hover {
background-color: var(--side-nav-hover-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
font-weight: bold;
}
.aboutTab {
@ -118,6 +107,10 @@
white-space: pre-wrap;
}
.channelSearch {
margin-top: 10px;
}
.elementList {
margin-top: 15px;
}
@ -139,3 +132,14 @@
cursor: pointer;
margin-top: 16px;
}
.thumbnailContainer {
display: flex
}
.channelLineContainer {
display: flex;
justify-content: center;
flex-direction: column;
padding-left: 1em;
}

View File

@ -260,6 +260,14 @@ export default Vue.extend({
this.thumbnailUrl = response.authorThumbnails[2].url
this.channelDescription = autolinker.link(response.description)
this.relatedChannels = response.relatedChannels.items
this.relatedChannels.forEach(relatedChannel => {
relatedChannel.authorThumbnails.map(thumbnail => {
if (!thumbnail.url.includes('https')) {
thumbnail.url = `https:${thumbnail.url}`
}
return thumbnail
})
})
if (response.authorBanners !== null) {
const bannerUrl = response.authorBanners[response.authorBanners.length - 1].url

View File

@ -19,51 +19,67 @@
v-else
class="defaultChannelBanner"
>
<div class="channelInfoContainer">
<div class="channelInfo">
<img
class="channelThumbnail"
:src="thumbnailUrl"
<div
class="channelInfoContainer"
>
<div
class="channelInfo"
>
<div
class="thumbnailContainer"
>
<span
class="channelName"
>
{{ channelName }}
</span>
<br>
<span
v-if="subCount !== null"
class="channelSubCount"
>
{{ formattedSubCount }}
<span v-if="subCount === 1">{{ $t("Channel.Subscriber") }}</span>
<span v-else>{{ $t("Channel.Subscribers") }}</span>
</span>
<img
class="channelThumbnail"
:src="thumbnailUrl"
>
<div
class="channelLineContainer"
>
<span
class="channelName"
>
{{ channelName }}
</span>
<span
v-if="subCount !== null"
class="channelSubCount"
>
{{ formattedSubCount }}
<span v-if="subCount === 1">{{ $t("Channel.Subscriber") }}</span>
<span v-else>{{ $t("Channel.Subscribers") }}</span>
</span>
</div>
</div>
<ft-button
:label="subscribedText"
background-color="var(--primary-color)"
text-color="var(--text-with-main-color)"
class="subscribeButton"
@click="handleSubscription"
/>
</div>
<ft-button
:label="subscribedText"
background-color="var(--primary-color)"
text-color="var(--text-with-main-color)"
class="subscribeButton"
@click="handleSubscription"
/>
<ft-flex-box
class="channelInfoTabs"
>
<div
class="tab"
:class="(currentTab==='videos')?'selectedTab':''"
@click="changeTab('videos')"
>
{{ $t("Channel.Videos.Videos").toUpperCase() }}
</div>
<div
class="tab"
:class="(currentTab==='playlists')?'selectedTab':''"
@click="changeTab('playlists')"
>
{{ $t("Channel.Playlists.Playlists").toUpperCase() }}
</div>
<div
class="tab"
:class="(currentTab==='about')?'selectedTab':''"
@click="changeTab('about')"
>
{{ $t("Channel.About.About").toUpperCase() }}

View File

@ -5,11 +5,17 @@
}
.floatingTopButton {
position: absolute;
position: fixed;
top: 70px;
right: 10px;
}
@media only screen and (max-width: 350px) {
.floatingTopButton {
position: absolute
}
}
@media only screen and (max-width: 680px) {
.card {
width: 90%;

View File

@ -0,0 +1,7 @@
hr {
height: 2px;
width: 85%;
margin: 0 auto;
border: 0;
background-color: var(--scrollbar-color-hover);
}

View File

@ -1,14 +1,23 @@
<template>
<div>
<general-settings />
<hr>
<theme-settings />
<hr>
<player-settings />
<hr>
<external-player-settings v-if="usingElectron" />
<hr>
<subscription-settings />
<hr>
<distraction-settings />
<hr>
<privacy-settings />
<hr>
<data-settings />
<hr>
<proxy-settings />
<hr>
<sponsor-block-settings />
</div>
</template>

View File

@ -9,11 +9,17 @@
}
.floatingTopButton {
position: absolute;
position: fixed;
top: 70px;
right: 10px;
}
@media only screen and (max-width: 350px) {
.floatingTopButton {
position: absolute
}
}
@media only screen and (max-width: 680px) {
.card {
width: 90%;

View File

@ -5,11 +5,46 @@
}
.floatingTopButton {
position: absolute;
position: fixed;
top: 70px;
right: 10px;
}
.trendingInfoTabs {
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
margin-top: -3px;
color: var(--tertiary-text-color);
margin-bottom: 10px;
}
.selectedTab {
border-bottom: 3px solid var(--primary-color);
color: var(--primary-text-color);
font-weight: bold;
box-sizing: border-box;
margin-bottom: -3px;
}
.tab {
text-align: center;
padding: 15px;
font-size: 15px;
cursor: pointer;
align-self: flex-end;
}
.tab:hover {
font-weight: bold;
}
@media only screen and (max-width: 350px) {
.floatingTopButton {
position: absolute
}
}
@media only screen and (max-width: 680px) {
.card {
width: 90%;

View File

@ -4,7 +4,9 @@ import FtCard from '../../components/ft-card/ft-card.vue'
import FtLoader from '../../components/ft-loader/ft-loader.vue'
import FtElementList from '../../components/ft-element-list/ft-element-list.vue'
import FtIconButton from '../../components/ft-icon-button/ft-icon-button.vue'
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
import $ from 'jquery'
import ytrend from 'yt-trending-scraper'
export default Vue.extend({
@ -13,12 +15,20 @@ export default Vue.extend({
'ft-card': FtCard,
'ft-loader': FtLoader,
'ft-element-list': FtElementList,
'ft-icon-button': FtIconButton
'ft-icon-button': FtIconButton,
'ft-flex-box': FtFlexBox
},
data: function () {
return {
isLoading: false,
shownResults: []
shownResults: [],
currentTab: 'default',
tabInfoValues: [
'default',
'music',
'gaming',
'movies'
]
}
},
computed: {
@ -42,13 +52,49 @@ export default Vue.extend({
}
},
mounted: function () {
if (this.trendingCache && this.trendingCache.length > 0) {
if (this.trendingCache[this.currentTab] && this.trendingCache[this.currentTab].length > 0) {
this.shownResults = this.trendingCache
} else {
this.getTrendingInfo()
}
},
methods: {
changeTab: function (tab, event) {
if (event instanceof KeyboardEvent) {
if (event.key === 'Tab') {
return
} else if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
// navigate trending tabs with arrow keys
const index = this.tabInfoValues.indexOf(tab)
// tabs wrap around from leftmost to rightmost, and vice versa
tab = (event.key === 'ArrowLeft')
? this.tabInfoValues[(index > 0 ? index : this.tabInfoValues.length) - 1]
: this.tabInfoValues[(index + 1) % this.tabInfoValues.length]
const tabNode = $(`#${tab}Tab`)
event.target.setAttribute('tabindex', '-1')
tabNode.attr('tabindex', '0')
tabNode[0].focus()
}
event.preventDefault()
if (event.key !== 'Enter' && event.key !== ' ') {
return
}
}
const currentTabNode = $('.trendingInfoTabs > .tab[aria-selected="true"]')
const newTabNode = $(`#${tab}Tab`)
// switch selectability from currently focused tab to new tab
$('.trendingInfoTabs > .tab[tabindex="0"]').attr('tabindex', '-1')
newTabNode.attr('tabindex', '0')
currentTabNode.attr('aria-selected', 'false')
newTabNode.attr('aria-selected', 'true')
this.currentTab = tab
this.getTrendingInfo()
},
getTrendingInfo () {
if (!this.usingElectron) {
this.getVideoInformationInvidious()
@ -70,7 +116,7 @@ export default Vue.extend({
console.log('getting local trending')
const param = {
parseCreatorOnRise: false,
page: 'default',
page: this.currentTab,
geoLocation: this.region
}
@ -81,7 +127,9 @@ export default Vue.extend({
this.shownResults = returnData
this.isLoading = false
this.$store.commit('setTrendingCache', this.shownResults)
this.$store.commit('setTrendingCache', this.shownResults, this.currentTab)
}).then(() => {
document.querySelector(`#${this.currentTab}Tab`).focus()
}).catch((err) => {
console.log(err)
const errorMessage = this.$t('Local API Error (Click to copy)')
@ -112,6 +160,10 @@ export default Vue.extend({
params: { region: this.region }
}
if (this.currentTab !== 'default') {
trendingPayload.params.type = this.currentTab.charAt(0).toUpperCase() + this.currentTab.slice(1)
}
this.invidiousAPICall(trendingPayload).then((result) => {
if (!result) {
return
@ -125,7 +177,9 @@ export default Vue.extend({
this.shownResults = returnData
this.isLoading = false
this.$store.commit('setTrendingCache', this.shownResults)
this.$store.commit('setTrendingCache', this.shownResults, this.trendingCache)
}).then(() => {
document.querySelector(`#${this.currentTab}Tab`).focus()
}).catch((err) => {
console.log(err)
const errorMessage = this.$t('Invidious API Error (Click to copy)')

View File

@ -8,8 +8,68 @@
v-else
class="card"
>
<h3>{{ $t("Trending") }}</h3>
<h3>{{ $t("Trending.Trending") }}</h3>
<ft-flex-box
class="trendingInfoTabs"
role="tablist"
:aria-label="$t('Trending.Trending Tabs')"
>
<div
id="defaultTab"
class="tab"
role="tab"
aria-selected="true"
aria-controls="trendingPanel"
tabindex="0"
:class="(currentTab=='default')?'selectedTab':''"
@click="changeTab('default')"
@keydown="changeTab('default', $event)"
>
{{ $t("Trending.Default").toUpperCase() }}
</div>
<div
id="musicTab"
class="tab"
role="tab"
aria-selected="false"
aria-controls="trendingPanel"
tabindex="-1"
:class="(currentTab=='music')?'selectedTab':''"
@click="changeTab('music')"
@keydown="changeTab('music', $event)"
>
{{ $t("Trending.Music").toUpperCase() }}
</div>
<div
id="gamingTab"
class="tab"
role="tab"
aria-selected="false"
aria-controls="trendingPanel"
tabindex="-1"
:class="(currentTab=='gaming')?'selectedTab':''"
@click="changeTab('gaming')"
@keydown="changeTab('gaming', $event)"
>
{{ $t("Trending.Gaming").toUpperCase() }}
</div>
<div
id="moviesTab"
class="tab"
role="tab"
aria-selected="false"
aria-controls="trendingPanel"
tabindex="-1"
:class="(currentTab=='movies')?'selectedTab':''"
@click="changeTab('movies')"
@keydown="changeTab('movies', $event)"
>
{{ $t("Trending.Movies").toUpperCase() }}
</div>
</ft-flex-box>
<ft-element-list
id="trendingPanel"
role="tabpanel"
:data="shownResults"
/>
</ft-card>

View File

@ -960,13 +960,13 @@ export default Vue.extend({
if (this.removeVideoMetaFiles) {
const userData = await this.getUserDataPath()
if (this.isDev) {
const dashFileLocation = `dashFiles/${this.videoId}.xml`
const vttFileLocation = `storyboards/${this.videoId}.vtt`
const dashFileLocation = `static/dashFiles/${this.videoId}.xml`
const vttFileLocation = `static/storyboards/${this.videoId}.vtt`
// only delete the file it actually exists
if (fs.existsSync('dashFiles/') && fs.existsSync(dashFileLocation)) {
if (fs.existsSync('static/dashFiles/') && fs.existsSync(dashFileLocation)) {
fs.rmSync(dashFileLocation)
}
if (fs.existsSync('storyboards/') && fs.existsSync(vttFileLocation)) {
if (fs.existsSync('static/storyboards/') && fs.existsSync(vttFileLocation)) {
fs.rmSync(vttFileLocation)
}
} else {
@ -1014,9 +1014,10 @@ export default Vue.extend({
fs.mkdirSync('static/dashFiles/')
}
fs.rm(fileLocation, () => {
fs.writeFileSync(fileLocation, xmlData)
})
if (fs.existsSync(fileLocation)) {
fs.rmSync(fileLocation)
}
fs.writeFileSync(fileLocation, xmlData)
} else {
fileLocation = `${userData}/dashFiles/${this.videoId}.xml`
uriSchema = `file://${fileLocation}`

View File

@ -93,7 +93,6 @@
:video-thumbnail="thumbnail"
class="watchVideo"
:class="{ theatreWatchVideo: useTheatreMode }"
@theatre-mode="toggleTheatreMode"
@pause-player="pausePlayer"
/>
<watch-video-description

View File

@ -76,7 +76,13 @@ Subscriptions:
Load More Videos: حمّل المزيد من الفيديوهات
This profile has a large number of subscriptions. Forcing RSS to avoid rate limiting: لدى
هذا الملف الشخصي عدد كبير من الاشتراكات. يتم فرض وضع RSS لتجنب تقييد الوصول للاشتراكات
Trending: 'المحتوى الرائج'
Trending:
Trending: 'المحتوى الرائج'
Trending Tabs: علامات التبويب الشائعة
Movies: أفلام
Gaming: الالعاب
Music: الموسيقى
Default: الإفتراضي
Most Popular: 'الأكثر شعبية'
Playlists: 'قوائم التشغيل'
User Playlists:
@ -130,6 +136,11 @@ Settings:
The currently set default instance is $: المثيل الافتراضي المحدد حاليا هو $
Current Invidious Instance: المثيل الحالي Invidious
Clear Default Instance: مسح المثيل الافتراضي
External Link Handling:
No Action: لا يوجد إجراء
Ask Before Opening Link: اسأل قبل فتح الرابط
Open Link: افتح الرابط
External Link Handling: معالجة الارتباط الخارجي
Theme Settings:
Theme Settings: 'إعدادات السِمة'
Match Top Bar with Main Color: 'طابق الشريط العلوي مع اللون الأساسي'
@ -138,6 +149,7 @@ Settings:
Black: 'أسود'
Dark: 'داكن'
Light: 'فاتح'
Dracula: 'دراكولا'
Main Color Theme:
Main Color Theme: 'لون السِمة الأساسي'
Red: 'أحمر'
@ -156,6 +168,13 @@ Settings:
Amber: 'كهرماني'
Orange: 'برتقالي'
Deep Orange: 'برتقالي داكن'
Dracula Cyan: 'دراكولا سماوي'
Dracula Green: 'دراكولا أخضر'
Dracula Orange: 'دراكولا برتقالي'
Dracula Pink: 'دراكولا وردي'
Dracula Purple: 'دراكولا إرجواني'
Dracula Red: 'دراكولا أحمر'
Dracula Yellow: 'دراكولا أصفر'
Secondary Color Theme: 'لون السِمة الثانوي'
#* Main Color Theme
UI Scale: مقياس واجهة المستخدم
@ -637,6 +656,7 @@ Comments:
Newest first: الأحدث أولاً
Top comments: أهم التعليقات
Sort by: الترتيب حسب
Show More Replies: إظهار المزيد من الردود
Up Next: 'التالي'
# Toast Messages
@ -679,6 +699,9 @@ Tooltips:
التراجع.
Region for Trending: الانتشار المحلي (Trend) يسمح لك بأن تشاهد الفيديوهات الأكثر
انتشارا حسب الدولة. ليست كل الدول المعروضة في هذه القائمة مدعومة من طرف يوتيوب.
External Link Handling: "اختر السلوك الافتراضي عند النقر فوق رابط، لا يمكن فتحه\
\ في FreeTube.\nبشكل افتراضي، سيفتح FreeTube الرابط الذي تم النقر عليه في المتصفح\
\ الافتراضي.\n"
Player Settings:
Proxy Videos Through Invidious: سيتم الاتصال ب Invidious لتقديم مقاطع الفيديو
بدلاً من إجراء اتصال مباشر مع يوتيوب. يلغي تفضيل الواجهة البرمجية.
@ -722,3 +745,8 @@ Open New Window: افتح نافذة جديدة
Default Invidious instance has been cleared: تم مسح مثيل Invidious الافتراضي
Default Invidious instance has been set to $: تم تعيين المثيل الافتراضي Invidious
إلى $
Search Bar:
Clear Input: مسح المدخلات
External link opening has been disabled in the general settings: تم تعطيل فتح الارتباط
الخارجي في الإعدادات العامة
Are you sure you want to open this link?: هل أنت متأكد أنك تريد فتح هذا الرابط؟

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