From fca104604799278d4a01927aef26e2b68e59dbeb Mon Sep 17 00:00:00 2001 From: root Date: Fri, 17 Oct 2025 10:55:38 +0200 Subject: [PATCH] add first letter navigation, switch channels with ctrl k and not ctrl shift c --- .../src/components/sidebar/ChannelList.vue | 49 ++++++++++++++++++- .../src/composables/useKeyboardShortcuts.ts | 9 ++-- frontend-vue/src/views/MainView.vue | 14 ++++-- 3 files changed, 64 insertions(+), 8 deletions(-) diff --git a/frontend-vue/src/components/sidebar/ChannelList.vue b/frontend-vue/src/components/sidebar/ChannelList.vue index e96318a..8eb0cb8 100644 --- a/frontend-vue/src/components/sidebar/ChannelList.vue +++ b/frontend-vue/src/components/sidebar/ChannelList.vue @@ -40,6 +40,11 @@ const props = defineProps() const containerRef = ref() const focusedChannelIndex = ref(0) +// For alphanumeric navigation +const lastSearchChar = ref('') +const lastSearchTime = ref(0) +const searchResetDelay = 1000 // Reset after 1 second + // Handle individual channel events const handleChannelSelect = (channelId: number) => { emit('select-channel', channelId) @@ -101,8 +106,15 @@ const handleChannelKeydown = (event: KeyboardEvent, channelIndex: number) => { return } break - + default: + // Handle alphanumeric navigation (a-z, 0-9) + const char = event.key.toLowerCase() + if (/^[a-z0-9]$/.test(char)) { + event.preventDefault() + handleAlphanumericNavigation(char, channelIndex) + return + } return } @@ -122,6 +134,41 @@ const focusChannel = (index: number) => { }) } +const handleAlphanumericNavigation = (char: string, currentIndex: number) => { + if (props.channels.length === 0) return + + const now = Date.now() + const sameChar = lastSearchChar.value === char && (now - lastSearchTime.value) < searchResetDelay + + lastSearchChar.value = char + lastSearchTime.value = now + + // Find channels starting with the character + const matchingIndices: number[] = [] + props.channels.forEach((channel, index) => { + if (channel.name.toLowerCase().startsWith(char)) { + matchingIndices.push(index) + } + }) + + if (matchingIndices.length === 0) return + + // If pressing the same character repeatedly, cycle through matches + if (sameChar) { + // Find the next match after current index + const nextMatch = matchingIndices.find(index => index > currentIndex) + if (nextMatch !== undefined) { + focusChannel(nextMatch) + } else { + // Wrap around to the first match + focusChannel(matchingIndices[0]) + } + } else { + // New character: jump to first match + focusChannel(matchingIndices[0]) + } +} + // Watch for channels changes and adjust focus watch(() => props.channels.length, (newLength) => { diff --git a/frontend-vue/src/composables/useKeyboardShortcuts.ts b/frontend-vue/src/composables/useKeyboardShortcuts.ts index 251ec50..d63acf4 100644 --- a/frontend-vue/src/composables/useKeyboardShortcuts.ts +++ b/frontend-vue/src/composables/useKeyboardShortcuts.ts @@ -42,10 +42,11 @@ export function useKeyboardShortcuts() { if (shortcut) { // Allow certain shortcuts to work globally, even in input fields - const isGlobalShortcut = (shortcut.ctrlKey && shortcut.shiftKey) || - shortcut.altKey || - shortcut.key === 'escape' - + const isGlobalShortcut = (shortcut.ctrlKey && shortcut.shiftKey) || + shortcut.altKey || + shortcut.key === 'escape' || + (shortcut.ctrlKey && shortcut.key === 'k') + // Skip shortcuts that shouldn't work in input fields if (isInInputField && !isGlobalShortcut) { return diff --git a/frontend-vue/src/views/MainView.vue b/frontend-vue/src/views/MainView.vue index 0a4f2fb..a3d053a 100644 --- a/frontend-vue/src/views/MainView.vue +++ b/frontend-vue/src/views/MainView.vue @@ -227,11 +227,10 @@ const setupKeyboardShortcuts = () => { handler: () => { showSearchDialog.value = true } }) - // Ctrl+Shift+C - Channel selector focus + // Ctrl+K - Channel selector focus addShortcut({ - key: 'c', + key: 'k', ctrlKey: true, - shiftKey: true, handler: () => { // Focus the first channel in the list const firstChannelButton = document.querySelector('.channel-item button') as HTMLElement @@ -557,6 +556,15 @@ const isUnsentMessage = (messageId: string | number): boolean => { return typeof messageId === 'string' && messageId.startsWith('unsent_') } +// Update document title when channel changes +watch(() => appStore.currentChannel, (channel) => { + if (channel) { + document.title = `${channel.name} - Notebrook` + } else { + document.title = 'Notebrook' + } +}, { immediate: true }) + // Initialize onMounted(async () => { // 1. Load saved state first (offline-first)