change select ui
This commit is contained in:
108
src/components/ui/AppSelect.vue
Normal file
108
src/components/ui/AppSelect.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
// filepath: src/components/ui/AppSelect.vue
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ChevronDown, Check } from 'lucide-vue-next'
|
||||
import { onClickOutside, useElementBounding } from '@vueuse/core'
|
||||
|
||||
interface Option {
|
||||
label: string
|
||||
value: string | number
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string | number
|
||||
options: Option[]
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string | number): void
|
||||
}>()
|
||||
|
||||
const isOpen = ref(false)
|
||||
const target = ref<HTMLElement | null>(null) // Ref for the main div (trigger)
|
||||
|
||||
// Use useElementBounding to get position of the trigger element
|
||||
const { x, y, width, height } = useElementBounding(target)
|
||||
|
||||
// Dropdown styles for fixed positioning
|
||||
const dropdownStyles = computed(() => {
|
||||
if (!isOpen.value) return {}
|
||||
return {
|
||||
top: `${y.value + height.value + 8}px`, // 8px for mt-2 from original
|
||||
left: `${x.value}px`,
|
||||
width: `${width.value}px`,
|
||||
}
|
||||
})
|
||||
|
||||
onClickOutside(target, () => isOpen.value = false)
|
||||
|
||||
const selectedLabel = computed(() => {
|
||||
const option = props.options.find(o => o.value === props.modelValue)
|
||||
return option ? option.label : props.modelValue
|
||||
})
|
||||
|
||||
function select(value: string | number) {
|
||||
emit('update:modelValue', value)
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (!props.disabled) {
|
||||
isOpen.value = !isOpen.value
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="target" class="relative">
|
||||
<!-- Trigger -->
|
||||
<div
|
||||
@click="toggle"
|
||||
class="flex items-center justify-between w-full px-4 py-3 bg-gray-50 dark:bg-zinc-800 rounded-xl cursor-pointer border border-transparent transition-all duration-200 outline-none"
|
||||
:class="[
|
||||
disabled ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-100 dark:hover:bg-zinc-700',
|
||||
isOpen ? 'ring-2 ring-blue-500 dark:ring-blue-500 border-transparent' : ''
|
||||
]"
|
||||
>
|
||||
<span class="text-zinc-900 dark:text-white font-medium truncate">{{ selectedLabel }}</span>
|
||||
<ChevronDown
|
||||
class="w-5 h-5 text-gray-400 transition-transform duration-200"
|
||||
:class="{ 'rotate-180': isOpen }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Menu - Teleported to body to escape overflow:hidden parent -->
|
||||
<Teleport to="body">
|
||||
<transition
|
||||
enter-active-class="transition duration-100 ease-out"
|
||||
enter-from-class="transform scale-95 opacity-0"
|
||||
enter-to-class="transform scale-100 opacity-100"
|
||||
leave-active-class="transition duration-75 ease-in"
|
||||
leave-from-class="transform scale-100 opacity-100"
|
||||
leave-to-class="transform scale-95 opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="fixed bg-white dark:bg-zinc-900 border border-gray-100 dark:border-zinc-700 rounded-xl shadow-xl overflow-hidden z-[100] py-1"
|
||||
:style="dropdownStyles"
|
||||
>
|
||||
<div
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
@click="select(option.value)"
|
||||
class="flex items-center justify-between px-4 py-2.5 cursor-pointer transition-colors text-sm"
|
||||
:class="[
|
||||
modelValue === option.value
|
||||
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400'
|
||||
: 'text-zinc-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-zinc-800'
|
||||
]"
|
||||
>
|
||||
<span>{{ option.label }}</span>
|
||||
<Check v-if="modelValue === option.value" class="w-4 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
@@ -2,15 +2,23 @@
|
||||
<script setup lang="ts">
|
||||
import { watch } from 'vue'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { Loader2, ChevronDown } from 'lucide-vue-next'
|
||||
import { Loader2 } from 'lucide-vue-next'
|
||||
import { useQueueStore } from '../stores/queue'
|
||||
import { useSettingsStore } from '../stores/settings'
|
||||
import { useAnalysisStore } from '../stores/analysis'
|
||||
import AppSelect from '../components/ui/AppSelect.vue'
|
||||
|
||||
const queueStore = useQueueStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
const analysisStore = useAnalysisStore()
|
||||
|
||||
const qualityOptions = [
|
||||
{ label: 'Best Quality', value: 'best' },
|
||||
{ label: '1080p', value: '1080' },
|
||||
{ label: '720p', value: '720' },
|
||||
{ label: '480p', value: '480' },
|
||||
]
|
||||
|
||||
// Sync default download path if not set
|
||||
watch(() => settingsStore.settings.download_path, (newPath) => {
|
||||
if (newPath && !analysisStore.options.output_path) {
|
||||
@@ -130,17 +138,11 @@ async function startDownload() {
|
||||
|
||||
<!-- Quality Dropdown -->
|
||||
<div class="relative">
|
||||
<select
|
||||
<AppSelect
|
||||
v-model="analysisStore.options.quality"
|
||||
class="w-full appearance-none bg-gray-50 dark:bg-zinc-800 border-none rounded-xl px-4 py-3 text-zinc-900 dark:text-white focus:ring-2 focus:ring-blue-500 outline-none"
|
||||
:options="qualityOptions"
|
||||
:disabled="analysisStore.options.is_audio_only"
|
||||
>
|
||||
<option value="best">Best Quality</option>
|
||||
<option value="1080">1080p</option>
|
||||
<option value="720">720p</option>
|
||||
<option value="480">480p</option>
|
||||
</select>
|
||||
<ChevronDown class="absolute right-4 top-3.5 w-5 h-5 text-gray-400 pointer-events-none" />
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user