Compare commits

...

12 Commits

Author SHA1 Message Date
Jason 1e25f0991d
Merge 748fb35c3d into 43a7fbdcb1 2024-04-27 03:07:32 +00:00
NEXI 43a7fbdcb1
Translated using Weblate (Serbian)
Currently translated at 100.0% (830 of 830 strings)

Translation: FreeTube/Translations
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/sr/
2024-04-27 05:07:17 +02:00
absidue 88bed9eaf6
Fix handling of emojis with ZWJ sequences in profile initials (#5023) 2024-04-27 10:53:03 +08:00
Sergio Marques 7f3925d0c5
Translated using Weblate (Portuguese)
Currently translated at 99.3% (825 of 830 strings)

Translation: FreeTube/Translations
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/pt/
2024-04-27 02:07:20 +02:00
Jason Henriquez 748fb35c3d Fix linting issue 2024-04-25 22:03:34 -05:00
Jason Henriquez 96bb71e837 Fix disabled ft-select label bug 2024-04-25 16:17:52 -05:00
Jason Henriquez b6d961dd5e Merge branch 'development' of github.com:FreeTubeApp/FreeTube into feat/intuitive-actions 2024-04-25 16:11:12 -05:00
Jason Henriquez 7337fe1d1b Add Ctrl+F handling to Channel search bar 2024-04-20 08:35:20 -05:00
Jason Henriquez fa1cd3632e Remove unnecessary 'return's 2024-04-20 08:02:41 -05:00
Jason Henriquez 5089cb3278 Move lambda data functions to methods 2024-04-17 13:46:01 -05:00
Jason Henriquez d32ebdc49c Configure Ctrl+F keyboard listeners on pages with prominent search bar 2024-04-17 08:38:52 -05:00
Jason Henriquez 62ae16d92c Configure 'Enter' to submit form being edited 2024-04-17 08:38:22 -05:00
20 changed files with 139 additions and 22 deletions

View File

@ -10,6 +10,7 @@ import FtSelect from '../../components/ft-select/ft-select.vue'
import FtToggleSwitch from '../../components/ft-toggle-switch/ft-toggle-switch.vue'
import {
showToast,
ctrlFHandler,
getIconForSortPreference
} from '../../helpers/utils'
@ -199,12 +200,13 @@ export default defineComponent({
},
mounted: function () {
this.lastActiveElement = document.activeElement
this.updateQueryDebounce = debounce(this.updateQuery, 500)
// User might want to search first if they have many playlists
this.$refs.searchBar.focus()
document.addEventListener('keydown', this.keyboardShortcutHandler)
},
beforeDestroy() {
document.removeEventListener('keydown', this.keyboardShortcutHandler)
this.lastActiveElement?.focus()
},
methods: {
@ -270,6 +272,10 @@ export default defineComponent({
this.query = query
},
keyboardShortcutHandler: function (event) {
ctrlFHandler(event, this.$refs.searchBar)
},
getIconForSortPreference: (s) => getIconForSortPreference(s),
...mapActions([

View File

@ -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

View File

@ -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

View File

@ -46,6 +46,7 @@
:value="translatedProfileName"
:show-action-button="false"
@input="e => profileName = e"
@keydown.enter.native="saveProfile"
/>
</div>
<div>

View File

@ -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()
: ''
})
}
},

View File

@ -26,9 +26,9 @@
<span class="select-highlight" />
<span class="select-bar" />
<label
v-if="!disabled"
class="select-label"
:for="sanitizedId ?? sanitizedPlaceholder"
:hidden="disabled"
>
<font-awesome-icon
:icon="icon"

View File

@ -6,6 +6,7 @@ import FtButton from '../../components/ft-button/ft-button.vue'
import { MAIN_PROFILE_ID } from '../../../constants'
import { deepCopy, showToast } from '../../helpers/utils'
import { getFirstCharacter } from '../../helpers/strings'
export default defineComponent({
name: 'FtSubscribeButton',
@ -45,9 +46,14 @@ export default defineComponent({
}
},
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()
: ''
})
},

View File

@ -22,6 +22,7 @@
input-type="password"
:value="password"
@input="e => password = e"
@keydown.enter.native="handleSetPassword"
/>
<ft-button
class="centerButton"

View File

@ -7,6 +7,7 @@ import FtInput from '../ft-input/ft-input.vue'
import FtPrompt from '../ft-prompt/ft-prompt.vue'
import FtButton from '../ft-button/ft-button.vue'
import {
ctrlFHandler,
formatNumber,
showToast,
} from '../../helpers/utils'
@ -411,19 +412,9 @@ export default defineComponent({
this.$emit('search-video-query-change', query)
},
keyboardShortcutHandler(event) {
switch (event.key) {
case 'F':
case 'f':
if (this.searchVideoModeAllowed && ((process.platform !== 'darwin' && event.ctrlKey) || (process.platform === 'darwin' && event.metaKey))) {
nextTick(() => {
// Some elements only present after rendering update
this.$refs.searchInput.focus()
})
}
}
keyboardShortcutHandler: function (event) {
ctrlFHandler(event, this.$refs.searchInput)
},
...mapActions([
'showAddToPlaylistPromptForManyVideos',
'updatePlaylist',

View File

@ -39,6 +39,7 @@
:show-label="false"
:value="newTitle"
@input="(input) => (newTitle = input)"
@keydown.enter.native="savePlaylistInfo"
/>
<h2
v-else
@ -66,6 +67,7 @@
:show-label="false"
:value="newDescription"
@input="(input) => newDescription = input"
@keydown.enter.native="savePlaylistInfo"
/>
<p
v-else

View File

@ -30,6 +30,7 @@
:show-label="true"
:value="proxyHostname"
@input="handleUpdateProxyHostname"
@keydown.enter.native="testProxy"
/>
<ft-input
:placeholder="$t('Settings.Proxy Settings.Proxy Port Number')"
@ -37,6 +38,7 @@
:show-label="true"
:value="proxyPort"
@input="handleUpdateProxyPort"
@keydown.enter.native="testProxy"
/>
</ft-flex-box>
<p

View File

@ -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]
}
}

View File

@ -4,6 +4,7 @@ import { IpcChannels } from '../../constants'
import FtToastEvents from '../components/ft-toast/ft-toast-events'
import i18n from '../i18n/index'
import router from '../router/index'
import { nextTick } from 'vue'
// allowed characters in channel handle: A-Z, a-z, 0-9, -, _, .
// https://support.google.com/youtube/answer/11585688#change_handle
@ -862,3 +863,13 @@ export async function fetchWithTimeout(timeoutMs, input, init) {
}
}
}
export function ctrlFHandler(event, inputElement) {
switch (event.key) {
case 'F':
case 'f':
if (((process.platform !== 'darwin' && event.ctrlKey) || (process.platform === 'darwin' && event.metaKey))) {
nextTick(() => inputElement?.focus())
}
}
}

