mirror of
https://github.com/TeamPiped/Piped.git
synced 2026-03-28 03:16:58 +00:00
Implement more reka ui and attempt to fix review issues.
This commit is contained in:
@@ -173,6 +173,7 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
color-scheme: only light;
|
color-scheme: only light;
|
||||||
|
scrollbar-color: #4b4f52 #d1d5db;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
@@ -195,10 +196,6 @@ onBeforeUnmount(() => {
|
|||||||
background-color: #0b0e0f;
|
background-color: #0b0e0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
|
||||||
scrollbar-color: #4b4f52 #d1d5db;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark ::-webkit-scrollbar {
|
.dark ::-webkit-scrollbar {
|
||||||
background-color: #15191a;
|
background-color: #15191a;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
|
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
|
||||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
|
||||||
|
--color-light-200: #cacaca;
|
||||||
--color-dark-100: #3f3f46;
|
--color-dark-100: #3f3f46;
|
||||||
--color-dark-300: #323232;
|
--color-dark-300: #323232;
|
||||||
--color-dark-400: #222222;
|
--color-dark-400: #222222;
|
||||||
@@ -33,7 +34,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@utility skip-segment-icon {
|
@utility skip-segment-icon {
|
||||||
font-size: 1.6em !important;
|
font-size: 1.6em;
|
||||||
line-height: inherit !important;
|
line-height: inherit;
|
||||||
margin-left: 0.4em;
|
margin-left: 0.4em;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,9 @@
|
|||||||
<div v-for="(group, index) in channelGroups" :key="group.groupName" class="px-1">
|
<div v-for="(group, index) in channelGroups" :key="group.groupName" class="px-1">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span>{{ group.groupName }}</span>
|
<span>{{ group.groupName }}</span>
|
||||||
<input
|
<UiCheckbox
|
||||||
type="checkbox"
|
:model-value="group.channels.includes(channelId)"
|
||||||
:checked="group.channels.includes(channelId)"
|
@update:model-value="onCheckedChange(index, group)"
|
||||||
@change="onCheckedChange(index, group)"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<hr class="h-1 w-full" />
|
<hr class="h-1 w-full" />
|
||||||
@@ -33,6 +32,7 @@
|
|||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import ModalComponent from "./ModalComponent.vue";
|
import ModalComponent from "./ModalComponent.vue";
|
||||||
import CreateGroupModal from "./CreateGroupModal.vue";
|
import CreateGroupModal from "./CreateGroupModal.vue";
|
||||||
|
import UiCheckbox from "./ui/Checkbox.vue";
|
||||||
import { getChannelGroups, createOrUpdateChannelGroup } from "@/composables/useChannelGroups.js";
|
import { getChannelGroups, createOrUpdateChannelGroup } from "@/composables/useChannelGroups.js";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ const channelId = computed(() => props.item.url.substr(-24));
|
|||||||
|
|
||||||
async function updateSubscribedStatus() {
|
async function updateSubscribedStatus() {
|
||||||
subscribed.value = await fetchSubscriptionStatus(channelId.value);
|
subscribed.value = await fetchSubscriptionStatus(channelId.value);
|
||||||
console.log(subscribed.value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function subscribeHandler() {
|
function subscribeHandler() {
|
||||||
|
|||||||
@@ -275,9 +275,3 @@ onUnmounted(() => {
|
|||||||
window.removeEventListener("scroll", handleScroll);
|
window.removeEventListener("scroll", handleScroll);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.active {
|
|
||||||
border: 0.1rem outset red;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -95,12 +95,10 @@ const isCurrentChapter = index => {
|
|||||||
defineEmits(["seek"]);
|
defineEmits(["seek"]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style scoped>
|
||||||
@reference "../app.css";
|
@reference "../app.css";
|
||||||
|
|
||||||
@layer base {
|
::-webkit-scrollbar {
|
||||||
::-webkit-scrollbar {
|
|
||||||
height: 5px;
|
height: 5px;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<select
|
<select
|
||||||
id="export-format"
|
id="export-format"
|
||||||
v-model="exportAs"
|
v-model="exportAs"
|
||||||
class="h-8 rounded-md bg-gray-300 px-2.5 text-gray-600 dark:bg-dark-400 dark:text-gray-400"
|
class="h-8 rounded-md bg-gray-300 px-2.5 text-gray-600 focus:shadow-red-400 focus:outline-2 focus:outline-red-500 dark:bg-dark-400 dark:text-gray-400"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
v-for="option in exportOptions"
|
v-for="option in exportOptions"
|
||||||
@@ -18,31 +18,24 @@
|
|||||||
/>
|
/>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="exportAs === 'history'">
|
<CheckboxGroupRoot v-if="exportAs === 'history'" v-model="selectedFields">
|
||||||
<label v-for="field in fields" :key="field" class="flex items-center gap-2">
|
<label v-for="field in fields" :key="field" class="flex items-center gap-2">
|
||||||
<input
|
<UiCheckbox :id="`export-field-${field}`" :value="field" :disabled="field === 'videoId'" />
|
||||||
v-model="selectedFields"
|
|
||||||
class="size-4"
|
|
||||||
type="checkbox"
|
|
||||||
:value="field"
|
|
||||||
:disabled="field === 'videoId'"
|
|
||||||
/>
|
|
||||||
<span v-text="formatField(field)" />
|
<span v-text="formatField(field)" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</CheckboxGroupRoot>
|
||||||
</form>
|
</form>
|
||||||
<button
|
<Button v-t="'actions.export'" class="mt-4" @click="handleExport" />
|
||||||
class="mt-4 inline-block w-auto cursor-pointer rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
|
||||||
@click="handleExport"
|
|
||||||
v-text="$t('actions.export')"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</ModalComponent>
|
</ModalComponent>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
|
import { CheckboxGroupRoot } from "reka-ui";
|
||||||
import ModalComponent from "./ModalComponent.vue";
|
import ModalComponent from "./ModalComponent.vue";
|
||||||
|
import Button from "./ui/Button.vue";
|
||||||
|
import UiCheckbox from "./ui/Checkbox.vue";
|
||||||
import { download } from "@/composables/useMisc.js";
|
import { download } from "@/composables/useMisc.js";
|
||||||
|
|
||||||
const exportOptions = ["playlist", "history"];
|
const exportOptions = ["playlist", "history"];
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
id="filters"
|
id="filters"
|
||||||
v-model="selectedFilter"
|
v-model="selectedFilter"
|
||||||
default="all"
|
default="all"
|
||||||
class="h-8 grow bg-gray-300 text-gray-600 dark:bg-dark-400 dark:text-gray-400"
|
class="h-8 grow rounded-md bg-gray-300 text-gray-600 dark:bg-dark-400 dark:text-gray-400"
|
||||||
@change="onFilterChange()"
|
@change="onFilterChange()"
|
||||||
>
|
>
|
||||||
<option v-for="filter in availableFilters" :key="filter" v-t="`video.${filter}`" :value="filter" />
|
<option v-for="filter in availableFilters" :key="filter" v-t="`video.${filter}`" :value="filter" />
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
id="group-selector"
|
id="group-selector"
|
||||||
v-model="selectedGroupName"
|
v-model="selectedGroupName"
|
||||||
default=""
|
default=""
|
||||||
class="h-8 grow bg-gray-300 text-gray-600 dark:bg-dark-400 dark:text-gray-400"
|
class="h-8 grow rounded-md bg-gray-300 text-gray-600 dark:bg-dark-400 dark:text-gray-400"
|
||||||
>
|
>
|
||||||
<option v-t="`video.all`" value="" />
|
<option v-t="`video.all`" value="" />
|
||||||
<option
|
<option
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
footer {
|
footer {
|
||||||
background-color: #cacaca;
|
background-color: var(--color-light-200);
|
||||||
}
|
}
|
||||||
.dark footer {
|
.dark footer {
|
||||||
background-color: var(--color-dark-800);
|
background-color: var(--color-dark-800);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ml-4 flex items-center">
|
<div class="ml-4 flex items-center">
|
||||||
<input id="autoDelete" v-model="autoDeleteHistory" type="checkbox" @change="onChange" />
|
<UiCheckbox id="autoDelete" v-model="autoDeleteHistory" @change="onChange" />
|
||||||
<label v-t="'actions.delete_automatically'" class="ml-2" for="autoDelete" />
|
<label v-t="'actions.delete_automatically'" class="ml-2" for="autoDelete" />
|
||||||
<select
|
<select
|
||||||
v-model="autoDeleteDelayHours"
|
v-model="autoDeleteDelayHours"
|
||||||
@@ -66,6 +66,7 @@ import VideoItem from "./VideoItem.vue";
|
|||||||
import SortingSelector from "./SortingSelector.vue";
|
import SortingSelector from "./SortingSelector.vue";
|
||||||
import ExportHistoryModal from "./ExportHistoryModal.vue";
|
import ExportHistoryModal from "./ExportHistoryModal.vue";
|
||||||
import ImportHistoryModal from "./ImportHistoryModal.vue";
|
import ImportHistoryModal from "./ImportHistoryModal.vue";
|
||||||
|
import UiCheckbox from "./ui/Checkbox.vue";
|
||||||
import { getPreferenceBoolean, getPreferenceString, setPreference } from "@/composables/usePreferences.js";
|
import { getPreferenceBoolean, getPreferenceString, setPreference } from "@/composables/usePreferences.js";
|
||||||
|
|
||||||
let currentVideoCount = 0;
|
let currentVideoCount = 0;
|
||||||
|
|||||||
@@ -19,12 +19,21 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong class="flex items-center justify-center gap-2">
|
<strong class="flex items-center justify-center gap-2">
|
||||||
<span v-t="'actions.override'" />: <input v-model="override" class="size-4" type="checkbox" />
|
<span v-t="'actions.override'" />: <UiCheckbox v-model="override" />
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div>
|
<div>
|
||||||
<progress :value="index" :max="itemsLength" />
|
<ProgressRoot
|
||||||
|
:model-value="itemsLength ? index : 0"
|
||||||
|
:max="itemsLength || 1"
|
||||||
|
class="relative h-2.5 w-full overflow-hidden rounded-full bg-gray-300 dark:bg-dark-400"
|
||||||
|
>
|
||||||
|
<ProgressIndicator
|
||||||
|
class="h-full rounded-full bg-red-500 transition-transform duration-200 ease-out"
|
||||||
|
:style="{ transform: `translateX(-${100 - progressPercent}%)` }"
|
||||||
|
/>
|
||||||
|
</ProgressRoot>
|
||||||
<div
|
<div
|
||||||
v-text="
|
v-text="
|
||||||
`${$t('info.success')}: ${success} ${$t('info.error')}: ${error} ${$t(
|
`${$t('info.success')}: ${success} ${$t('info.error')}: ${error} ${$t(
|
||||||
@@ -47,7 +56,9 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
|
import { ProgressIndicator, ProgressRoot } from "reka-ui";
|
||||||
import ModalComponent from "./ModalComponent.vue";
|
import ModalComponent from "./ModalComponent.vue";
|
||||||
|
import UiCheckbox from "./ui/Checkbox.vue";
|
||||||
|
|
||||||
const fileSelector = ref(null);
|
const fileSelector = ref(null);
|
||||||
const items = ref([]);
|
const items = ref([]);
|
||||||
@@ -58,6 +69,10 @@ const error = ref(0);
|
|||||||
const skipped = ref(0);
|
const skipped = ref(0);
|
||||||
|
|
||||||
const itemsLength = computed(() => items.value.length);
|
const itemsLength = computed(() => items.value.length);
|
||||||
|
const progressPercent = computed(() => {
|
||||||
|
if (!itemsLength.value) return 0;
|
||||||
|
return Math.min((index.value / itemsLength.value) * 100, 100);
|
||||||
|
});
|
||||||
|
|
||||||
function fileChange() {
|
function fileChange() {
|
||||||
const file = fileSelector.value.files[0];
|
const file = fileSelector.value.files[0];
|
||||||
|
|||||||
@@ -8,16 +8,10 @@
|
|||||||
<strong v-text="`${$t('info.selected_subscriptions')}: ${selectedSubscriptions}`" />
|
<strong v-text="`${$t('info.selected_subscriptions')}: ${selectedSubscriptions}`" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong
|
<strong><span v-t="'actions.override'" />: <UiCheckbox v-model="override" /></strong>
|
||||||
><span v-t="'actions.override'" />: <input v-model="override" class="size-4" type="checkbox"
|
|
||||||
/></strong>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a
|
<Button v-t="'actions.import'" @click="handleImport" />
|
||||||
v-t="'actions.import'"
|
|
||||||
class="inline-block w-auto cursor-pointer rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
|
||||||
@click="handleImport"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<br />
|
<br />
|
||||||
@@ -64,6 +58,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onActivated } from "vue";
|
import { ref, computed, onActivated } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
import Button from "./ui/Button.vue";
|
||||||
|
import UiCheckbox from "./ui/Checkbox.vue";
|
||||||
import { fetchJson, authApiUrl, getAuthToken, isAuthenticated } from "@/composables/useApi.js";
|
import { fetchJson, authApiUrl, getAuthToken, isAuthenticated } from "@/composables/useApi.js";
|
||||||
import { getLocalSubscriptions } from "@/composables/useSubscriptions.js";
|
import { getLocalSubscriptions } from "@/composables/useSubscriptions.js";
|
||||||
|
|
||||||
|
|||||||
@@ -29,11 +29,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a
|
<Button v-t="'titles.login'" @click="login" />
|
||||||
v-t="'titles.login'"
|
|
||||||
class="inline-block w-auto cursor-pointer rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
|
||||||
@click="login"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -45,6 +41,7 @@ import { useRouter } from "vue-router";
|
|||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { fetchJson, authApiUrl, getAuthToken, hashCode } from "@/composables/useApi.js";
|
import { fetchJson, authApiUrl, getAuthToken, hashCode } from "@/composables/useApi.js";
|
||||||
import { setPreference } from "@/composables/usePreferences.js";
|
import { setPreference } from "@/composables/usePreferences.js";
|
||||||
|
import Button from "./ui/Button.vue";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|||||||
@@ -1,36 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="fixed top-0 left-0 z-50 table size-full bg-gray-500/80 transition-opacity dark:bg-dark-900/80">
|
<DialogRoot :open="true" @update:open="onOpenChange">
|
||||||
<div class="table-cell align-middle" @click="handleClick">
|
<DialogPortal>
|
||||||
<div class="relative m-auto w-min min-w-[20vw] rounded-xl bg-white p-5 dark:bg-dark-700">
|
<DialogOverlay class="fixed inset-0 z-50 bg-gray-500/80 transition-opacity dark:bg-dark-900/80" />
|
||||||
<button class="absolute top-1 right-2.5" @click="$emit('close')"><i-fa6-solid-xmark /></button>
|
<DialogContent
|
||||||
|
class="fixed top-1/2 left-1/2 z-50 w-min min-w-[20vw] -translate-1/2 rounded-xl bg-white p-5 text-black focus:outline-none dark:bg-dark-700 dark:text-white"
|
||||||
|
>
|
||||||
|
<DialogClose as-child>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="absolute top-1 right-2.5 text-gray-700 hover:text-black dark:text-gray-300 dark:hover:text-white"
|
||||||
|
>
|
||||||
|
<i-fa6-solid-xmark />
|
||||||
|
</button>
|
||||||
|
</DialogClose>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</DialogContent>
|
||||||
</div>
|
</DialogPortal>
|
||||||
</div>
|
</DialogRoot>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, onUnmounted } from "vue";
|
import { DialogClose, DialogContent, DialogOverlay, DialogPortal, DialogRoot } from "reka-ui";
|
||||||
|
|
||||||
const emit = defineEmits(["close"]);
|
const emit = defineEmits(["close"]);
|
||||||
|
|
||||||
function handleKeyDown(event) {
|
function onOpenChange(open) {
|
||||||
if (event.code === "Escape") {
|
if (!open) {
|
||||||
emit("close");
|
emit("close");
|
||||||
} else return;
|
}
|
||||||
event.preventDefault();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClick(event) {
|
|
||||||
if (event.target !== event.currentTarget) return;
|
|
||||||
emit("close");
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener("keydown", handleKeyDown);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -25,16 +25,11 @@
|
|||||||
@focus="onInputFocus"
|
@focus="onInputFocus"
|
||||||
@blur="onInputBlur"
|
@blur="onInputBlur"
|
||||||
/>
|
/>
|
||||||
<span
|
<ClearButton v-if="searchText" @clear="searchText = ''" />
|
||||||
v-if="searchText"
|
|
||||||
class="absolute right-3 size-4 cursor-pointer rounded-full bg-[#ccc] text-center text-[10px] text-black opacity-50 hover:opacity-70"
|
|
||||||
@click="searchText = ''"
|
|
||||||
>⨉</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
id="search-btn"
|
id="search-btn"
|
||||||
class="mx-1 inline-block h-10 w-auto cursor-pointer rounded-sm bg-gray-300 px-2.5 py-2 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
class="mx-1 hidden h-10 w-auto cursor-pointer rounded-sm bg-gray-300 px-2.5 py-2 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 max-[848px]:hidden max-md:px-2 min-[848px]:inline-block md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
||||||
@click="onSearchClick"
|
@click="onSearchClick"
|
||||||
>
|
>
|
||||||
<i-fa6-solid-magnifying-glass />
|
<i-fa6-solid-magnifying-glass />
|
||||||
@@ -134,12 +129,7 @@
|
|||||||
@focus="onInputFocus"
|
@focus="onInputFocus"
|
||||||
@blur="onInputBlur"
|
@blur="onInputBlur"
|
||||||
/>
|
/>
|
||||||
<span
|
<ClearButton v-if="searchText" @clear="searchText = ''" />\n
|
||||||
v-if="searchText"
|
|
||||||
class="absolute right-3 size-4 cursor-pointer rounded-full bg-[#ccc] text-center text-[10px] text-black opacity-50 hover:opacity-70"
|
|
||||||
@click="searchText = ''"
|
|
||||||
>⨉</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<SearchSuggestions
|
<SearchSuggestions
|
||||||
v-show="(searchText || showSearchHistory) && suggestionsVisible"
|
v-show="(searchText || showSearchHistory) && suggestionsVisible"
|
||||||
@@ -153,6 +143,7 @@
|
|||||||
import { ref, computed, watch, onMounted } from "vue";
|
import { ref, computed, watch, onMounted } from "vue";
|
||||||
import { useRouter, useRoute } from "vue-router";
|
import { useRouter, useRoute } from "vue-router";
|
||||||
import SearchSuggestions from "./SearchSuggestions.vue";
|
import SearchSuggestions from "./SearchSuggestions.vue";
|
||||||
|
import ClearButton from "./ui/ClearButton.vue";
|
||||||
import hotkeys from "hotkeys-js";
|
import hotkeys from "hotkeys-js";
|
||||||
import { fetchJson, authApiUrl, getAuthToken } from "@/composables/useApi.js";
|
import { fetchJson, authApiUrl, getAuthToken } from "@/composables/useApi.js";
|
||||||
import { getPreferenceBoolean, getPreferenceString } from "@/composables/usePreferences.js";
|
import { getPreferenceBoolean, getPreferenceString } from "@/composables/usePreferences.js";
|
||||||
@@ -266,10 +257,4 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style></style>
|
||||||
@media screen and (max-width: 848px) {
|
|
||||||
#search-btn {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const homeUrl = import.meta.env.BASE_URL;
|
|||||||
<h2 v-t="'info.page_not_found'" class="text-2xl!" />
|
<h2 v-t="'info.page_not_found'" class="text-2xl!" />
|
||||||
<a
|
<a
|
||||||
v-t="'actions.back_to_home'"
|
v-t="'actions.back_to_home'"
|
||||||
class="mt-16 inline-block w-auto cursor-pointer rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
class="mt-16 inline-block w-auto rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
||||||
:href="homeUrl"
|
:href="homeUrl"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,11 +7,7 @@
|
|||||||
<div class="relative">
|
<div class="relative">
|
||||||
<img loading="lazy" class="w-full" :src="props.item.thumbnail" />
|
<img loading="lazy" class="w-full" :src="props.item.thumbnail" />
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p class="line-clamp-2 pt-2 leading-tight font-bold" :title="props.item.name" v-text="props.item.name" />
|
||||||
class="line-clamp-2 pt-2 leading-tight font-bold hover:text-red-500 focus:text-red-500 dark:hover:text-red-400 dark:focus:text-red-400"
|
|
||||||
:title="props.item.name"
|
|
||||||
v-text="props.item.name"
|
|
||||||
/>
|
|
||||||
</router-link>
|
</router-link>
|
||||||
<p
|
<p
|
||||||
v-if="props.item.description"
|
v-if="props.item.description"
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ onMounted(() => {
|
|||||||
if (json.error) alert(json.error);
|
if (json.error) alert(json.error);
|
||||||
else if (json.some(pl => pl.id === playlistId)) admin.value = true;
|
else if (json.some(pl => pl.id === playlistId)) admin.value = true;
|
||||||
});
|
});
|
||||||
else if (playlistId.startsWith("local")) admin.value = true;
|
else if (playlistId?.startsWith("local")) admin.value = true;
|
||||||
checkPlaylistBookmarked();
|
checkPlaylistBookmarked();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -43,8 +43,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical"
|
class="my-2 line-clamp-2 hover:text-red-500 focus:text-red-500 dark:hover:text-red-400 dark:focus:text-red-400"
|
||||||
class="my-2 flex overflow-hidden hover:text-red-500 focus:text-red-500 dark:hover:text-red-400 dark:focus:text-red-400"
|
|
||||||
:title="playlist.name"
|
:title="playlist.name"
|
||||||
v-text="playlist.name"
|
v-text="playlist.name"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -27,13 +27,7 @@
|
|||||||
:aria-label="$t('login.password')"
|
:aria-label="$t('login.password')"
|
||||||
@keyup.enter="register"
|
@keyup.enter="register"
|
||||||
/>
|
/>
|
||||||
<button
|
<PasswordToggle v-model="showPassword" />
|
||||||
type="button"
|
|
||||||
class="ml-2 inline-block w-auto cursor-pointer rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
|
||||||
@click="showPassword = !showPassword"
|
|
||||||
>
|
|
||||||
<i-fa6-solid-eye />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<input
|
<input
|
||||||
@@ -45,20 +39,10 @@
|
|||||||
:aria-label="$t('login.password_confirm')"
|
:aria-label="$t('login.password_confirm')"
|
||||||
@keyup.enter="register"
|
@keyup.enter="register"
|
||||||
/>
|
/>
|
||||||
<button
|
<PasswordToggle v-model="showConfirmPassword" />
|
||||||
type="button"
|
|
||||||
class="ml-2 inline-block w-auto cursor-pointer rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
|
||||||
@click="showConfirmPassword = !showConfirmPassword"
|
|
||||||
>
|
|
||||||
<i-fa6-solid-eye />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a
|
<Button v-t="'titles.register'" @click="register" />
|
||||||
v-t="'titles.register'"
|
|
||||||
class="inline-block w-auto cursor-pointer rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
|
||||||
@click="register"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -80,6 +64,8 @@ import { useRouter } from "vue-router";
|
|||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { isEmail } from "../utils/Misc.js";
|
import { isEmail } from "../utils/Misc.js";
|
||||||
import ConfirmModal from "./ConfirmModal.vue";
|
import ConfirmModal from "./ConfirmModal.vue";
|
||||||
|
import Button from "./ui/Button.vue";
|
||||||
|
import PasswordToggle from "./ui/PasswordToggle.vue";
|
||||||
import { fetchJson, authApiUrl, getAuthToken, hashCode } from "@/composables/useApi.js";
|
import { fetchJson, authApiUrl, getAuthToken, hashCode } from "@/composables/useApi.js";
|
||||||
import { setPreference } from "@/composables/usePreferences.js";
|
import { setPreference } from "@/composables/usePreferences.js";
|
||||||
|
|
||||||
|
|||||||
@@ -3,16 +3,16 @@
|
|||||||
<h2 v-t="'actions.share'" class="mb-5" />
|
<h2 v-t="'actions.share'" class="mb-5" />
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<label v-t="'actions.piped_link'" />
|
<label v-t="'actions.piped_link'" />
|
||||||
<input v-model="pipedLink" type="checkbox" @change="onChange" />
|
<UiCheckbox v-model="pipedLink" @change="onChange" />
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div v-if="hasPlaylist" class="flex justify-between">
|
<div v-if="hasPlaylist" class="flex justify-between">
|
||||||
<label v-t="'actions.with_playlist'" />
|
<label v-t="'actions.with_playlist'" />
|
||||||
<input v-model="withPlaylist" type="checkbox" @change="onChange" />
|
<UiCheckbox v-model="withPlaylist" @change="onChange" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<label v-t="'actions.with_timecode'" for="withTimeCode" />
|
<label v-t="'actions.with_timecode'" for="withTimeCode" />
|
||||||
<input id="withTimeCode" v-model="withTimeCode" type="checkbox" @change="onChange" />
|
<UiCheckbox id="withTimeCode" v-model="withTimeCode" @change="onChange" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="withTimeCode" class="mt-2 flex items-center justify-between">
|
<div v-if="withTimeCode" class="mt-2 flex items-center justify-between">
|
||||||
<label v-t="'actions.time_code'" />
|
<label v-t="'actions.time_code'" />
|
||||||
@@ -29,21 +29,9 @@
|
|||||||
</a>
|
</a>
|
||||||
<QrCode v-if="showQrCode" :text="generatedLink" />
|
<QrCode v-if="showQrCode" :text="generatedLink" />
|
||||||
<div class="mt-4 flex justify-end">
|
<div class="mt-4 flex justify-end">
|
||||||
<button
|
<Button v-t="'actions.generate_qrcode'" @click="showQrCode = !showQrCode" />
|
||||||
v-t="'actions.generate_qrcode'"
|
<Button v-t="'actions.follow_link'" class="ml-3" @click="followLink()" />
|
||||||
class="inline-block w-auto cursor-pointer rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
<Button v-t="'actions.copy_link'" class="ml-3" @click="copyLink()" />
|
||||||
@click="showQrCode = !showQrCode"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
v-t="'actions.follow_link'"
|
|
||||||
class="ml-3 inline-block w-auto cursor-pointer rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
|
||||||
@click="followLink()"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
v-t="'actions.copy_link'"
|
|
||||||
class="ml-3 inline-block w-auto cursor-pointer rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
|
||||||
@click="copyLink()"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</ModalComponent>
|
</ModalComponent>
|
||||||
</template>
|
</template>
|
||||||
@@ -52,6 +40,8 @@
|
|||||||
import { ref, computed, onMounted, defineAsyncComponent } from "vue";
|
import { ref, computed, onMounted, defineAsyncComponent } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import ModalComponent from "./ModalComponent.vue";
|
import ModalComponent from "./ModalComponent.vue";
|
||||||
|
import Button from "./ui/Button.vue";
|
||||||
|
import UiCheckbox from "./ui/Checkbox.vue";
|
||||||
import { getPreferenceBoolean, setPreference } from "@/composables/usePreferences.js";
|
import { getPreferenceBoolean, setPreference } from "@/composables/usePreferences.js";
|
||||||
|
|
||||||
const QrCode = defineAsyncComponent(() => import("./QrCode.vue"));
|
const QrCode = defineAsyncComponent(() => import("./QrCode.vue"));
|
||||||
|
|||||||
@@ -126,11 +126,9 @@
|
|||||||
<img :src="subscription.avatar" class="size-8 rounded-full" />
|
<img :src="subscription.avatar" class="size-8 rounded-full" />
|
||||||
<span class="ml-2">{{ subscription.name }}</span>
|
<span class="ml-2">{{ subscription.name }}</span>
|
||||||
</a>
|
</a>
|
||||||
<input
|
<UiCheckbox
|
||||||
type="checkbox"
|
:model-value="selectedGroup.channels.includes(subscription.url.substr(-24))"
|
||||||
class="size-4"
|
@update:model-value="checkedChange(subscription)"
|
||||||
:checked="selectedGroup.channels.includes(subscription.url.substr(-24))"
|
|
||||||
@change="checkedChange(subscription)"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
@@ -144,6 +142,7 @@ import { ref, computed, onMounted, onActivated } from "vue";
|
|||||||
import ModalComponent from "./ModalComponent.vue";
|
import ModalComponent from "./ModalComponent.vue";
|
||||||
import CreateGroupModal from "./CreateGroupModal.vue";
|
import CreateGroupModal from "./CreateGroupModal.vue";
|
||||||
import ConfirmModal from "./ConfirmModal.vue";
|
import ConfirmModal from "./ConfirmModal.vue";
|
||||||
|
import UiCheckbox from "./ui/Checkbox.vue";
|
||||||
import { fetchJson, authApiUrl, getAuthToken, isAuthenticated } from "@/composables/useApi.js";
|
import { fetchJson, authApiUrl, getAuthToken, isAuthenticated } from "@/composables/useApi.js";
|
||||||
import { fetchSubscriptions, handleLocalSubscriptions } from "@/composables/useSubscriptions.js";
|
import { fetchSubscriptions, handleLocalSubscriptions } from "@/composables/useSubscriptions.js";
|
||||||
import { getChannelGroups, createOrUpdateChannelGroup, deleteChannelGroup } from "@/composables/useChannelGroups.js";
|
import { getChannelGroups, createOrUpdateChannelGroup, deleteChannelGroup } from "@/composables/useChannelGroups.js";
|
||||||
|
|||||||
@@ -16,8 +16,7 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p
|
<p
|
||||||
style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical"
|
class="line-clamp-2 pt-2 leading-tight font-bold hover:text-red-500 focus:text-red-500 dark:hover:text-red-400 dark:focus:text-red-400"
|
||||||
class="flex overflow-hidden pt-2 leading-tight font-bold hover:text-red-500 focus:text-red-500 dark:hover:text-red-400 dark:focus:text-red-400"
|
|
||||||
:title="title"
|
:title="title"
|
||||||
v-text="title"
|
v-text="title"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
class="aspect-video w-full rounded-md object-contain"
|
class="aspect-video w-full rounded-md object-contain"
|
||||||
:src="item.thumbnail"
|
:src="item.thumbnail"
|
||||||
:alt="item.title"
|
:alt="item.title"
|
||||||
:class="{ 'w-full object-contain': item.isShort, 'opacity-75': item.watched }"
|
:class="{ 'opacity-75': item.watched }"
|
||||||
/>
|
/>
|
||||||
<!-- progress bar -->
|
<!-- progress bar -->
|
||||||
<div class="relative h-1 w-full">
|
<div class="relative h-1 w-full">
|
||||||
|
|||||||
@@ -138,7 +138,7 @@
|
|||||||
class="inline-block w-auto cursor-pointer rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 max-md:hidden max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
class="inline-block w-auto cursor-pointer rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 max-md:hidden max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
||||||
@click="downloadCurrentFrame"
|
@click="downloadCurrentFrame"
|
||||||
>
|
>
|
||||||
{{ $t("actions.download_frame") }}<i-fa6-solid-download />
|
{{ $t("actions.download_frame") }}<i-fa6-solid-download class="ml-1" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="inline-block w-auto cursor-pointer rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
class="inline-block w-auto cursor-pointer rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
||||||
@@ -226,7 +226,7 @@
|
|||||||
v-show="video?.chapters?.length > 0"
|
v-show="video?.chapters?.length > 0"
|
||||||
class="ml-2 inline-block w-auto cursor-default rounded-sm bg-gray-300 py-2 text-gray-600 max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400"
|
class="ml-2 inline-block w-auto cursor-default rounded-sm bg-gray-300 py-2 text-gray-600 max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400"
|
||||||
>
|
>
|
||||||
<input id="showChapters" v-model="showChapters" type="checkbox" />
|
<UiCheckbox id="showChapters" v-model="showChapters" />
|
||||||
<label v-t="'actions.show_chapters'" class="ml-2" for="showChapters" />
|
<label v-t="'actions.show_chapters'" class="ml-2" for="showChapters" />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -247,7 +247,7 @@
|
|||||||
<router-link
|
<router-link
|
||||||
v-for="tag in video.tags"
|
v-for="tag in video.tags"
|
||||||
:key="tag"
|
:key="tag"
|
||||||
class="line-clamp-1 inline-block w-auto cursor-pointer rounded-sm rounded-s bg-gray-300 px-2 py-1 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
class="line-clamp-1 inline-block w-auto cursor-pointer rounded-sm bg-gray-300 px-2 py-1 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
||||||
:to="`/results?search_query=${encodeURIComponent(tag)}`"
|
:to="`/results?search_query=${encodeURIComponent(tag)}`"
|
||||||
>{{ tag }}</router-link
|
>{{ tag }}</router-link
|
||||||
>
|
>
|
||||||
@@ -258,13 +258,7 @@
|
|||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<label for="chkAutoLoop"><strong v-text="`${$t('actions.loop_this_video')}:`" /></label>
|
<label for="chkAutoLoop"><strong v-text="`${$t('actions.loop_this_video')}:`" /></label>
|
||||||
<input
|
<UiCheckbox id="chkAutoLoop" v-model="selectedAutoLoop" class="ml-1.5" @change="onChange($event)" />
|
||||||
id="chkAutoLoop"
|
|
||||||
v-model="selectedAutoLoop"
|
|
||||||
class="ml-1.5"
|
|
||||||
type="checkbox"
|
|
||||||
@change="onChange($event)"
|
|
||||||
/>
|
|
||||||
<br />
|
<br />
|
||||||
<label for="chkAutoPlay"><strong v-text="`${$t('actions.auto_play_next_video')}:`" /></label>
|
<label for="chkAutoPlay"><strong v-text="`${$t('actions.auto_play_next_video')}:`" /></label>
|
||||||
<select
|
<select
|
||||||
@@ -383,6 +377,7 @@ import PlaylistVideos from "./PlaylistVideos.vue";
|
|||||||
import WatchOnButton from "./WatchOnButton.vue";
|
import WatchOnButton from "./WatchOnButton.vue";
|
||||||
import LoadingIndicatorPage from "./LoadingIndicatorPage.vue";
|
import LoadingIndicatorPage from "./LoadingIndicatorPage.vue";
|
||||||
import ToastComponent from "./ToastComponent.vue";
|
import ToastComponent from "./ToastComponent.vue";
|
||||||
|
import UiCheckbox from "./ui/Checkbox.vue";
|
||||||
import { parseTimeParam } from "@/utils/Misc";
|
import { parseTimeParam } from "@/utils/Misc";
|
||||||
import { purifyHTML, rewriteDescription } from "@/utils/HtmlUtils";
|
import { purifyHTML, rewriteDescription } from "@/utils/HtmlUtils";
|
||||||
import { fetchJson, apiUrl } from "@/composables/useApi.js";
|
import { fetchJson, apiUrl } from "@/composables/useApi.js";
|
||||||
|
|||||||
36
src/components/ui/Button.vue
Normal file
36
src/components/ui/Button.vue
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<template>
|
||||||
|
<button
|
||||||
|
:type="type"
|
||||||
|
:class="[
|
||||||
|
'inline-block w-auto cursor-pointer rounded-sm py-2 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 max-md:px-2 md:px-4 dark:text-gray-400 dark:hover:bg-dark-300',
|
||||||
|
variant === 'primary' ? 'bg-gray-300 dark:bg-dark-400' : 'bg-gray-300 dark:bg-dark-400',
|
||||||
|
customClass,
|
||||||
|
]"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineOptions({
|
||||||
|
name: "UiButton",
|
||||||
|
});
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: "button",
|
||||||
|
validator: val => ["button", "submit"].includes(val),
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
type: String,
|
||||||
|
default: "primary",
|
||||||
|
validator: val => ["primary", "secondary"].includes(val),
|
||||||
|
},
|
||||||
|
customClass: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
75
src/components/ui/Checkbox.vue
Normal file
75
src/components/ui/Checkbox.vue
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<CheckboxRoot
|
||||||
|
v-if="hasModelValue"
|
||||||
|
:id="id"
|
||||||
|
:model-value="modelValue"
|
||||||
|
:value="value"
|
||||||
|
:disabled="disabled"
|
||||||
|
:class="[
|
||||||
|
'inline-flex size-4 shrink-0 items-center justify-center rounded-sm border border-gray-500 bg-gray-300 text-white outline-none focus:shadow-red-400 focus:outline-2 focus:outline-red-500 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-red-500 data-[state=checked]:bg-red-500 dark:border-gray-400 dark:bg-dark-400 dark:data-[state=checked]:border-red-400 dark:data-[state=checked]:bg-red-400',
|
||||||
|
customClass,
|
||||||
|
]"
|
||||||
|
@update:model-value="handleUpdate"
|
||||||
|
>
|
||||||
|
<CheckboxIndicator class="inline-flex items-center justify-center text-current">
|
||||||
|
<i-fa6-solid-check class="size-3" />
|
||||||
|
</CheckboxIndicator>
|
||||||
|
</CheckboxRoot>
|
||||||
|
|
||||||
|
<CheckboxRoot
|
||||||
|
v-else
|
||||||
|
:id="id"
|
||||||
|
:value="value"
|
||||||
|
:disabled="disabled"
|
||||||
|
:class="[
|
||||||
|
'inline-flex size-4 shrink-0 items-center justify-center rounded-sm border border-gray-500 bg-gray-300 text-white outline-none focus:shadow-red-400 focus:outline-2 focus:outline-red-500 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-red-500 data-[state=checked]:bg-red-500 dark:border-gray-400 dark:bg-dark-400 dark:data-[state=checked]:border-red-400 dark:data-[state=checked]:bg-red-400',
|
||||||
|
customClass,
|
||||||
|
]"
|
||||||
|
@update:model-value="handleUpdate"
|
||||||
|
>
|
||||||
|
<CheckboxIndicator class="inline-flex items-center justify-center text-current">
|
||||||
|
<i-fa6-solid-check class="size-3" />
|
||||||
|
</CheckboxIndicator>
|
||||||
|
</CheckboxRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { CheckboxRoot, CheckboxIndicator } from "reka-ui";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "UiCheckbox",
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: [Boolean, String, Number, Array],
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: [Boolean, String, Number],
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
customClass: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:modelValue", "change"]);
|
||||||
|
|
||||||
|
const hasModelValue = computed(() => props.modelValue !== undefined);
|
||||||
|
|
||||||
|
function handleUpdate(value) {
|
||||||
|
emit("update:modelValue", value);
|
||||||
|
emit("change", value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
17
src/components/ui/ClearButton.vue
Normal file
17
src/components/ui/ClearButton.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<template>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="absolute right-3 size-4 cursor-pointer rounded-full bg-gray-300 text-center text-[10px] text-black opacity-50 hover:opacity-70 dark:bg-gray-400"
|
||||||
|
:aria-label="$t('actions.clear', { count: 0 }) || 'Clear'"
|
||||||
|
@click="$emit('clear')"
|
||||||
|
>
|
||||||
|
⨉
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineEmits(["clear"]);
|
||||||
|
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
const { t: $t } = useI18n();
|
||||||
|
</script>
|
||||||
21
src/components/ui/PasswordToggle.vue
Normal file
21
src/components/ui/PasswordToggle.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ml-2 inline-block w-auto cursor-pointer rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
||||||
|
@click="$emit('update:modelValue', !modelValue)"
|
||||||
|
>
|
||||||
|
<i-fa6-solid-eye v-if="!modelValue" />
|
||||||
|
<i-fa6-solid-eye-slash v-else />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits(["update:modelValue"]);
|
||||||
|
</script>
|
||||||
@@ -23,11 +23,12 @@ function getOrCreatePreferenceRef(key, createRef) {
|
|||||||
return preferenceRef;
|
return preferenceRef;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPreferenceRefForValue(key, value) {
|
function createPreferenceRefForValue(key, valueForTypeInference) {
|
||||||
if (typeof value === "string" || value === undefined) return usePreferenceString(key);
|
if (typeof valueForTypeInference === "string" || valueForTypeInference === undefined)
|
||||||
if (typeof value === "boolean") return usePreferenceBoolean(key, value);
|
return usePreferenceString(key);
|
||||||
if (typeof value === "number") return usePreferenceNumber(key, value);
|
if (typeof valueForTypeInference === "boolean") return usePreferenceBoolean(key);
|
||||||
return usePreferenceJSON(key, value);
|
if (typeof valueForTypeInference === "number") return usePreferenceNumber(key);
|
||||||
|
return usePreferenceJSON(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePreferenceString(key, defaultVal) {
|
export function usePreferenceString(key, defaultVal) {
|
||||||
|
|||||||
Reference in New Issue
Block a user