315 lines
9.5 KiB
Vue
315 lines
9.5 KiB
Vue
<!-- ui/app/pages/options.vue -->
|
|
<template>
|
|
<div class="p-1 container" style="border-radius: 0; padding: 0">
|
|
<div class="box m-2">
|
|
<div class="columns is-multiline is-vcentered">
|
|
<div class="column is-12 is-6-desktop">
|
|
<label class="label is-small">Search</label>
|
|
<div class="control has-icons-left">
|
|
<input
|
|
v-model.trim="filters.query"
|
|
type="text"
|
|
class="input"
|
|
placeholder="Filter by flag or description..."
|
|
autocomplete="off"
|
|
/>
|
|
<span class="icon is-left"><i class="fa-solid fa-magnifying-glass" /></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="column is-6-tablet is-3-desktop">
|
|
<label class="label is-small">Group Filter</label>
|
|
<div class="select is-fullwidth">
|
|
<select v-model="filters.group">
|
|
<option value="">All</option>
|
|
<option v-for="g in groupNames" :key="g" :value="g">{{ g }}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="column is-6-tablet is-3-desktop">
|
|
<label class="label is-small">Display</label>
|
|
<div class="control">
|
|
<div class="select is-fullwidth">
|
|
<select v-model="displayMode">
|
|
<option value="grouped">Grouped</option>
|
|
<option value="list">List</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="column is-6-tablet is-3-desktop">
|
|
<label class="label is-small">Sort By</label>
|
|
<div class="select is-fullwidth">
|
|
<select v-model="sortBy">
|
|
<option value="flag">Flag</option>
|
|
<option value="group">Group</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="column is-6-tablet is-3-desktop">
|
|
<label class="label is-small">Order</label>
|
|
<div class="select is-fullwidth">
|
|
<select v-model="sortDir">
|
|
<option value="asc">Asc</option>
|
|
<option value="desc">Desc</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="column is-12 is-6-desktop">
|
|
<label class="label is-small">Flags</label>
|
|
<div class="buttons are-small">
|
|
<button
|
|
class="button"
|
|
:class="{ 'is-link': filters.flagKind === 'any' }"
|
|
@click="filters.flagKind = 'any'"
|
|
>
|
|
Any
|
|
</button>
|
|
<button
|
|
class="button"
|
|
:class="{ 'is-link': filters.flagKind === 'short' }"
|
|
@click="filters.flagKind = 'short'"
|
|
>
|
|
Short Only (-x)
|
|
</button>
|
|
<button
|
|
class="button"
|
|
:class="{ 'is-link': filters.flagKind === 'long' }"
|
|
@click="filters.flagKind = 'long'"
|
|
>
|
|
Long Only (--xyz)
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="0 === visible.length" class="has-text-centered has-text-grey">
|
|
<p>No options match your criteria.</p>
|
|
</div>
|
|
|
|
<template v-if="'grouped' === displayMode && 0 !== grouped.length">
|
|
<section v-for="group in grouped" :key="group.name" class="m-2 mb-5">
|
|
<h2 class="title is-6 mb-3">
|
|
<span class="icon-text">
|
|
<span class="icon"><i class="fa-regular fa-folder-open" /></span>
|
|
<span
|
|
>{{ group.name }}
|
|
<small class="has-text-grey">({{ group.items.length }})</small></span
|
|
>
|
|
</span>
|
|
</h2>
|
|
<div class="table-container">
|
|
<table class="table is-fullwidth is-striped is-hoverable is-bordered">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 30%">Flags</th>
|
|
<th>Description</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template v-for="opt in group.items" :key="opt.flags.join('|')">
|
|
<tr v-if="!opt.ignored">
|
|
<td>
|
|
<i
|
|
class="has-text-primary is-pointer is-pulled-right fa-regular fa-copy is-unselectable"
|
|
@click="copyFlag(opt.flags)"
|
|
/>
|
|
<div class="is-flex is-align-items-center">
|
|
<div class="tags">
|
|
<span v-for="f in opt.flags" :key="f" class="tag is-info">{{ f }}</span>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span v-if="opt.description && 0 !== opt.description.length">{{
|
|
opt.description
|
|
}}</span>
|
|
<span v-else class="has-text-grey">—</span>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<div class="table-container">
|
|
<table class="table is-fullwidth is-striped is-hoverable is-bordered m-2">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 30%">Flags</th>
|
|
<th style="width: 20%">Group</th>
|
|
<th>Description</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template v-for="opt in visible" :key="opt.flags.join('|')">
|
|
<tr v-if="!opt.ignored">
|
|
<td>
|
|
<i
|
|
class="has-text-primary is-pointer is-pulled-right fa-regular fa-copy is-unselectable"
|
|
@click="copyFlag(opt.flags)"
|
|
/>
|
|
<div class="is-flex is-align-items-center">
|
|
<div class="tags">
|
|
<span v-for="f in opt.flags" :key="f" class="tag is-info">{{ f }}</span>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="is-bold">
|
|
{{ opt.group || 'root' }}
|
|
</td>
|
|
<td>
|
|
<span v-if="opt.description && 0 !== opt.description.length">{{
|
|
opt.description
|
|
}}</span>
|
|
<span v-else class="has-text-grey">—</span>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useStorage } from '@vueuse/core';
|
|
import type { YTDLPOption } from '~/types/ytdlp';
|
|
|
|
const isLoading = ref(false);
|
|
const options = ref<YTDLPOption[]>([]);
|
|
const displayMode = useStorage<'grouped' | 'list'>('opts_display', 'grouped');
|
|
const sortBy = useStorage<'flag' | 'group'>('opts_sort_by', 'flag');
|
|
const sortDir = useStorage<'asc' | 'desc'>('opts_sort_dir', 'asc');
|
|
|
|
const filters = reactive({
|
|
query: '',
|
|
group: '',
|
|
flagKind: 'any' as 'any' | 'short' | 'long',
|
|
});
|
|
|
|
const reload = async (): Promise<void> => {
|
|
try {
|
|
isLoading.value = true;
|
|
const resp = await request('/api/yt-dlp/options');
|
|
if (!resp.ok) {
|
|
return;
|
|
}
|
|
const data = await resp.json();
|
|
if (Array.isArray(data)) {
|
|
options.value = data as YTDLPOption[];
|
|
}
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
};
|
|
|
|
const copyFlag = async (flags: string[]): Promise<void> => {
|
|
const longFlag = flags.find((f) => f.startsWith('--'));
|
|
if (!longFlag) {
|
|
return;
|
|
}
|
|
copyText(longFlag);
|
|
};
|
|
|
|
onMounted(async () => await reload());
|
|
|
|
const groupNames = computed<string[]>(() => {
|
|
const s = new Set<string>();
|
|
for (const o of options.value) {
|
|
s.add(o.group || 'root');
|
|
}
|
|
return Array.from(s).sort((a, b) => a.localeCompare(b));
|
|
});
|
|
|
|
const filtered = computed<YTDLPOption[]>(() => {
|
|
const q = filters.query.toLowerCase();
|
|
const g = filters.group;
|
|
|
|
return options.value.filter((o) => {
|
|
if (g && (o.group || 'root') !== g) {
|
|
return false;
|
|
}
|
|
|
|
if ('short' === filters.flagKind && !o.flags.some((f) => /^-\w(,|$)|^-\w$/.test(f))) {
|
|
return false;
|
|
}
|
|
|
|
if ('long' === filters.flagKind && !o.flags.some((f) => /^--[a-zA-Z0-9][\w-]*/.test(f))) {
|
|
return false;
|
|
}
|
|
|
|
if (0 !== q.length) {
|
|
const hay = [o.flags.join(' '), o.description || '', o.group || 'root']
|
|
.join(' ')
|
|
.toLowerCase();
|
|
if (-1 === hay.indexOf(q)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
});
|
|
|
|
const sorted = computed<YTDLPOption[]>(() => {
|
|
const dir = 'asc' === sortDir.value ? 1 : -1;
|
|
const arr = [...filtered.value];
|
|
|
|
arr.sort((a, b) => {
|
|
if ('group' === sortBy.value) {
|
|
const ga = (a.group || 'root').localeCompare(b.group || 'root');
|
|
if (0 !== ga) {
|
|
return ga * dir;
|
|
}
|
|
}
|
|
|
|
const fa = (a.flags[0] || '').localeCompare(b.flags[0] || '');
|
|
return fa * dir;
|
|
});
|
|
|
|
return arr;
|
|
});
|
|
|
|
const visible = computed<YTDLPOption[]>(() => sorted.value);
|
|
|
|
const grouped = computed<{ name: string; items: YTDLPOption[] }[]>(() => {
|
|
const map = new Map<string, YTDLPOption[]>();
|
|
|
|
for (const o of visible.value) {
|
|
const key = o.group || 'root';
|
|
if (!map.has(key)) {
|
|
map.set(key, []);
|
|
}
|
|
map.get(key)!.push(o);
|
|
}
|
|
|
|
const dir = 'asc' === sortDir.value ? 1 : -1;
|
|
const out = Array.from(map.entries()).map(([name, items]) => ({ name, items }));
|
|
|
|
if ('group' === sortBy.value) {
|
|
out.sort((a, b) => a.name.localeCompare(b.name) * dir);
|
|
} else {
|
|
// When sorting by flag, groups should still be in alphabetical order
|
|
out.sort((a, b) => a.name.localeCompare(b.name));
|
|
}
|
|
|
|
return out;
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.table td,
|
|
.table th {
|
|
vertical-align: top;
|
|
}
|
|
</style>
|