mirror of https://github.com/FreeTubeApp/FreeTube
Compare commits
9 Commits
33cd2f458e
...
2b005a78dc
Author | SHA1 | Date |
---|---|---|
Mehul Sweeti Agrawal | 2b005a78dc | |
NEXI | 43a7fbdcb1 | |
absidue | 88bed9eaf6 | |
Sergio Marques | 7f3925d0c5 | |
msagr | a47666a5ef | |
msagr | a36c62b63b | |
msagr | 8241129151 | |
msagr | 4bbece374e | |
msagr | 833355bbbd |
|
@ -1,6 +1,7 @@
|
|||
import { defineComponent } from 'vue'
|
||||
import { sanitizeForHtmlId } from '../../helpers/accessibility'
|
||||
import { MAIN_PROFILE_ID } from '../../../constants'
|
||||
import { getFirstCharacter } from '../../helpers/strings'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FtProfileBubble',
|
||||
|
@ -24,6 +25,9 @@ export default defineComponent({
|
|||
},
|
||||
emits: ['click'],
|
||||
computed: {
|
||||
locale: function () {
|
||||
return this.$i18n.locale.replace('_', '-')
|
||||
},
|
||||
isMainProfile: function () {
|
||||
return this.profileId === MAIN_PROFILE_ID
|
||||
},
|
||||
|
@ -31,7 +35,9 @@ export default defineComponent({
|
|||
return 'profileBubble' + sanitizeForHtmlId(this.profileId)
|
||||
},
|
||||
profileInitial: function () {
|
||||
return this?.profileName?.length > 0 ? Array.from(this.translatedProfileName)[0].toUpperCase() : ''
|
||||
return this.profileName
|
||||
? getFirstCharacter(this.translatedProfileName, this.locale).toUpperCase()
|
||||
: ''
|
||||
},
|
||||
translatedProfileName: function () {
|
||||
return this.isMainProfile ? this.$t('Profile.All Channels') : this.profileName
|
||||
|
|
|
@ -8,6 +8,7 @@ import FtButton from '../../components/ft-button/ft-button.vue'
|
|||
import { MAIN_PROFILE_ID } from '../../../constants'
|
||||
import { calculateColorLuminance, colors } from '../../helpers/colors'
|
||||
import { showToast } from '../../helpers/utils'
|
||||
import { getFirstCharacter } from '../../helpers/strings'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FtProfileEdit',
|
||||
|
@ -47,11 +48,16 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
locale: function () {
|
||||
return this.$i18n.locale.replace('_', '-')
|
||||
},
|
||||
colorValues: function () {
|
||||
return colors.map(color => color.value)
|
||||
},
|
||||
profileInitial: function () {
|
||||
return this?.profileName?.length > 0 ? Array.from(this.translatedProfileName)[0].toUpperCase() : ''
|
||||
return this.profileName
|
||||
? getFirstCharacter(this.translatedProfileName, this.locale).toUpperCase()
|
||||
: ''
|
||||
},
|
||||
activeProfile: function () {
|
||||
return this.$store.getters.getActiveProfile
|
||||
|
|
|
@ -5,6 +5,7 @@ import FtCard from '../../components/ft-card/ft-card.vue'
|
|||
import FtIconButton from '../../components/ft-icon-button/ft-icon-button.vue'
|
||||
import { showToast } from '../../helpers/utils'
|
||||
import { MAIN_PROFILE_ID } from '../../../constants'
|
||||
import { getFirstCharacter } from '../../helpers/strings'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FtProfileSelector',
|
||||
|
@ -19,6 +20,9 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
locale: function () {
|
||||
return this.$i18n.locale.replace('_', '-')
|
||||
},
|
||||
profileList: function () {
|
||||
return this.$store.getters.getProfileList
|
||||
},
|
||||
|
@ -26,12 +30,15 @@ export default defineComponent({
|
|||
return this.$store.getters.getActiveProfile
|
||||
},
|
||||
activeProfileInitial: function () {
|
||||
// use Array.from, so that emojis don't get split up into individual character codes
|
||||
return this.activeProfile?.name?.length > 0 ? Array.from(this.translatedProfileName(this.activeProfile))[0].toUpperCase() : ''
|
||||
return this.activeProfile?.name
|
||||
? getFirstCharacter(this.translatedProfileName(this.activeProfile), this.locale).toUpperCase()
|
||||
: ''
|
||||
},
|
||||
profileInitials: function () {
|
||||
return this.profileList.map((profile) => {
|
||||
return profile?.name?.length > 0 ? Array.from(this.translatedProfileName(profile))[0].toUpperCase() : ''
|
||||
return profile?.name
|
||||
? getFirstCharacter(this.translatedProfileName(profile), this.locale).toUpperCase()
|
||||
: ''
|
||||
})
|
||||
}
|
||||
},
|
||||
|
|
|
@ -3,14 +3,17 @@ import { defineComponent } from 'vue'
|
|||
import { mapActions } from 'vuex'
|
||||
|
||||
import FtButton from '../../components/ft-button/ft-button.vue'
|
||||
import FtPrompt from '../../components/ft-prompt/ft-prompt.vue'
|
||||
|
||||
import { MAIN_PROFILE_ID } from '../../../constants'
|
||||
import { deepCopy, showToast } from '../../helpers/utils'
|
||||
import { getFirstCharacter } from '../../helpers/strings'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FtSubscribeButton',
|
||||
components: {
|
||||
'ft-button': FtButton
|
||||
'ft-button': FtButton,
|
||||
'ft-prompt': FtPrompt
|
||||
},
|
||||
props: {
|
||||
channelId: {
|
||||
|
@ -41,13 +44,19 @@ export default defineComponent({
|
|||
},
|
||||
data: function () {
|
||||
return {
|
||||
isProfileDropdownOpen: false
|
||||
isProfileDropdownOpen: false,
|
||||
showUnsubscribePopupForProfile: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
locale: function () {
|
||||
return this.$i18n.locale.replace('_', '-')
|
||||
},
|
||||
profileInitials: function () {
|
||||
return this.profileDisplayList.map((profile) => {
|
||||
return profile?.name?.length > 0 ? Array.from(profile.name)[0].toUpperCase() : ''
|
||||
return profile.name
|
||||
? getFirstCharacter(profile.name, this.locale).toUpperCase()
|
||||
: ''
|
||||
})
|
||||
},
|
||||
|
||||
|
@ -102,34 +111,14 @@ export default defineComponent({
|
|||
return
|
||||
}
|
||||
|
||||
const currentProfile = deepCopy(profile)
|
||||
|
||||
if (this.isProfileSubscribed(profile)) {
|
||||
currentProfile.subscriptions = currentProfile.subscriptions.filter((channel) => {
|
||||
return channel.id !== this.channelId
|
||||
})
|
||||
|
||||
this.updateProfile(currentProfile)
|
||||
showToast(this.$t('Channel.Channel has been removed from your subscriptions'))
|
||||
|
||||
if (profile._id === MAIN_PROFILE_ID) {
|
||||
// Check if a subscription exists in a different profile.
|
||||
// Remove from there as well.
|
||||
let duplicateSubscriptions = 0
|
||||
|
||||
this.profileList.forEach((profileInList) => {
|
||||
if (profileInList._id === MAIN_PROFILE_ID) {
|
||||
return
|
||||
}
|
||||
duplicateSubscriptions += this.unsubscribe(profileInList, this.channelId)
|
||||
})
|
||||
|
||||
if (duplicateSubscriptions > 0) {
|
||||
const message = this.$t('Channel.Removed subscription from {count} other channel(s)', { count: duplicateSubscriptions })
|
||||
showToast(message)
|
||||
}
|
||||
if (this.$store.getters.getUnsubscriptionPopupStatus) {
|
||||
this.showUnsubscribePopupForProfile = profile
|
||||
} else {
|
||||
this.handleUnsubscription(profile)
|
||||
}
|
||||
} else {
|
||||
const currentProfile = deepCopy(profile)
|
||||
const subscription = {
|
||||
id: this.channelId,
|
||||
name: this.channelName,
|
||||
|
@ -167,10 +156,46 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
|
||||
toggleProfileDropdown: function() {
|
||||
toggleProfileDropdown: function () {
|
||||
this.isProfileDropdownOpen = !this.isProfileDropdownOpen
|
||||
},
|
||||
|
||||
handleUnsubscribeConfirmation: async function (value) {
|
||||
const profile = this.showUnsubscribePopupForProfile
|
||||
this.showUnsubscribePopupForProfile = null
|
||||
if (value === 'yes') {
|
||||
this.handleUnsubscription(profile)
|
||||
}
|
||||
},
|
||||
|
||||
handleUnsubscription: function (profile) {
|
||||
const currentProfile = deepCopy(profile)
|
||||
currentProfile.subscriptions = currentProfile.subscriptions.filter((channel) => {
|
||||
return channel.id !== this.channelId
|
||||
})
|
||||
|
||||
this.updateProfile(currentProfile)
|
||||
showToast(this.$t('Channel.Channel has been removed from your subscriptions'))
|
||||
|
||||
if (profile._id === MAIN_PROFILE_ID) {
|
||||
// Check if a subscription exists in a different profile.
|
||||
// Remove from there as well.
|
||||
let duplicateSubscriptions = 0
|
||||
|
||||
this.profileList.forEach((profileInList) => {
|
||||
if (profileInList._id === MAIN_PROFILE_ID) {
|
||||
return
|
||||
}
|
||||
duplicateSubscriptions += this.unsubscribe(profileInList, this.channelId)
|
||||
})
|
||||
|
||||
if (duplicateSubscriptions > 0) {
|
||||
const message = this.$t('Channel.Removed subscription from {count} other channel(s)', { count: duplicateSubscriptions })
|
||||
showToast(message)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
isActiveProfile: function (profile) {
|
||||
return profile._id === this.activeProfile._id
|
||||
},
|
||||
|
|
|
@ -17,6 +17,14 @@
|
|||
text-color="var(--text-with-main-color)"
|
||||
@click="handleSubscription"
|
||||
/>
|
||||
<ft-prompt
|
||||
v-if="showUnsubscribePopupForProfile !== null"
|
||||
:label="$t('Channels.Unsubscribe Prompt', { channelName: channelName })"
|
||||
:option-names="[$t('Yes'), $t('No')]"
|
||||
:option-values="['yes', 'no']"
|
||||
:autosize="true"
|
||||
@click="handleUnsubscribeConfirmation"
|
||||
/>
|
||||
<ft-button
|
||||
v-if="isProfileDropdownEnabled"
|
||||
:no-border="true"
|
||||
|
|
|
@ -21,6 +21,9 @@ export default defineComponent({
|
|||
},
|
||||
fetchSubscriptionsAutomatically: function () {
|
||||
return this.$store.getters.getFetchSubscriptionsAutomatically
|
||||
},
|
||||
unsubscriptionPopupStatus: function () {
|
||||
return this.$store.getters.getUnsubscriptionPopupStatus
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -28,7 +31,8 @@ export default defineComponent({
|
|||
'updateHideWatchedSubs',
|
||||
'updateUseRssFeeds',
|
||||
'updateFetchSubscriptionsAutomatically',
|
||||
'updateOnlyShowLatestFromChannel'
|
||||
'updateOnlyShowLatestFromChannel',
|
||||
'updateUnsubscriptionPopupStatus'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -33,6 +33,14 @@
|
|||
@change="updateOnlyShowLatestFromChannel"
|
||||
/>
|
||||
</div>
|
||||
<div class="switchColumn">
|
||||
<ft-toggle-switch
|
||||
:label="$t('Settings.Subscription Settings.Avoid Accidental Unsubscription')"
|
||||
:default-value="unsubscriptionPopupStatus"
|
||||
:compact="true"
|
||||
@change="updateUnsubscriptionPopupStatus"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ft-settings-section>
|
||||
</template>
|
||||
|
|
|
@ -52,3 +52,31 @@ export function translateWindowTitle(title, i18n) {
|
|||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first user-perceived character,
|
||||
* respecting language specific rules and
|
||||
* emojis made up of multiple codepoints
|
||||
* like flags, families and skin tone modifiers.
|
||||
* @param {string} text
|
||||
* @param {string} locale
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getFirstCharacter(text, locale) {
|
||||
if (text.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// Firefox only received support for Intl.Segmenter support in version 125 (2024-04-16)
|
||||
// so fallback to Array.from just in case.
|
||||
// TODO: Remove fallback in the future
|
||||
if (Intl.Segmenter) {
|
||||
const segmenter = new Intl.Segmenter([locale, 'en'], { granularity: 'grapheme' })
|
||||
|
||||
// Use iterator directly as we only need the first segment
|
||||
const firstSegment = segmenter.segment(text)[Symbol.iterator]().next().value
|
||||
return firstSegment.segment
|
||||
} else {
|
||||
return Array.from(text)[0]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -226,6 +226,7 @@ const state = {
|
|||
hideVideoLikesAndDislikes: false,
|
||||
hideVideoViews: false,
|
||||
hideWatchedSubs: false,
|
||||
unsubscriptionPopupStatus: false,
|
||||
hideLabelsSideBar: false,
|
||||
hideChapters: false,
|
||||
showDistractionFreeTitles: false,
|
||||
|
|
|
@ -437,6 +437,7 @@ Settings:
|
|||
Manage Subscriptions: Manage Subscriptions
|
||||
Fetch Automatically: Fetch Feed Automatically
|
||||
Only Show Latest Video for Each Channel: Only Show Latest Video for Each Channel
|
||||
Avoid Accidental Unsubscription: Avoid Accidental Unsubscription
|
||||
Distraction Free Settings:
|
||||
Distraction Free Settings: Distraction Free Settings
|
||||
Sections:
|
||||
|
|
|
@ -276,6 +276,9 @@ Settings:
|
|||
Open Link: Abrir ligação
|
||||
Ask Before Opening Link: Perguntar antes de abrir a ligação
|
||||
No Action: Nenhuma ação
|
||||
Auto Load Next Page:
|
||||
Label: Carregar seguinte automaticamente
|
||||
Tooltip: Carrega as páginas e comentários automaticamente.
|
||||
Theme Settings:
|
||||
Theme Settings: 'Configurações de tema'
|
||||
Match Top Bar with Main Color: 'Utilizar cor principal na barra superior'
|
||||
|
@ -633,6 +636,7 @@ Settings:
|
|||
Set Password: Definir palavra-passe
|
||||
Remove Password: Remover palavra-passe
|
||||
Expand All Settings Sections: Expandir todas as secções de configurações
|
||||
Sort Settings Sections (A-Z): Ordenar definições (A-Z)
|
||||
About:
|
||||
#On About page
|
||||
About: 'Acerca'
|
||||
|
@ -974,6 +978,13 @@ Playlist:
|
|||
Playlist: Lista de reprodução
|
||||
Sort By:
|
||||
Sort By: Ordenar por
|
||||
AuthorAscending: Autor (A-Z)
|
||||
DateAddedNewest: Últimas adições primeiro
|
||||
DateAddedOldest: Primeiras adições primeiro
|
||||
AuthorDescending: Autor (Z-A)
|
||||
VideoTitleAscending: Título (A-Z)
|
||||
VideoTitleDescending: Título (Z-A)
|
||||
Custom: Personalizado
|
||||
Toggle Theatre Mode: 'Alternar modo cinema'
|
||||
Change Format:
|
||||
Change Media Formats: 'Alterar formatos multimédia'
|
||||
|
@ -1192,3 +1203,6 @@ Age Restricted:
|
|||
Feed:
|
||||
Feed Last Updated: 'Última atualização de {feedName}: {date}'
|
||||
Refresh Feed: Recarregar {subscriptionName}
|
||||
Display Label: '{label}: {value}'
|
||||
Moments Ago: há momentos
|
||||
checkmark: ✓
|
||||
|
|
|
@ -608,6 +608,7 @@ Settings:
|
|||
Enter Password To Unlock: Унесите лозинку да бисте откључали подешавања
|
||||
Unlock: Откључај
|
||||
Expand All Settings Sections: Прошири све одељке подешавања
|
||||
Sort Settings Sections (A-Z): Сортирање одељка подешавања (A-Z)
|
||||
About:
|
||||
#On About page
|
||||
About: 'О апликацији'
|
||||
|
|
Loading…
Reference in New Issue