Merge branch 'development' of github.com:FreeTubeApp/FreeTube into feat/add-select-mode

This commit is contained in:
Jason Henriquez 2023-11-22 11:43:44 -06:00
commit 74f14ea273
30 changed files with 455 additions and 123 deletions

View File

@ -11,17 +11,12 @@
overflow: hidden; overflow: hidden;
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
-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; transition: background 0.2s ease-out;
} }
.bubblePadding:hover { .bubblePadding:hover {
background-color: var(--side-nav-hover-color); background-color: var(--side-nav-hover-color);
color: var(--side-nav-hover-text-color); color: var(--side-nav-hover-text-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in; transition: background 0.2s ease-in;
} }

View File

@ -15,6 +15,7 @@
}" }"
tabindex="0" tabindex="0"
role="button" role="button"
:aria-expanded="dropdownShown"
@click.stop="handleIconClick" @click.stop="handleIconClick"
@mousedown.stop="handleIconMouseDown" @mousedown.stop="handleIconMouseDown"
@keydown.enter.stop.prevent="handleIconClick" @keydown.enter.stop.prevent="handleIconClick"
@ -34,7 +35,6 @@
v-if="dropdownOptions.length > 0" v-if="dropdownOptions.length > 0"
class="list" class="list"
role="listbox" role="listbox"
:aria-expanded="dropdownShown"
> >
<li <li
v-for="(option, index) in dropdownOptions" v-for="(option, index) in dropdownOptions"
@ -72,7 +72,6 @@
v-if="dropdownOptions.length > 0" v-if="dropdownOptions.length > 0"
class="list" class="list"
role="listbox" role="listbox"
:aria-expanded="dropdownShown"
> >
<li <li
v-for="(option, index) in dropdownOptions" v-for="(option, index) in dropdownOptions"

View File

