|
|
|
@ -47,6 +47,58 @@ |
|
|
|
/> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- Advanced filters (only for MVD directories) --> |
|
|
|
<div v-if="hasMvdData" class="mb-4 flex flex-col items-end"> |
|
|
|
<button |
|
|
|
@click="filtersOpen = !filtersOpen" |
|
|
|
class="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition-colors" |
|
|
|
> |
|
|
|
<svg class="w-4 h-4 transition-transform" :class="{ 'rotate-90': filtersOpen }" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/> |
|
|
|
</svg> |
|
|
|
Advanced |
|
|
|
<span v-if="activeFiltersCount > 0" class="bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 text-xs px-2 py-0.5 rounded-full"> |
|
|
|
{{ activeFiltersCount }} active |
|
|
|
</span> |
|
|
|
</button> |
|
|
|
|
|
|
|
<div v-if="filtersOpen" class="mt-3 p-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-gray-700 flex flex-wrap gap-4 items-end"> |
|
|
|
<div> |
|
|
|
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Map</label> |
|
|
|
<select |
|
|
|
v-model="filterMap" |
|
|
|
class="px-3 py-1.5 text-sm rounded-lg border border-gray-300 dark:border-gray-600 |
|
|
|
bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 |
|
|
|
focus:outline-none focus:ring-2 focus:ring-blue-500" |
|
|
|
> |
|
|
|
<option value="">All maps</option> |
|
|
|
<option v-for="map in availableMaps" :key="map" :value="map">{{ map }}</option> |
|
|
|
</select> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div> |
|
|
|
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Player</label> |
|
|
|
<input |
|
|
|
v-model="filterPlayer" |
|
|
|
type="text" |
|
|
|
placeholder="Player name…" |
|
|
|
class="px-3 py-1.5 text-sm rounded-lg border border-gray-300 dark:border-gray-600 |
|
|
|
bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 |
|
|
|
focus:outline-none focus:ring-2 focus:ring-blue-500" |
|
|
|
/> |
|
|
|
</div> |
|
|
|
|
|
|
|
<button |
|
|
|
v-if="activeFiltersCount > 0" |
|
|
|
@click="clearFilters" |
|
|
|
class="px-3 py-1.5 text-sm rounded-lg border border-gray-300 dark:border-gray-600 |
|
|
|
text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors" |
|
|
|
> |
|
|
|
Clear filters |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- Empty --> |
|
|
|
<div v-if="entries.length === 0" class="text-gray-400 dark:text-gray-500 text-sm"> |
|
|
|
Directory is empty. |
|
|
|
@ -73,7 +125,7 @@ |
|
|
|
</thead> |
|
|
|
<tbody> |
|
|
|
<tr |
|
|
|
v-for="entry in entries" |
|
|
|
v-for="entry in filteredEntries" |
|
|
|
:key="entry.name" |
|
|
|
class="border-b border-gray-100 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800" |
|
|
|
:class="{ 'bg-blue-50 dark:bg-blue-900/20': selected.has(entry.name) }" |
|
|
|
@ -240,7 +292,52 @@ function closeConfirm() { |
|
|
|
captchaError.value = '' |
|
|
|
} |
|
|
|
|
|
|
|
const fileEntries = computed(() => entries.value.filter(e => !e.is_dir)) |
|
|
|
// --- Filters --- |
|
|
|
const filtersOpen = ref(false) |
|
|
|
const filterMap = ref('') |
|
|
|
const filterPlayer = ref('') |
|
|
|
|
|
|
|
const hasMvdData = computed(() => entries.value.some(e => e.mvd_info)) |
|
|
|
|
|
|
|
const availableMaps = computed(() => { |
|
|
|
const maps = new Set() |
|
|
|
for (const e of entries.value) { |
|
|
|
if (e.mvd_info?.map) maps.add(e.mvd_info.map) |
|
|
|
} |
|
|
|
return [...maps].sort() |
|
|
|
}) |
|
|
|
|
|
|
|
const activeFiltersCount = computed(() => (filterMap.value ? 1 : 0) + (filterPlayer.value.trim() ? 1 : 0)) |
|
|
|
|
|
|
|
const filteredEntries = computed(() => { |
|
|
|
const player = filterPlayer.value.trim().toLowerCase() |
|
|
|
return entries.value.filter(e => { |
|
|
|
if (e.is_dir) return true |
|
|
|
if (filterMap.value && e.mvd_info?.map !== filterMap.value) return false |
|
|
|
if (player) { |
|
|
|
const a = (e.mvd_info?.players_a ?? '').toLowerCase() |
|
|
|
const b = (e.mvd_info?.players_b ?? '').toLowerCase() |
|
|
|
if (!a.includes(player) && !b.includes(player)) return false |
|
|
|
} |
|
|
|
return true |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
function clearFilters() { |
|
|
|
filterMap.value = '' |
|
|
|
filterPlayer.value = '' |
|
|
|
selected.value = new Set() |
|
|
|
} |
|
|
|
|
|
|
|
watch([filterMap, filterPlayer], () => { selected.value = new Set() }) |
|
|
|
|
|
|
|
watch(currentPath, () => { |
|
|
|
filterMap.value = '' |
|
|
|
filterPlayer.value = '' |
|
|
|
filtersOpen.value = false |
|
|
|
}) |
|
|
|
|
|
|
|
const fileEntries = computed(() => filteredEntries.value.filter(e => !e.is_dir)) |
|
|
|
const showMultiselect = computed(() => fileEntries.value.length >= 3) |
|
|
|
const allFilesSelected = computed(() => fileEntries.value.length > 0 && fileEntries.value.every(e => selected.value.has(e.name))) |
|
|
|
const someFilesSelected = computed(() => !allFilesSelected.value && fileEntries.value.some(e => selected.value.has(e.name))) |
|
|
|
|