Implement more reka ui and attempt to fix review issues.

This commit is contained in:
Kavin
2026-03-27 13:36:04 +05:30
parent d1ef96e7d4
commit 75201a8083
30 changed files with 256 additions and 170 deletions

View File

@@ -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;
} }

View File

@@ -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;
} }

View File

@@ -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({

View File

@@ -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() {

View File

@@ -275,9 +275,3 @@ onUnmounted(() => {
window.removeEventListener("scroll", handleScroll); window.removeEventListener("scroll", handleScroll);
}); });
</script> </script>
<style>
.active {
border: 0.1rem outset red;
}
</style>

View File

@@ -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>

View File

@@ -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"];

View File

@@ -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

View File

@@ -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);

View File

@@ -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;

View File

@@ -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];

View File

@@ -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";

View File

@@ -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();

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"

View File

@@ -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();
}); });

View File

@@ -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"
/> />

View File

@@ -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";

View File

@@ -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"));

View File

@@ -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";

View File

@@ -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"
/> />

View File

@@ -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">

View File

@@ -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";

View 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>

View 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>

View 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>

View 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>

View File

@@ -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) {