@ -57,8 +57,6 @@
border-radius: 100%; border-radius: 100%;
color: var(--primary-text-color); color: var(--primary-text-color);
opacity: 0; opacity: 0;
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in; transition: background 0.2s ease-in;
} }
@ -74,8 +72,6 @@
.clearInputTextButton.visible:active { .clearInputTextButton.visible:active {
background-color: var(--tertiary-text-color); background-color: var(--tertiary-text-color);
color: var(--side-nav-active-text-color); color: var(--side-nav-active-text-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in; transition: background 0.2s ease-in;
} }
@ -172,8 +168,6 @@
.inputAction.enabled:hover { .inputAction.enabled:hover {
background-color: var(--side-nav-hover-color); background-color: var(--side-nav-hover-color);
color: var(--side-nav-hover-text-color); color: var(--side-nav-hover-text-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in; transition: background 0.2s ease-in;
} }
@ -184,8 +178,6 @@
.inputAction.enabled:active { .inputAction.enabled:active {
background-color: var(--tertiary-text-color); background-color: var(--tertiary-text-color);
color: var(--side-nav-active-text-color); color: var(--side-nav-active-text-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in; transition: background 0.2s ease-in;
} }

View File

@ -4,17 +4,12 @@
padding-block: 10px 30px; padding-block: 10px 30px;
padding-inline: 10px; padding-inline: 10px;
cursor: pointer; cursor: pointer;
-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; transition: background 0.2s ease-out;
} }
.bubblePadding:hover { .bubblePadding:hover {
background-color: var(--side-nav-hover-color); background-color: var(--side-nav-hover-color);
color: var(--side-nav-hover-text-color); color: var(--side-nav-hover-text-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in; transition: background 0.2s ease-in;
} }

View File

@ -27,7 +27,7 @@
inset-block-start: 60px; inset-block-start: 60px;
inset-inline-end: 10px; inset-inline-end: 10px;
min-inline-size: 250px; min-inline-size: 250px;
block-size: 400px; block-size: auto;
padding: 5px; padding: 5px;
background-color: var(--card-bg-color); background-color: var(--card-bg-color);
box-shadow: 0 0 4px var(--scrollbar-color-hover); box-shadow: 0 0 4px var(--scrollbar-color-hover);
@ -35,24 +35,33 @@
.profileWrapper { .profileWrapper {
margin-block-start: 60px; margin-block-start: 60px;
block-size: 340px; block-size: auto;
overflow-y: auto; overflow-y: auto;
/*
profile list max height: 90% of window size - 100 px. It's scaled to be 340px on 800x600 resolution.
Offset of 100px is to compensate for the fixed size of elements above the list, which takes more screen space on lower resolutions
*/
max-block-size: calc(90vh - 100px);
min-block-size: 340px;
}
/* Navbar changes position to horizontal with this media rule.
Height adjust for profile list so it won't cover navbar. */
@media only screen and (max-width: 680px){
.profileWrapper {
max-block-size: calc(95vh - 180px);
}
} }
.profile { .profile {
cursor: pointer; cursor: pointer;
block-size: 50px; block-size: 50px;
-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; transition: background 0.2s ease-out;
} }
.profile:hover { .profile:hover {
background-color: var(--side-nav-hover-color); background-color: var(--side-nav-hover-color);
color: var(--side-nav-hover-text-color); color: var(--side-nav-hover-text-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in; transition: background 0.2s ease-in;
} }

View File

@ -35,6 +35,10 @@ export default defineComponent({
} }
}, },
methods: { methods: {
isActiveProfile: function (profile) {
return profile._id === this.activeProfile._id
},
toggleProfileList: function () { toggleProfileList: function () {
this.profileListShown = !this.profileListShown this.profileListShown = !this.profileListShown

View File

@ -6,6 +6,8 @@
:style="{ background: activeProfile.bgColor, color: activeProfile.textColor }" :style="{ background: activeProfile.bgColor, color: activeProfile.textColor }"
tabindex="0" tabindex="0"
role="button" role="button"
:aria-expanded="profileListShown"
aria-controls="profileSelectorList"
@click="toggleProfileList" @click="toggleProfileList"
@mousedown="handleIconMouseDown" @mousedown="handleIconMouseDown"
@keydown.space.prevent="toggleProfileList" @keydown.space.prevent="toggleProfileList"
@ -19,6 +21,7 @@
</div> </div>
<ft-card <ft-card
v-show="profileListShown" v-show="profileListShown"
id="profileSelectorList"
ref="profileList" ref="profileList"
class="profileList" class="profileList"
tabindex="-1" tabindex="-1"
@ -46,7 +49,7 @@
:key="index" :key="index"
class="profile" class="profile"
:aria-labelledby="'profile-' + index + '-name'" :aria-labelledby="'profile-' + index + '-name'"
aria-selected="false" :aria-selected="isActiveProfile(profile)"
tabindex="0" tabindex="0"
role="option" role="option"
@click="setActiveProfile(profile)" @click="setActiveProfile(profile)"

View File

@ -1,8 +0,0 @@
/* Ensures style here overrides style of .btn */
.subscribeButton.btn {
align-self: center;
block-size: 50%;
margin-block-end: 10px;
min-inline-size: 150px;
white-space: initial;
}

View File

@ -25,29 +25,54 @@ export default defineComponent({
type: String, type: String,
required: true required: true
}, },
hideProfileDropdownToggle: {
type: Boolean,
default: false
},
subscriptionCountText: { subscriptionCountText: {
default: '', default: '',
type: String, type: String,
required: false required: false
} }
}, },
data: function () {
return {
isProfileDropdownOpen: false
}
},
computed: { computed: {
profileInitials: function () {
return this.profileDisplayList.map((profile) => {
return profile?.name?.length > 0 ? Array.from(profile.name)[0].toUpperCase() : ''
})
},
profileList: function () { profileList: function () {
return this.$store.getters.getProfileList return this.$store.getters.getProfileList
}, },
/* sort by 'All Channels' -> active profile -> unsubscribed channels -> subscribed channels */
profileDisplayList: function () {
const mainProfileAndActiveProfile = [this.profileList[0]]
if (this.activeProfile._id !== MAIN_PROFILE_ID) {
mainProfileAndActiveProfile.push(this.activeProfile)
}
return [
...mainProfileAndActiveProfile,
...this.profileList.filter((profile, i) =>
i !== 0 && !this.isActiveProfile(profile) && !this.isProfileSubscribed(profile)),
...this.profileList.filter((profile, i) =>
i !== 0 && !this.isActiveProfile(profile) && this.isProfileSubscribed(profile))
]
},
activeProfile: function () { activeProfile: function () {
return this.$store.getters.getActiveProfile return this.$store.getters.getActiveProfile
}, },
subscriptionInfo: function () { subscriptionInfo: function () {
return this.activeProfile.subscriptions.find((channel) => { return this.subscriptionInfoForProfile(this.activeProfile)
return channel.id === this.channelId
}) ?? null
},
isSubscribed: function () {
return this.subscriptionInfo !== null
}, },
hideChannelSubscriptions: function () { hideChannelSubscriptions: function () {
@ -55,23 +80,27 @@ export default defineComponent({
}, },
subscribedText: function () { subscribedText: function () {
let subscribedValue = (this.isSubscribed ? this.$t('Channel.Unsubscribe') : this.$t('Channel.Subscribe')).toUpperCase() let subscribedValue = (this.isProfileSubscribed(this.activeProfile) ? this.$t('Channel.Unsubscribe') : this.$t('Channel.Subscribe')).toUpperCase()
if (this.subscriptionCountText !== '' && !this.hideChannelSubscriptions) { if (this.subscriptionCountText !== '' && !this.hideChannelSubscriptions) {
subscribedValue += ' ' + this.subscriptionCountText subscribedValue += ' ' + this.subscriptionCountText
} }
return subscribedValue return subscribedValue
},
isProfileDropdownEnabled: function () {
return !this.hideProfileDropdownToggle && this.profileList.length > 1
} }
}, },
methods: { methods: {
handleSubscription: function () { handleSubscription: function (profile = this.activeProfile) {
if (this.channelId === '') { if (this.channelId === '') {
return return
} }
const currentProfile = deepCopy(this.activeProfile) const currentProfile = deepCopy(profile)
if (this.isSubscribed) { if (this.isProfileSubscribed(profile)) {
currentProfile.subscriptions = currentProfile.subscriptions.filter((channel) => { currentProfile.subscriptions = currentProfile.subscriptions.filter((channel) => {
return channel.id !== this.channelId return channel.id !== this.channelId
}) })
@ -79,16 +108,16 @@ export default defineComponent({
this.updateProfile(currentProfile) this.updateProfile(currentProfile)
showToast(this.$t('Channel.Channel has been removed from your subscriptions')) showToast(this.$t('Channel.Channel has been removed from your subscriptions'))
if (this.activeProfile._id === MAIN_PROFILE_ID) { if (profile._id === MAIN_PROFILE_ID) {
// Check if a subscription exists in a different profile. // Check if a subscription exists in a different profile.
// Remove from there as well. // Remove from there as well.
let duplicateSubscriptions = 0 let duplicateSubscriptions = 0
this.profileList.forEach((profile) => { this.profileList.forEach((profileInList) => {
if (profile._id === MAIN_PROFILE_ID) { if (profileInList._id === MAIN_PROFILE_ID) {
return return
} }
duplicateSubscriptions += this.unsubscribe(profile, this.channelId) duplicateSubscriptions += this.unsubscribe(profileInList, this.channelId)
}) })
if (duplicateSubscriptions > 0) { if (duplicateSubscriptions > 0) {
@ -107,7 +136,7 @@ export default defineComponent({
this.updateProfile(currentProfile) this.updateProfile(currentProfile)
showToast(this.$t('Channel.Added channel to your subscriptions')) showToast(this.$t('Channel.Added channel to your subscriptions'))
if (this.activeProfile._id !== MAIN_PROFILE_ID) { if (profile._id !== MAIN_PROFILE_ID) {
const primaryProfile = deepCopy(this.profileList.find(prof => { const primaryProfile = deepCopy(this.profileList.find(prof => {
return prof._id === MAIN_PROFILE_ID return prof._id === MAIN_PROFILE_ID
})) }))
@ -122,6 +151,34 @@ export default defineComponent({
} }
} }
} }
if (this.isProfileDropdownEnabled && !this.isProfileDropdownOpen) {
this.toggleProfileDropdown()
}
},
handleProfileDropdownFocusOut: function () {
if (!this.$refs.subscribeButton.matches(':focus-within')) {
this.isProfileDropdownOpen = false
}
},
toggleProfileDropdown: function() {
this.isProfileDropdownOpen = !this.isProfileDropdownOpen
},
isActiveProfile: function (profile) {
return profile._id === this.activeProfile._id
},
subscriptionInfoForProfile: function (profile) {
return profile.subscriptions.find((channel) => {
return channel.id === this.channelId
}) ?? null
},
isProfileSubscribed: function (profile) {
return this.subscriptionInfoForProfile(profile) !== null
}, },
unsubscribe: function(profile, channelId) { unsubscribe: function(profile, channelId) {

View File

@ -0,0 +1,141 @@
.buttonList {
margin: 5px;
margin-block-end: 10px;
border-radius: 4px;
block-size: fit-content;
box-shadow: 0px 1px 2px rgb(0 0 0 / 50%);
display: flex;
flex-wrap: nowrap;
/* addresses odd clipping behavior when adjusting window size */
background-color: var(--primary-color);
}
.ftSubscribeButton {
position: relative;
text-align: start;
}
/* Ensures style here overrides style of .btn */
.subscribeButton.btn {
min-inline-size: 150px;
white-space: initial;
}
.subscribeButton.btn, .profileDropdownToggle.btn {
align-self: center;
margin-block: 0;
margin-inline: 0;
}
.dropdownOpened {
.subscribeButton, .profileDropdownToggle {
border-end-start-radius: 0;
border-end-end-radius: 0;
}
}
.profileDropdownToggle.btn {
border-inline-start: none !important;
border-start-start-radius: 0;
border-end-start-radius: 0;
display: inline-block;
min-inline-size: 1em;
padding-inline: 10px;
box-sizing: content-box;
}
.hasProfileDropdownToggle {
.subscribeButton.btn {
min-inline-size: 100px;
padding-inline: 5px;
border-inline-end: 2px solid var(--primary-color-active) !important;
border-start-end-radius: 0;
border-end-end-radius: 0;
box-sizing: content-box;
}
}
.hasProfileDropdownToggle > .subscribeButton.btn, .profileDropdownToggle.btn {
padding-block: 5px;
padding-inline: 6px;
box-shadow: none;
flex: auto;
block-size: 2em;
}
.profileDropdown {
background-color: var(--side-nav-color);
box-shadow: 0 1px 2px rgb(0 0 0 / 50%);
color: var(--secondary-text-color);
display: inline;
font-size: 12px;
max-block-size: 200px;
margin-block: -10px 0;
margin-inline: 5px 0;
overflow-y: scroll;
position: absolute;
text-align: center;
-webkit-user-select: none;
user-select: none;
z-index: 3;
// accounts for parent's left and right margins
inline-size: calc(100% - 10px);
.profileList {
list-style-type: none;
margin: 0;
padding-inline: 0;
}
.profile {
cursor: pointer;
display: flex;
gap: 0.5em;
padding-inline-start: 0.5em;
block-size: 50px;
align-items: center;
transition: background 0.2s ease-out;
&:hover {
background-color: var(--side-nav-hover-color);
color: var(--side-nav-hover-text-color);
transition: background 0.2s ease-in;
}
.colorOption {
inline-size: 40px;
block-size: 40px;
cursor: pointer;
align-items: center;
display: flex;
justify-content: center;
flex-shrink: 0;
border-radius: 50%;
-webkit-border-radius: 50%;
}
.initial {
font-size: 20px;
line-height: 1em;
text-align: center;
user-select: none;
-webkit-user-select: none;
}
.profileName {
padding-inline-end: 1em;
text-align: start;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&.subscribed {
background-color: var(--primary-color);
.profileName {
color: var(--text-with-main-color);
}
}
}
}

View File

@ -1,12 +1,83 @@
<template> <template>
<div
ref="subscribeButton"
class="ftSubscribeButton"
:class="{ dropdownOpened: isProfileDropdownOpen}"
@focusout="handleProfileDropdownFocusOut"
>
<div
class="buttonList"
:class="{ hasProfileDropdownToggle: isProfileDropdownEnabled}"
>
<ft-button <ft-button
:label="subscribedText" :label="subscribedText"
:no-border="true"
class="subscribeButton" class="subscribeButton"
background-color="var(--primary-color)" background-color="var(--primary-color)"
text-color="var(--text-with-main-color)" text-color="var(--text-with-main-color)"
@click="handleSubscription" @click="handleSubscription"
/> />
<ft-button
v-if="isProfileDropdownEnabled"
:no-border="true"
:title="isProfileDropdownOpen ? $t('Profile.Close Profile Dropdown') : $t('Profile.Open Profile Dropdown')"
class="profileDropdownToggle"
background-color="var(--primary-color)"
text-color="var(--text-with-main-color)"
:aria-expanded="isProfileDropdownOpen"
@click="toggleProfileDropdown"
>
<font-awesome-icon
:icon="isProfileDropdownOpen ? ['fas', 'angle-up'] : ['fas', 'angle-down']"
/>
</ft-button>
</div>
<template v-if="isProfileDropdownOpen">
<div
tabindex="-1"
class="profileDropdown"
>
<ul
class="profileList"
>
<li
v-for="(profile, index) in profileDisplayList"
:id="'subscription-profile-' + index"
:key="index"
class="profile"
:class="{
subscribed: isProfileSubscribed(profile)
}"
:aria-labelledby="'subscription-profile-' + index + '-name'"
:aria-selected="isActiveProfile(profile)"
:aria-checked="isProfileSubscribed(profile)"
tabindex="0"
role="checkbox"
@click.stop.prevent="handleSubscription(profile)"
@keydown.space.stop.prevent="handleSubscription(profile)"
>
<div
class="colorOption"
:style="{ background: profile.bgColor, color: profile.textColor }"
>
<div
class="initial"
>
{{ isProfileSubscribed(profile) ? '✓' : profileInitials[index] }}
</div>
</div>
<p
:id="'subscription-profile-' + index + '-name'"
class="profileName"
>
{{ profile.name }}
</p>
</li>
</ul>
</div>
</template>
</div>
</template> </template>
<script src="./ft-subscribe-button.js" /> <script src="./ft-subscribe-button.js" />
<style src="./ft-subscribe-button.css" /> <style lang="scss" src="./ft-subscribe-button.scss" />

View File

@ -14,16 +14,12 @@
.navOption:hover { .navOption:hover {
background-color: var(--side-nav-hover-color); background-color: var(--side-nav-hover-color);
color: var(--side-nav-hover-text-color); color: var(--side-nav-hover-text-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in; transition: background 0.2s ease-in;
} }
.navOption:active { .navOption:active {
background-color: var(--side-nav-active-color); background-color: var(--side-nav-active-color);
color: var(--side-nav-active-text-color); color: var(--side-nav-active-text-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in; transition: background 0.2s ease-in;
} }

View File

@ -53,16 +53,12 @@
.navOption:hover, .navChannel:hover { .navOption:hover, .navChannel:hover {
background-color: var(--side-nav-hover-color); background-color: var(--side-nav-hover-color);
color: var(--side-nav-hover-text-color); color: var(--side-nav-hover-text-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in; transition: background 0.2s ease-in;
} }
.navOption:active, .navChannel:active { .navOption:active, .navChannel:active {
background-color: var(--side-nav-active-color); background-color: var(--side-nav-active-color);
color: var(--side-nav-active-text-color); color: var(--side-nav-active-text-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in; transition: background 0.2s ease-in;
} }

View File

@ -22,6 +22,13 @@
row-gap: 16px; row-gap: 16px;
} }
.watchingCount {
font-weight: normal;
margin-inline-start: 5px;
font-size: 15px;
color: var(--tertiary-text-color);
}
.message { .message {
font-size: 18px; font-size: 18px;
color: var(--tertiary-text-color); color: var(--tertiary-text-color);

View File

@ -6,6 +6,7 @@ import FtButton from '../ft-button/ft-button.vue'
import autolinker from 'autolinker' import autolinker from 'autolinker'
import { getRandomColorClass } from '../../helpers/colors' import { getRandomColorClass } from '../../helpers/colors'
import { getLocalVideoInfo, parseLocalTextRuns } from '../../helpers/api/local' import { getLocalVideoInfo, parseLocalTextRuns } from '../../helpers/api/local'
import { formatNumber } from '../../helpers/utils'
export default defineComponent({ export default defineComponent({
name: 'WatchVideoLiveChat', name: 'WatchVideoLiveChat',
@ -30,6 +31,7 @@ export default defineComponent({
}, },
data: function () { data: function () {
return { return {
/** @type {import('youtubei.js').YT.LiveChat|null} */
liveChatInstance: null, liveChatInstance: null,
isLoading: true, isLoading: true,
hasError: false, hasError: false,
@ -52,7 +54,9 @@ export default defineComponent({
amount: '', amount: '',
colorClass: '' colorClass: ''
} }
} },
/** @type {number|null} */
watchingCount: null,
} }
}, },
computed: { computed: {
@ -74,6 +78,14 @@ export default defineComponent({
scrollingBehaviour: function () { scrollingBehaviour: function () {
return this.$store.getters.getDisableSmoothScrolling ? 'auto' : 'smooth' return this.$store.getters.getDisableSmoothScrolling ? 'auto' : 'smooth'
},
hideVideoViews: function () {
return this.$store.getters.getHideVideoViews
},
formattedWatchingCount: function () {
return this.watchingCount !== null ? formatNumber(this.watchingCount) : '0'
} }
}, },
beforeDestroy: function () { beforeDestroy: function () {
@ -181,6 +193,12 @@ export default defineComponent({
} }
}) })
this.liveChatInstance.on('metadata-update', metadata => {
if (!this.hideVideoViews && metadata.views && !isNaN(metadata.views.original_view_count)) {
this.watchingCount = metadata.views.original_view_count
}
})
this.liveChatInstance.once('end', () => { this.liveChatInstance.once('end', () => {
this.hasEnded = true this.hasEnded = true
this.liveChatInstance = null this.liveChatInstance = null

View File

@ -42,7 +42,15 @@
v-else v-else
class="relative" class="relative"
> >
<h4>{{ $t("Video.Live Chat") }}</h4> <h4>
{{ $t("Video.Live Chat") }}
<span
v-if="!hideVideoViews && watchingCount !== null"
class="watchingCount"
>
{{ $tc('Global.Counts.Watching Count', watchingCount, { count: formattedWatchingCount }) }}
</span>
</h4>
<div <div
v-if="superChatComments.length > 0" v-if="superChatComments.length > 0"
class="superChatComments" class="superChatComments"

View File

@ -9,6 +9,7 @@ import { library } from '@fortawesome/fontawesome-svg-core'
// to avoid code conflict and duplicate entries // to avoid code conflict and duplicate entries
import { import {
faAngleDown, faAngleDown,
faAngleUp,
faArrowDown, faArrowDown,
faArrowLeft, faArrowLeft,
faArrowRight, faArrowRight,
@ -82,6 +83,7 @@ Vue.config.productionTip = process.env.NODE_ENV === 'development'
library.add( library.add(
// solid icons // solid icons
faAngleDown, faAngleDown,
faAngleUp,
faArrowDown, faArrowDown,
faArrowLeft, faArrowLeft,
faArrowRight, faArrowRight,

View File

@ -69,6 +69,7 @@
display: flex; display: flex;
gap: 30px; gap: 30px;
justify-content: space-between; justify-content: space-between;
align-items: center;
} }
.subscribeButton { .subscribeButton {

View File

@ -26,7 +26,7 @@
<ft-flex-box class="channels"> <ft-flex-box class="channels">
<div <div
v-for="channel in channelList" v-for="channel in channelList"
:key="channel.key" :key="channel.id"
class="channel" class="channel"
> >
<router-link <router-link
@ -53,7 +53,6 @@
class="unsubscribeContainer" class="unsubscribeContainer"
> >
<ft-subscribe-button <ft-subscribe-button
class="btn"
:channel-id="channel.id" :channel-id="channel.id"
:channel-name="channel.name" :channel-name="channel.name"
:channel-thumbnail="channel.thumbnail" :channel-thumbnail="channel.thumbnail"

View File

@ -811,6 +811,8 @@ Video:
'Live Chat is unavailable for this stream. It may have been disabled by the uploader.': الدردشة 'Live Chat is unavailable for this stream. It may have been disabled by the uploader.': الدردشة
المباشرة غير متاحة لهذا البث. ربما تم تعطيلها من قبل القائم بالتحميل. المباشرة غير متاحة لهذا البث. ربما تم تعطيلها من قبل القائم بالتحميل.
Pause on Current Video: توقف مؤقتًا على الفيديو الحالي Pause on Current Video: توقف مؤقتًا على الفيديو الحالي
Unhide Channel: عرض القناة
Hide Channel: إخفاء القناة
Videos: Videos:
#& Sort By #& Sort By
Sort By: Sort By:
@ -1037,3 +1039,6 @@ Playlist will pause when current video is finished: ستتوقف قائمة ال
انتهاء الفيديو الحالي انتهاء الفيديو الحالي
Playlist will not pause when current video is finished: لن تتوقف قائمة التشغيل مؤقتًا Playlist will not pause when current video is finished: لن تتوقف قائمة التشغيل مؤقتًا
عند انتهاء الفيديو الحالي عند انتهاء الفيديو الحالي
Channel Hidden: تم إضافة {channel} إلى مرشح القناة
Go to page: إذهب إلى {page}
Channel Unhidden: تمت إزالة {channel} من مرشح القناة

View File

@ -3,7 +3,7 @@ Locale Name: 'ئیگلیزی (وڵاتە یەکگرتووەکانی ئەمریک
FreeTube: 'فریتیوب' FreeTube: 'فریتیوب'
# Currently on Subscriptions, Playlists, and History # Currently on Subscriptions, Playlists, and History
'This part of the app is not ready yet. Come back later when progress has been made.': >- 'This part of the app is not ready yet. Come back later when progress has been made.': >-
بەشێک لە بەرنامۆچکە هێشتا ئامادە نییە. کە ڕەوتەکە درووست کرا دووبارە وەرەوە. بەشێک لە نەرمەواڵەکە هێشتا ئامادە نییە. کە ڕەوتەکە درووست کرا دووبارە وەرەوە.
# Webkit Menu Bar # Webkit Menu Bar
File: 'پەڕگە' File: 'پەڕگە'
@ -32,9 +32,9 @@ Back: 'دواوە'
Forward: 'پێشەوە' Forward: 'پێشەوە'
Open New Window: 'کردنەوەی پەنجەرەیەکی نوێ' Open New Window: 'کردنەوەی پەنجەرەیەکی نوێ'
Version {versionNumber} is now available! Click for more details: 'وەشانی {versionNumber} Version {versionNumber} is now available! Click for more details: 'ئێستا وەشانی {versionNumber}
ئێستا بەردەستە! بۆ زانیاری زۆرتر کرتە بکە' بەردەستە..بۆ زانیاری زۆرتر کرتە بکە'
Download From Site: 'داگرتن لە وێبگە' Download From Site: 'لە وێبگەوە دایگرە'
A new blog is now available, {blogTitle}. Click to view more: '' A new blog is now available, {blogTitle}. Click to view more: ''
Are you sure you want to open this link?: 'دڵنیایت دەتەوێت ئەم بەستەرە بکەیتەوە؟' Are you sure you want to open this link?: 'دڵنیایت دەتەوێت ئەم بەستەرە بکەیتەوە؟'
@ -44,13 +44,13 @@ Global:
Videos: 'ڤیدیۆکان' Videos: 'ڤیدیۆکان'
Shorts: '' Shorts: ''
Live: 'ڕاستەوخۆ' Live: 'ڕاستەوخۆ'
Community: '' Community: 'کۆمەڵگە'
Counts: Counts:
Video Count: '١ ڤیدیۆ | {count} ڤیدیۆ' Video Count: '١ ڤیدیۆ | {count} ڤیدیۆ'
Channel Count: '١ کەناڵ | {count} کەناڵ' Channel Count: '١ کەناڵ | {count} کەناڵ'
Subscriber Count: '١ بەشداربوو | {count} بەشداربوو' Subscriber Count: '١ بەشداربوو | {count} بەشداربوو'
View Count: 'بینینەک | {count} بینین' View Count: 'بینینەک | {count} بینین'
Watching Count: '' Watching Count: '١ تەمەشاکردن | {count} تەمەشاکردن'
# Search Bar # Search Bar
Search / Go to URL: '' Search / Go to URL: ''
@ -58,7 +58,7 @@ Search Bar:
Clear Input: '' Clear Input: ''
# In Filter Button # In Filter Button
Search Filters: Search Filters:
Search Filters: '' Search Filters: 'پاڵفتەکردنی گەڕان'
Sort By: Sort By:
Sort By: 'ڕیزکردن بە' Sort By: 'ڕیزکردن بە'
Most Relevant: '' Most Relevant: ''
@ -87,10 +87,10 @@ Search Filters:
Medium (4 - 20 minutes): 'ناوەند (٤ - ٢٠ خولەک)' Medium (4 - 20 minutes): 'ناوەند (٤ - ٢٠ خولەک)'
Long (> 20 minutes): 'درێژ (> ٢٠ خولەک)' Long (> 20 minutes): 'درێژ (> ٢٠ خولەک)'
# On Search Page # On Search Page
Search Results: '' Search Results: 'ئەنجامەکانی گەڕان'
Fetching results. Please wait: '' Fetching results. Please wait: ''
Fetch more results: '' Fetch more results: ''
There are no more results for this search: '' There are no more results for this search: 'ئەنجامەکی تر نییە بۆ ئەم گەڕانە'
# Sidebar # Sidebar
Subscriptions: Subscriptions:
# On Subscriptions Page # On Subscriptions Page
@ -121,26 +121,27 @@ Channels:
Unsubscribe Prompt: '' Unsubscribe Prompt: ''
Trending: Trending:
Trending: '' Trending: ''
Default: '' Default: 'بنەڕەت'
Music: 'مۆسیقا' Music: 'مۆسیقا'
Gaming: 'یاری' Gaming: 'یاری'
Movies: 'فیلم' Movies: 'فیلم'
Trending Tabs: '' Trending Tabs: ''
Most Popular: 'باوترین' Most Popular: 'باوترین'
Playlists: '' Playlists: 'پێڕستی لێدانەکان'
User Playlists: User Playlists:
Your Playlists: '' Your Playlists: 'پێڕستی لێدانەکانت'
Playlist Message: '' Playlist Message: ''
Your saved videos are empty. Click on the save button on the corner of a video to have it listed here: '' Your saved videos are empty. Click on the save button on the corner of a video to have it listed here: ''
Empty Search Message: '' Empty Search Message: ''
Search bar placeholder: '' Search bar placeholder: 'لەناو پێڕستی لێدان بگەڕێ'
History: History:
# On History Page # On History Page
History: 'مێژوو' History: 'مێژوو'
Watch History: 'مێژووی تەمەشاکردن' Watch History: 'مێژووی تەمەشاکردن'
Your history list is currently empty.: '' Your history list is currently empty.: 'ئێستا لیستەی مێژووت بەتاڵە.'
Empty Search Message: '' Empty Search Message: 'هیچ ڤیدیۆیەک لە مێژووت نەدۆزرایەوە کە بەرانبەری گەڕانەکەت
Search bar placeholder: "" بێت'
Search bar placeholder: "لەناو مێژوو بگەڕێ"
Settings: Settings:
# On Settings Page # On Settings Page
Settings: 'ڕێکخستنەکان' Settings: 'ڕێکخستنەکان'
@ -153,7 +154,7 @@ Settings:
Enable Search Suggestions: '' Enable Search Suggestions: ''
Default Landing Page: '' Default Landing Page: ''
Locale Preference: '' Locale Preference: ''
System Default: '' System Default: 'بنەڕەتی سیستەم'
Preferred API Backend: Preferred API Backend:
Preferred API Backend: '' Preferred API Backend: ''
Local API: '' Local API: ''
@ -164,8 +165,8 @@ Settings:
List: 'پێڕست' List: 'پێڕست'
Thumbnail Preference: Thumbnail Preference:
Thumbnail Preference: '' Thumbnail Preference: ''
Default: '' Default: 'بنەڕەت'
Beginning: '' Beginning: 'سەرەتا'
Middle: 'ناوەڕاست' Middle: 'ناوەڕاست'
End: 'کۆتایی' End: 'کۆتایی'
Hidden: 'شاراوە' Hidden: 'شاراوە'
@ -193,10 +194,10 @@ Settings:
Hide Side Bar Labels: '' Hide Side Bar Labels: ''
Hide FreeTube Header Logo: '' Hide FreeTube Header Logo: ''
Base Theme: Base Theme:
Base Theme: '' Base Theme: 'ڕووکاری بنچینە'
Black: 'ڕەش' Black: 'ڕەش'
Dark: 'تاریک' Dark: 'تاریک'
System Default: '' System Default: 'بنەڕەتی سیستەم'
Light: 'ڕووناک' Light: 'ڕووناک'
Dracula: '' Dracula: ''
Catppuccin Mocha: '' Catppuccin Mocha: ''
@ -208,11 +209,11 @@ Settings:
Pink: 'پەمبە' Pink: 'پەمبە'
Purple: 'وەنەوشەیی' Purple: 'وەنەوشەیی'
Deep Purple: 'وەنەوشەیی تۆخ' Deep Purple: 'وەنەوشەیی تۆخ'
Indigo: '' Indigo: 'نیلی'
Blue: 'شین' Blue: 'شین'
Light Blue: 'شینی ئاڵ' Light Blue: 'شینی ئاڵ'
Cyan: '' Cyan: 'شینی تۆخ'
Teal: '' Teal: 'شەدری'
Green: 'کەسک' Green: 'کەسک'
Light Green: 'کەسکی ئاڵ' Light Green: 'کەسکی ئاڵ'
Lime: '' Lime: ''
@ -856,3 +857,4 @@ Hashtag:
Yes: '' Yes: ''
No: '' No: ''
Ok: '' Ok: ''
Go to page: بڕۆ بۆ {page}

View File

@ -806,6 +806,8 @@ Video:
'Live Chat is unavailable for this stream. It may have been disabled by the uploader.': Živý 'Live Chat is unavailable for this stream. It may have been disabled by the uploader.': Živý
chat není pro tento stream k dispozici. Je možné, že byl vypnut nahrávajícím. chat není pro tento stream k dispozici. Je možné, že byl vypnut nahrávajícím.
Pause on Current Video: Pozastavit na současném videu Pause on Current Video: Pozastavit na současném videu
Unhide Channel: Zobrazit kanál
Hide Channel: Skrýt kanál
Videos: Videos:
#& Sort By #& Sort By
Sort By: Sort By:
@ -1036,3 +1038,6 @@ Playlist will pause when current video is finished: Po přehrání aktuálního
playlist pozastaven playlist pozastaven
Playlist will not pause when current video is finished: Po přehrání aktuálního videa Playlist will not pause when current video is finished: Po přehrání aktuálního videa
nebude playlist pozastaven nebude playlist pozastaven
Channel Hidden: Kanál {channel} přidán do filtru kanálů
Go to page: Přejít na {page}
Channel Unhidden: Kanál {channel} odebrán z filtrů kanálů

View File

@ -554,6 +554,8 @@ Profile:
Are you sure you want to delete the selected channels? This will not delete the channel from any other profile.: Are Are you sure you want to delete the selected channels? This will not delete the channel from any other profile.: Are
you sure you want to delete the selected channels? This will not delete the channel you sure you want to delete the selected channels? This will not delete the channel
from any other profile. from any other profile.
Close Profile Dropdown: Close Profile Dropdown
Open Profile Dropdown: Open Profile Dropdown
#On Channel Page #On Channel Page
Channel: Channel:
Subscribe: Subscribe Subscribe: Subscribe

View File

@ -827,6 +827,8 @@ Video:
chat en vivo no está disponible para esta transmisión. Tal vez estaba deshabilitado chat en vivo no está disponible para esta transmisión. Tal vez estaba deshabilitado
antes de la retransmisión. antes de la retransmisión.
Pause on Current Video: Pausa en el vídeo actual Pause on Current Video: Pausa en el vídeo actual
Unhide Channel: Mostrar el canal
Hide Channel: Ocultar el canal
Videos: Videos:
#& Sort By #& Sort By
Sort By: Sort By:
@ -1073,3 +1075,5 @@ Playlist will pause when current video is finished: La lista de reproducción se
Playlist will not pause when current video is finished: La lista de reproducción no Playlist will not pause when current video is finished: La lista de reproducción no
se detendrá cuando termine el vídeo actual se detendrá cuando termine el vídeo actual
Go to page: Ir a la {page} Go to page: Ir a la {page}
Channel Hidden: '{channel} añadido al filtro de canales'
Channel Unhidden: '{channel} eliminado del filtro de canales'

View File

@ -764,6 +764,8 @@ Video:
'Live Chat is unavailable for this stream. It may have been disabled by the uploader.': Otsevestlus 'Live Chat is unavailable for this stream. It may have been disabled by the uploader.': Otsevestlus
pole selle videovoo puhul saadaval. Võib-olla on üleslaadija vestluse keelanud. pole selle videovoo puhul saadaval. Võib-olla on üleslaadija vestluse keelanud.
Pause on Current Video: Peata hetkel esitatav video Pause on Current Video: Peata hetkel esitatav video
Unhide Channel: Näita kanalit
Hide Channel: Peida kanal
Videos: Videos:
#& Sort By #& Sort By
Sort By: Sort By:
@ -991,3 +993,6 @@ Playlist will pause when current video is finished: Hetkel mängiva video lõppe
esitusloendi esitamine peatub esitusloendi esitamine peatub
Playlist will not pause when current video is finished: Hetkel mängiva video lõppemisel Playlist will not pause when current video is finished: Hetkel mängiva video lõppemisel
esitusloendi esitamine jätkub esitusloendi esitamine jätkub
Channel Hidden: '{channel} on lisatud kanalite filtrisse'
Go to page: 'Ava leht: {page}'
Channel Unhidden: '{channel} on eemaldatud kanalite filtrist'

View File

@ -794,6 +794,8 @@ Video:
chat dal vivo non è disponibile per questo video. Potrebbe essere stata disattivata chat dal vivo non è disponibile per questo video. Potrebbe essere stata disattivata
dall'autore del caricamento. dall'autore del caricamento.
Pause on Current Video: Pausa sul video attuale Pause on Current Video: Pausa sul video attuale
Unhide Channel: Mostra canale
Hide Channel: Nascondi canale
Videos: Videos:
#& Sort By #& Sort By
Sort By: Sort By:
@ -1082,3 +1084,6 @@ Playlist will pause when current video is finished: La playlist verrà messa in
al termine del video attuale al termine del video attuale
Playlist will not pause when current video is finished: La playlist non verrà messa Playlist will not pause when current video is finished: La playlist non verrà messa
in pausa al termine del video attuale in pausa al termine del video attuale
Channel Hidden: '{channel} aggiunto al filtro canali'
Go to page: Vai a {page}
Channel Unhidden: '{channel} rimosso dal filtro canali'

View File

@ -1,5 +1,5 @@
# Put the name of your locale in the same language # Put the name of your locale in the same language
Locale Name: 'kur-ckb' Locale Name: 'کوردی ناوەڕاست'
FreeTube: 'فریتیوب' FreeTube: 'فریتیوب'
# Currently on Subscriptions, Playlists, and History # Currently on Subscriptions, Playlists, and History
'This part of the app is not ready yet. Come back later when progress has been made.': >- 'This part of the app is not ready yet. Come back later when progress has been made.': >-
@ -7,20 +7,20 @@ FreeTube: 'فریتیوب'
رویداوە. رویداوە.
# Webkit Menu Bar # Webkit Menu Bar
File: 'فایل' File: 'پەڕگە'
Quit: 'چونەدەرەوە' Quit: 'دەرچوون'
Edit: 'دەستکاریکردن' Edit: 'دەستکاری'
Undo: 'گەڕانەوە' Undo: 'پووچکردنەوە'
Redo: 'هێنانەوە' Redo: 'هێنانەوە'
Cut: 'بڕین' Cut: 'بڕین'
Copy: 'کۆپی' Copy: 'لەبەرگرتنەوە'
Paste: 'پەیست' Paste: 'لکاندن'
Delete: 'سڕینەوە' Delete: 'سڕینەوە'
Select all: 'دیاریکردنی هەمووی' Select all: 'دیاریکردنی هەمووی'
Reload: 'دوبارە دابەزاندن' Reload: 'بارکردنەوە'
Force Reload: 'دوباری دابەزاندی بەهێز' Force Reload: 'بارکردنەوەی بەزۆر'
Toggle Developer Tools: 'ئەدەواتەکانی دیڤیڵۆپەر بەردەست بخە' Toggle Developer Tools: 'زامنی ئامرازەکانی گەشەپێدەر'
Actual size: 'گەورەیی راستی' Actual size: 'قەبارەی ڕاستەقینە'
Zoom in: 'زووم کردنە ناوەوە' Zoom in: 'زووم کردنە ناوەوە'
Zoom out: 'زووم کردنە دەرەوە' Zoom out: 'زووم کردنە دەرەوە'
Toggle fullscreen: 'شاشەکەت پرکەرەوە' Toggle fullscreen: 'شاشەکەت پرکەرەوە'
@ -30,9 +30,9 @@ Close: 'داخستن'
Back: 'گەڕانەوە' Back: 'گەڕانەوە'
Forward: 'چونەپێشەوە' Forward: 'چونەپێشەوە'
Version {versionNumber} is now available! Click for more details: 'ڤێرژنی {versionNumber} Version {versionNumber} is now available! Click for more details: 'ئێستا وەشانی {versionNumber}
ئێستا بەردەستە! کلیک بکە بۆ زانیاری زیاتر' بەردەستە..بۆ زانیاری زۆرتر کرتە بکە'
Download From Site: 'دایبەزێنە لە سایتەکەوە' Download From Site: 'لە وێبگەوە دایگرە'
A new blog is now available, {blogTitle}. Click to view more: 'بڵۆگێکی نوێ بەردەستە، A new blog is now available, {blogTitle}. Click to view more: 'بڵۆگێکی نوێ بەردەستە،
{blogTitle}. کلیک بکە بۆ بینینی زیاتر' {blogTitle}. کلیک بکە بۆ بینینی زیاتر'
@ -190,3 +190,8 @@ Profile:
Removed {profile} from your profiles: 'سڕاوەتەوە لە پرۆفایلەکانت {profile}' Removed {profile} from your profiles: 'سڕاوەتەوە لە پرۆفایلەکانت {profile}'
Channel: Channel:
Playlists: {} Playlists: {}
New Window: پەنجەرەی نوێ
Go to page: بڕۆ بۆ {page}
Preferences: هەڵبژاردەکان
Are you sure you want to open this link?: دڵنیایت دەتەوێت ئەم بەستەرە بکەیتەوە؟
Open New Window: کردنەوەی پەنجەرەیەکی نوێ

View File

@ -761,6 +761,8 @@ Video:
YouTube-ом. YouTube-ом.
'Live Chat is unavailable for this stream. It may have been disabled by the uploader.': Ћаскање 'Live Chat is unavailable for this stream. It may have been disabled by the uploader.': Ћаскање
уживо није доступно за овај стрим. Можда га је онемогућио аутор. уживо није доступно за овај стрим. Можда га је онемогућио аутор.
Unhide Channel: Прикажи канал
Hide Channel: Сакриј канал
Tooltips: Tooltips:
Subscription Settings: Subscription Settings:
Fetch Feeds from RSS: 'Када је омогућено, FreeTube ће користити RSS уместо свог Fetch Feeds from RSS: 'Када је омогућено, FreeTube ће користити RSS уместо свог
@ -982,3 +984,6 @@ Screenshot Error: Снимак екрана није успео. {error}
Downloading has completed: „{videoTitle}“ је завршио преузимање Downloading has completed: „{videoTitle}“ је завршио преузимање
Loop is now enabled: Понављање је сада омогућено Loop is now enabled: Понављање је сада омогућено
Downloading failed: Дошло је до проблема при преузимању „{videoTitle}“ Downloading failed: Дошло је до проблема при преузимању „{videoTitle}“
Channel Hidden: '{channel} је додат на филтер канала'
Go to page: Иди на {page}
Channel Unhidden: '{channel} је уклоњен из филтера канала'

View File

@ -713,6 +713,8 @@ Video:
Upcoming: 即将到来 Upcoming: 即将到来
'Live Chat is unavailable for this stream. It may have been disabled by the uploader.': 实时聊天对此音视频流不可用。上传者可能禁用了它。 'Live Chat is unavailable for this stream. It may have been disabled by the uploader.': 实时聊天对此音视频流不可用。上传者可能禁用了它。
Pause on Current Video: 当前视频播完后不自动播放列表中下一视频 Pause on Current Video: 当前视频播完后不自动播放列表中下一视频
Unhide Channel: 显示频道
Hide Channel: 隐藏频道
Videos: Videos:
#& Sort By #& Sort By
Sort By: Sort By:
@ -936,3 +938,5 @@ Hashtag:
Playlist will pause when current video is finished: 当前视频播完后播放列表会暂停 Playlist will pause when current video is finished: 当前视频播完后播放列表会暂停
Playlist will not pause when current video is finished: 当前视频播完后播放列表不会暂停 Playlist will not pause when current video is finished: 当前视频播完后播放列表不会暂停
Go to page: 转到页{page} Go to page: 转到页{page}
Channel Hidden: '{channel} 频道已添加到频道过滤器'
Channel Unhidden: 从频道过滤器删除了{channel} 频道

View File

@ -131,7 +131,7 @@ Settings:
Preferred API Backend: Preferred API Backend:
Preferred API Backend: '偏好API伺服器' Preferred API Backend: '偏好API伺服器'
Local API: '本機 API' Local API: '本機 API'
Invidious API: 'Invidious API(應用程式介面)' Invidious API: 'Invidious API'
Video View Type: Video View Type:
Video View Type: '影片觀看類別' Video View Type: '影片觀看類別'
Grid: '網格' Grid: '網格'
@ -141,7 +141,7 @@ Settings:
Default: '預設' Default: '預設'
Beginning: '片頭' Beginning: '片頭'
Middle: '中間' Middle: '中間'
End: '尾' End: '尾'
Hidden: 隱藏 Hidden: 隱藏
Blur: 模糊 Blur: 模糊
'Invidious Instance (Default is https://invidious.snopyta.org)': 'Invidious實例(預設為 'Invidious Instance (Default is https://invidious.snopyta.org)': 'Invidious實例(預設為
@ -410,7 +410,7 @@ Settings:
Hide Channels Disabled Message: 某些頻道被使用 ID 封鎖且無法處理。當這些 ID 更新時,功能將會被封鎖 Hide Channels Disabled Message: 某些頻道被使用 ID 封鎖且無法處理。當這些 ID 更新時,功能將會被封鎖
Hide Channels Already Exists: 頻道 ID 已存在 Hide Channels Already Exists: 頻道 ID 已存在
Hide Channels API Error: 使用提供的 ID 擷取使用者時發生錯誤。請再次檢查 ID 是否正確。 Hide Channels API Error: 使用提供的 ID 擷取使用者時發生錯誤。請再次檢查 ID 是否正確。
The app needs to restart for changes to take effect. Restart and apply change?: 此變更需要重啟讓修改生效。重啟並且套用變更? The app needs to restart for changes to take effect. Restart and apply change?: 必須重新啟動應用程式以生效。重新啟動並套用變更嗎?
Proxy Settings: Proxy Settings:
Error getting network information. Is your proxy configured properly?: 取得網路資訊時發生錯誤。您的代理伺服器設定正確嗎? Error getting network information. Is your proxy configured properly?: 取得網路資訊時發生錯誤。您的代理伺服器設定正確嗎?
City: 城市 City: 城市
@ -590,12 +590,12 @@ Channel:
Releases: 發布 Releases: 發布
This channel does not currently have any releases: 此頻道目前沒有任何發布 This channel does not currently have any releases: 此頻道目前沒有任何發布
Video: Video:
Open in YouTube: '在YouTube中開啟' Open in YouTube: '在 YouTube 中開啟'
Copy YouTube Link: '複製YouTube連結' Copy YouTube Link: '複製 YouTube 連結'
Open YouTube Embedded Player: '開啟YouTube內嵌播放器' Open YouTube Embedded Player: '開啟 YouTube 內嵌播放器'
Copy YouTube Embedded Player Link: '複製YouTube內嵌播放器連結' Copy YouTube Embedded Player Link: '複製 YouTube 內嵌播放器連結'
Open in Invidious: '在Invidious中開啟' Open in Invidious: '在 Invidious 中開啟'
Copy Invidious Link: '複製Invidious連結' Copy Invidious Link: '複製 Invidious 連結'
Views: '觀看' Views: '觀看'
Watched: '已觀看' Watched: '已觀看'
# As in a Live Video # As in a Live Video
@ -661,10 +661,10 @@ Video:
audio only: 僅音訊 audio only: 僅音訊
video only: 僅影片 video only: 僅影片
Download Video: 下載影片 Download Video: 下載影片
Copy Invidious Channel Link: 複製Invidious頻道連結 Copy Invidious Channel Link: 複製 Invidious 頻道連結
Open Channel in Invidious: Invidious開啟頻道 Open Channel in Invidious: Invidious 開啟頻道
Copy YouTube Channel Link: 複製YouTube頻道連結 Copy YouTube Channel Link: 複製 YouTube 頻道連結
Open Channel in YouTube: YouTube開啟頻道 Open Channel in YouTube: YouTube 開啟頻道
Started streaming on: '開始直播時間' Started streaming on: '開始直播時間'
Streamed on: 直播於 Streamed on: 直播於
Video has been removed from your saved list: 影片已從您的播放清單移除 Video has been removed from your saved list: 影片已從您的播放清單移除
@ -722,6 +722,8 @@ Video:
Upcoming: 即將到來 Upcoming: 即將到來
'Live Chat is unavailable for this stream. It may have been disabled by the uploader.': 即時聊天在此串流不可用。其可能被上傳者停用了。 'Live Chat is unavailable for this stream. It may have been disabled by the uploader.': 即時聊天在此串流不可用。其可能被上傳者停用了。
Pause on Current Video: 暫停目前影片 Pause on Current Video: 暫停目前影片
Unhide Channel: 顯示頻道
Hide Channel: 隱藏頻道
Videos: Videos:
#& Sort By #& Sort By
Sort By: Sort By:
@ -944,3 +946,6 @@ Hashtag:
This hashtag does not currently have any videos: 此標籤目前沒有任何影片 This hashtag does not currently have any videos: 此標籤目前沒有任何影片
Playlist will pause when current video is finished: 當目前影片結束時,播放清單將會暫停 Playlist will pause when current video is finished: 當目前影片結束時,播放清單將會暫停
Playlist will not pause when current video is finished: 當目前影片結束時,播放清單將不會暫停 Playlist will not pause when current video is finished: 當目前影片結束時,播放清單將不會暫停
Channel Hidden: '{channel} 已新增至頻道過濾條件'
Go to page: 到 {page}
Channel Unhidden: '{channel} 已從頻道過濾條件移除'