View File

@ -16,6 +16,7 @@ import autolinker from 'autolinker'
import {
setPublishedTimestampsInvidious,
copyToClipboard,
ctrlFHandler,
extractNumberFromString,
formatNumber,
showToast,
@ -435,6 +436,7 @@ export default defineComponent({
},
mounted: function () {
this.isLoading = true
document.addEventListener('keydown', this.keyboardShortcutHandler)
if (this.$route.query.url) {
this.resolveChannelUrl(this.$route.query.url, this.$route.params.currentTab)
@ -461,6 +463,9 @@ export default defineComponent({
})
}
},
beforeDestroy() {
document.removeEventListener('keydown', this.keyboardShortcutHandler)
},
methods: {
resolveChannelUrl: async function (url, tab = undefined) {
let id
@ -1951,6 +1956,10 @@ export default defineComponent({
})
},
keyboardShortcutHandler: function (event) {
ctrlFHandler(event, this.$refs.channelSearchBar)
},
getIconForSortPreference: (s) => getIconForSortPreference(s),
...mapActions([

View File

@ -206,6 +206,7 @@
<ft-input
v-if="showSearchBar"
ref="channelSearchBar"
:placeholder="$t('Channel.Search Channel')"
:show-clear-text-button="true"
class="channelSearch"

View File

@ -7,6 +7,7 @@ import FtElementList from '../../components/ft-element-list/ft-element-list.vue'
import FtButton from '../../components/ft-button/ft-button.vue'
import FtInput from '../../components/ft-input/ft-input.vue'
import FtAutoLoadNextPageWrapper from '../../components/ft-auto-load-next-page-wrapper/ft-auto-load-next-page-wrapper.vue'
import { ctrlFHandler } from '../../helpers/utils'
export default defineComponent({
name: 'History',
@ -53,6 +54,7 @@ export default defineComponent({
}
},
mounted: function () {
document.addEventListener('keydown', this.keyboardShortcutHandler)
const limit = sessionStorage.getItem('historyLimit')
if (limit !== null) {
@ -69,6 +71,9 @@ export default defineComponent({
this.filterHistoryDebounce = debounce(this.filterHistory, 500)
},
beforeDestroy: function () {
document.removeEventListener('keydown', this.keyboardShortcutHandler)
},
methods: {
increaseLimit: function () {
if (this.query !== '') {
@ -113,5 +118,8 @@ export default defineComponent({
this.activeData = filteredQuery.length < this.searchDataLimit ? filteredQuery : filteredQuery.slice(0, this.searchDataLimit)
}
},
keyboardShortcutHandler: function (event) {
ctrlFHandler(event, this.$refs.searchBar)
}
}
})

View File

@ -6,6 +6,7 @@ import FtInput from '../../components/ft-input/ft-input.vue'
import FtSubscribeButton from '../../components/ft-subscribe-button/ft-subscribe-button.vue'
import { invidiousGetChannelInfo, youtubeImageUrlToInvidious, invidiousImageUrlToInvidious } from '../../helpers/api/invidious'
import { getLocalChannel } from '../../helpers/api/local'
import { ctrlFHandler } from '../../helpers/utils'
export default defineComponent({
name: 'SubscribedChannels',
@ -26,7 +27,7 @@ export default defineComponent({
},
thumbnailSize: 176,
ytBaseURL: 'https://yt3.ggpht.com',
errorCount: 0
errorCount: 0,
}
},
computed: {
@ -78,8 +79,12 @@ export default defineComponent({
}
},
mounted: function () {
document.addEventListener('keydown', this.keyboardShortcutHandler)
this.getSubscription()
},
beforeDestroy: function () {
document.removeEventListener('keydown', this.keyboardShortcutHandler)
},
methods: {
getSubscription: function () {
this.subscribedChannels = this.activeSubscriptionList.slice().sort((a, b) => {
@ -155,6 +160,10 @@ export default defineComponent({
}
},
keyboardShortcutHandler: function (event) {
ctrlFHandler(event, this.$refs.searchBarChannels)
},
...mapActions([
'updateSubscriptionDetails'
])

View File

@ -12,7 +12,7 @@ import FtInput from '../../components/ft-input/ft-input.vue'
import FtIconButton from '../../components/ft-icon-button/ft-icon-button.vue'
import FtToggleSwitch from '../../components/ft-toggle-switch/ft-toggle-switch.vue'
import FtAutoLoadNextPageWrapper from '../../components/ft-auto-load-next-page-wrapper/ft-auto-load-next-page-wrapper.vue'
import { getIconForSortPreference } from '../../helpers/utils'
import { ctrlFHandler, getIconForSortPreference } from '../../helpers/utils'
const SORT_BY_VALUES = {
NameAscending: 'name_ascending',
@ -184,6 +184,7 @@ export default defineComponent({
},
},
mounted: function () {
document.addEventListener('keydown', this.keyboardShortcutHandler)
const limit = sessionStorage.getItem('favoritesLimit')
if (limit !== null) {
this.dataLimit = limit
@ -200,6 +201,9 @@ export default defineComponent({
this.filterPlaylistDebounce = debounce(this.filterPlaylist, 500)
},
beforeDestroy: function () {
document.removeEventListener('keydown', this.keyboardShortcutHandler)
},
methods: {
increaseLimit: function () {
if (this.query !== '') {
@ -243,6 +247,10 @@ export default defineComponent({
})
},
keyboardShortcutHandler: function (event) {
ctrlFHandler(event, this.$refs.searchBar)
},
getIconForSortPreference: (s) => getIconForSortPreference(s),
...mapActions([

View File

@ -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:

View File

@ -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: 'О апликацији'