FIX - make phone input and overlays CEF safe

This commit is contained in:
DerEchteAlec
2026-08-12 18:59:33 +02:00
parent ff7e00596b
commit b54839ee74
24 changed files with 730 additions and 112 deletions
+45 -17
View File
@@ -12,6 +12,10 @@ import { useMarketplaceStore } from '@/stores/marketplace'
import { useDarkChatStore } from '@/stores/darkchat'
import { usePhoneStore } from '@/stores/phone'
import type { PhoneAppDefinition } from '@/types/apps'
import {
reorderDirectionFromKeyboard,
type ReorderDirection,
} from '@/utils/keyboard'
const props = withDefaults(
defineProps<{
@@ -33,6 +37,7 @@ const emit = defineEmits<{
dragstart: [event: PointerEvent]
edit: []
remove: []
reorder: [direction: ReorderDirection]
}>()
const phone = usePhoneStore()
@@ -65,6 +70,8 @@ const suppressClick = ref(false)
let holdTimer: number | undefined
let calendarTimer: number | undefined
let pointerStart = { x: 0, y: 0 }
let pointerTarget: HTMLElement | null = null
let pointerId: number | null = null
watch(
() => props.app.iconImage,
@@ -135,6 +142,9 @@ function clearHold(): void {
function onPointerDown(event: PointerEvent): void {
if (props.compact || event.button !== 0) return
pointerTarget = event.currentTarget as HTMLElement
pointerId = event.pointerId
pointerTarget.setPointerCapture(pointerId)
pointerStart = { x: event.clientX, y: event.clientY }
clearHold()
if (props.editMode) {
@@ -173,35 +183,48 @@ function beginPointerDrag(event: PointerEvent): void {
.closest<HTMLElement>('.springboard-page')
?.getBoundingClientRect().width ?? 0
isDragging.value = true
window.addEventListener('pointermove', onPointerMove)
window.addEventListener('pointerup', onPointerUp)
window.addEventListener('pointercancel', cancelPointerDrag)
emit('dragstart', event)
}
function onPointerUp(event: PointerEvent): void {
clearHold()
if (!isDragging.value) return
suppressClick.value = true
emit('dragend', event)
isDragging.value = false
dragOffset.value = { x: 0, y: 0 }
removeDragListeners()
if (isDragging.value) {
suppressClick.value = true
emit('dragend', event)
isDragging.value = false
dragOffset.value = { x: 0, y: 0 }
}
releasePointerCapture()
}
function cancelPointerDrag(): void {
clearHold()
if (!isDragging.value) return
const wasDragging = isDragging.value
isDragging.value = false
dragOffset.value = { x: 0, y: 0 }
removeDragListeners()
emit('dragcancel')
releasePointerCapture()
if (wasDragging) emit('dragcancel')
}
function removeDragListeners(): void {
window.removeEventListener('pointermove', onPointerMove)
window.removeEventListener('pointerup', onPointerUp)
window.removeEventListener('pointercancel', cancelPointerDrag)
function releasePointerCapture(): void {
if (
pointerTarget &&
pointerId !== null &&
pointerTarget.hasPointerCapture(pointerId)
) {
pointerTarget.releasePointerCapture(pointerId)
}
pointerTarget = null
pointerId = null
}
function onKeydown(event: KeyboardEvent): void {
if (!props.editMode) return
const direction = reorderDirectionFromKeyboard(event)
if (!direction) return
event.preventDefault()
event.stopPropagation()
emit('reorder', direction)
}
onMounted(() => {
@@ -214,7 +237,7 @@ onMounted(() => {
onBeforeUnmount(() => {
clearHold()
if (calendarTimer !== undefined) window.clearInterval(calendarTimer)
removeDragListeners()
releasePointerCapture()
})
</script>
@@ -234,10 +257,15 @@ onBeforeUnmount(() => {
type="button"
:aria-label="getPhoneAppLabel(app, phone.t)"
:aria-disabled="!app.route"
:aria-keyshortcuts="
editMode ? 'ArrowLeft ArrowRight ArrowUp ArrowDown' : undefined
"
@click="launch"
@contextmenu.prevent
@keydown="onKeydown"
@pointercancel="cancelPointerDrag"
@pointerdown="onPointerDown"
@lostpointercapture="cancelPointerDrag"
@pointerleave="isDragging || clearHold()"
@pointermove="onPointerMove"
@pointerup="onPointerUp"
+4 -1
View File
@@ -35,7 +35,10 @@ function closeFromOutside(event: PointerEvent): void {
}
function closeFromEscape(event: KeyboardEvent): void {
if (event.key === 'Escape') opened.value = false
if (event.key !== 'Escape' || !opened.value) return
event.preventDefault()
event.stopPropagation()
opened.value = false
}
onMounted(() => {
File diff suppressed because one or more lines are too long
+45 -17
View File
@@ -31,6 +31,10 @@ import { useCallsStore } from '@/stores/calls'
import { useMessagesStore } from '@/stores/messages'
import { usePhoneStore } from '@/stores/phone'
import type { WidgetInstance } from '@/types/widgets'
import {
reorderDirectionFromKeyboard,
type ReorderDirection,
} from '@/utils/keyboard'
import type { WeatherConditionId } from '@/types/weather'
import { WIDGET_SPANS } from '@/utils/widgetLayout'
@@ -50,6 +54,7 @@ const emit = defineEmits<{
dragstart: [event: PointerEvent]
menu: []
remove: []
reorder: [direction: ReorderDirection]
}>()
const phone = usePhoneStore()
@@ -68,6 +73,8 @@ let holdTimer: number | undefined
let pointerStart = { x: 0, y: 0 }
let dragStartPage = 0
let dragPageWidth = 0
let pointerTarget: HTMLElement | null = null
let pointerId: number | null = null
const weatherIcons: Record<WeatherConditionId, Component> = {
sunny: Sun,
@@ -173,6 +180,9 @@ function onPointerDown(event: PointerEvent): void {
) {
return
}
pointerTarget = event.currentTarget as HTMLElement
pointerId = event.pointerId
pointerTarget.setPointerCapture(pointerId)
pointerStart = { x: event.clientX, y: event.clientY }
clearHold()
if (props.editMode) {
@@ -210,35 +220,48 @@ function beginDrag(event: PointerEvent): void {
.closest<HTMLElement>('.springboard-page')
?.getBoundingClientRect().width ?? 0
isDragging.value = true
window.addEventListener('pointermove', onPointerMove)
window.addEventListener('pointerup', onPointerUp)
window.addEventListener('pointercancel', cancelDrag)
emit('dragstart', event)
}
function onPointerUp(event: PointerEvent): void {
clearHold()
if (!isDragging.value) return
suppressClick.value = true
emit('dragend', event)
isDragging.value = false
dragOffset.value = { x: 0, y: 0 }
removeDragListeners()
if (isDragging.value) {
suppressClick.value = true
emit('dragend', event)
isDragging.value = false
dragOffset.value = { x: 0, y: 0 }
}
releasePointerCapture()
}
function cancelDrag(): void {
clearHold()
if (!isDragging.value) return
const wasDragging = isDragging.value
isDragging.value = false
dragOffset.value = { x: 0, y: 0 }
removeDragListeners()
emit('dragcancel')
releasePointerCapture()
if (wasDragging) emit('dragcancel')
}
function removeDragListeners(): void {
window.removeEventListener('pointermove', onPointerMove)
window.removeEventListener('pointerup', onPointerUp)
window.removeEventListener('pointercancel', cancelDrag)
function releasePointerCapture(): void {
if (
pointerTarget &&
pointerId !== null &&
pointerTarget.hasPointerCapture(pointerId)
) {
pointerTarget.releasePointerCapture(pointerId)
}
pointerTarget = null
pointerId = null
}
function onKeydown(event: KeyboardEvent): void {
if (!props.editMode) return
const direction = reorderDirectionFromKeyboard(event)
if (!direction) return
event.preventDefault()
event.stopPropagation()
emit('reorder', direction)
}
function openWidget(): void {
@@ -276,7 +299,7 @@ async function messageContact(phoneNumber: string): Promise<void> {
onBeforeUnmount(() => {
clearHold()
removeDragListeners()
releasePointerCapture()
})
</script>
@@ -300,9 +323,14 @@ onBeforeUnmount(() => {
:class="`home-widget--${instance.kind}`"
role="button"
tabindex="0"
:aria-keyshortcuts="
editMode ? 'ArrowLeft ArrowRight ArrowUp ArrowDown' : undefined
"
@click="openWidget"
@contextmenu.prevent
@keydown="onKeydown"
@keydown.enter="openWidget"
@lostpointercapture="cancelDrag"
@pointercancel="cancelDrag"
@pointerdown="onPointerDown"
@pointerleave="isDragging || clearHold()"
@@ -3,6 +3,7 @@ import { computed } from 'vue'
import SpringboardWidget from '@/components/SpringboardWidget.vue'
import { useWidgetsStore } from '@/stores/widgets'
import type { ReorderDirection } from '@/utils/keyboard'
import { WIDGET_HOME_ROWS, WIDGET_PAGE_ROWS } from '@/utils/widgetLayout'
const props = defineProps<{
@@ -17,6 +18,7 @@ const emit = defineEmits<{
dragstart: [id: string, event: PointerEvent]
menu: [id: string]
remove: [id: string]
reorder: [id: string, direction: ReorderDirection]
}>()
const widgets = useWidgetsStore()
const rows = computed(() =>
@@ -51,6 +53,7 @@ const instances = computed(() =>
@dragstart="emit('dragstart', instance.id, $event)"
@menu="emit('menu', instance.id)"
@remove="emit('remove', instance.id)"
@reorder="emit('reorder', instance.id, $event)"
/>
</div>
</template>
@@ -41,6 +41,9 @@ function select(value: string): void {
function handleKeydown(event: KeyboardEvent): void {
if (event.key === 'Escape') {
if (!isOpen.value) return
event.preventDefault()
event.stopPropagation()
isOpen.value = false
return
}
+10
View File
@@ -5,6 +5,7 @@ import {
createDefaultHomeLayout,
deleteHomePage,
HOME_GRID_PAGE_SIZE,
homeKeyboardTarget,
MAX_HOME_GRID_PAGES,
moveHomeApp,
parseHomeLayout,
@@ -134,6 +135,15 @@ describe('home layout', () => {
expect(moved.grid[4]).toBe('notes')
})
it('provides bounded keyboard reorder targets without wrapping rows', () => {
expect(homeKeyboardTarget(defaults, 'grid', 1, 'right')).toBe(2)
expect(homeKeyboardTarget(defaults, 'grid', 3, 'right')).toBeNull()
expect(homeKeyboardTarget(defaults, 'grid', 0, 'up')).toBeNull()
expect(homeKeyboardTarget(defaults, 'grid', 0, 'down')).toBe(4)
expect(homeKeyboardTarget(defaults, 'dock', 1, 'left')).toBe(0)
expect(homeKeyboardTarget(defaults, 'dock', 1, 'down')).toBeNull()
})
it('shifts occupied grid slots instead of replacing their apps', () => {
const reordered = moveHomeApp(defaults, 'grid', 2, 'grid', 0)
expect(reordered.grid.slice(0, 5)).toEqual([
+30
View File
@@ -1,6 +1,8 @@
import type { LaunchablePhoneAppId } from '@/types/apps'
import type { ReorderDirection } from '@/utils/keyboard'
export const HOME_DOCK_CAPACITY = 4
export const HOME_GRID_COLUMNS = 4
export const HOME_GRID_PAGE_SIZE = 20
export const MAX_HOME_GRID_PAGES = 5
@@ -304,3 +306,31 @@ export function moveHomeApp(
source[sourceIndex] = insertIntoSlot(target, targetIndex, appId)
return next
}
export function homeKeyboardTarget(
layout: HomeLayout,
area: HomeArea,
sourceIndex: number,
direction: ReorderDirection,
): number | null {
const source = layout[area]
if (!source[sourceIndex]) return null
if (area === 'dock') {
if (direction !== 'left' && direction !== 'right') return null
const targetIndex = sourceIndex + (direction === 'left' ? -1 : 1)
return targetIndex >= 0 && targetIndex < source.length ? targetIndex : null
}
const column = sourceIndex % HOME_GRID_COLUMNS
if (direction === 'left' && column === 0) return null
if (direction === 'right' && column === HOME_GRID_COLUMNS - 1) return null
const deltas: Record<ReorderDirection, number> = {
down: HOME_GRID_COLUMNS,
left: -1,
right: 1,
up: -HOME_GRID_COLUMNS,
}
const targetIndex = sourceIndex + deltas[direction]
return targetIndex >= 0 && targetIndex < source.length ? targetIndex : null
}
+98
View File
@@ -0,0 +1,98 @@
import { describe, expect, it, vi } from 'vitest'
import {
consumeEscape,
handleEnterAction,
reorderDirectionFromKeyboard,
} from '@/utils/keyboard'
describe('keyboard interaction', () => {
it('does not submit while an IME composition is active', () => {
const action = vi.fn()
const preventDefault = vi.fn()
expect(
handleEnterAction({ isComposing: true, preventDefault }, action),
).toBe(false)
expect(action).not.toHaveBeenCalled()
expect(preventDefault).not.toHaveBeenCalled()
})
it('prevents the completed Enter key and runs its action once', () => {
const action = vi.fn()
const preventDefault = vi.fn()
expect(
handleEnterAction({ isComposing: false, preventDefault }, action),
).toBe(true)
expect(preventDefault).toHaveBeenCalledOnce()
expect(action).toHaveBeenCalledOnce()
})
it('consumes only an unhandled Escape outside IME composition', () => {
const preventDefault = vi.fn()
const stopImmediatePropagation = vi.fn()
expect(
consumeEscape({
defaultPrevented: false,
isComposing: false,
key: 'Escape',
preventDefault,
stopImmediatePropagation,
}),
).toBe(true)
expect(preventDefault).toHaveBeenCalledOnce()
expect(stopImmediatePropagation).toHaveBeenCalledOnce()
expect(
consumeEscape({
defaultPrevented: false,
isComposing: true,
key: 'Escape',
preventDefault,
stopImmediatePropagation,
}),
).toBe(false)
})
it('blocks a second Escape owner on the same event target', () => {
const target = new EventTarget()
const rootHandler = vi.fn()
target.addEventListener('keydown', (event) => {
consumeEscape(event as KeyboardEvent)
})
target.addEventListener('keydown', rootHandler)
const event = new Event('keydown', { cancelable: true })
Object.defineProperties(event, {
isComposing: { value: false },
key: { value: 'Escape' },
})
target.dispatchEvent(event)
expect(event.defaultPrevented).toBe(true)
expect(rootHandler).not.toHaveBeenCalled()
})
it('maps only unmodified arrow keys to reorder directions', () => {
expect(
reorderDirectionFromKeyboard({
altKey: false,
ctrlKey: false,
isComposing: false,
key: 'ArrowLeft',
metaKey: false,
}),
).toBe('left')
expect(
reorderDirectionFromKeyboard({
altKey: false,
ctrlKey: true,
isComposing: false,
key: 'ArrowLeft',
metaKey: false,
}),
).toBeNull()
})
})
+47
View File
@@ -0,0 +1,47 @@
export type ReorderDirection = 'down' | 'left' | 'right' | 'up'
export function consumeEscape(
event: Pick<
KeyboardEvent,
| 'defaultPrevented'
| 'isComposing'
| 'key'
| 'preventDefault'
| 'stopImmediatePropagation'
>,
): boolean {
if (event.key !== 'Escape' || event.isComposing || event.defaultPrevented) {
return false
}
event.preventDefault()
event.stopImmediatePropagation()
return true
}
export function handleEnterAction(
event: Pick<KeyboardEvent, 'isComposing' | 'preventDefault'>,
action: () => unknown,
): boolean {
if (event.isComposing) return false
event.preventDefault()
void action()
return true
}
export function reorderDirectionFromKeyboard(
event: Pick<
KeyboardEvent,
'altKey' | 'ctrlKey' | 'isComposing' | 'key' | 'metaKey'
>,
): ReorderDirection | null {
if (event.isComposing || event.altKey || event.ctrlKey || event.metaKey) {
return null
}
const directions: Partial<Record<string, ReorderDirection>> = {
ArrowDown: 'down',
ArrowLeft: 'left',
ArrowRight: 'right',
ArrowUp: 'up',
}
return directions[event.key] ?? null
}
+38
View File
@@ -0,0 +1,38 @@
import { describe, expect, it } from 'vitest'
import { musicEscapeLayer } from '@/utils/musicEscape'
const closedState = {
actionMenuOpened: false,
activeSheet: false,
addMenuOpened: false,
confirmDeletePlaylist: false,
confirmRemoveTrack: false,
playerOpened: false,
}
describe('music Escape ownership', () => {
it('owns Escape while either music popover is open', () => {
expect(
musicEscapeLayer({ ...closedState, addMenuOpened: true }),
).toBe('menu')
expect(
musicEscapeLayer({ ...closedState, actionMenuOpened: true }),
).toBe('menu')
})
it('keeps a real form sheet above menus and the player', () => {
expect(
musicEscapeLayer({
...closedState,
activeSheet: true,
addMenuOpened: true,
playerOpened: true,
}),
).toBe('sheet')
})
it('does not claim Escape with no music overlay open', () => {
expect(musicEscapeLayer(closedState)).toBeNull()
})
})
+22
View File
@@ -0,0 +1,22 @@
export type MusicEscapeLayer =
| 'delete-playlist-confirmation'
| 'menu'
| 'player'
| 'remove-track-confirmation'
| 'sheet'
export function musicEscapeLayer(state: {
actionMenuOpened: boolean
activeSheet: boolean
addMenuOpened: boolean
confirmDeletePlaylist: boolean
confirmRemoveTrack: boolean
playerOpened: boolean
}): MusicEscapeLayer | null {
if (state.confirmRemoveTrack) return 'remove-track-confirmation'
if (state.confirmDeletePlaylist) return 'delete-playlist-confirmation'
if (state.activeSheet) return 'sheet'
if (state.addMenuOpened || state.actionMenuOpened) return 'menu'
if (state.playerOpened) return 'player'
return null
}
+16
View File
@@ -9,6 +9,7 @@ import {
removeWidget,
resizeWidget,
widgetOccupiedCells,
widgetKeyboardTarget,
} from '@/utils/widgetLayout'
describe('widget layout', () => {
@@ -47,6 +48,21 @@ describe('widget layout', () => {
expect(next.instances).toHaveLength(layout.instances.length)
})
it('provides bounded keyboard targets for each widget size', () => {
const layout = createDefaultWidgetLayout()
const clock = layout.instances.find(
(instance) => instance.id === 'home-clock',
)!
const music = layout.instances.find(
(instance) => instance.id === 'home-music',
)!
expect(widgetKeyboardTarget(clock, 'right')).toEqual({ column: 1, row: 0 })
expect(widgetKeyboardTarget(clock, 'up')).toBeNull()
expect(widgetKeyboardTarget(music, 'right')).toBeNull()
expect(widgetKeyboardTarget(music, 'down')).toEqual({ column: 0, row: 3 })
})
it('allows a small widget in the center with app cells on both sides', () => {
const layout = createDefaultWidgetLayout()
const moved = moveWidget(layout, 'home-clock', 2, 1, 1)
+27
View File
@@ -6,6 +6,7 @@ import type {
WidgetSettings,
WidgetSize,
} from '@/types/widgets'
import type { ReorderDirection } from '@/utils/keyboard'
export const WIDGET_GRID_COLUMNS = 4
export const WIDGET_HOME_ROWS = 5
@@ -296,6 +297,32 @@ export function moveWidget(
return { instances: placed, version: 1 }
}
export function widgetKeyboardTarget(
instance: WidgetInstance,
direction: ReorderDirection,
): { column: number; row: number } | null {
const span = WIDGET_SPANS[instance.size]
const maximumColumn = WIDGET_GRID_COLUMNS - span.columns
const maximumRow = rowsForPage(instance.page) - span.rows
const target = {
column:
instance.column +
(direction === 'left' ? -1 : direction === 'right' ? 1 : 0),
row:
instance.row +
(direction === 'up' ? -1 : direction === 'down' ? 1 : 0),
}
if (
target.column < 0 ||
target.column > maximumColumn ||
target.row < 0 ||
target.row > maximumRow
) {
return null
}
return target
}
export function resizeWidget(
layout: WidgetLayout,
id: string,
+30
View File
@@ -17,13 +17,16 @@ import type { WidgetKind, WidgetSettings, WidgetSize } from '@/types/widgets'
import {
deleteHomePage as previewHomePageDelete,
HOME_GRID_PAGE_SIZE,
homeKeyboardTarget,
MAX_HOME_GRID_PAGES,
type HomeArea,
} from '@/utils/homeLayout'
import type { ReorderDirection } from '@/utils/keyboard'
import {
deleteWidgetPage as previewWidgetPageDelete,
moveWidget as previewWidgetMove,
WIDGET_GRID_COLUMNS,
widgetKeyboardTarget,
widgetOccupiedCells,
} from '@/utils/widgetLayout'
@@ -515,6 +518,14 @@ function stopWidgetDrag(): void {
clearWidgetDragPreview()
}
function reorderWidget(id: string, direction: ReorderDirection): void {
const instance = widgets.layout.instances.find((widget) => widget.id === id)
if (!instance) return
const target = widgetKeyboardTarget(instance, direction)
if (!target) return
widgets.move(id, instance.page, target.column, target.row)
}
function removeWidget(id: string): void {
widgets.remove(id)
if (widgetActionId.value === id) widgetActionId.value = null
@@ -620,6 +631,21 @@ function stopHomeDrag(): void {
draggingHomeApp.value = null
}
function reorderHomeApp(
area: HomeArea,
sourceIndex: number,
direction: ReorderDirection,
): void {
const targetIndex = homeKeyboardTarget(
appStore.homeLayout,
area,
sourceIndex,
direction,
)
if (targetIndex === null) return
appStore.moveHomeApp(area, sourceIndex, area, targetIndex)
}
async function addHomePage(): Promise<void> {
if (addingHomePage.value) return
addingHomePage.value = true
@@ -704,6 +730,7 @@ watch(isEditablePage, (visible) => {
@dragstart="startWidgetDrag"
@menu="openWidgetMenu"
@remove="removeWidget"
@reorder="reorderWidget"
/>
</div>
</section>
@@ -725,6 +752,7 @@ watch(isEditablePage, (visible) => {
@dragstart="startWidgetDrag"
@menu="openWidgetMenu"
@remove="removeWidget"
@reorder="reorderWidget"
/>
<TransitionGroup
:name="
@@ -750,6 +778,7 @@ watch(isEditablePage, (visible) => {
@dragstart="startHomeDrag('grid', cell.sourceIndex)"
@edit="enterEditMode"
@remove="removeHomeApp(cell.app.id)"
@reorder="reorderHomeApp('grid', cell.sourceIndex, $event)"
/>
<div
v-else
@@ -946,6 +975,7 @@ watch(isEditablePage, (visible) => {
@dragstart="startHomeDrag('dock', appIndex)"
@edit="enterEditMode"
@remove="removeHomeApp(app.id)"
@reorder="reorderHomeApp('dock', appIndex, $event)"
/>
<div
v-else
+85 -28
View File
@@ -40,6 +40,7 @@ import type {
BankingTransactionKind,
} from '@/types/banking'
import type { PhoneContact } from '@/types/phone'
import { handleEnterAction } from '@/utils/keyboard'
import { formatPhoneNumber, normalizePhoneNumber } from '@/utils/phone'
type BankingTab = 'home' | 'activity'
@@ -153,7 +154,9 @@ function closeAction(): void {
function updateTarget(event: Event): void {
if (!(event.target instanceof HTMLInputElement)) {
console.error('[banking] Phone number input emitted without an input target.')
console.error(
'[banking] Phone number input emitted without an input target.',
)
return
}
target.value = event.target.value
@@ -163,7 +166,9 @@ function updateTarget(event: Event): void {
function selectContact(contact: PhoneContact): void {
target.value = contact.phone_number
formError.value = ''
void nextTick(() => document.getElementById('banking-transfer-amount')?.focus())
void nextTick(() =>
document.getElementById('banking-transfer-amount')?.focus(),
)
}
function updateAmount(event: Event): void {
@@ -237,6 +242,7 @@ function focusableSheetElements(): HTMLElement[] {
function handleSheetKeydown(event: KeyboardEvent): void {
if (event.key === 'Escape') {
event.preventDefault()
event.stopPropagation()
closeAction()
return
}
@@ -258,6 +264,15 @@ function handleSheetKeydown(event: KeyboardEvent): void {
}
}
function handleWindowKeydown(event: KeyboardEvent): void {
if (event.key !== 'Escape' || !action.value || event.defaultPrevented) {
return
}
event.preventDefault()
event.stopImmediatePropagation()
closeAction()
}
function errorMessage(code: string): string {
return phone.t(`Apps.banking.errors.${code}`) ===
`Apps.banking.errors.${code}`
@@ -268,9 +283,8 @@ function errorMessage(code: string): string {
async function submitAction(): Promise<void> {
if (!action.value) return
const parsedAmount = Number(amount.value)
const phoneNumber = action.value === 'transfer'
? normalizePhoneNumber(target.value)
: undefined
const phoneNumber =
action.value === 'transfer' ? normalizePhoneNumber(target.value) : undefined
if (
!Number.isSafeInteger(parsedAmount) ||
parsedAmount <= 0 ||
@@ -291,13 +305,17 @@ async function submitAction(): Promise<void> {
action.value = null
}
onMounted(() => void banking.load())
onMounted(() => {
window.addEventListener('keydown', handleWindowKeydown)
void banking.load()
})
watch(action, async (currentAction) => {
if (currentAction) {
previousFocus = document.activeElement instanceof HTMLElement
? document.activeElement
: null
previousFocus =
document.activeElement instanceof HTMLElement
? document.activeElement
: null
await nextTick()
document.getElementById('banking-transfer-target')?.focus()
return
@@ -307,6 +325,7 @@ watch(action, async (currentAction) => {
})
onBeforeUnmount(() => {
window.removeEventListener('keydown', handleWindowKeydown)
if (wheelRefreshTimeout) clearTimeout(wheelRefreshTimeout)
previousFocus?.focus()
})
@@ -379,12 +398,17 @@ onBeforeUnmount(() => {
</div>
<strong>{{ formatMoney(banking.overview.bank) }}</strong>
<div class="banking-balance__trend">
<span>{{ formatMoney(totals.incoming - totals.outgoing, true) }}</span>
<span>{{
formatMoney(totals.incoming - totals.outgoing, true)
}}</span>
{{ phone.t('Apps.banking.recentPeriod') }}
</div>
</k-glass>
<section class="banking-actions" :aria-label="phone.t('Apps.banking.actions')">
<section
class="banking-actions"
:aria-label="phone.t('Apps.banking.actions')"
>
<k-glass
component="button"
type="button"
@@ -435,17 +459,27 @@ onBeforeUnmount(() => {
>
<k-list inset strong class="banking-transaction-list">
<k-list-item
v-for="transaction in banking.overview.transactions.slice(0, 5)"
:key="transaction.id"
v-for="transaction in banking.overview.transactions.slice(0, 5)"
:key="transaction.id"
:subtitle="formatDate(transaction.createdAt)"
:title="transactionTitle(transaction)"
>
<template #media>
<component :is="transactionIcons[transaction.kind]" :size="17" />
<component
:is="transactionIcons[transaction.kind]"
:size="17"
/>
</template>
<template #after>
<b :class="{ 'is-incoming': isIncoming(transaction.kind) }">
{{ formatMoney(isIncoming(transaction.kind) ? transaction.amount : -transaction.amount, true) }}
{{
formatMoney(
isIncoming(transaction.kind)
? transaction.amount
: -transaction.amount,
true,
)
}}
</b>
</template>
</k-list-item>
@@ -460,13 +494,18 @@ onBeforeUnmount(() => {
<template v-else>
<section class="banking-activity-hero">
<span>{{ phone.t('Apps.banking.activity') }}</span>
<strong>{{ formatMoney(totals.incoming - totals.outgoing, true) }}</strong>
<strong>{{
formatMoney(totals.incoming - totals.outgoing, true)
}}</strong>
<small>{{ phone.t('Apps.banking.recentPeriod') }}</small>
</section>
<k-card :content-wrap="false" class="banking-card banking-chart-card">
<div class="banking-chart-legend">
<span><i class="is-incoming"></i>{{ phone.t('Apps.banking.incoming') }}</span>
<span
><i class="is-incoming"></i
>{{ phone.t('Apps.banking.incoming') }}</span
>
<span><i></i>{{ phone.t('Apps.banking.outgoing') }}</span>
</div>
<div class="banking-chart">
@@ -475,14 +514,19 @@ onBeforeUnmount(() => {
:key="day.date.getTime()"
class="banking-chart__day"
role="img"
:aria-label="phone.t('Apps.banking.chartDaySummary', {
day: day.label,
incoming: formatMoney(day.incoming),
outgoing: formatMoney(day.outgoing),
})"
:aria-label="
phone.t('Apps.banking.chartDaySummary', {
day: day.label,
incoming: formatMoney(day.incoming),
outgoing: formatMoney(day.outgoing),
})
"
>
<div>
<i class="is-incoming" :style="{ height: `${day.incomingHeight}%` }"></i>
<i
class="is-incoming"
:style="{ height: `${day.incomingHeight}%` }"
></i>
<i :style="{ height: `${day.outgoingHeight}%` }"></i>
</div>
<span>{{ day.label }}</span>
@@ -494,7 +538,10 @@ onBeforeUnmount(() => {
<div class="banking-section-title">
<h2>{{ phone.t('Apps.banking.allTransactions') }}</h2>
</div>
<k-card :content-wrap="false" class="banking-card banking-transaction-card">
<k-card
:content-wrap="false"
class="banking-card banking-transaction-card"
>
<k-list inset strong class="banking-transaction-list">
<k-list-item
v-for="transaction in banking.overview.transactions"
@@ -503,11 +550,21 @@ onBeforeUnmount(() => {
:title="transactionTitle(transaction)"
>
<template #media>
<component :is="transactionIcons[transaction.kind]" :size="17" />
<component
:is="transactionIcons[transaction.kind]"
:size="17"
/>
</template>
<template #after>
<b :class="{ 'is-incoming': isIncoming(transaction.kind) }">
{{ formatMoney(isIncoming(transaction.kind) ? transaction.amount : -transaction.amount, true) }}
{{
formatMoney(
isIncoming(transaction.kind)
? transaction.amount
: -transaction.amount,
true,
)
}}
</b>
</template>
</k-list-item>
@@ -553,7 +610,7 @@ onBeforeUnmount(() => {
</k-toolbar-pane>
</k-tabbar>
<k-sheet :opened="Boolean(action)" class="banking-sheet" @backdropclick="closeAction">
<k-sheet :opened="Boolean(action)" @backdropclick="closeAction">
<section
v-if="action"
class="banking-sheet__content"
@@ -600,7 +657,7 @@ onBeforeUnmount(() => {
type="number"
:value="amount"
@input="updateAmount"
@keydown.enter="submitAction"
@keydown.enter="handleEnterAction($event, submitAction)"
/>
</k-list>
<div class="banking-contact-picker">
+2 -1
View File
@@ -94,6 +94,7 @@ import type {
CompanySummary,
} from '@/types/companies'
import type { PhoneMedia } from '@/types/media'
import { handleEnterAction } from '@/utils/keyboard'
import { nuiCall } from '@/utils/nui'
type CompaniesTab = 'directory' | 'requests' | 'work'
@@ -1764,7 +1765,7 @@ onBeforeUnmount(() => {
:placeholder="phone.t('Apps.companies.requests.replyPlaceholder')"
:value="threadDraft"
@input="threadDraft = messagebarValue($event)"
@keydown.enter.exact.prevent="sendThreadMessage"
@keydown.enter.exact="handleEnterAction($event, sendThreadMessage)"
>
<template #right>
<k-toolbar-pane class="ios:h-10">
+31 -6
View File
@@ -82,6 +82,7 @@ import type {
} from '@/types/crewlink'
import { copyText } from '@/utils/clipboard'
import { easyShareCrewLinkInviteCode } from '@/utils/easyshare'
import { consumeEscape, handleEnterAction } from '@/utils/keyboard'
import { nuiCall } from '@/utils/nui'
import { isTrustedRootMessageSource } from '@/utils/windowMessages'
@@ -699,7 +700,30 @@ function onCrewLinkMessage(event: MessageEvent): void {
if (event.data?.type === 'crewlink:changed') void crew.bootstrap()
}
function onKeydown(event: KeyboardEvent): void {
if (
!confirmAction.value &&
!sheet.value &&
!selectedMember.value &&
!selectedPing.value
) {
return
}
if (!consumeEscape(event)) return
if (confirmAction.value) {
cancelConfirmation()
} else if (sheet.value) {
closeSheet()
} else if (selectedMember.value) {
selectedMember.value = null
} else {
selectedPing.value = null
}
}
onMounted(async () => {
window.addEventListener('keydown', onKeydown, true)
await crew.bootstrap()
username.value = crew.profile?.username ?? ''
openSharedInvite()
@@ -713,6 +737,7 @@ onBeforeUnmount(() => {
if (liveTimer) window.clearInterval(liveTimer)
if (toastTimer) window.clearTimeout(toastTimer)
if (pointerFrame) cancelAnimationFrame(pointerFrame)
window.removeEventListener('keydown', onKeydown, true)
window.removeEventListener('message', onCrewLinkMessage)
})
</script>
@@ -756,7 +781,7 @@ onBeforeUnmount(() => {
maxlength="20"
outline
@input="updateValue('username', $event)"
@keydown.enter="createProfile"
@keydown.enter="handleEnterAction($event, createProfile)"
/>
</k-list>
<p v-if="formError" class="crewlink-error" role="alert">{{ formError }}</p>
@@ -1036,7 +1061,7 @@ onBeforeUnmount(() => {
</k-tabbar>
</template>
<k-sheet :opened="Boolean(sheet)" class="crewlink-sheet" @backdropclick="closeSheet">
<k-sheet :opened="Boolean(sheet)" @backdropclick="closeSheet">
<section v-if="sheet" class="crewlink-sheet__content" role="dialog" aria-modal="true">
<k-link component="button" class="crewlink-sheet__close" :link-props="{ type: 'button' }" :aria-label="phone.t('Common.close')" @click="closeSheet"><X /></k-link>
@@ -1051,7 +1076,7 @@ onBeforeUnmount(() => {
<template v-else-if="sheet === 'join-group'">
<span class="crewlink-sheet__icon"><UserPlus /></span><h2>{{ t('joinWithCode') }}</h2><p>{{ t('joinWithCodeBody') }}</p>
<k-list inset strong class="crewlink-form-list"><k-list-input input-id="crewlink-invite-code" :label="t('inviteCode')" :placeholder="t('inviteCodePlaceholder')" :value="inviteCode" maxlength="8" outline @input="updateValue('inviteCode', $event)" @keydown.enter="joinGroup" /></k-list>
<k-list inset strong class="crewlink-form-list"><k-list-input input-id="crewlink-invite-code" :label="t('inviteCode')" :placeholder="t('inviteCodePlaceholder')" :value="inviteCode" maxlength="8" outline @input="updateValue('inviteCode', $event)" @keydown.enter="handleEnterAction($event, joinGroup)" /></k-list>
<p v-if="formError" class="crewlink-error">{{ formError }}</p>
<k-button large rounded :disabled="crew.isLoading" @click="joinGroup">{{ t('joinCrew') }}</k-button>
<template v-if="crew.invitations.length">
@@ -1143,13 +1168,13 @@ onBeforeUnmount(() => {
<template v-else-if="sheet === 'member' && !selectedMember && crew.profile">
<span class="crewlink-sheet__icon"><UserRound /></span><h2>{{ t('editUsername') }}</h2><p>{{ t('editUsernameBody') }}</p>
<k-list inset strong class="crewlink-form-list"><k-list-input input-id="crewlink-edit-username" :label="t('username')" :value="username" maxlength="20" outline @input="updateValue('username', $event)" @keydown.enter="saveProfile" /></k-list>
<k-list inset strong class="crewlink-form-list"><k-list-input input-id="crewlink-edit-username" :label="t('username')" :value="username" maxlength="20" outline @input="updateValue('username', $event)" @keydown.enter="handleEnterAction($event, saveProfile)" /></k-list>
<p v-if="formError" class="crewlink-error">{{ formError }}</p><k-button large rounded @click="saveProfile">{{ phone.t('Common.save') }}</k-button>
</template>
</section>
</k-sheet>
<k-sheet :opened="Boolean(selectedMember && !sheet)" class="crewlink-sheet" @backdropclick="selectedMember = null">
<k-sheet :opened="Boolean(selectedMember && !sheet)" @backdropclick="selectedMember = null">
<section v-if="selectedMember" class="crewlink-sheet__content crewlink-member-preview">
<k-link component="button" class="crewlink-sheet__close" :link-props="{ type: 'button' }" @click="selectedMember = null"><X /></k-link>
<span class="crewlink-sheet__avatar" :style="{ '--crew': activeColour }">{{ memberInitials(selectedMember) }}</span><h2>@{{ selectedMember.username }}</h2><p>{{ roleLabel(selectedMember.role) }} · {{ memberStatus(selectedMember) }}</p>
@@ -1157,7 +1182,7 @@ onBeforeUnmount(() => {
</section>
</k-sheet>
<k-sheet :opened="Boolean(selectedPing)" class="crewlink-sheet" @backdropclick="selectedPing = null">
<k-sheet :opened="Boolean(selectedPing)" @backdropclick="selectedPing = null">
<section v-if="selectedPing" class="crewlink-sheet__content crewlink-member-preview">
<k-link component="button" class="crewlink-sheet__close" :link-props="{ type: 'button' }" @click="selectedPing = null"><X /></k-link>
<span class="crewlink-sheet__icon" :style="{ background: pingColours[selectedPing.type] }"><component :is="pingIcons[selectedPing.type]" /></span><small>{{ t(`pingTypes.${selectedPing.type}`) }}</small><h2>{{ selectedPing.label }}</h2><p>{{ t('sharedBy', { username: selectedPing.creatorUsername }) }} · {{ expiresIn(selectedPing.expiresAt) }}</p>
+3 -2
View File
@@ -68,6 +68,7 @@ import type { EasySharePayload } from '@/types/easyshare'
import type { MediaType, PhoneMedia } from '@/types/media'
import { copyText } from '@/utils/clipboard'
import { parseDatabaseDate, type DatabaseDateValue } from '@/utils/date'
import { handleEnterAction } from '@/utils/keyboard'
const VOICE_MAX_DURATION_MS = 60_000
const VOICE_MAX_BYTES = 270_000
@@ -2332,7 +2333,7 @@ onBeforeUnmount(() => {
clear-button
@input="setIdentifier"
@clear="identifier = ''"
@keydown.enter.prevent="requestStart()"
@keydown.enter="handleEnterAction($event, requestStart)"
/></k-list>
<k-button
large
@@ -2601,7 +2602,7 @@ onBeforeUnmount(() => {
:placeholder="t('message')"
:colors="darkMessagebarColors"
@input="setDraft"
@keydown.enter.exact.prevent="sendText"
@keydown.enter.exact="handleEnterAction($event, sendText)"
>
<template #left
><k-link
+96 -5
View File
@@ -279,9 +279,17 @@ function moveMediaPreview(direction: number): void {
function handleMediaPreviewKeydown(event: KeyboardEvent): void {
if (!mediaPreview.value) return
if (event.key === 'Escape') closeMediaPreview()
if (event.key === 'ArrowLeft') moveMediaPreview(-1)
if (event.key === 'ArrowRight') moveMediaPreview(1)
if (event.key === 'Escape') {
event.preventDefault()
event.stopImmediatePropagation()
closeMediaPreview()
} else if (event.key === 'ArrowLeft') {
event.preventDefault()
moveMediaPreview(-1)
} else if (event.key === 'ArrowRight') {
event.preventDefault()
moveMediaPreview(1)
}
}
function toast(path: string): void {
@@ -731,11 +739,11 @@ watch(
onBeforeUnmount(() => {
if (exploreSearchTimer !== undefined) window.clearTimeout(exploreSearchTimer)
if (networkSearchTimer !== undefined) window.clearTimeout(networkSearchTimer)
window.removeEventListener('keydown', handleMediaPreviewKeydown)
window.removeEventListener('keydown', handleMediaPreviewKeydown, true)
})
onMounted(async () => {
window.addEventListener('keydown', handleMediaPreviewKeydown)
window.addEventListener('keydown', handleMediaPreviewKeydown, true)
const selection =
messageMedia.consumeMany<ComposerContext>('feather:composer')
const profileSelection =
@@ -4929,4 +4937,87 @@ onMounted(async () => {
color: inherit;
background: var(--feather-panel);
}
@supports not (color: color-mix(in srgb, white, black)) {
.feather-navbar {
--k-navbar-bg-color: rgb(255 255 255 / 91%);
border-bottom-color: rgb(127 127 127 / 18%);
}
:global(.dark) .feather-navbar {
--k-navbar-bg-color: rgb(9 13 18 / 91%);
}
.dark.feather-app .feather-navbar {
--k-navbar-bg-color: rgb(0 0 0 / 90%);
background: rgb(0 0 0 / 88%);
}
.feather-app--active .feather-navbar {
--k-navbar-bg-color: rgb(18 23 27 / 91%);
background: rgb(18 23 27 / 88%);
}
.feather-app--active.feather-app--light .feather-navbar {
--k-navbar-bg-color: rgb(251 251 246 / 91%);
background: rgb(251 251 246 / 88%);
}
.feather-feed-tabs,
.feather-explore-head,
.feather-activity-head,
.feather-trend,
.feather-composer__tools,
.feather-section-title,
.feather-profile,
.feather-profile-tabs,
.feather-activity,
.feather-person {
border-color: rgb(127 127 127 / 18%);
}
.feather-trend:active,
.feather-profile__stats button,
.feather-report select,
.feather-report textarea,
.feather-app--active .feather-explore-search :deep(form) {
background: rgb(127 127 127 / 7%);
}
.feather-profile__stats button:active,
.feather-thread-reply__comment-target,
.feather-thread-title span,
.feather-app--active .feather-profile__stats span,
.feather-composer-media > header > span,
.feather-composer-media > header > b,
.feather-app--active .feather-follow-button--pending:hover,
.feather-app--active :deep(.feather-follow:hover),
.feather-composer-media__actions button:not(:disabled):hover {
background: rgb(29 155 240 / 13%);
}
.feather-report select,
.feather-report textarea,
.feather-app--active .feather-follow-button--following {
border-color: rgb(127 127 127 / 24%);
}
.feather-app--active :deep(.feather-post-glass),
.feather-network-list,
.feather-app--active .feather-thread-reply,
.feather-profile-suggestion,
.feather-edit__identity,
.feather-edit__photo,
.feather-edit__fields,
.feather-connections__tabs,
.feather-connections__list {
background: var(--feather-panel);
}
.feather-network-person__bio {
color: var(--feather-muted);
}
.feather-network-person__avatar .feather-avatar,
.feather-app--active .feather-follow-button--pending,
.feather-app--active .feather-profile__actions :deep(.k-button),
.feather-edit__photo-actions :deep(.k-button) {
border-color: var(--feather-blue);
}
.feather-compose-fab,
.feather-edit__avatar {
border-color: #70c5fa;
}
.feather-connections__remove {
border-color: #f04f65;
}
}
</style>
+18 -2
View File
@@ -85,6 +85,7 @@ import type { PhoneMedia } from '@/types/media'
import type { EasySharePayload } from '@/types/easyshare'
import type { GifSearchResult, SmsAttachmentType } from '@/types/messages'
import { parseDatabaseDate, type DatabaseDateValue } from '@/utils/date'
import { handleEnterAction } from '@/utils/keyboard'
type FlareTab = 'discover' | 'explore' | 'likes' | 'matches' | 'profile'
type ExploreMode = 'all' | 'dates' | 'friends' | 'longTerm'
@@ -1145,7 +1146,7 @@ onBeforeUnmount(() => {
:value="draft"
:disabled="flare.sending"
@input="draft = eventValue($event)"
@keydown.enter.exact.prevent="sendMessage"
@keydown.enter.exact="handleEnterAction($event, sendMessage)"
>
<template #left>
<k-toolbar-pane class="ios:h-10 messages-messagebar__tools">
@@ -1861,7 +1862,7 @@ onBeforeUnmount(() => {
:aria-modal="choiceOpened ? 'true' : undefined"
aria-labelledby="flare-choice-sheet-title"
:inert="!choiceOpened"
@keydown.esc="closeChoice"
@keydown.esc.stop.prevent="closeChoice"
>
<span class="flare-choice-sheet__grabber" aria-hidden="true" />
<header class="flare-choice-sheet__header">
@@ -3097,4 +3098,19 @@ onBeforeUnmount(() => {
:global(.phone-app.dark .flare-action) {
box-shadow: none;
}
@supports not (color: color-mix(in srgb, white, black)) {
.flare-navbar {
border-bottom-color: rgb(127 127 127 / 18%);
}
.flare-photo-slot {
background: rgb(127 127 127 / 18%);
}
.flare-photo-grid :deep(.flare-photo-add) {
border-color: rgb(127 127 127 / 44%);
background: var(--flare-surface);
}
.flare-choice-sheet__grabber {
background: rgb(127 127 127 / 38%);
}
}
</style>
+2 -1
View File
@@ -36,6 +36,7 @@ import { useMapStore } from '@/stores/map'
import { useEasyShareStore } from '@/stores/easyshare'
import { usePhoneStore } from '@/stores/phone'
import type { MapMarker, MapMarkerColor } from '@/types/map'
import { handleEnterAction } from '@/utils/keyboard'
import { nuiCall, type NuiResponse } from '@/utils/nui'
type MapStyle = 'default' | 'satellite' | 'atlas' | 'roads'
@@ -656,7 +657,7 @@ onBeforeUnmount(() => {
maxlength="40"
outline
@input="updateMarkerLabel"
@keydown.enter="saveMarker"
@keydown.enter="handleEnterAction($event, saveMarker)"
/>
</k-list>
<span class="map-marker-sheet__label">{{
+2 -1
View File
@@ -54,6 +54,7 @@ import { useMessagesStore } from '@/stores/messages'
import { useMessageMediaStore } from '@/stores/messageMedia'
import { usePhoneStore } from '@/stores/phone'
import { parseDatabaseDate, type DatabaseDateValue } from '@/utils/date'
import { handleEnterAction } from '@/utils/keyboard'
import { sortContactsByMessageRecency } from '@/utils/messages'
import type {
GifSearchResult,
@@ -1496,7 +1497,7 @@ onBeforeUnmount(() => {
:value="draft"
:disabled="sending"
@input="draft = eventValue($event)"
@keydown.enter.exact.prevent="sendTextMessage"
@keydown.enter.exact="handleEnterAction($event, sendTextMessage)"
>
<template #left>
<k-toolbar-pane class="ios:h-10 messages-messagebar__tools">
+53 -21
View File
@@ -53,6 +53,8 @@ import { useEasyShareStore } from '@/stores/easyshare'
import { usePhoneStore } from '@/stores/phone'
import type { MusicPlaylist, MusicTrack } from '@/types/music'
import { easyShareMusicTarget } from '@/utils/easyshare'
import { consumeEscape, handleEnterAction } from '@/utils/keyboard'
import { musicEscapeLayer } from '@/utils/musicEscape'
type MusicTab = 'library' | 'playlists' | 'search'
type MusicSheet =
@@ -498,6 +500,31 @@ function updateVolume(event: Event): void {
music.setVolume(Number(eventValue(event)) / 100)
}
function onKeydown(event: KeyboardEvent): void {
const layer = musicEscapeLayer({
actionMenuOpened: actionMenuOpened.value,
activeSheet: Boolean(activeSheet.value),
addMenuOpened: addMenuOpened.value,
confirmDeletePlaylist: confirmDeletePlaylist.value,
confirmRemoveTrack: confirmRemoveTrack.value,
playerOpened: playerOpened.value,
})
if (!layer) return
if (!consumeEscape(event)) return
if (layer === 'remove-track-confirmation') {
cancelRemoveTrack()
} else if (layer === 'delete-playlist-confirmation') {
confirmDeletePlaylist.value = false
} else if (layer === 'sheet') {
closeSheet()
} else if (layer === 'menu') {
dismissMenus()
} else if (layer === 'player') {
playerOpened.value = false
}
}
watch(
() => music.playlists,
() => {
@@ -510,6 +537,7 @@ watch(
)
onMounted(async () => {
window.addEventListener('keydown', onKeydown, true)
await music.load()
const target = easyShareMusicTarget(
route.query.easyShareKind,
@@ -531,6 +559,7 @@ onMounted(async () => {
onBeforeUnmount(() => {
if (playerPickerTimer !== null) window.clearTimeout(playerPickerTimer)
window.removeEventListener('keydown', onKeydown, true)
})
</script>
@@ -1061,12 +1090,12 @@ onBeforeUnmount(() => {
</k-list>
</section>
<k-sheet
:opened="Boolean(activeSheet)"
class="music-form-sheet"
@backdropclick="closeSheet"
>
<section class="music-sheet-content">
<div class="music-form-sheet">
<k-sheet
:opened="Boolean(activeSheet)"
@backdropclick="closeSheet"
>
<section class="music-sheet-content">
<header>
<k-link component="button" @click="closeSheet">{{
phone.t(
@@ -1089,7 +1118,7 @@ onBeforeUnmount(() => {
type="url"
:value="youtubeUrl"
@input="youtubeUrl = eventValue($event)"
@keydown.enter="submitYouTube"
@keydown.enter="handleEnterAction($event, submitYouTube)"
/>
<k-list-input
:label="phone.t('Apps.music.youtubeTitle')"
@@ -1098,7 +1127,7 @@ onBeforeUnmount(() => {
:placeholder="phone.t('Apps.music.youtubeTitlePlaceholder')"
:value="youtubeTitle"
@input="youtubeTitle = eventValue($event)"
@keydown.enter="submitYouTube"
@keydown.enter="handleEnterAction($event, submitYouTube)"
/>
<k-list-input
:label="phone.t('Apps.music.youtubeArtist')"
@@ -1107,7 +1136,7 @@ onBeforeUnmount(() => {
:placeholder="phone.t('Apps.music.youtubeArtistPlaceholder')"
:value="youtubeArtist"
@input="youtubeArtist = eventValue($event)"
@keydown.enter="submitYouTube"
@keydown.enter="handleEnterAction($event, submitYouTube)"
/>
</k-list>
<p v-if="music.error" class="music-form-error" role="alert">
@@ -1256,7 +1285,7 @@ onBeforeUnmount(() => {
:placeholder="phone.t('Apps.music.playlistPlaceholder')"
:value="playlistName"
@input="playlistName = eventValue($event)"
@keydown.enter="submitPlaylist"
@keydown.enter="handleEnterAction($event, submitPlaylist)"
/>
</k-list>
<p v-if="music.error" class="music-form-error" role="alert">
@@ -1272,15 +1301,16 @@ onBeforeUnmount(() => {
<template v-else>{{ phone.t('Common.save') }}</template>
</k-button>
</template>
</section>
</k-sheet>
</section>
</k-sheet>
</div>
<k-sheet
:opened="playerOpened"
class="music-player-sheet"
@backdropclick="playerOpened = false"
>
<section v-if="music.currentTrack" class="music-player">
<div class="music-player-sheet">
<k-sheet
:opened="playerOpened"
@backdropclick="playerOpened = false"
>
<section v-if="music.currentTrack" class="music-player">
<header>
<k-link
component="button"
@@ -1383,8 +1413,9 @@ onBeforeUnmount(() => {
<p v-if="music.playbackError" class="music-player-error">
{{ phone.t('Apps.music.errors.playback_failed') }}
</p>
</section>
</k-sheet>
</section>
</k-sheet>
</div>
<k-dialog
:opened="confirmRemoveTrack"
@@ -1874,6 +1905,7 @@ onBeforeUnmount(() => {
.music-form-sheet,
.music-player-sheet {
--music-accent: #fa2d48;
display: contents;
color: var(--music-label);
}
@@ -1985,7 +2017,7 @@ onBeforeUnmount(() => {
color: #ff453a !important;
}
.music-player-sheet {
.music-player-sheet :deep(.k-sheet) {
background: rgb(22 22 25 / 96%) !important;
}