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">
|
<script setup lang="ts">
|
||||||
import { watch } from 'vue'
|
import { watch } from 'vue'
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
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 { useQueueStore } from '../stores/queue'
|
||||||
import { useSettingsStore } from '../stores/settings'
|
import { useSettingsStore } from '../stores/settings'
|
||||||
import { useAnalysisStore } from '../stores/analysis'
|
import { useAnalysisStore } from '../stores/analysis'
|
||||||
|
import AppSelect from '../components/ui/AppSelect.vue'
|
||||||
|
|
||||||
const queueStore = useQueueStore()
|
const queueStore = useQueueStore()
|
||||||
const settingsStore = useSettingsStore()
|
const settingsStore = useSettingsStore()
|
||||||
const analysisStore = useAnalysisStore()
|
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
|
// Sync default download path if not set
|
||||||
watch(() => settingsStore.settings.download_path, (newPath) => {
|
watch(() => settingsStore.settings.download_path, (newPath) => {
|
||||||
if (newPath && !analysisStore.options.output_path) {
|
if (newPath && !analysisStore.options.output_path) {
|
||||||
@@ -130,17 +138,11 @@ async function startDownload() {
|
|||||||
|
|
||||||
<!-- Quality Dropdown -->
|
<!-- Quality Dropdown -->
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<select
|
<AppSelect
|
||||||
v-model="analysisStore.options.quality"
|
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"
|
: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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user