Skip to content

Commit

Permalink
Add keyboard shortcuts to titles (FreeTubeApp#5857)
Browse files Browse the repository at this point in the history
* Update title for custom FreeTube button labels with keyboard shortcuts

* Add keyboard shortcut to unmodified Shaka control labels

* Replace in-code use of available shortcuts with the corresponding constants

* Add explanatory comments

* Fix captions constant name

* Prevent creating new shortcut localization if Shaka localization key has no matching value

* Add util functions to localize special keys

* Replace more video player shortcut usage in code with corresponding constants

* Add labels for History and Settings app shortcuts, and update variable naming

* Display 'Option' instead of 'Alt' for Mac users

* Use Mac icons in keyboard shortcuts for Macs

* Update Mac arrow icon choice

* Update KeyboardShortcuts constant organization in preparation for keyboard shorcut modal

* Adjust nameSpan._textContent to be set to same value as aria-label

* Add code comment explaining shakaControlKeysToShortcuts

* Move changes from deleted Popular.js to Popular.vue
  • Loading branch information
kommunarr authored and Soham456 committed Dec 5, 2024
1 parent fcd2bc2 commit 8c230bd
Show file tree
Hide file tree
Showing 16 changed files with 285 additions and 72 deletions.
45 changes: 45 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,50 @@ const SyncEvents = {
},
}

// note: the multi-key shortcut values are currently just for display use in action titles
const KeyboardShortcuts = {
APP: {
GENERAL: {
HISTORY_BACKWARD: 'alt+arrowleft',
HISTORY_FORWARD: 'alt+arrowright',
NEW_WINDOW: 'ctrl+N',
NAVIGATE_TO_SETTINGS: 'ctrl+,',
NAVIGATE_TO_HISTORY: 'ctrl+H',
NAVIGATE_TO_HISTORY_MAC: 'cmd+Y',
},
SITUATIONAL: {
REFRESH: 'r'
},
},
VIDEO_PLAYER: {
GENERAL: {
CAPTIONS: 'c',
THEATRE_MODE: 't',
FULLSCREEN: 'f',
FULLWINDOW: 's',
PICTURE_IN_PICTURE: 'i',
MUTE: 'm',
VOLUME_UP: 'arrowup',
VOLUME_DOWN: 'arrowdown',
STATS: 'd',
TAKE_SCREENSHOT: 'u',
},
PLAYBACK: {
PLAY: 'k',
LARGE_REWIND: 'j',
LARGE_FAST_FORWARD: 'l',
SMALL_REWIND: 'arrowleft',
SMALL_FAST_FORWARD: 'arrowright',
DECREASE_VIDEO_SPEED: 'o',
INCREASE_VIDEO_SPEED: 'p',
LAST_FRAME: ',',
NEXT_FRAME: '.',
LAST_CHAPTER: 'ctrl+arrowleft',
NEXT_CHAPTER: 'ctrl+arrowright',
}
},
}

// Utils
const MAIN_PROFILE_ID = 'allChannels'

