mirror of
https://github.com/FreeTubeApp/FreeTube.git
synced 2025-12-05 01:10:31 +00:00
* Update to Vue 3 * Fix toasts and removing videos from playlists * Fix duplicate app ID * Simplify aria-selected handling now that false doesn't remove attributes * Fix various errors * Fix toasts and hiding watched videos * Update vue-router to 4.6.3 --------- Co-authored-by: efb4f5ff-1298-471a-8973-3d47447115dc <73130443+efb4f5ff-1298-471a-8973-3d47447115dc@users.noreply.github.com>
271 lines
7.9 KiB
Vue
271 lines
7.9 KiB
Vue
<template>
|
|
<div>
|
|
<FtCard class="card">
|
|
<h2>{{ editOrCreateProfileLabel }}</h2>
|
|
<FtFlexBox class="profileEdit">
|
|
<div>
|
|
<h3>{{ $t("Profile.Color Picker") }}</h3>
|
|
<FtFlexBox
|
|
class="colorOptions"
|
|
>
|
|
<div
|
|
v-for="color in COLOR_VALUES"
|
|
:key="color"
|
|
class="colorOption"
|
|
:title="color + ' ' + $t('Profile.Custom Color')"
|
|
:style="{ background: color }"
|
|
tabindex="0"
|
|
role="button"
|
|
@click="profileBgColor = color"
|
|
@keydown.space.prevent="profileBgColor = color"
|
|
@keydown.enter.prevent="profileBgColor = color"
|
|
/>
|
|
</FtFlexBox>
|
|
<div class="customColorSection">
|
|
<label for="colorPicker">{{ $t("Profile.Custom Color") }}</label>
|
|
<input
|
|
id="colorPicker"
|
|
v-model="profileBgColor"
|
|
type="color"
|
|
>
|
|
</div>
|
|
<FtInput
|
|
class="colorSelection"
|
|
placeholder=""
|
|
:value="profileBgColor"
|
|
:show-action-button="false"
|
|
:disabled="true"
|
|
/>
|
|
</div>
|
|
<div class="secondEditRow">
|
|
<div>
|
|
<h3>{{ editOrCreateProfileNameLabel }}</h3>
|
|
<FtInput
|
|
class="profileName"
|
|
:placeholder="$t('Profile.Profile Name')"
|
|
:disabled="isMainProfile"
|
|
:value="translatedProfileName"
|
|
:show-action-button="false"
|
|
:maxlength="100"
|
|
@input="profileName = $event"
|
|
@keydown.enter="saveProfile"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<h3>{{ $t("Profile.Profile Preview") }}</h3>
|
|
<div class="profilePreviewSection">
|
|
<div
|
|
class="colorOption"
|
|
:style="{ background: profileBgColor, color: profileTextColor }"
|
|
>
|
|
<div
|
|
class="initial"
|
|
>
|
|
{{ profileInitial }}
|
|
</div>
|
|
</div>
|
|
<FtFlexBox>
|
|
<FtButton
|
|
v-if="isNew"
|
|
:label="$t('Profile.Create Profile')"
|
|
@click="saveProfile"
|
|
/>
|
|
<template
|
|
v-else
|
|
>
|
|
<FtButton
|
|
:label="$t('Profile.Update Profile')"
|
|
@click="saveProfile"
|
|
/>
|
|
<FtButton
|
|
:label="$t('Profile.Make Default Profile')"
|
|
@click="setDefaultProfile"
|
|
/>
|
|
<FtButton
|
|
v-if="!isMainProfile"
|
|
:label="$t('Profile.Delete Profile')"
|
|
text-color="var(--destructive-text-color)"
|
|
background-color="var(--destructive-color)"
|
|
:icon="['fas', 'trash']"
|
|
@click="showDeletePrompt = true"
|
|
/>
|
|
</template>
|
|
</FtFlexBox>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</FtFlexBox>
|
|
</FtCard>
|
|
<FtPrompt
|
|
v-if="showDeletePrompt"
|
|
:label="deletePromptLabel"
|
|
:option-names="deletePromptNames"
|
|
:option-values="DELETE_PROMPT_VALUES"
|
|
:is-first-option-destructive="true"
|
|
@click="handleDeletePrompt"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, ref, watch } from 'vue'
|
|
import { useI18n } from '../../composables/use-i18n-polyfill'
|
|
|
|
import FtCard from '../ft-card/ft-card.vue'
|
|
import FtPrompt from '../FtPrompt/FtPrompt.vue'
|
|
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
|
|
import FtInput from '../ft-input/ft-input.vue'
|
|
import FtButton from '../FtButton/FtButton.vue'
|
|
|
|
import store from '../../store/index'
|
|
|
|
import { MAIN_PROFILE_ID } from '../../../constants'
|
|
import { calculateColorLuminance, colors } from '../../helpers/colors'
|
|
import { deepCopy, showToast } from '../../helpers/utils'
|
|
import { getFirstCharacter } from '../../helpers/strings'
|
|
|
|
/**
|
|
* @typedef {object} Profile
|
|
* @property {string} _id
|
|
* @property {string} name
|
|
* @property {string} bgColor
|
|
* @property {string} textColor
|
|
* @property {object[]} subscriptions
|
|
* @property {string} subscriptions[].id
|
|
* @property {string|undefined} subscriptions[].name
|
|
* @property {string|undefined} subscriptions[].thumbnail
|
|
*/
|
|
|
|
const { locale, t } = useI18n()
|
|
|
|
const props = defineProps({
|
|
isMainProfile: {
|
|
type: Boolean,
|
|
required: true
|
|
},
|
|
isNew: {
|
|
type: Boolean,
|
|
required: true
|
|
},
|
|
profile: {
|
|
type: Object,
|
|
required: true
|
|
}
|
|
})
|
|
|
|
const emit = defineEmits(['new-profile-created', 'profile-deleted'])
|
|
|
|
const COLOR_VALUES = colors.map(color => color.value)
|
|
|
|
/** @type {import('vue').ComputedRef<Profile>} */
|
|
const activeProfile = computed(() => store.getters.getActiveProfile)
|
|
|
|
/** @type {import('vue').Ref<string | undefined>} */
|
|
const profileId = ref(props.profile._id)
|
|
|
|
/** @type {import('vue').Ref<string>} */
|
|
const profileName = ref(props.profile.name)
|
|
|
|
/** @type {import('vue').Ref<string>} */
|
|
const profileBgColor = ref(props.profile.bgColor)
|
|
|
|
/** @type {import('vue').Ref<string>} */
|
|
const profileTextColor = ref(props.profile.textColor)
|
|
|
|
watch(profileBgColor, (value) => {
|
|
profileTextColor.value = calculateColorLuminance(value)
|
|
})
|
|
|
|
const translatedProfileName = computed(() => {
|
|
return props.isMainProfile ? t('Profile.All Channels') : profileName.value
|
|
})
|
|
|
|
const profileInitial = computed(() => {
|
|
return profileName.value
|
|
? getFirstCharacter(translatedProfileName.value, locale.value).toUpperCase()
|
|
: ''
|
|
})
|
|
|
|
const editOrCreateProfileLabel = computed(() => {
|
|
return props.isNew ? t('Profile.Create Profile') : t('Profile.Edit Profile')
|
|
})
|
|
|
|
const editOrCreateProfileNameLabel = computed(() => {
|
|
return props.isNew ? t('Profile.Create Profile Name') : t('Profile.Edit Profile Name')
|
|
})
|
|
|
|
function saveProfile() {
|
|
if (profileName.value === '') {
|
|
showToast(t('Profile.Your profile name cannot be empty'))
|
|
return
|
|
}
|
|
|
|
const profile = {
|
|
name: profileName.value,
|
|
bgColor: profileBgColor.value,
|
|
textColor: profileTextColor.value,
|
|
subscriptions: deepCopy(props.profile.subscriptions)
|
|
}
|
|
|
|
if (!props.isNew) {
|
|
profile._id = profileId.value
|
|
}
|
|
|
|
if (props.isNew) {
|
|
store.dispatch('createProfile', profile)
|
|
showToast(t('Profile.Profile has been created'))
|
|
emit('new-profile-created')
|
|
} else {
|
|
store.dispatch('updateProfile', profile)
|
|
showToast(t('Profile.Profile has been updated'))
|
|
}
|
|
}
|
|
|
|
function setDefaultProfile() {
|
|
store.dispatch('updateDefaultProfile', profileId.value)
|
|
showToast(t('Profile.Your default profile has been set to {profile}', { profile: translatedProfileName.value }))
|
|
}
|
|
|
|
const DELETE_PROMPT_VALUES = ['delete', 'cancel']
|
|
|
|
const deletePromptNames = computed(() => [
|
|
t('Yes, Delete'),
|
|
t('Cancel')
|
|
])
|
|
|
|
const deletePromptLabel = computed(() => {
|
|
return `${t('Profile.Are you sure you want to delete this profile?')} ${t('Profile["All subscriptions will also be deleted."]')}`
|
|
})
|
|
|
|
const showDeletePrompt = ref(false)
|
|
|
|
/** @type {import('vue').ComputedRef<string>} */
|
|
const defaultProfile = computed(() => store.getters.getDefaultProfile)
|
|
|
|
/**
|
|
* @param {'delete' | 'cancel' | null} response
|
|
*/
|
|
function handleDeletePrompt(response) {
|
|
if (response === 'delete') {
|
|
if (activeProfile.value._id === profileId.value) {
|
|
store.dispatch('updateActiveProfile', MAIN_PROFILE_ID)
|
|
}
|
|
|
|
store.dispatch('removeProfile', profileId.value)
|
|
|
|
showToast(t('Profile.Removed {profile} from your profiles', { profile: translatedProfileName.value }))
|
|
|
|
if (defaultProfile.value === profileId.value) {
|
|
store.dispatch('updateDefaultProfile', MAIN_PROFILE_ID)
|
|
showToast(t('Profile.Your default profile has been changed to your primary profile'))
|
|
}
|
|
|
|
emit('profile-deleted')
|
|
} else {
|
|
showDeletePrompt.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped src="./FtProfileEdit.css" />
|