Initial release: DictIA v0.8.14-alpha (fork de Speakr, AGPL-3.0)
This commit is contained in:
604
templates/components/sidebar.html
Normal file
604
templates/components/sidebar.html
Normal 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>
|
||||
Reference in New Issue
Block a user