mirror of
https://github.com/TeamPiped/Piped.git
synced 2026-03-27 19:07:02 +00:00
Implement more reka ui and attempt to fix review issues.
This commit is contained in:
@@ -173,6 +173,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
:root {
|
||||
color-scheme: only light;
|
||||
scrollbar-color: #4b4f52 #d1d5db;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@@ -195,10 +196,6 @@ onBeforeUnmount(() => {
|
||||
background-color: #0b0e0f;
|
||||
}
|
||||
|
||||
:root {
|
||||
scrollbar-color: #4b4f52 #d1d5db;
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar {
|
||||
background-color: #15191a;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
--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";
|
||||
|
||||
--color-light-200: #cacaca;
|
||||
--color-dark-100: #3f3f46;
|
||||
--color-dark-300: #323232;
|
||||
--color-dark-400: #222222;
|
||||
@@ -33,7 +34,7 @@
|
||||
}
|
||||
|
||||
@utility skip-segment-icon {
|
||||
font-size: 1.6em !important;
|
||||
line-height: inherit !important;
|
||||
font-size: 1.6em;
|
||||
line-height: inherit;
|
||||
margin-left: 0.4em;
|
||||
}
|
||||
|
||||
@@ -6,10 +6,9 @@
|
||||
<div v-for="(group, index) in channelGroups" :key="group.groupName" class="px-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>{{ group.groupName }}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="group.channels.includes(channelId)"
|
||||
@change="onCheckedChange(index, group)"
|
||||
<UiCheckbox
|
||||
:model-value="group.channels.includes(channelId)"
|
||||
@update:model-value="onCheckedChange(index, group)"
|
||||
/>
|
||||
</div>
|
||||
<hr class="h-1 w-full" />
|
||||
@@ -33,6 +32,7 @@
|
||||
import { ref, onMounted } from "vue";
|
||||
import ModalComponent from "./ModalComponent.vue";
|
||||
import CreateGroupModal from "./CreateGroupModal.vue";
|
||||
import UiCheckbox from "./ui/Checkbox.vue";
|
||||
import { getChannelGroups, createOrUpdateChannelGroup } from "@/composables/useChannelGroups.js";
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
@@ -69,7 +69,6 @@ const channelId = computed(() => props.item.url.substr(-24));
|
||||
|
||||
async function updateSubscribedStatus() {
|
||||
subscribed.value = await fetchSubscriptionStatus(channelId.value);
|
||||
console.log(subscribed.value);
|
||||
}
|
||||
|
||||
function subscribeHandler() {
|
||||
|
||||
@@ -275,9 +275,3 @@ onUnmounted(() => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.active {
|
||||
border: 0.1rem outset red;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -95,12 +95,10 @@ const isCurrentChapter = index => {
|
||||
defineEmits(["seek"]);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style scoped>
|
||||
@reference "../app.css";
|
||||
|
||||
@layer base {
|
||||
::-webkit-scrollbar {
|
||||
height: 5px;
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
height: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<select
|
||||
id="export-format"
|
||||
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
|
||||
v-for="option in exportOptions"
|
||||
@@ -18,31 +18,24 @@
|
||||
/>
|
||||
</select>
|
||||
</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">
|
||||
<input
|
||||
v-model="selectedFields"
|
||||
class="size-4"
|
||||
type="checkbox"
|
||||
:value="field"
|
||||
:disabled="field === 'videoId'"
|
||||
/>
|
||||
<UiCheckbox :id="`export-field-${field}`" :value="field" :disabled="field === 'videoId'" />
|
||||
<span v-text="formatField(field)" />
|
||||
</label>
|
||||
</div>
|
||||
</CheckboxGroupRoot>
|
||||
</form>
|
||||
<button
|
||||
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')"
|
||||
/>
|
||||
<Button v-t="'actions.export'" class="mt-4" @click="handleExport" />
|
||||
</div>
|
||||
</ModalComponent>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { CheckboxGroupRoot } from "reka-ui";
|
||||
import ModalComponent from "./ModalComponent.vue";
|
||||
import Button from "./ui/Button.vue";
|
||||
import UiCheckbox from "./ui/Checkbox.vue";
|
||||
import { download } from "@/composables/useMisc.js";
|
||||
|
||||
const exportOptions = ["playlist", "history"];
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
id="filters"
|
||||
v-model="selectedFilter"
|
||||
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()"
|
||||
>
|
||||
<option v-for="filter in availableFilters" :key="filter" v-t="`video.${filter}`" :value="filter" />
|
||||
@@ -25,7 +25,7 @@
|
||||
id="group-selector"
|
||||
v-model="selectedGroupName"
|
||||
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
|
||||
|
||||
@@ -63,7 +63,7 @@ onMounted(() => {
|
||||
|
||||
@layer components {
|
||||
footer {
|
||||
background-color: #cacaca;
|
||||
background-color: var(--color-light-200);
|
||||
}
|
||||
.dark footer {
|
||||
background-color: var(--color-dark-800);
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</div>
|
||||
|
||||
<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" />
|
||||
<select
|
||||
v-model="autoDeleteDelayHours"
|
||||
@@ -66,6 +66,7 @@ import VideoItem from "./VideoItem.vue";
|
||||
import SortingSelector from "./SortingSelector.vue";
|
||||
import ExportHistoryModal from "./ExportHistoryModal.vue";
|
||||
import ImportHistoryModal from "./ImportHistoryModal.vue";
|
||||
import UiCheckbox from "./ui/Checkbox.vue";
|
||||
import { getPreferenceBoolean, getPreferenceString, setPreference } from "@/composables/usePreferences.js";
|
||||
|
||||
let currentVideoCount = 0;
|
||||
|
||||
@@ -19,12 +19,21 @@
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
<br />
|
||||
<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
|
||||
v-text="
|
||||
`${$t('info.success')}: ${success} ${$t('info.error')}: ${error} ${$t(
|
||||
@@ -47,7 +56,9 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import { ProgressIndicator, ProgressRoot } from "reka-ui";
|
||||
import ModalComponent from "./ModalComponent.vue";
|
||||
import UiCheckbox from "./ui/Checkbox.vue";
|
||||
|
||||
const fileSelector = ref(null);
|
||||
const items = ref([]);
|
||||
@@ -58,6 +69,10 @@ const error = ref(0);
|
||||
const skipped = ref(0);
|
||||
|
||||
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() {
|
||||
const file = fileSelector.value.files[0];
|
||||
|
||||
@@ -8,16 +8,10 @@
|
||||
<strong v-text="`${$t('info.selected_subscriptions')}: ${selectedSubscriptions}`" />
|
||||
</div>
|
||||
<div>
|
||||
<strong
|
||||
><span v-t="'actions.override'" />: <input v-model="override" class="size-4" type="checkbox"
|
||||
/></strong>
|
||||
<strong><span v-t="'actions.override'" />: <UiCheckbox v-model="override" /></strong>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
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"
|
||||
/>
|
||||
<Button v-t="'actions.import'" @click="handleImport" />
|
||||
</div>
|
||||
</form>
|
||||
<br />
|
||||
@@ -64,6 +58,8 @@
|
||||
<script setup>
|
||||
import { ref, computed, onActivated } from "vue";
|
||||
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 { getLocalSubscriptions } from "@/composables/useSubscriptions.js";
|
||||
|
||||
|
||||
@@ -29,11 +29,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
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"
|
||||
/>
|
||||
<Button v-t="'titles.login'" @click="login" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -45,6 +41,7 @@ import { useRouter } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { fetchJson, authApiUrl, getAuthToken, hashCode } from "@/composables/useApi.js";
|
||||
import { setPreference } from "@/composables/usePreferences.js";
|
||||
import Button from "./ui/Button.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -1,36 +1,32 @@
|
||||
<template>
|
||||
<div class="fixed top-0 left-0 z-50 table size-full bg-gray-500/80 transition-opacity dark:bg-dark-900/80">
|
||||
<div class="table-cell align-middle" @click="handleClick">
|
||||
<div class="relative m-auto w-min min-w-[20vw] rounded-xl bg-white p-5 dark:bg-dark-700">
|
||||
<button class="absolute top-1 right-2.5" @click="$emit('close')"><i-fa6-solid-xmark /></button>
|
||||
<DialogRoot :open="true" @update:open="onOpenChange">
|
||||
<DialogPortal>
|
||||
<DialogOverlay class="fixed inset-0 z-50 bg-gray-500/80 transition-opacity dark:bg-dark-900/80" />
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</DialogRoot>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted } from "vue";
|
||||
import { DialogClose, DialogContent, DialogOverlay, DialogPortal, DialogRoot } from "reka-ui";
|
||||
|
||||
const emit = defineEmits(["close"]);
|
||||
|
||||
function handleKeyDown(event) {
|
||||
if (event.code === "Escape") {
|
||||
function onOpenChange(open) {
|
||||
if (!open) {
|
||||
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>
|
||||
|
||||
@@ -25,16 +25,11 @@
|
||||
@focus="onInputFocus"
|
||||
@blur="onInputBlur"
|
||||
/>
|
||||
<span
|
||||
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
|
||||
>
|
||||
<ClearButton v-if="searchText" @clear="searchText = ''" />
|
||||
</div>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<i-fa6-solid-magnifying-glass />
|
||||
@@ -134,12 +129,7 @@
|
||||
@focus="onInputFocus"
|
||||
@blur="onInputBlur"
|
||||
/>
|
||||
<span
|
||||
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
|
||||
>
|
||||
<ClearButton v-if="searchText" @clear="searchText = ''" />\n
|
||||
</div>
|
||||
<SearchSuggestions
|
||||
v-show="(searchText || showSearchHistory) && suggestionsVisible"
|
||||
@@ -153,6 +143,7 @@
|
||||
import { ref, computed, watch, onMounted } from "vue";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import SearchSuggestions from "./SearchSuggestions.vue";
|
||||
import ClearButton from "./ui/ClearButton.vue";
|
||||
import hotkeys from "hotkeys-js";
|
||||
import { fetchJson, authApiUrl, getAuthToken } from "@/composables/useApi.js";
|
||||
import { getPreferenceBoolean, getPreferenceString } from "@/composables/usePreferences.js";
|
||||
@@ -266,10 +257,4 @@ onMounted(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@media screen and (max-width: 848px) {
|
||||
#search-btn {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style></style>
|
||||
|
||||
@@ -8,7 +8,7 @@ const homeUrl = import.meta.env.BASE_URL;
|
||||
<h2 v-t="'info.page_not_found'" class="text-2xl!" />
|
||||
<a
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,7 @@
|
||||
<div class="relative">
|
||||
<img loading="lazy" class="w-full" :src="props.item.thumbnail" />
|
||||
</div>
|
||||
<p
|
||||
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"
|
||||
/>
|
||||
<p class="line-clamp-2 pt-2 leading-tight font-bold" :title="props.item.name" v-text="props.item.name" />
|
||||
</router-link>
|
||||
<p
|
||||
v-if="props.item.description"
|
||||
|
||||
@@ -222,7 +222,7 @@ onMounted(() => {
|
||||
if (json.error) alert(json.error);
|
||||
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();
|
||||
});
|
||||
|
||||
|
||||
@@ -43,8 +43,7 @@
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical"
|
||||
class="my-2 flex overflow-hidden hover:text-red-500 focus:text-red-500 dark:hover:text-red-400 dark:focus:text-red-400"
|
||||
class="my-2 line-clamp-2 hover:text-red-500 focus:text-red-500 dark:hover:text-red-400 dark:focus:text-red-400"
|
||||
:title="playlist.name"
|
||||
v-text="playlist.name"
|
||||
/>
|
||||
|
||||
@@ -27,13 +27,7 @@
|
||||
:aria-label="$t('login.password')"
|
||||
@keyup.enter="register"
|
||||
/>
|
||||
<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="showPassword = !showPassword"
|
||||
>
|
||||
<i-fa6-solid-eye />
|
||||
</button>
|
||||
<PasswordToggle v-model="showPassword" />
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<input
|
||||
@@ -45,20 +39,10 @@
|
||||
:aria-label="$t('login.password_confirm')"
|
||||
@keyup.enter="register"
|
||||
/>
|
||||
<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="showConfirmPassword = !showConfirmPassword"
|
||||
>
|
||||
<i-fa6-solid-eye />
|
||||
</button>
|
||||
<PasswordToggle v-model="showConfirmPassword" />
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
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"
|
||||
/>
|
||||
<Button v-t="'titles.register'" @click="register" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -80,6 +64,8 @@ import { useRouter } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { isEmail } from "../utils/Misc.js";
|
||||
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 { setPreference } from "@/composables/usePreferences.js";
|
||||
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
<h2 v-t="'actions.share'" class="mb-5" />
|
||||
<div class="flex justify-between">
|
||||
<label v-t="'actions.piped_link'" />
|
||||
<input v-model="pipedLink" type="checkbox" @change="onChange" />
|
||||
<UiCheckbox v-model="pipedLink" @change="onChange" />
|
||||
</div>
|
||||
<hr />
|
||||
<div v-if="hasPlaylist" class="flex justify-between">
|
||||
<label v-t="'actions.with_playlist'" />
|
||||
<input v-model="withPlaylist" type="checkbox" @change="onChange" />
|
||||
<UiCheckbox v-model="withPlaylist" @change="onChange" />
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<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 v-if="withTimeCode" class="mt-2 flex items-center justify-between">
|
||||
<label v-t="'actions.time_code'" />
|
||||
@@ -29,21 +29,9 @@
|
||||
</a>
|
||||
<QrCode v-if="showQrCode" :text="generatedLink" />
|
||||
<div class="mt-4 flex justify-end">
|
||||
<button
|
||||
v-t="'actions.generate_qrcode'"
|
||||
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="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()"
|
||||
/>
|
||||
<Button v-t="'actions.generate_qrcode'" @click="showQrCode = !showQrCode" />
|
||||
<Button v-t="'actions.follow_link'" class="ml-3" @click="followLink()" />
|
||||
<Button v-t="'actions.copy_link'" class="ml-3" @click="copyLink()" />
|
||||
</div>
|
||||
</ModalComponent>
|
||||
</template>
|
||||
@@ -52,6 +40,8 @@
|
||||
import { ref, computed, onMounted, defineAsyncComponent } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import ModalComponent from "./ModalComponent.vue";
|
||||
import Button from "./ui/Button.vue";
|
||||
import UiCheckbox from "./ui/Checkbox.vue";
|
||||
import { getPreferenceBoolean, setPreference } from "@/composables/usePreferences.js";
|
||||
|
||||
const QrCode = defineAsyncComponent(() => import("./QrCode.vue"));
|
||||
|
||||
@@ -126,11 +126,9 @@
|
||||
<img :src="subscription.avatar" class="size-8 rounded-full" />
|
||||
<span class="ml-2">{{ subscription.name }}</span>
|
||||
</a>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="size-4"
|
||||
:checked="selectedGroup.channels.includes(subscription.url.substr(-24))"
|
||||
@change="checkedChange(subscription)"
|
||||
<UiCheckbox
|
||||
:model-value="selectedGroup.channels.includes(subscription.url.substr(-24))"
|
||||
@update:model-value="checkedChange(subscription)"
|
||||
/>
|
||||
</div>
|
||||
<hr />
|
||||
@@ -144,6 +142,7 @@ import { ref, computed, onMounted, onActivated } from "vue";
|
||||
import ModalComponent from "./ModalComponent.vue";
|
||||
import CreateGroupModal from "./CreateGroupModal.vue";
|
||||
import ConfirmModal from "./ConfirmModal.vue";
|
||||
import UiCheckbox from "./ui/Checkbox.vue";
|
||||
import { fetchJson, authApiUrl, getAuthToken, isAuthenticated } from "@/composables/useApi.js";
|
||||
import { fetchSubscriptions, handleLocalSubscriptions } from "@/composables/useSubscriptions.js";
|
||||
import { getChannelGroups, createOrUpdateChannelGroup, deleteChannelGroup } from "@/composables/useChannelGroups.js";
|
||||
|
||||
@@ -16,8 +16,7 @@
|
||||
|
||||
<div>
|
||||
<p
|
||||
style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical"
|
||||
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"
|
||||
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="title"
|
||||
v-text="title"
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
class="aspect-video w-full rounded-md object-contain"
|
||||
:src="item.thumbnail"
|
||||
:alt="item.title"
|
||||
:class="{ 'w-full object-contain': item.isShort, 'opacity-75': item.watched }"
|
||||
:class="{ 'opacity-75': item.watched }"
|
||||
/>
|
||||
<!-- progress bar -->
|
||||
<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"
|
||||
@click="downloadCurrentFrame"
|
||||
>
|
||||
{{ $t("actions.download_frame") }}<i-fa6-solid-download />
|
||||
{{ $t("actions.download_frame") }}<i-fa6-solid-download class="ml-1" />
|
||||
</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"
|
||||
@@ -226,7 +226,7 @@
|
||||
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"
|
||||
>
|
||||
<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" />
|
||||
</span>
|
||||
|
||||
@@ -247,7 +247,7 @@
|
||||
<router-link
|
||||
v-for="tag in video.tags"
|
||||
: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)}`"
|
||||
>{{ tag }}</router-link
|
||||
>
|
||||
@@ -258,13 +258,7 @@
|
||||
<hr />
|
||||
|
||||
<label for="chkAutoLoop"><strong v-text="`${$t('actions.loop_this_video')}:`" /></label>
|
||||
<input
|
||||
id="chkAutoLoop"
|
||||
v-model="selectedAutoLoop"
|
||||
class="ml-1.5"
|
||||
type="checkbox"
|
||||
@change="onChange($event)"
|
||||
/>
|
||||
<UiCheckbox id="chkAutoLoop" v-model="selectedAutoLoop" class="ml-1.5" @change="onChange($event)" />
|
||||
<br />
|
||||
<label for="chkAutoPlay"><strong v-text="`${$t('actions.auto_play_next_video')}:`" /></label>
|
||||
<select
|
||||
@@ -383,6 +377,7 @@ import PlaylistVideos from "./PlaylistVideos.vue";
|
||||
import WatchOnButton from "./WatchOnButton.vue";
|
||||
import LoadingIndicatorPage from "./LoadingIndicatorPage.vue";
|
||||
import ToastComponent from "./ToastComponent.vue";
|
||||
import UiCheckbox from "./ui/Checkbox.vue";
|
||||
import { parseTimeParam } from "@/utils/Misc";
|
||||
import { purifyHTML, rewriteDescription } from "@/utils/HtmlUtils";
|
||||
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;
|
||||
}
|
||||
|
||||
function createPreferenceRefForValue(key, value) {
|
||||
if (typeof value === "string" || value === undefined) return usePreferenceString(key);
|
||||
if (typeof value === "boolean") return usePreferenceBoolean(key, value);
|
||||
if (typeof value === "number") return usePreferenceNumber(key, value);
|
||||
return usePreferenceJSON(key, value);
|
||||
function createPreferenceRefForValue(key, valueForTypeInference) {
|
||||
if (typeof valueForTypeInference === "string" || valueForTypeInference === undefined)
|
||||
return usePreferenceString(key);
|
||||
if (typeof valueForTypeInference === "boolean") return usePreferenceBoolean(key);
|
||||
if (typeof valueForTypeInference === "number") return usePreferenceNumber(key);
|
||||
return usePreferenceJSON(key);
|
||||
}
|
||||
|
||||
export function usePreferenceString(key, defaultVal) {
|
||||
|
||||
Reference in New Issue
Block a user