Initial release: DictIA v0.8.14-alpha (fork de Speakr, AGPL-3.0)

This commit is contained in:
InnovA AI
2026-03-16 21:47:37 +00:00
commit 42772a31ed
365 changed files with 103572 additions and 0 deletions

View File

@@ -0,0 +1,604 @@
<!-- Mobile Sidebar Backdrop -->
<div v-if="!isSidebarCollapsed && isMobileScreen"
@click="toggleSidebar"
class="fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden">
</div>
<!-- Sidebar -->
<aside :class="['sidebar', isSidebarCollapsed ? 'collapsed' : '']">
<div class="sidebar-content-wrapper">
<!-- Sidebar Header -->
<div class="p-4 border-b border-[var(--border-primary)] flex-shrink-0">
<div class="flex items-center gap-2 mb-3">
<!-- Folder Selector (replaces static "Recording" title) -->
<div v-if="foldersEnabled && availableFolders.length > 0" class="relative flex-1 min-w-0">
<select v-model="filterFolder"
class="w-full h-9 pl-8 pr-7 text-base font-semibold rounded-md cursor-pointer appearance-none border-0 bg-transparent text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)] transition-colors"
style="outline: none !important; box-shadow: none !important; background-image: url('data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%2712%27 height=%2712%27 fill=%27%236B7280%27 viewBox=%270 0 16 16%27%3E%3Cpath d=%27M8 10.5l-4-4h8l-4 4z%27/%3E%3C/svg%3E'); background-repeat: no-repeat; background-position: right 8px center;">
<option value="">All Recordings</option>
<option value="none">Unfiled</option>
<option v-for="folder in availableFolders" :key="folder.id" :value="folder.id">
${ folder.name }
</option>
</select>
<i class="fas fa-folder absolute left-2.5 top-1/2 transform -translate-y-1/2 pointer-events-none text-sm"
:style="{ color: filterFolder && filterFolder !== 'none' ? getFolderColor(filterFolder) : 'var(--text-muted)' }"></i>
</div>
<h2 v-else class="text-lg font-semibold flex-1" v-text="t('nav.recording')"></h2>
<div class="flex gap-2 flex-shrink-0">
<!-- Selection Mode Toggle -->
<button v-if="!selectionMode && recordings.length > 0"
@click="enterSelectionMode"
class="w-9 h-9 flex items-center justify-center bg-[var(--bg-accent)] text-[var(--text-accent)] rounded-md hover:opacity-80 transition-opacity"
title="Select multiple">
<i class="fas fa-list-check"></i>
</button>
<button v-if="selectionMode"
@click="exitSelectionMode"
class="h-9 px-3 flex items-center bg-[var(--bg-button)] text-[var(--text-button)] rounded-md hover:opacity-80 transition-opacity text-sm"
title="Exit selection mode">
<i class="fas fa-times mr-1"></i>Done
</button>
<!-- New button - compact when folders enabled -->
<button v-if="!selectionMode && foldersEnabled && availableFolders.length > 0"
@click="switchToUploadView"
class="w-9 h-9 flex items-center justify-center bg-[var(--bg-button)] text-[var(--text-button)] rounded-md hover:bg-[var(--bg-button-hover)] transition-colors"
title="New Recording">
<i class="fas fa-plus"></i>
</button>
<button v-else-if="!selectionMode"
@click="switchToUploadView"
class="h-9 px-3 flex items-center bg-[var(--bg-button)] text-[var(--text-button)] rounded-md hover:bg-[var(--bg-button-hover)] transition-colors text-sm">
<i class="fas fa-plus mr-1"></i><span v-text="t('common.new')"></span>
</button>
</div>
</div>
<!-- Search and Sort Controls -->
<div class="space-y-3">
<!-- Filter Toggle Button -->
<div class="flex items-center gap-1.5">
<button @click="showAdvancedFilters = !showAdvancedFilters"
class="flex-1 h-7 flex items-center justify-between pl-2 pr-2 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded text-xs hover:bg-[var(--bg-tertiary)] transition-colors">
<span class="flex items-center">
<i class="fas fa-filter mr-1.5 text-[var(--text-muted)] text-[10px]"></i>
<span v-if="!searchQuery && filterTags.length === 0 && filterSpeakers.length === 0 && !filterDatePreset && !filterDateRange.start && !filterDateRange.end && !filterTextQuery && !filterStarred && !filterInbox"
v-text="t('sidebar.searchRecordings')">
</span>
<span v-else class="text-[var(--text-accent)]">
Active filters (${ (filterTags.length > 0 ? 1 : 0) + (filterSpeakers.length > 0 ? 1 : 0) + (filterDatePreset || filterDateRange.start || filterDateRange.end ? 1 : 0) + (filterTextQuery ? 1 : 0) + (filterStarred ? 1 : 0) + (filterInbox ? 1 : 0) })
</span>
</span>
<i :class="['fas fa-chevron-down transition-transform text-[var(--text-muted)] text-[10px]', showAdvancedFilters ? 'rotate-180' : '']"></i>
</button>
<button v-if="searchQuery || filterTags.length > 0 || filterSpeakers.length > 0 || filterDatePreset || filterDateRange.start || filterDateRange.end || filterTextQuery || filterStarred || filterInbox"
@click="clearAllFilters"
class="w-7 h-7 flex items-center justify-center bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded text-xs hover:bg-[var(--bg-tertiary)] hover:text-[var(--text-primary)] transition-colors"
:title="t('buttons.clearAllFilters')">
<i class="fas fa-times text-[var(--text-muted)] text-[10px]"></i>
</button>
</div>
<!-- Advanced Filters Panel -->
<div v-show="showAdvancedFilters" class="p-3 bg-[var(--bg-tertiary)] border border-[var(--border-secondary)] rounded-lg space-y-3">
<!-- Text Search -->
<div>
<label class="block text-xs font-medium text-[var(--text-muted)] mb-1" v-text="t('sidebar.filters')"></label>
<div class="relative">
<input v-model="filterTextQuery"
type="text"
:placeholder="t('sidebar.searchRecordings')"
class="w-full px-3 py-1.5 pl-8 pr-8 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-[var(--ring-focus)]">
<i class="fas fa-search absolute left-2.5 top-1/2 transform -translate-y-1/2 text-[var(--text-muted)] text-xs"></i>
<button v-if="filterTextQuery"
@click="filterTextQuery = ''"
class="absolute right-2.5 top-1/2 transform -translate-y-1/2 text-[var(--text-muted)] hover:text-[var(--text-primary)] text-xs"
:title="t('buttons.clearSearchText')">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<!-- Quick Filters (Starred/Inbox) -->
<div class="flex gap-2">
<button @click="filterStarred = !filterStarred"
:class="[
'flex-1 flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all border',
filterStarred
? 'bg-yellow-500/20 border-yellow-500/50 text-yellow-400'
: 'bg-[var(--bg-input)] border-[var(--border-secondary)] text-[var(--text-muted)] hover:border-yellow-500/30 hover:text-yellow-400'
]">
<i class="fas fa-star" style="font-size: 10px;"></i>
<span v-text="t('sidebar.starred')"></span>
</button>
<button @click="filterInbox = !filterInbox"
:class="[
'flex-1 flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all border',
filterInbox
? 'bg-blue-500/20 border-blue-500/50 text-blue-400'
: 'bg-[var(--bg-input)] border-[var(--border-secondary)] text-[var(--text-muted)] hover:border-blue-500/30 hover:text-blue-400'
]">
<i class="fas fa-inbox" style="font-size: 10px;"></i>
<span v-text="t('sidebar.inbox')"></span>
</button>
</div>
<!-- Tag Filter -->
<div class="bg-[var(--bg-secondary)] rounded-lg p-2.5 border border-[var(--border-primary)]">
<div class="flex items-center justify-between gap-2 mb-2">
<label class="text-xs font-medium text-[var(--text-muted)] whitespace-nowrap" v-text="t('tags.filterByTag')"></label>
<div class="relative flex-1 max-w-[140px]" v-if="availableTags.length > 5">
<input v-model="filterTagSearch"
type="text"
:placeholder="t('tags.searchTags')"
class="w-full px-2 py-1 pl-7 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded text-xs focus:outline-none focus:ring-1 focus:ring-[var(--ring-focus)]">
<i class="fas fa-search absolute left-2 top-1/2 transform -translate-y-1/2 text-[var(--text-muted)]" style="font-size: 10px;"></i>
</div>
</div>
<div class="max-h-24 overflow-y-auto scrollbar-thin pr-1">
<div class="flex flex-wrap gap-1.5">
<button v-for="tag in filteredTagsForFilter"
:key="tag.id"
@click="filterTags.includes(tag.id) ? filterTags = filterTags.filter(id => id !== tag.id) : filterTags.push(tag.id)"
:class="[
'px-2 py-1 rounded-full text-xs font-medium transition-all',
filterTags.includes(tag.id)
? 'bg-[var(--bg-accent)] text-[var(--text-accent)] ring-1 ring-[var(--border-accent)]'
: 'bg-[var(--bg-input)] text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)]'
]">
<span class="inline-block w-2 h-2 rounded-full mr-1" :style="{ backgroundColor: tag.color || '#6B7280' }"></span>
${ tag.name }
</button>
</div>
</div>
<p v-if="availableTags.length === 0" class="text-xs text-[var(--text-muted)] italic" v-text="t('tags.noTags')"></p>
<p v-else-if="filteredTagsForFilter.length === 0" class="text-xs text-[var(--text-muted)] italic py-1" v-text="t('tags.noMatchingTags')"></p>
</div>
<!-- Speaker Filter -->
<div v-if="availableSpeakers.length > 0" class="bg-[var(--bg-secondary)] rounded-lg p-2.5 border border-[var(--border-primary)]">
<div class="flex items-center justify-between gap-2 mb-2">
<label class="text-xs font-medium text-[var(--text-muted)] whitespace-nowrap" v-text="t('speakers.filterBySpeaker')"></label>
<div class="relative flex-1 max-w-[140px]" v-if="availableSpeakers.length > 5">
<input v-model="filterSpeakerSearch"
type="text"
:placeholder="t('speakers.searchSpeakers')"
class="w-full px-2 py-1 pl-7 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded text-xs focus:outline-none focus:ring-1 focus:ring-[var(--ring-focus)]">
<i class="fas fa-search absolute left-2 top-1/2 transform -translate-y-1/2 text-[var(--text-muted)]" style="font-size: 10px;"></i>
</div>
</div>
<div class="max-h-24 overflow-y-auto scrollbar-thin pr-1">
<div class="flex flex-wrap gap-1.5">
<button v-for="speaker in filteredSpeakersForFilter"
:key="speaker.id"
@click="filterSpeakers.includes(speaker.name) ? filterSpeakers = filterSpeakers.filter(n => n !== speaker.name) : filterSpeakers.push(speaker.name)"
:class="[
'px-2 py-1 rounded-full text-xs font-medium transition-all',
filterSpeakers.includes(speaker.name)
? 'bg-[var(--bg-accent)] text-[var(--text-accent)] ring-1 ring-[var(--border-accent)]'
: 'bg-[var(--bg-input)] text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)]'
]">
<i class="fas fa-user mr-1" style="font-size: 9px;"></i>
${ speaker.name }
</button>
</div>
</div>
<p v-if="filteredSpeakersForFilter.length === 0 && filterSpeakerSearch" class="text-xs text-[var(--text-muted)] italic py-1" v-text="t('speakers.noMatchingSpeakers')"></p>
</div>
<!-- Date Filter -->
<div>
<label class="block text-xs font-medium text-[var(--text-muted)] mb-1" v-text="t('sidebar.dateRange')"></label>
<div class="grid grid-cols-3 gap-1.5 mb-2">
<button v-for="preset in datePresetOptions"
:key="preset.value"
@click="filterDatePreset = filterDatePreset === preset.value ? '' : preset.value; filterDateRange = { start: '', end: '' }"
:class="[
'px-2 py-1 rounded-md text-xs transition-all',
filterDatePreset === preset.value
? 'bg-[var(--bg-accent)] text-[var(--text-accent)]'
: 'bg-[var(--bg-secondary)] text-[var(--text-secondary)] hover:bg-[var(--bg-input)]'
]"
:title="preset.label">
${ preset.label }
</button>
</div>
<div class="grid grid-cols-2 gap-2">
<div>
<input v-model="filterDateRange.start"
@change="filterDatePreset = ''"
type="date"
class="w-full px-2 py-1 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded-md text-xs focus:outline-none focus:ring-1 focus:ring-[var(--ring-focus)]"
:placeholder="t('form.dateFrom')">
</div>
<div>
<input v-model="filterDateRange.end"
@change="filterDatePreset = ''"
type="date"
class="w-full px-2 py-1 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded-md text-xs focus:outline-none focus:ring-1 focus:ring-[var(--ring-focus)]"
:placeholder="t('form.dateTo')">
</div>
</div>
</div>
</div>
<!-- Compact Controls Row -->
<div class="flex items-center gap-1.5">
<!-- Sort Toggle -->
<button @click="sortBy = sortBy === 'created_at' ? 'meeting_date' : 'created_at'"
:class="[
'flex-1 flex items-center justify-center gap-1 px-2 py-1.5 border rounded-md text-xs transition-colors',
'bg-[var(--bg-input)] border-[var(--border-secondary)] hover:bg-[var(--bg-tertiary)]'
]"
:title="sortBy === 'meeting_date' ? t('sidebar.sortByMeetingDate') : t('sidebar.sortByDate')">
<i :class="['fas', sortBy === 'meeting_date' ? 'fa-calendar' : 'fa-upload']"></i>
<span class="hidden sm:inline">Sort</span>
<i class="fas fa-exchange-alt text-[var(--text-muted)] text-[10px]"></i>
</button>
<!-- Archived Toggle (only show when audio-only deletion mode is active) -->
<button v-if="enableArchiveToggle" @click="showArchivedRecordings = !showArchivedRecordings"
:class="[
'flex-1 flex items-center justify-center gap-1 px-2 py-1.5 border rounded-md text-xs transition-colors',
showArchivedRecordings
? 'bg-[var(--bg-accent)] border-[var(--border-accent)] text-[var(--text-accent)]'
: 'bg-[var(--bg-input)] border-[var(--border-secondary)] hover:bg-[var(--bg-tertiary)]'
]"
:title="t('sidebar.archivedRecordings')">
<i class="fas fa-archive"></i>
<span class="hidden sm:inline">Archived</span>
<i :class="['fas text-[10px]', showArchivedRecordings ? 'fa-toggle-on' : 'fa-toggle-off']"></i>
</button>
<!-- Shared Toggle -->
<button v-if="enableInternalSharing" @click="showSharedWithMe = !showSharedWithMe"
:class="[
'flex-1 flex items-center justify-center gap-1 px-2 py-1.5 border rounded-md text-xs transition-colors',
showSharedWithMe
? 'bg-[var(--bg-accent)] border-[var(--border-accent)] text-[var(--text-accent)]'
: 'bg-[var(--bg-input)] border-[var(--border-secondary)] hover:bg-[var(--bg-tertiary)]'
]"
:title="t('sidebar.sharedWithMe')">
<i class="fas fa-users"></i>
<span class="hidden sm:inline">Shared</span>
<i :class="['fas text-[10px]', showSharedWithMe ? 'fa-toggle-on' : 'fa-toggle-off']"></i>
</button>
</div>
</div>
</div>
<!-- Recordings List -->
<div class="flex-1 overflow-y-auto p-4"
@scroll="(e) => {
const element = e.target;
const threshold = 100; // Load more when 100px from bottom
if (element.scrollHeight - element.scrollTop - element.clientHeight < threshold) {
loadMoreRecordings();
}
}">
<!-- Incognito Recording (styled like a regular recording item with a subtle indicator) -->
<div v-if="enableIncognitoMode && incognitoRecording"
@click="selectIncognitoRecording"
:class="[
'group mb-3 p-3 rounded-lg cursor-pointer transition-all duration-200',
selectedRecording?.id === 'incognito'
? 'bg-[var(--bg-accent)] border-l-4 border-[var(--border-accent)]'
: 'bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] border-l-4 border-transparent hover:border-[var(--border-secondary)]'
]">
<div class="flex items-start justify-between gap-2">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<span class="font-medium text-sm text-[var(--text-primary)] truncate">
${ incognitoRecording.title || 'Incognito Recording' }
</span>
</div>
<div class="flex items-center gap-2 flex-wrap">
<!-- Incognito pill badge (matches tag style with contrasting border) -->
<span class="inline-flex items-center px-1.5 py-0.5 rounded-md text-xs font-medium bg-violet-500/15 dark:bg-violet-500/25 text-violet-700 dark:text-violet-300 ring-1 ring-violet-400/50 dark:ring-violet-500/50">
<i class="fas fa-user-secret mr-1" style="font-size: 9px;"></i>
Incognito
</span>
<!-- Duration pill -->
<span v-if="incognitoRecording.audio_duration_seconds" class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] text-[var(--text-muted)] bg-[var(--bg-tertiary)]">
<i class="fas fa-clock mr-1" style="font-size: 8px;"></i>
${ Math.floor(incognitoRecording.audio_duration_seconds / 60) }:${ String(incognitoRecording.audio_duration_seconds % 60).padStart(2, '0') }
</span>
</div>
<p class="text-[10px] text-[var(--text-muted)] mt-1.5 opacity-70">
<i class="fas fa-eye-slash mr-1"></i>
Session only
</p>
</div>
<button @click.stop="clearIncognitoRecordingWithConfirm"
class="opacity-0 group-hover:opacity-100 p-1.5 text-[var(--text-muted)] hover:text-red-500 hover:bg-red-500/10 rounded transition-all"
title="Discard">
<i class="fas fa-times text-xs"></i>
</button>
</div>
</div>
<div v-if="isLoadingRecordings && recordings.length === 0" class="text-center py-8">
<i class="fas fa-spinner fa-spin text-2xl text-[var(--text-muted)]"></i>
<p class="mt-2 text-[var(--text-muted)]" v-text="t('help.loadingRecordings')"></p>
</div>
<div v-else-if="recordings.length === 0 && !isLoadingRecordings" class="text-center py-8">
<i class="fas fa-microphone-slash text-3xl text-[var(--text-muted)] mb-3"></i>
<p class="text-[var(--text-muted)]" v-text="t('sidebar.noRecordings')"></p>
<p v-if="searchQuery.trim()" class="text-sm text-[var(--text-muted)] mt-1">
<span v-text="t('help.tryAdjustingSearch')"></span> <button @click="clearAllFilters()" class="text-[var(--text-accent)] hover:underline" v-text="t('help.clearFilters')"></button>
</p>
</div>
<div v-if="recordings.length > 0" class="space-y-2">
<!-- Selection Mode Controls -->
<div v-if="selectionMode" class="flex items-center justify-between px-1 py-2 mb-2 border-b border-[var(--border-primary)]">
<div class="flex items-center gap-3">
<button @click="allVisibleSelected ? clearSelection() : selectAll()"
class="text-xs text-[var(--text-accent)] hover:underline">
${ allVisibleSelected ? 'Clear all' : 'Select all' }
</button>
<span class="text-xs text-[var(--text-muted)]">
${ selectedCount } selected
</span>
</div>
</div>
<div v-for="group in groupedRecordings" :key="group.title" class="mb-6">
<h3 class="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wide mb-2">
${group.title}
</h3>
<div class="space-y-1">
<div v-for="recording in group.items"
:key="recording.id"
@click="selectionMode ? toggleSelection(recording.id) : selectRecording(recording)"
:class="[
'p-3 rounded-lg cursor-pointer transition-all duration-200 border-2',
selectionMode && isSelected(recording.id)
? 'bg-[var(--bg-accent)] border-[var(--border-accent)]'
: selectedRecording?.id === recording.id && !selectionMode
? 'bg-[var(--bg-accent)] border-[var(--border-accent)]'
: recording.is_shared
? 'bg-[var(--bg-secondary)] border-[var(--bg-tertiary)] hover:bg-[var(--bg-accent-hover)]'
: 'bg-[var(--bg-tertiary)] border-transparent hover:bg-[var(--bg-accent-hover)]'
]">
<!-- Title and Status Row -->
<div class="flex items-center justify-between mb-1">
<!-- Checkbox for selection mode -->
<div v-if="selectionMode" @click.stop="toggleSelection(recording.id)" class="flex-shrink-0 mr-2">
<input type="checkbox"
:checked="isSelected(recording.id)"
class="selection-checkbox"
@click.stop="toggleSelection(recording.id)">
</div>
<h4 class="font-medium text-sm truncate flex-1 mr-2" :class="selectedRecording?.id === recording.id && !selectionMode ? 'text-[var(--text-accent)]' : 'text-[var(--text-primary)]'">
${ recording.title || t('common.untitled') }
</h4>
<div class="flex items-center gap-0.5 flex-shrink-0">
<!-- Combined sharing status badge -->
<span v-if="recording.is_shared || recording.has_group_tags || recording.shared_with_count > 0 || recording.public_share_count > 0"
class="inline-flex items-center justify-center gap-1 h-5 px-2 rounded-full text-[9px] leading-none bg-violet-100 dark:bg-violet-900/50 border border-violet-300 dark:border-violet-600"
:title="[
recording.is_shared ? t('sharing.sharedBy') + ' ' + (recording.owner_username || t('sharing.unknown')) : '',
recording.has_group_tags ? t('sharing.teamRecording') : '',
!recording.is_shared && recording.shared_with_count > 0 ? t('sharing.sharedWith') + ' ' + recording.shared_with_count + ' ' + t('sharing.users') : '',
!recording.is_shared && recording.public_share_count > 0 ? recording.public_share_count + ' ' + t('sharing.publicLinksGenerated') : ''
].filter(s => s).join(' • ')">
<i v-if="recording.is_shared" class="fas fa-arrow-down text-purple-600 dark:text-purple-400"></i>
<i v-if="recording.has_group_tags" class="fas fa-users text-blue-600 dark:text-blue-400"></i>
<i v-if="!recording.is_shared && recording.shared_with_count > 0" class="fas fa-arrow-up text-indigo-600 dark:text-indigo-400"></i>
<i v-if="!recording.is_shared && recording.public_share_count > 0" class="fas fa-globe text-teal-600 dark:text-teal-400"></i>
</span>
<!-- Show Audio Deleted badge -->
<span v-if="recording.audio_deleted_at"
class="inline-flex items-center justify-center h-5 px-1.5 rounded-full text-[9px] leading-none bg-gray-200 dark:bg-gray-700 border border-gray-300 dark:border-gray-600"
:title="t('sidebar.archived')">
<i class="fas fa-file-audio text-gray-600 dark:text-gray-400"></i>
</span>
<!-- Show Failed status for failed recordings -->
<span v-if="recording.status === 'FAILED'"
class="inline-flex items-center justify-center h-5 px-1.5 rounded-full text-[9px] leading-none bg-red-100 dark:bg-red-900/50 border border-red-300 dark:border-red-700 text-red-700 dark:text-red-400">
<i class="fas fa-exclamation-circle"></i>
</span>
<!-- Show Processing status for non-completed recordings -->
<span v-else-if="recording.status !== 'COMPLETED'"
:class="getStatusClass(recording.status)"
class="status-badge">
${formatStatus(recording.status)}
</span>
<!-- For completed recordings, show highlight and inbox badges -->
<template v-else>
<span v-if="recording.is_highlighted"
@click.stop="toggleHighlight(recording)"
class="inline-flex items-center justify-center h-5 px-1.5 rounded-full text-[9px] leading-none bg-amber-100 dark:bg-amber-900/50 border border-amber-400 dark:border-amber-600 cursor-pointer hover:bg-amber-200 dark:hover:bg-amber-800/50"
:title="t('sidebar.removeFromHighlighted')">
<i class="fas fa-star text-amber-600 dark:text-amber-400"></i>
</span>
<span v-if="recording.is_inbox"
@click.stop="toggleInbox(recording)"
class="inline-flex items-center justify-center h-5 px-1.5 rounded-full text-[9px] leading-none bg-blue-100 dark:bg-blue-900/50 border border-blue-400 dark:border-blue-600 cursor-pointer hover:bg-blue-200 dark:hover:bg-blue-800/50"
:title="t('sidebar.markAsRead')">
<i class="fas fa-inbox text-blue-600 dark:text-blue-400"></i>
</span>
</template>
</div>
</div>
<!-- Date and Participants -->
<div class="flex flex-wrap items-center gap-x-3 gap-y-1 mt-1 text-xs text-[var(--text-muted)]">
<!-- Date -->
<div class="flex items-center flex-shrink-0">
<i class="fas fa-calendar-alt mr-1 text-[var(--text-muted)]" style="font-size: 10px;"></i>
${formatShortDate(sortBy === 'meeting_date' ? recording.meeting_date : recording.created_at)}
</div>
<!-- Participants -->
<div v-if="recording.participants" class="flex items-center min-w-0 flex-1">
<i class="fas fa-users mr-1 text-[var(--text-muted)] flex-shrink-0" style="font-size: 10px;"></i>
<span class="truncate">
${recording.participants}
</span>
</div>
</div>
<!-- Tags -->
<div v-if="getRecordingTags(recording).length > 0 || recording.duplicate_info" class="flex flex-wrap items-center gap-1 mt-1">
<button v-for="tag in getRecordingTags(recording).slice(0, 4)" :key="tag.id"
@click.stop="filterByTag(tag)"
class="inline-flex items-center px-1.5 py-0.5 rounded-md text-xs font-medium hover:opacity-80 transition-all cursor-pointer"
:style="{ backgroundColor: tag.color, color: getContrastTextColor(tag.color) }"
:title="tag.group_id ? ('Group: ' + tag.group_name) : ('Filter by ' + tag.name)">
<i v-if="tag.group_id" class="fas fa-users mr-1" style="font-size: 9px; vertical-align: middle; line-height: 0;"></i>
<span v-text="tag.name"></span>
</button>
<span v-if="getRecordingTags(recording).length > 4"
class="inline-flex items-center px-1.5 py-0.5 rounded-md text-xs text-[var(--text-muted)]"
:title="'More tags...'">
+${getRecordingTags(recording).length - 4}
</span>
<button v-if="recording.duplicate_info"
@click.stop="openDuplicatesModal(recording.duplicate_info)"
class="text-amber-500 hover:text-amber-400 transition-colors"
:title="recording.duplicate_info.total_copies + ' ' + (t('upload.copies') || 'copies')">
<i class="fas fa-copy text-xs"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Load More Indicator -->
<div v-if="isLoadingMore" class="text-center py-4">
<i class="fas fa-spinner fa-spin text-lg text-[var(--text-muted)]"></i>
<p class="text-sm text-[var(--text-muted)] mt-1" v-text="t('help.loadingMore')"></p>
</div>
<!-- End of Results Indicator -->
<div v-else-if="!hasNextPage && totalRecordings > 0" class="text-center py-4 text-xs text-[var(--text-muted)]">
<i class="fas fa-check-circle mr-1"></i>
<span v-text="t('help.allRecordingsLoaded')"></span>
</div>
</div>
</div>
</div>
</aside>
<!-- Vertical Floating Bulk Action Bar - To the right of sidebar -->
<transition name="slide-right">
<div v-if="selectionMode && selectedCount > 0 && !isSidebarCollapsed"
class="fixed top-1/2 -translate-y-1/2 left-80 z-50 flex flex-col gap-1 p-2 bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-r-xl shadow-lg">
<!-- Selection count badge -->
<div class="text-center text-xs font-medium text-[var(--text-accent)] py-1 border-b border-[var(--border-primary)] mb-1">
${ selectedCount }
</div>
<button @click="openBulkTagModal('add')"
class="w-10 h-10 flex items-center justify-center bg-[var(--bg-tertiary)] hover:bg-[var(--bg-input)] rounded-lg transition-colors border border-[var(--border-secondary)]"
title="Add or remove tags">
<i class="fas fa-tags text-[var(--text-muted)]"></i>
</button>
<button v-if="foldersEnabled"
@click="showBulkFolderModal = true"
class="w-10 h-10 flex items-center justify-center bg-[var(--bg-tertiary)] hover:bg-[var(--bg-input)] rounded-lg transition-colors border border-[var(--border-secondary)]"
title="Move to folder">
<i class="fas fa-folder text-emerald-500"></i>
</button>
<button @click="bulkToggleInbox()"
class="w-10 h-10 flex items-center justify-center bg-[var(--bg-tertiary)] hover:bg-[var(--bg-input)] rounded-lg transition-colors border border-[var(--border-secondary)]"
title="Toggle inbox status">
<i class="fas fa-inbox text-blue-500"></i>
</button>
<button @click="bulkToggleHighlight()"
class="w-10 h-10 flex items-center justify-center bg-[var(--bg-tertiary)] hover:bg-[var(--bg-input)] rounded-lg transition-colors border border-[var(--border-secondary)]"
title="Toggle highlight">
<i class="fas fa-star text-amber-500"></i>
</button>
<button @click="openBulkReprocessModal"
class="w-10 h-10 flex items-center justify-center bg-[var(--bg-tertiary)] hover:bg-[var(--bg-input)] rounded-lg transition-colors border border-[var(--border-secondary)]"
title="Reprocess">
<i class="fas fa-redo text-[var(--text-muted)]"></i>
</button>
<div class="border-t border-[var(--border-primary)] my-1"></div>
<button @click="openBulkDeleteModal"
class="w-10 h-10 flex items-center justify-center bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors"
title="Delete selected">
<i class="fas fa-trash"></i>
</button>
<button @click="exitSelectionMode"
class="w-10 h-10 flex items-center justify-center text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)] rounded-lg transition-colors"
title="Exit selection mode">
<i class="fas fa-times"></i>
</button>
</div>
</transition>
<!-- Bulk Folder Assignment Modal -->
<div v-if="showBulkFolderModal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-[60]" @click.self="showBulkFolderModal = false">
<div class="bg-[var(--bg-secondary)] rounded-xl shadow-2xl max-w-sm w-full mx-4 overflow-hidden">
<div class="p-4 border-b border-[var(--border-primary)]">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-[var(--text-primary)]">
<i class="fas fa-folder mr-2 text-emerald-500"></i>
Move to Folder
</h3>
<button @click="showBulkFolderModal = false" class="text-[var(--text-muted)] hover:text-[var(--text-primary)]">
<i class="fas fa-times"></i>
</button>
</div>
<p class="text-sm text-[var(--text-muted)] mt-1">
${ selectedCount } recording(s) selected
</p>
</div>
<div class="p-4 max-h-64 overflow-y-auto">
<!-- No Folder Option -->
<button @click="bulkAssignFolder(null)"
class="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left hover:bg-[var(--bg-tertiary)] transition-colors mb-2 border border-[var(--border-secondary)]">
<i class="fas fa-folder-minus text-[var(--text-muted)]"></i>
<span class="text-sm text-[var(--text-secondary)]">Remove from folder</span>
</button>
<!-- Divider -->
<div v-if="availableFolders.length > 0" class="border-t border-[var(--border-primary)] my-2"></div>
<!-- Folder Options -->
<button v-for="folder in availableFolders"
:key="folder.id"
@click="bulkAssignFolder(folder.id)"
class="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left hover:bg-[var(--bg-tertiary)] transition-colors mb-1"
:style="{ borderLeft: '3px solid ' + (folder.color || '#10B981') }">
<i class="fas fa-folder" :style="{ color: folder.color || '#10B981' }"></i>
<div class="flex-1 min-w-0">
<span class="text-sm text-[var(--text-primary)] block truncate">${ folder.name }</span>
<span v-if="folder.group_name" class="text-xs text-[var(--text-muted)]">
<i class="fas fa-users mr-1" style="font-size: 9px;"></i>${ folder.group_name }
</span>
</div>
<span class="text-xs text-[var(--text-muted)]">${ folder.recording_count || 0 }</span>
</button>
<!-- Empty State -->
<p v-if="availableFolders.length === 0" class="text-sm text-[var(--text-muted)] text-center py-4">
No folders created. Create folders in your account settings.
</p>
</div>
<div class="p-4 border-t border-[var(--border-primary)] bg-[var(--bg-tertiary)]">
<button @click="showBulkFolderModal = false"
class="w-full px-4 py-2 text-sm text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors">
Cancel
</button>
</div>
</div>
</div>