Expand All @@ -132,6 +176,7 @@ export {
IpcChannels,
DBActions,
SyncEvents,
KeyboardShortcuts,
MAIN_PROFILE_ID,
MOBILE_WIDTH_THRESHOLD,
PLAYLIST_HEIGHT_FORCE_LIST_THRESHOLD,
Expand Down
10 changes: 10 additions & 0 deletions src/renderer/components/ft-refresh-widget/ft-refresh-widget.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { defineComponent } from 'vue'

import FtIconButton from '../ft-icon-button/ft-icon-button.vue'
import { KeyboardShortcuts } from '../../../constants'
import { addKeyboardShortcutToActionTitle } from '../../helpers/utils'

export default defineComponent({
name: 'FtRefreshWidget',
Expand All @@ -22,6 +24,14 @@ export default defineComponent({
}
},
emits: ['click'],
computed: {
refreshFeedButtonTitle: function() {
return addKeyboardShortcutToActionTitle(
this.$t('Feed.Refresh Feed', { subscriptionName: this.title }),
KeyboardShortcuts.APP.SITUATIONAL.REFRESH
)
}
},
methods: {
click: function() {
this.$emit('click')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
:disabled="disableRefresh"
:icon="['fas', 'sync']"
class="refreshButton"
:title="$t('Feed.Refresh Feed', { subscriptionName: title })"
:title="refreshFeedButtonTitle"
:size="12"
theme="primary"
@click="click"
Expand Down
100 changes: 63 additions & 37 deletions src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import shaka from 'shaka-player'
import { useI18n } from '../../composables/use-i18n-polyfill'

import store from '../../store/index'
import { IpcChannels } from '../../../constants'
import { IpcChannels, KeyboardShortcuts } from '../../../constants'
import { AudioTrackSelection } from './player-components/AudioTrackSelection'
import { FullWindowButton } from './player-components/FullWindowButton'
import { LegacyQualitySelection } from './player-components/LegacyQualitySelection'
Expand All @@ -22,6 +22,7 @@ import {
translateSponsorBlockCategory
} from '../../helpers/player/utils'
import {
addKeyboardShortcutToActionTitle,
getPicturesPath,
showSaveDialog,
showToast
Expand All @@ -39,6 +40,23 @@ const RequestType = shaka.net.NetworkingEngine.RequestType
const AdvancedRequestType = shaka.net.NetworkingEngine.AdvancedRequestType
const TrackLabelFormat = shaka.ui.Overlay.TrackLabelFormat

/*
Mapping of Shaka localization keys for control labels to FreeTube shortcuts.
See: https://github.com/shaka-project/shaka-player/blob/main/ui/locales/en.json
*/
const shakaControlKeysToShortcuts = {
MUTE: KeyboardShortcuts.VIDEO_PLAYER.GENERAL.MUTE,
UNMUTE: KeyboardShortcuts.VIDEO_PLAYER.GENERAL.MUTE,
PLAY: KeyboardShortcuts.VIDEO_PLAYER.PLAYBACK.PLAY,
PAUSE: KeyboardShortcuts.VIDEO_PLAYER.PLAYBACK.PLAY,
PICTURE_IN_PICTURE: KeyboardShortcuts.VIDEO_PLAYER.GENERAL.PICTURE_IN_PICTURE,
ENTER_PICTURE_IN_PICTURE: KeyboardShortcuts.VIDEO_PLAYER.GENERAL.PICTURE_IN_PICTURE,
EXIT_PICTURE_IN_PICTURE: KeyboardShortcuts.VIDEO_PLAYER.GENERAL.PICTURE_IN_PICTURE,
CAPTIONS: KeyboardShortcuts.VIDEO_PLAYER.GENERAL.CAPTIONS,
FULL_SCREEN: KeyboardShortcuts.VIDEO_PLAYER.GENERAL.FULLSCREEN,
EXIT_FULL_SCREEN: KeyboardShortcuts.VIDEO_PLAYER.GENERAL.FULLSCREEN
}

/** @type {Map<string, string>} */
const LOCALE_MAPPINGS = new Map(process.env.SHAKA_LOCALE_MAPPINGS)

Expand Down Expand Up @@ -969,7 +987,7 @@ export default defineComponent({
* @param {string} locale
*/
async function setLocale(locale) {
// For most of FreeTube's locales their is an equivalent one in shaka-player,
// For most of FreeTube's locales, there is an equivalent one in shaka-player,
// however if there isn't one we should fall back to US English.
// At the time of writing "et", "eu", "gl", "is" don't have any translations
const shakaLocale = LOCALE_MAPPINGS.get(locale) ?? 'en'
Expand All @@ -990,6 +1008,27 @@ export default defineComponent({

localization.changeLocale([shakaLocale])

// Add the keyboard shortcut to the label for the default Shaka controls

const shakaControlKeysToShortcutLocalizations = new Map()
Object.entries(shakaControlKeysToShortcuts).forEach(([shakaControlKey, shortcut]) => {
const originalLocalization = localization.resolve(shakaControlKey)
if (originalLocalization === '') {
// e.g., A Shaka localization key in shakaControlKeysToShortcuts has fallen out of date and need to be updated
console.error('Mising Shaka localization key "%s"', shakaControlKey)
return
}

const localizationWithShortcut = addKeyboardShortcutToActionTitle(
originalLocalization,
shortcut
)

shakaControlKeysToShortcutLocalizations.set(shakaControlKey, localizationWithShortcut)
})

localization.insert(shakaLocale, shakaControlKeysToShortcutLocalizations)

events.dispatchEvent(new CustomEvent('localeChanged'))
}

Expand Down Expand Up @@ -1959,55 +1998,47 @@ export default defineComponent({

const video_ = video.value

switch (event.key) {
switch (event.key.toLowerCase()) {
case ' ':
case 'Spacebar': // older browsers might return spacebar instead of a space character
case 'K':
case 'k':
case 'spacebar': // older browsers might return spacebar instead of a space character
case KeyboardShortcuts.VIDEO_PLAYER.PLAYBACK.PLAY:
// Toggle Play/Pause
event.preventDefault()
video_.paused ? video_.play() : video_.pause()
break
case 'J':
case 'j':
case KeyboardShortcuts.VIDEO_PLAYER.PLAYBACK.LARGE_REWIND:
// Rewind by 2x the time-skip interval (in seconds)
event.preventDefault()
seekBySeconds(-defaultSkipInterval.value * video_.playbackRate * 2)
break
case 'L':
case 'l':
case KeyboardShortcuts.VIDEO_PLAYER.PLAYBACK.LARGE_FAST_FORWARD:
// Fast-Forward by 2x the time-skip interval (in seconds)
event.preventDefault()
seekBySeconds(defaultSkipInterval.value * video_.playbackRate * 2)
break
case 'O':
case 'o':
case KeyboardShortcuts.VIDEO_PLAYER.PLAYBACK.DECREASE_VIDEO_SPEED:
// Decrease playback rate by user configured interval
event.preventDefault()
changePlayBackRate(-videoPlaybackRateInterval.value)
break
case 'P':
case 'p':
case KeyboardShortcuts.VIDEO_PLAYER.PLAYBACK.INCREASE_VIDEO_SPEED:
// Increase playback rate by user configured interval
event.preventDefault()
changePlayBackRate(videoPlaybackRateInterval.value)
break
case 'F':
case 'f':
case KeyboardShortcuts.VIDEO_PLAYER.GENERAL.FULLSCREEN:
// Toggle full screen
event.preventDefault()
ui.getControls().toggleFullScreen()
break
case 'M':
case 'm':
case KeyboardShortcuts.VIDEO_PLAYER.GENERAL.MUTE:
// Toggle mute only if metakey is not pressed
if (!event.metaKey) {
event.preventDefault()
video_.muted = !video_.muted
}
break
case 'C':
case 'c':
case KeyboardShortcuts.VIDEO_PLAYER.GENERAL.CAPTIONS:
// Toggle caption/subtitles
if (player.getTextTracks().length > 0) {
event.preventDefault()
Expand All @@ -2016,17 +2047,17 @@ export default defineComponent({
player.setTextTrackVisibility(!currentlyVisible)
}
break
case 'ArrowUp':
case KeyboardShortcuts.VIDEO_PLAYER.GENERAL.VOLUME_UP:
// Increase volume
event.preventDefault()
changeVolume(0.05)
break
case 'ArrowDown':
case KeyboardShortcuts.VIDEO_PLAYER.GENERAL.VOLUME_DOWN:
// Decrease Volume
event.preventDefault()
changeVolume(-0.05)
break
case 'ArrowLeft':
case KeyboardShortcuts.VIDEO_PLAYER.PLAYBACK.SMALL_REWIND:
event.preventDefault()
if (canChapterJump(event, 'previous')) {
// Jump to the previous chapter
Expand All @@ -2036,7 +2067,7 @@ export default defineComponent({
seekBySeconds(-defaultSkipInterval.value * video_.playbackRate)
}
break
case 'ArrowRight':
case KeyboardShortcuts.VIDEO_PLAYER.PLAYBACK.SMALL_FAST_FORWARD:
event.preventDefault()
if (canChapterJump(event, 'next')) {
// Jump to the next chapter
Expand All @@ -2046,8 +2077,7 @@ export default defineComponent({
seekBySeconds(defaultSkipInterval.value * video_.playbackRate)
}
break
case 'I':
case 'i':
case KeyboardShortcuts.VIDEO_PLAYER.GENERAL.PICTURE_IN_PICTURE:
// Toggle picture in picture
if (props.format !== 'audio') {
const controls = ui.getControls()
Expand Down Expand Up @@ -2080,29 +2110,28 @@ export default defineComponent({
}
break
}
case ',':
case KeyboardShortcuts.VIDEO_PLAYER.PLAYBACK.LAST_FRAME:
// `⌘+,` is for settings in MacOS
if (!event.metaKey) {
event.preventDefault()
// Return to previous frame
frameByFrame(-1)
}
break
case '.':
case KeyboardShortcuts.VIDEO_PLAYER.PLAYBACK.NEXT_FRAME:
event.preventDefault()
// Advance to next frame
frameByFrame(1)
break
case 'D':
case 'd':
case KeyboardShortcuts.VIDEO_PLAYER.GENERAL.STATS:
// Toggle stats display
event.preventDefault()

events.dispatchEvent(new CustomEvent('setStatsVisibility', {
detail: !showStats.value
}))
break
case 'Escape':
case 'escape':
// Exit full window
if (fullWindowEnabled.value) {
event.preventDefault()
Expand All @@ -2112,16 +2141,14 @@ export default defineComponent({
}))
}
break
case 'S':
case 's':
case KeyboardShortcuts.VIDEO_PLAYER.GENERAL.FULLWINDOW:
// Toggle full window mode
event.preventDefault()
events.dispatchEvent(new CustomEvent('setFullWindow', {
detail: !fullWindowEnabled.value
}))
break
case 'T':
case 't':
case KeyboardShortcuts.VIDEO_PLAYER.GENERAL.THEATRE_MODE:
// Toggle theatre mode
if (props.theatrePossible) {
event.preventDefault()
Expand All @@ -2131,8 +2158,7 @@ export default defineComponent({
}))
}
break
case 'U':
case 'u':
case KeyboardShortcuts.VIDEO_PLAYER.GENERAL.TAKE_SCREENSHOT:
if (process.env.IS_ELECTRON && enableScreenshot.value && props.format !== 'audio') {
event.preventDefault()
// Take screenshot
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import shaka from 'shaka-player'

import i18n from '../../../i18n/index'
import { KeyboardShortcuts } from '../../../../constants'
import { addKeyboardShortcutToActionTitle } from '../../../helpers/utils'

export class FullWindowButton extends shaka.ui.Element {
/**
Expand Down Expand Up @@ -67,12 +69,14 @@ export class FullWindowButton extends shaka.ui.Element {

/** @private */
updateLocalisedStrings_() {
this.nameSpan_.textContent = i18n.t('Video.Player.Full Window')

this.icon_.textContent = this.fullWindowEnabled_ ? 'close_fullscreen' : 'open_in_full'

this.currentState_.textContent = this.localization.resolve(this.fullWindowEnabled_ ? 'ON' : 'OFF')

this.button_.ariaLabel = this.fullWindowEnabled_ ? i18n.t('Video.Player.Exit Full Window') : i18n.t('Video.Player.Full Window')
const baseAriaLabel = this.fullWindowEnabled_ ? i18n.t('Video.Player.Exit Full Window') : i18n.t('Video.Player.Full Window')
const newLabel = addKeyboardShortcutToActionTitle(
baseAriaLabel,
KeyboardShortcuts.VIDEO_PLAYER.GENERAL.FULLWINDOW
)
this.nameSpan_.textContent = this.button_.ariaLabel = newLabel
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import shaka from 'shaka-player'

import i18n from '../../../i18n/index'
import { KeyboardShortcuts } from '../../../../constants'
import { addKeyboardShortcutToActionTitle } from '../../../helpers/utils'

export class ScreenshotButton extends shaka.ui.Element {
/**
Expand Down Expand Up @@ -49,8 +51,10 @@ export class ScreenshotButton extends shaka.ui.Element {

/** @private */
updateLocalisedStrings_() {
this.nameSpan_.textContent = i18n.t('Video.Player.Take Screenshot')

this.button_.ariaLabel = i18n.t('Video.Player.Take Screenshot')
const label = addKeyboardShortcutToActionTitle(
i18n.t('Video.Player.Take Screenshot'),
KeyboardShortcuts.VIDEO_PLAYER.GENERAL.TAKE_SCREENSHOT
)
this.nameSpan_.textContent = this.button_.ariaLabel = label
}
}
Loading

0 comments on commit 8c230bd

Please sign in to comment.