mirror of
https://github.com/TeamPiped/Piped.git
synced 2026-04-03 06:16:59 +00:00
Migrate to tailwind + reka ui
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
import js from "@eslint/js";
|
import js from "@eslint/js";
|
||||||
import unoConfig from "@unocss/eslint-config/flat";
|
|
||||||
import { defineConfig } from "eslint/config";
|
import { defineConfig } from "eslint/config";
|
||||||
|
import betterTailwindcss from "eslint-plugin-better-tailwindcss";
|
||||||
import prettierRecommended from "eslint-plugin-prettier/recommended";
|
import prettierRecommended from "eslint-plugin-prettier/recommended";
|
||||||
import vue from "eslint-plugin-vue";
|
import vue from "eslint-plugin-vue";
|
||||||
import globals from "globals";
|
import globals from "globals";
|
||||||
|
import vueParser from "vue-eslint-parser";
|
||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
{
|
{
|
||||||
@@ -25,16 +26,28 @@ export default defineConfig([
|
|||||||
files: ["**/*.{js,mjs,cjs,vue}"],
|
files: ["**/*.{js,mjs,cjs,vue}"],
|
||||||
},
|
},
|
||||||
...vue.configs["flat/recommended"],
|
...vue.configs["flat/recommended"],
|
||||||
{
|
|
||||||
...unoConfig,
|
|
||||||
files: ["**/*.{js,mjs,cjs,vue}"],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
files: ["**/*.vue"],
|
files: ["**/*.vue"],
|
||||||
rules: {
|
rules: {
|
||||||
"no-useless-assignment": "off",
|
"no-useless-assignment": "off",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
files: ["**/*.vue"],
|
||||||
|
extends: [betterTailwindcss.configs.recommended],
|
||||||
|
settings: {
|
||||||
|
"better-tailwindcss": {
|
||||||
|
entryPoint: "src/app.css",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
languageOptions: {
|
||||||
|
parser: vueParser,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"better-tailwindcss/enforce-consistent-line-wrapping": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
...prettierRecommended,
|
...prettierRecommended,
|
||||||
files: ["**/*.{js,mjs,cjs,vue}"],
|
files: ["**/*.{js,mjs,cjs,vue}"],
|
||||||
|
|||||||
15
package.json
15
package.json
@@ -11,6 +11,7 @@
|
|||||||
"lint": "eslint --fix --color ."
|
"lint": "eslint --fix --color ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@vueuse/core": "^14.2.1",
|
||||||
"dompurify": "3.3.3",
|
"dompurify": "3.3.3",
|
||||||
"fast-xml-parser": "5.5.9",
|
"fast-xml-parser": "5.5.9",
|
||||||
"hotkeys-js": "4.0.2",
|
"hotkeys-js": "4.0.2",
|
||||||
@@ -18,6 +19,7 @@
|
|||||||
"linkify-html": "4.3.2",
|
"linkify-html": "4.3.2",
|
||||||
"linkifyjs": "4.3.2",
|
"linkifyjs": "4.3.2",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
|
"reka-ui": "^2.9.2",
|
||||||
"shaka-player": "5.0.8",
|
"shaka-player": "5.0.8",
|
||||||
"vue": "3.5.31",
|
"vue": "3.5.31",
|
||||||
"vue-i18n": "11.3.0",
|
"vue-i18n": "11.3.0",
|
||||||
@@ -28,24 +30,21 @@
|
|||||||
"@iconify-json/fa6-brands": "1.2.6",
|
"@iconify-json/fa6-brands": "1.2.6",
|
||||||
"@iconify-json/fa6-solid": "1.2.4",
|
"@iconify-json/fa6-solid": "1.2.4",
|
||||||
"@intlify/unplugin-vue-i18n": "11.1.1",
|
"@intlify/unplugin-vue-i18n": "11.1.1",
|
||||||
"@unocss/eslint-config": "66.6.7",
|
"@tailwindcss/vite": "^4.2.2",
|
||||||
"@unocss/preset-icons": "66.6.7",
|
|
||||||
"@unocss/preset-uno": "66.6.7",
|
|
||||||
"@unocss/preset-web-fonts": "66.6.7",
|
|
||||||
"@unocss/reset": "66.6.7",
|
|
||||||
"@unocss/transformer-directives": "66.6.7",
|
|
||||||
"@unocss/transformer-variant-group": "66.6.7",
|
|
||||||
"@vitejs/plugin-legacy": "8.0.1",
|
"@vitejs/plugin-legacy": "8.0.1",
|
||||||
"@vitejs/plugin-vue": "6.0.5",
|
"@vitejs/plugin-vue": "6.0.5",
|
||||||
"@vue/compiler-sfc": "3.5.31",
|
"@vue/compiler-sfc": "3.5.31",
|
||||||
"eslint": "10.1.0",
|
"eslint": "10.1.0",
|
||||||
"eslint-config-prettier": "10.1.8",
|
"eslint-config-prettier": "10.1.8",
|
||||||
|
"eslint-plugin-better-tailwindcss": "^4.3.2",
|
||||||
"eslint-plugin-prettier": "5.5.5",
|
"eslint-plugin-prettier": "5.5.5",
|
||||||
"eslint-plugin-vue": "10.8.0",
|
"eslint-plugin-vue": "10.8.0",
|
||||||
"globals": "17.4.0",
|
"globals": "17.4.0",
|
||||||
"lightningcss": "1.32.0",
|
"lightningcss": "1.32.0",
|
||||||
"prettier": "3.8.1",
|
"prettier": "3.8.1",
|
||||||
"unocss": "66.6.7",
|
"tailwindcss": "^4.2.2",
|
||||||
|
"unplugin-icons": "^23.0.1",
|
||||||
|
"unplugin-vue-components": "^32.0.0",
|
||||||
"vite": "8.0.3",
|
"vite": "8.0.3",
|
||||||
"vite-plugin-eslint": "1.8.1",
|
"vite-plugin-eslint": "1.8.1",
|
||||||
"vite-plugin-pwa": "1.2.0",
|
"vite-plugin-pwa": "1.2.0",
|
||||||
|
|||||||
1205
pnpm-lock.yaml
generated
1205
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
194
src/App.vue
194
src/App.vue
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="reset min-h-screen w-full flex flex-col px-1vw py-5 antialiased" :class="[theme]">
|
<div
|
||||||
|
class="flex min-h-screen w-full flex-col bg-white px-[1vw] py-5 text-black antialiased dark:bg-dark-900 dark:text-white"
|
||||||
|
:class="[theme]"
|
||||||
|
>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<NavBar />
|
<NavBar />
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }">
|
||||||
@@ -14,27 +17,28 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from "vue";
|
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||||
import NavBar from "./components/NavBar.vue";
|
import NavBar from "./components/NavBar.vue";
|
||||||
import FooterComponent from "./components/FooterComponent.vue";
|
import FooterComponent from "./components/FooterComponent.vue";
|
||||||
import { getPreferenceString } from "@/composables/usePreferences.js";
|
import { testLocalStorage, usePreferenceString } from "@/composables/usePreferences.js";
|
||||||
import { getDefaultLanguage, TimeAgo, TimeAgoConfig } from "@/composables/useFormatting.js";
|
import { getDefaultLanguage, TimeAgo, TimeAgoConfig } from "@/composables/useFormatting.js";
|
||||||
import { fetchSubscriptions } from "@/composables/useSubscriptions.js";
|
import { fetchSubscriptions } from "@/composables/useSubscriptions.js";
|
||||||
import { getChannelGroups, createOrUpdateChannelGroup } from "@/composables/useChannelGroups.js";
|
import { getChannelGroups, createOrUpdateChannelGroup } from "@/composables/useChannelGroups.js";
|
||||||
|
|
||||||
const darkModePreference = window.matchMedia("(prefers-color-scheme: dark)");
|
const darkModePreference = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
|
|
||||||
|
const themePreference = usePreferenceString("theme", "dark");
|
||||||
|
const localePreference = usePreferenceString("hl", "en");
|
||||||
const theme = ref("dark");
|
const theme = ref("dark");
|
||||||
|
|
||||||
function setTheme() {
|
function setTheme() {
|
||||||
let themePref = getPreferenceString("theme", "dark");
|
|
||||||
const themes = {
|
const themes = {
|
||||||
dark: "dark",
|
dark: "dark",
|
||||||
light: "light",
|
light: "light",
|
||||||
auto: darkModePreference.matches ? "dark" : "light",
|
auto: darkModePreference.matches ? "dark" : "light",
|
||||||
};
|
};
|
||||||
|
|
||||||
theme.value = themes[themePref];
|
theme.value = themes[themePreference.value] ?? themes.dark;
|
||||||
|
|
||||||
changeTitleBarColor();
|
changeTitleBarColor();
|
||||||
|
|
||||||
@@ -48,11 +52,42 @@ function changeTitleBarColor() {
|
|||||||
themeColor.setAttribute("content", currentColor[theme.value]);
|
themeColor.setAttribute("content", currentColor[theme.value]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function applyLocale(locale = localePreference.value) {
|
||||||
|
const resolvedLocale = locale || (await getDefaultLanguage());
|
||||||
|
|
||||||
|
if (resolvedLocale !== TimeAgoConfig.locale) {
|
||||||
|
const localeTime = await import(`../node_modules/javascript-time-ago/locale/${resolvedLocale}.json`)
|
||||||
|
.catch(() => null)
|
||||||
|
.then(module => module?.default);
|
||||||
|
if (localeTime) {
|
||||||
|
TimeAgo.addLocale(localeTime);
|
||||||
|
TimeAgoConfig.locale = resolvedLocale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.i18n.global.locale.value !== resolvedLocale) {
|
||||||
|
if (!window.i18n.global.availableLocales.includes(resolvedLocale)) {
|
||||||
|
const messages = await import(`./locales/${resolvedLocale}.json`).then(module => module.default);
|
||||||
|
window.i18n.global.setLocaleMessage(resolvedLocale, messages);
|
||||||
|
}
|
||||||
|
window.i18n.global.locale.value = resolvedLocale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePreferredColorSchemeChange() {
|
||||||
|
if (themePreference.value === "auto") setTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
themePreference,
|
||||||
|
() => {
|
||||||
|
setTheme();
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setTheme();
|
darkModePreference.addEventListener("change", handlePreferredColorSchemeChange);
|
||||||
darkModePreference.addEventListener("change", () => {
|
|
||||||
setTheme();
|
|
||||||
});
|
|
||||||
|
|
||||||
if ("indexedDB" in window) {
|
if ("indexedDB" in window) {
|
||||||
const request = indexedDB.open("piped-db", 6);
|
const request = indexedDB.open("piped-db", 6);
|
||||||
@@ -107,29 +142,27 @@ onMounted(() => {
|
|||||||
} else console.log("This browser doesn't support IndexedDB");
|
} else console.log("This browser doesn't support IndexedDB");
|
||||||
|
|
||||||
(async function () {
|
(async function () {
|
||||||
const defaultLang = await getDefaultLanguage();
|
const initialLocale =
|
||||||
const locale = getPreferenceString("hl", defaultLang);
|
testLocalStorage() && localStorage.getItem("hl") === null
|
||||||
if (locale !== TimeAgoConfig.locale) {
|
? await getDefaultLanguage()
|
||||||
const localeTime = await import(`../node_modules/javascript-time-ago/locale/${locale}.json`)
|
: localePreference.value;
|
||||||
.catch(() => null)
|
await applyLocale(initialLocale);
|
||||||
.then(module => module?.default);
|
|
||||||
if (localeTime) {
|
watch(localePreference, locale => {
|
||||||
TimeAgo.addLocale(localeTime);
|
applyLocale(locale);
|
||||||
TimeAgoConfig.locale = locale;
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
if (window.i18n.global.locale.value !== locale) {
|
|
||||||
if (!window.i18n.global.availableLocales.includes(locale)) {
|
|
||||||
const messages = await import(`./locales/${locale}.json`).then(module => module.default);
|
|
||||||
window.i18n.global.setLocaleMessage(locale, messages);
|
|
||||||
}
|
|
||||||
window.i18n.global.locale.value = locale;
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
darkModePreference.removeEventListener("change", handlePreferredColorSchemeChange);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@reference "./app.css";
|
||||||
|
|
||||||
|
@layer base {
|
||||||
h1,
|
h1,
|
||||||
p,
|
p,
|
||||||
a,
|
a,
|
||||||
@@ -191,118 +224,33 @@ b {
|
|||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@apply font-sans;
|
font-family: var(--font-sans);
|
||||||
}
|
|
||||||
|
|
||||||
.video-grid {
|
|
||||||
@apply grid grid-cols-1 mx-2 sm:mx-0 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 col-auto lt-md:gap-x-3 md:gap-x-6 gap-y-5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
@apply py-2 lt-md:px-2 md:px-4 rounded cursor-pointer inline-block hover:bg-gray-500 hover:text-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reset {
|
|
||||||
@apply text-black bg-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
@apply text-white bg-dark-900;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input,
|
|
||||||
.select,
|
|
||||||
.btn {
|
|
||||||
@apply w-auto text-gray-600 bg-gray-300;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input,
|
|
||||||
.select {
|
|
||||||
@apply h-8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox {
|
|
||||||
@apply h-4 w-4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .input,
|
|
||||||
.dark .select,
|
|
||||||
.dark .btn {
|
|
||||||
@apply text-gray-400 bg-dark-400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .btn {
|
|
||||||
@apply hover:bg-dark-300;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input {
|
|
||||||
@apply px-2.5 rounded-md;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input:focus {
|
|
||||||
@apply outline-red-500;
|
|
||||||
outline-style: solid;
|
|
||||||
outline-width: 2px;
|
|
||||||
box-shadow: 0 0 15px rgba(239, 68, 68, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
@apply !mt-2 !mb-3 border-gray-300;
|
margin-top: 0.5rem !important;
|
||||||
|
margin-bottom: 0.75rem !important;
|
||||||
|
border-color: #d1d5db;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark hr {
|
.dark hr {
|
||||||
@apply border-dark-100;
|
border-color: var(--color-dark-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
h2 {
|
h2 {
|
||||||
@apply m-0 font-bold;
|
margin: 0;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
@apply !text-5xl;
|
font-size: 3rem !important;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
@apply !text-3xl;
|
font-size: 1.875rem !important;
|
||||||
|
line-height: 2.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table {
|
|
||||||
@apply w-full text-lg text-left font-light border;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link {
|
|
||||||
@apply focus:text-red-500 hover:text-red-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-secondary {
|
|
||||||
@apply hover:text-dark-400 focus:text-dark-400 underline underline-dark-400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .link {
|
|
||||||
@apply focus:text-red-400 hover:text-red-400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .link-secondary {
|
|
||||||
@apply text-gray-300 hover:(text-gray-400 underline underline-gray-400);
|
|
||||||
}
|
|
||||||
|
|
||||||
.line {
|
|
||||||
@apply px-2.5 py-0.25 my-0.45 rounded-xl bg-dark-900;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .line {
|
|
||||||
@apply bg-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumbnail-overlay {
|
|
||||||
@apply rounded-md absolute bg-black text-white bg-opacity-75 px-5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumbnail-right {
|
|
||||||
@apply bottom-5px right-5px;
|
|
||||||
}
|
|
||||||
.thumbnail-left {
|
|
||||||
@apply bottom-5px left-5px text-xs font-bold bg-red-600 uppercase;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
39
src/app.css
Normal file
39
src/app.css
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--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-dark-100: #3f3f46;
|
||||||
|
--color-dark-300: #323232;
|
||||||
|
--color-dark-400: #222222;
|
||||||
|
--color-dark-700: #1a1a1a;
|
||||||
|
--color-dark-800: #141414;
|
||||||
|
--color-dark-900: #0f0f0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility skip-segment-button {
|
||||||
|
z-index: 1000;
|
||||||
|
position: absolute;
|
||||||
|
transform: translate(0, -50%);
|
||||||
|
top: 50%;
|
||||||
|
right: 0;
|
||||||
|
background-color: rgb(0 0 0 / 0.5);
|
||||||
|
border: 2px rgba(255, 255, 255, 0.75) solid;
|
||||||
|
border-right: 0;
|
||||||
|
border-radius: 0.75em;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
padding: 0.5em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility skip-segment-icon {
|
||||||
|
font-size: 1.6em !important;
|
||||||
|
line-height: inherit !important;
|
||||||
|
margin-left: 0.4em;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<ModalComponent @close="$emit('close')">
|
<ModalComponent @close="$emit('close')">
|
||||||
<div class="min-w-[50vw] flex flex-col">
|
<div class="flex min-w-[50vw] flex-col">
|
||||||
<div class="h-[70vh] overflow-y-scroll pr-4">
|
<div class="h-[70vh] overflow-y-scroll pr-4">
|
||||||
<span v-t="'actions.add_to_group'" class="mb-3 inline-block w-max text-2xl" />
|
<span v-t="'actions.add_to_group'" class="mb-3 inline-block w-max text-2xl" />
|
||||||
<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">
|
||||||
@@ -15,7 +15,11 @@
|
|||||||
<hr class="h-1 w-full" />
|
<hr class="h-1 w-full" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button v-t="'actions.create_group'" class="btn ml-auto w-max" @click="showCreateGroupModal = true" />
|
<button
|
||||||
|
v-t="'actions.create_group'"
|
||||||
|
class="ml-auto inline-block w-max 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="showCreateGroupModal = true"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ModalComponent>
|
</ModalComponent>
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +1,53 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col flex-justify-between">
|
<div class="flex flex-col justify-between">
|
||||||
<router-link :to="item.url" class="link font-bold">
|
<router-link
|
||||||
|
:to="item.url"
|
||||||
|
class="font-bold hover:text-red-500 focus:text-red-500 dark:hover:text-red-400 dark:focus:text-red-400"
|
||||||
|
>
|
||||||
<div class="my-4 flex justify-center">
|
<div class="my-4 flex justify-center">
|
||||||
<img loading="lazy" class="aspect-square w-[50%] rounded-full" :src="item.thumbnail" />
|
<img loading="lazy" class="aspect-square w-[50%] rounded-full" :src="item.thumbnail" />
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p class="line-clamp-2 leading-tight">
|
||||||
<span v-text="item.name" />
|
<span v-text="item.name" />
|
||||||
<i v-if="item.verified" class="i-fa6-solid:check ml-1.5" />
|
<i-fa6-solid-check v-if="item.verified" class="ml-1.5" />
|
||||||
</p>
|
</p>
|
||||||
</router-link>
|
</router-link>
|
||||||
<p v-if="item.description" class="pt-1 text-sm" v-text="item.description" />
|
<p
|
||||||
<router-link v-if="item.uploaderUrl" class="link" :to="item.uploaderUrl">
|
v-if="item.description"
|
||||||
<p>
|
class="line-clamp-2 pt-1 text-sm text-gray-600 dark:text-gray-400"
|
||||||
|
v-text="item.description"
|
||||||
|
/>
|
||||||
|
<router-link
|
||||||
|
v-if="item.uploaderUrl"
|
||||||
|
class="mt-1 line-clamp-1 block text-sm underline decoration-dark-400 hover:text-dark-400 focus:text-dark-400 dark:text-gray-300 dark:decoration-dark-100 dark:hover:text-gray-400"
|
||||||
|
:to="item.uploaderUrl"
|
||||||
|
>
|
||||||
|
<p class="leading-tight">
|
||||||
<span v-text="item.uploader" />
|
<span v-text="item.uploader" />
|
||||||
<i v-if="item.uploaderVerified" class="i-fa6-solid:check ml-1.5" />
|
<i-fa6-solid-check v-if="item.uploaderVerified" class="ml-1.5" />
|
||||||
</p>
|
</p>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<a v-if="item.uploaderName" class="link" v-text="item.uploaderName" />
|
<a
|
||||||
|
v-if="item.uploaderName"
|
||||||
|
class="mt-1 line-clamp-1 block text-sm hover:text-red-500 focus:text-red-500 dark:text-gray-300 dark:hover:text-red-400 dark:focus:text-red-400"
|
||||||
|
v-text="item.uploaderName"
|
||||||
|
/>
|
||||||
<template v-if="item.videos >= 0">
|
<template v-if="item.videos >= 0">
|
||||||
<br v-if="item.uploaderName" />
|
<strong
|
||||||
<strong v-text="`${item.videos} ${$t('video.videos')}`" />
|
class="mt-1 text-sm text-gray-800 dark:text-gray-200"
|
||||||
|
v-text="`${item.videos} ${$t('video.videos')}`"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-if="subscribed != null"
|
v-if="subscribed != null"
|
||||||
class="btn mt-2 w-max"
|
class="mt-2 inline-block w-max 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="subscribeHandler"
|
@click="subscribeHandler"
|
||||||
v-text="
|
v-text="
|
||||||
$t('actions.' + (subscribed ? 'unsubscribe' : 'subscribe')) + ' - ' + numberFormat(item.subscribers)
|
$t('actions.' + (subscribed ? 'unsubscribe' : 'subscribe')) + ' - ' + numberFormat(item.subscribers)
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<br />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -12,14 +12,14 @@
|
|||||||
<div class="flex place-items-center">
|
<div class="flex place-items-center">
|
||||||
<img height="48" width="48" class="m-1 rounded-full" :src="channel.avatarUrl" />
|
<img height="48" width="48" class="m-1 rounded-full" :src="channel.avatarUrl" />
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<h1 class="!text-xl" v-text="channel.name" />
|
<h1 class="text-xl!" v-text="channel.name" />
|
||||||
<i v-if="channel.verified" class="i-fa6-solid:check !text-xl" />
|
<i-fa6-solid-check v-if="channel.verified" class="text-xl!" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
class="btn"
|
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="subscribeHandler"
|
@click="subscribeHandler"
|
||||||
v-text="
|
v-text="
|
||||||
$t('actions.' + (subscribed ? 'unsubscribe' : 'subscribe')) +
|
$t('actions.' + (subscribed ? 'unsubscribe' : 'subscribe')) +
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<button
|
<button
|
||||||
v-if="subscribed"
|
v-if="subscribed"
|
||||||
v-t="'actions.add_to_group'"
|
v-t="'actions.add_to_group'"
|
||||||
class="btn"
|
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="showGroupModal = true"
|
@click="showGroupModal = true"
|
||||||
></button>
|
></button>
|
||||||
|
|
||||||
@@ -43,9 +43,9 @@
|
|||||||
role="button"
|
role="button"
|
||||||
:href="`${apiUrl()}/feed/unauthenticated/rss?channels=${channel.id}`"
|
:href="`${apiUrl()}/feed/unauthenticated/rss?channels=${channel.id}`"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="btn flex-col"
|
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"
|
||||||
>
|
>
|
||||||
<i class="i-fa6-solid:rss" />
|
<i-fa6-solid-rss />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,20 +58,28 @@
|
|||||||
<button
|
<button
|
||||||
v-for="(tab, index) in tabs"
|
v-for="(tab, index) in tabs"
|
||||||
:key="tab.name"
|
:key="tab.name"
|
||||||
class="btn mr-2"
|
class="mr-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"
|
||||||
:class="{ active: selectedTab == index }"
|
:class="{
|
||||||
|
'bg-gray-500 text-white dark:bg-dark-300': selectedTab == index,
|
||||||
|
}"
|
||||||
@click="loadTab(index)"
|
@click="loadTab(index)"
|
||||||
>
|
>
|
||||||
<span v-text="tab.translatedName"></span>
|
<span v-text="tab.translatedName"></span>
|
||||||
</button>
|
</button>
|
||||||
<router-link :to="`/playlist?list=UU${channel.id.substring(2)}`">
|
<router-link :to="`/playlist?list=UU${channel.id.substring(2)}`">
|
||||||
<button class="btn h-full">Play all videos</button>
|
<button
|
||||||
|
class="inline-block h-full 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"
|
||||||
|
>
|
||||||
|
Play all videos
|
||||||
|
</button>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div class="video-grid">
|
<div
|
||||||
|
class="mx-2 grid grid-cols-1 gap-y-5 max-md:gap-x-3 sm:mx-0 sm:grid-cols-2 md:grid-cols-3 md:gap-x-6 lg:grid-cols-4 xl:grid-cols-5"
|
||||||
|
>
|
||||||
<ContentItem
|
<ContentItem
|
||||||
v-for="item in contentItems"
|
v-for="item in contentItems"
|
||||||
:key="item.url"
|
:key="item.url"
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- desktop view -->
|
<!-- desktop view -->
|
||||||
<div v-if="!mobileLayout" class="max-h-75vh max-w-35vw min-h-64 flex-col overflow-y-auto lt-lg:hidden">
|
<div v-if="!mobileLayout" class="max-h-[75vh] min-h-64 max-w-[35vw] flex-col overflow-y-auto max-lg:hidden">
|
||||||
<h2 class="mb-2 bg-gray-500/50 p-2" aria-label="chapters" title="chapters">
|
<h2 class="mb-2 bg-gray-500/50 p-2" aria-label="chapters" title="chapters">
|
||||||
{{ $t("video.chapters") }} ({{ chapters.length }})
|
{{ $t("video.chapters") }} ({{ chapters.length }})
|
||||||
</h2>
|
</h2>
|
||||||
<div
|
<div
|
||||||
v-for="(chapter, index) in chapters"
|
v-for="(chapter, index) in chapters"
|
||||||
:key="chapter.start"
|
:key="chapter.start"
|
||||||
class="chapter-vertical"
|
class="cursor-pointer self-center p-2.5 hover:bg-gray-500"
|
||||||
:class="{ 'bg-red-500/50': isCurrentChapter(index) }"
|
:class="{ 'bg-red-500/50': isCurrentChapter(index) }"
|
||||||
@click="$emit('seek', chapter.start)"
|
@click="$emit('seek', chapter.start)"
|
||||||
>
|
>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<span class="mr-2 mt-5 text-current" v-text="index + 1" />
|
<span class="mt-5 mr-2 text-current" v-text="index + 1" />
|
||||||
<img class="shrink-0" :src="chapter.image" :alt="chapter.title" />
|
<img class="h-[30%] w-[30%] shrink-0" :src="chapter.image" :alt="chapter.title" />
|
||||||
<div class="m-2 flex flex-col">
|
<div class="m-2 flex flex-col">
|
||||||
<span class="text-sm" :title="chapter.title" v-text="chapter.title" />
|
<span class="text-sm" :title="chapter.title" v-text="chapter.title" />
|
||||||
<span class="text-sm text-blue-500 font-bold" v-text="timeFormat(chapter.start)" />
|
<span class="text-sm font-bold text-blue-500" v-text="timeFormat(chapter.start)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
<!-- mobile vertical view -->
|
<!-- mobile vertical view -->
|
||||||
<div
|
<div
|
||||||
v-if="mobileLayout && getPreferenceString('mobileChapterLayout') == 'Vertical'"
|
v-if="mobileLayout && getPreferenceString('mobileChapterLayout') == 'Vertical'"
|
||||||
class="max-h-64 flex flex-col overflow-y-auto"
|
class="flex max-h-64 flex-col overflow-y-auto"
|
||||||
>
|
>
|
||||||
<h2 class="mb-2 bg-gray-500/50 p-2" aria-label="chapters" title="chapters">
|
<h2 class="mb-2 bg-gray-500/50 p-2" aria-label="chapters" title="chapters">
|
||||||
{{ $t("video.chapters") }} ({{ chapters.length }})
|
{{ $t("video.chapters") }} ({{ chapters.length }})
|
||||||
@@ -33,16 +33,16 @@
|
|||||||
<div
|
<div
|
||||||
v-for="(chapter, index) in chapters"
|
v-for="(chapter, index) in chapters"
|
||||||
:key="chapter.start"
|
:key="chapter.start"
|
||||||
class="chapter-vertical"
|
class="cursor-pointer self-center p-2.5 hover:bg-gray-500"
|
||||||
:class="{ 'bg-red-500/50': isCurrentChapter(index) }"
|
:class="{ 'bg-red-500/50': isCurrentChapter(index) }"
|
||||||
@click="$emit('seek', chapter.start)"
|
@click="$emit('seek', chapter.start)"
|
||||||
>
|
>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<span class="mr-2 mt-5 text-current" v-text="index + 1" />
|
<span class="mt-5 mr-2 text-current" v-text="index + 1" />
|
||||||
<img class="shrink-0" :src="chapter.image" :alt="chapter.title" />
|
<img class="h-[30%] w-[30%] shrink-0" :src="chapter.image" :alt="chapter.title" />
|
||||||
<div class="m-2 flex flex-col">
|
<div class="m-2 flex flex-col">
|
||||||
<span class="text-sm" :title="chapter.title" v-text="chapter.title" />
|
<span class="text-sm" :title="chapter.title" v-text="chapter.title" />
|
||||||
<span class="text-sm text-blue-500 font-bold" v-text="timeFormat(chapter.start)" />
|
<span class="text-sm font-bold text-blue-500" v-text="timeFormat(chapter.start)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -52,14 +52,18 @@
|
|||||||
<div
|
<div
|
||||||
v-for="(chapter, index) in chapters"
|
v-for="(chapter, index) in chapters"
|
||||||
:key="chapter.start"
|
:key="chapter.start"
|
||||||
class="chapter"
|
class="cursor-pointer self-center p-2.5"
|
||||||
:class="{ 'bg-red-500/50': isCurrentChapter(index) }"
|
:class="{ 'bg-red-500/50': isCurrentChapter(index) }"
|
||||||
@click="$emit('seek', chapter.start)"
|
@click="$emit('seek', chapter.start)"
|
||||||
>
|
>
|
||||||
<img :src="chapter.image" :alt="chapter.title" />
|
<img class="size-full" :src="chapter.image" :alt="chapter.title" />
|
||||||
<div class="m-1 flex">
|
<div class="m-1 flex">
|
||||||
<span class="text-truncate text-sm" :title="chapter.title" v-text="chapter.title" />
|
<span
|
||||||
<span class="px-1 text-sm text-blue-500 font-bold" v-text="timeFormat(chapter.start)" />
|
class="inline-block w-[10em] truncate overflow-hidden text-sm"
|
||||||
|
:title="chapter.title"
|
||||||
|
v-text="chapter.title"
|
||||||
|
/>
|
||||||
|
<span class="px-1 text-sm font-bold text-blue-500" v-text="timeFormat(chapter.start)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,26 +96,11 @@ defineEmits(["seek"]);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@reference "../app.css";
|
||||||
|
|
||||||
|
@layer base {
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
height: 5px;
|
height: 5px;
|
||||||
}
|
}
|
||||||
.chapter {
|
|
||||||
@apply cursor-pointer self-center p-2.5;
|
|
||||||
}
|
|
||||||
.chapter img {
|
|
||||||
@apply w-full h-full;
|
|
||||||
}
|
|
||||||
.chapter-vertical {
|
|
||||||
@apply cursor-pointer self-center p-2.5;
|
|
||||||
}
|
|
||||||
.chapter-vertical img {
|
|
||||||
@apply w-3/10 h-3/10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chapter-vertical:hover {
|
|
||||||
@apply bg-gray-500;
|
|
||||||
}
|
|
||||||
.text-truncate {
|
|
||||||
@apply truncate overflow-hidden inline-block w-10em;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="text" class="mx-1 whitespace-pre-wrap py-2">
|
<div v-if="text" class="mx-1 py-2 whitespace-pre-wrap">
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
<span v-if="showFullText" class="contentText" v-html="fullText()" />
|
<span v-if="showFullText" class="wrap-anywhere" v-html="fullText()" />
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
<span v-else v-html="collapsedText()" />
|
<span v-else v-html="collapsedText()" />
|
||||||
<span v-if="text.length > visibleLimit && !showFullText">...</span>
|
<span v-if="text.length > visibleLimit && !showFullText">...</span>
|
||||||
<button
|
<button
|
||||||
v-if="text.length > visibleLimit"
|
v-if="text.length > visibleLimit"
|
||||||
class="block whitespace-normal text-neutral-500 font-semibold hover:underline"
|
class="block font-semibold whitespace-normal text-neutral-500 hover:underline"
|
||||||
@click="showFullText = !showFullText"
|
@click="showFullText = !showFullText"
|
||||||
>
|
>
|
||||||
[{{ showFullText ? $t("actions.show_less") : $t("actions.show_more") }}]
|
[{{ showFullText ? $t("actions.show_less") : $t("actions.show_more") }}]
|
||||||
@@ -40,9 +40,3 @@ function collapsedText() {
|
|||||||
return purifyHTML(rewriteDescription(props.text.slice(0, props.visibleLimit)));
|
return purifyHTML(rewriteDescription(props.text.slice(0, props.visibleLimit)));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.contentText {
|
|
||||||
word-wrap: anywhere;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,18 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="comment mt-1.5 flex">
|
<div class="mt-1.5 flex">
|
||||||
<img
|
<img loading="lazy" :src="comment.thumbnail" class="size-12 rounded-full" height="48" width="48" alt="Avatar" />
|
||||||
loading="lazy"
|
|
||||||
:src="comment.thumbnail"
|
|
||||||
class="comment-avatar h-12 w-12 rounded-full"
|
|
||||||
height="48"
|
|
||||||
width="48"
|
|
||||||
alt="Avatar"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="comment-content pl-2">
|
<div class="pl-2">
|
||||||
<div class="comment-header">
|
<div>
|
||||||
<div v-if="comment.pinned" class="comment-pinned">
|
<div v-if="comment.pinned">
|
||||||
<i class="i-fa6-solid:thumbtack" />
|
<i-fa6-solid-thumbtack />
|
||||||
<span
|
<span
|
||||||
v-t="{
|
v-t="{
|
||||||
path: 'comment.pinned_by',
|
path: 'comment.pinned_by',
|
||||||
@@ -22,45 +15,49 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="comment-author">
|
<div>
|
||||||
<router-link class="link font-bold" :to="comment.commentorUrl">{{ comment.author }}</router-link>
|
<router-link
|
||||||
<i v-if="comment.verified" class="i-fa6-solid:check ml-1.5" />
|
class="font-bold hover:text-red-500 focus:text-red-500 dark:hover:text-red-400 dark:focus:text-red-400"
|
||||||
|
:to="comment.commentorUrl"
|
||||||
|
>{{ comment.author }}</router-link
|
||||||
|
>
|
||||||
|
<i-fa6-solid-check v-if="comment.verified" class="ml-1.5" />
|
||||||
</div>
|
</div>
|
||||||
<div class="comment-meta mb-1.5 text-sm" v-text="comment.commentedTime" />
|
<div class="mb-1.5 text-sm" v-text="comment.commentedTime" />
|
||||||
</div>
|
</div>
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
<CollapsableText :text="comment.commentText" :visible-limit="500" />
|
<CollapsableText :text="comment.commentText" :visible-limit="500" />
|
||||||
<div class="comment-footer my-1 flex items-center gap-3">
|
<div class="my-1 flex items-center gap-3">
|
||||||
<div class="i-fa6-solid:thumbs-up" />
|
<i-fa6-solid-thumbs-up />
|
||||||
<span v-text="numberFormat(comment.likeCount)" />
|
<span v-text="numberFormat(comment.likeCount)" />
|
||||||
<i v-if="comment.hearted" class="i-fa6-solid:heart" :title="$t('actions.creator_liked')" />
|
<i-fa6-solid-heart v-if="comment.hearted" :title="$t('actions.creator_liked')" />
|
||||||
<img
|
<img
|
||||||
v-if="comment.creatorReplied"
|
v-if="comment.creatorReplied"
|
||||||
:src="uploaderAvatarUrl"
|
:src="uploaderAvatarUrl"
|
||||||
class="h-5 w-5 rounded-full"
|
class="size-5 rounded-full"
|
||||||
:title="$t('actions.creator_replied')"
|
:title="$t('actions.creator_replied')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="comment.repliesPage && (!loadingReplies || !showingReplies)">
|
<template v-if="comment.repliesPage && (!loadingReplies || !showingReplies)">
|
||||||
<div class="cursor-pointer" @click="loadReplies">
|
<div class="cursor-pointer" @click="loadReplies">
|
||||||
<a v-text="`${$t('actions.reply_count', comment.replyCount)}`" />
|
<a v-text="`${$t('actions.reply_count', comment.replyCount)}`" />
|
||||||
<i class="i-fa6-solid:level-down-alt ml-1.5" />
|
<i-fa6-solid-turn-down class="ml-1.5" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="showingReplies">
|
<template v-if="showingReplies">
|
||||||
<div class="cursor-pointer" @click="hideReplies">
|
<div class="cursor-pointer" @click="hideReplies">
|
||||||
<a v-t="'actions.hide_replies'" />
|
<a v-t="'actions.hide_replies'" />
|
||||||
<i class="i-fa6-solid:level-up-alt ml-1.5" />
|
<i-fa6-solid-turn-up class="ml-1.5" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-show="showingReplies" v-if="replies" class="replies">
|
<div v-show="showingReplies" v-if="replies">
|
||||||
<div v-for="reply in replies" :key="reply.commentId" class="w-full">
|
<div v-for="reply in replies" :key="reply.commentId" class="w-full">
|
||||||
<!-- eslint-disable-next-line vue/no-undef-components -->
|
<!-- eslint-disable-next-line vue/no-undef-components -->
|
||||||
<CommentItem :comment="reply" :uploader="uploader" :video-id="videoId" />
|
<CommentItem :comment="reply" :uploader="uploader" :video-id="videoId" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="nextpage" class="cursor-pointer" @click="loadReplies">
|
<div v-if="nextpage" class="cursor-pointer" @click="loadReplies">
|
||||||
<a v-t="'actions.load_more_replies'" />
|
<a v-t="'actions.load_more_replies'" />
|
||||||
<i class="i-fa6-solid:level-down-alt ml-1.5" />
|
<i-fa6-solid-turn-down class="ml-1.5" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,9 +2,17 @@
|
|||||||
<ModalComponent @close="$emit('close')">
|
<ModalComponent @close="$emit('close')">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-xl" v-text="message" />
|
<h3 class="text-xl" v-text="message" />
|
||||||
<div class="ml-auto mt-8 w-min flex gap-2">
|
<div class="mt-8 ml-auto flex w-min gap-2">
|
||||||
<button v-t="'actions.cancel'" class="btn" @click="$emit('close')" />
|
<button
|
||||||
<button v-t="'actions.okay'" class="btn" @click="$emit('confirm')" />
|
v-t="'actions.cancel'"
|
||||||
|
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="$emit('close')"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-t="'actions.okay'"
|
||||||
|
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="$emit('confirm')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalComponent>
|
</ModalComponent>
|
||||||
|
|||||||
@@ -2,8 +2,17 @@
|
|||||||
<ModalComponent @close="$emit('close')">
|
<ModalComponent @close="$emit('close')">
|
||||||
<h2 v-t="'actions.create_group'" />
|
<h2 v-t="'actions.create_group'" />
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<input v-model="groupName" class="input my-4" type="text" :placeholder="$t('actions.group_name')" />
|
<input
|
||||||
<button v-t="'actions.create_group'" class="btn ml-auto w-max" @click="createGroup()" />
|
v-model="groupName"
|
||||||
|
class="my-4 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"
|
||||||
|
type="text"
|
||||||
|
:placeholder="$t('actions.group_name')"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-t="'actions.create_group'"
|
||||||
|
class="ml-auto inline-block w-max 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="createGroup()"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ModalComponent>
|
</ModalComponent>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,10 +2,23 @@
|
|||||||
<ModalComponent @close="$emit('close')">
|
<ModalComponent @close="$emit('close')">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<h2 v-t="'actions.create_playlist'" />
|
<h2 v-t="'actions.create_playlist'" />
|
||||||
<input ref="input" v-model="playlistName" type="text" class="input mt-2" />
|
<input
|
||||||
<div class="ml-auto mt-3 w-min flex">
|
ref="input"
|
||||||
<button v-t="'actions.cancel'" class="btn" @click="$emit('close')" />
|
v-model="playlistName"
|
||||||
<button v-t="'actions.okay'" class="btn ml-2" @click="onCreatePlaylist" />
|
type="text"
|
||||||
|
class="mt-2 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"
|
||||||
|
/>
|
||||||
|
<div class="mt-3 ml-auto flex w-min">
|
||||||
|
<button
|
||||||
|
v-t="'actions.cancel'"
|
||||||
|
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="$emit('close')"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-t="'actions.okay'"
|
||||||
|
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="onCreatePlaylist"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalComponent>
|
</ModalComponent>
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
<div v-for="(customInstance, index) in customInstances" :key="customInstance.name">
|
<div v-for="(customInstance, index) in customInstances" :key="customInstance.name">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span>{{ customInstance.name }} - {{ customInstance.api_url }}</span>
|
<span>{{ customInstance.name }} - {{ customInstance.api_url }}</span>
|
||||||
<span
|
<i-fa6-solid-circle-minus
|
||||||
class="i-fa6-solid:circle-minus cursor-pointer"
|
class="cursor-pointer"
|
||||||
@click="removeInstance(customInstance, index)"
|
@click="removeInstance(customInstance, index)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -16,15 +16,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form class="flex flex-col items-end gap-2">
|
<form class="flex flex-col items-end gap-2">
|
||||||
<input v-model="name" class="input w-full" type="text" :placeholder="$t('preferences.instance_name')" />
|
<input
|
||||||
|
v-model="name"
|
||||||
|
class="h-8 w-full 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"
|
||||||
|
type="text"
|
||||||
|
:placeholder="$t('preferences.instance_name')"
|
||||||
|
/>
|
||||||
<input
|
<input
|
||||||
v-model="url"
|
v-model="url"
|
||||||
class="input w-full"
|
class="h-8 w-full 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"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="$t('preferences.api_url')"
|
:placeholder="$t('preferences.api_url')"
|
||||||
@keyup.enter="addInstance"
|
@keyup.enter="addInstance"
|
||||||
/>
|
/>
|
||||||
<button v-t="'actions.add'" class="btn w-min" @click.prevent="addInstance" />
|
<button
|
||||||
|
v-t="'actions.add'"
|
||||||
|
class="inline-block w-min 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.prevent="addInstance"
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</ModalComponent>
|
</ModalComponent>
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<p v-text="message" />
|
<p v-text="message" />
|
||||||
<button v-t="'actions.show_more'" class="btn" @click="toggleTrace" />
|
<button
|
||||||
|
v-t="'actions.show_more'"
|
||||||
|
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="toggleTrace"
|
||||||
|
/>
|
||||||
<p ref="stacktrace" class="whitespace-pre-wrap" hidden v-text="error" />
|
<p ref="stacktrace" class="whitespace-pre-wrap" hidden v-text="error" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<ModalComponent>
|
<ModalComponent>
|
||||||
<div class="min-w-max flex flex-col">
|
<div class="flex min-w-max flex-col">
|
||||||
<h2 v-t="'actions.export_history'" class="mb-4 text-center text-xl font-bold" />
|
<h2 v-t="'actions.export_history'" class="mb-4 text-center text-xl font-bold" />
|
||||||
<form>
|
<form>
|
||||||
<div>
|
<div>
|
||||||
<label v-t="'actions.file_format'" class="mr-2" for="export-format" />
|
<label v-t="'actions.file_format'" class="mr-2" for="export-format" />
|
||||||
<select id="export-format" v-model="exportAs" class="select">
|
<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"
|
||||||
|
>
|
||||||
<option
|
<option
|
||||||
v-for="option in exportOptions"
|
v-for="option in exportOptions"
|
||||||
:key="option"
|
:key="option"
|
||||||
@@ -18,7 +22,7 @@
|
|||||||
<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
|
<input
|
||||||
v-model="selectedFields"
|
v-model="selectedFields"
|
||||||
class="checkbox"
|
class="size-4"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:value="field"
|
:value="field"
|
||||||
:disabled="field === 'videoId'"
|
:disabled="field === 'videoId'"
|
||||||
@@ -27,7 +31,11 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<button class="btn mt-4" @click="handleExport" v-text="$t('actions.export')" />
|
<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')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ModalComponent>
|
</ModalComponent>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<h1 v-t="'titles.feed'" class="my-4 text-center font-bold" />
|
<h1 v-t="'titles.feed'" class="my-4 text-center font-bold" />
|
||||||
|
|
||||||
<div class="flex flex-col flex-wrap gap-2 children:(flex items-center gap-1) md:flex-row md:items-center">
|
<div class="flex flex-col flex-wrap gap-2 *:flex *:items-center *:gap-1 md:flex-row md:items-center">
|
||||||
<span>
|
<span>
|
||||||
<label for="filters">
|
<label for="filters">
|
||||||
<strong v-text="`${$t('actions.filter')}:`" />
|
<strong v-text="`${$t('actions.filter')}:`" />
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
id="filters"
|
id="filters"
|
||||||
v-model="selectedFilter"
|
v-model="selectedFilter"
|
||||||
default="all"
|
default="all"
|
||||||
class="select flex-grow"
|
class="h-8 grow 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" />
|
||||||
@@ -21,7 +21,12 @@
|
|||||||
<label for="group-selector">
|
<label for="group-selector">
|
||||||
<strong v-text="`${$t('titles.channel_groups')}:`" />
|
<strong v-text="`${$t('titles.channel_groups')}:`" />
|
||||||
</label>
|
</label>
|
||||||
<select id="group-selector" v-model="selectedGroupName" default="" class="select flex-grow">
|
<select
|
||||||
|
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"
|
||||||
|
>
|
||||||
<option v-t="`video.all`" value="" />
|
<option v-t="`video.all`" value="" />
|
||||||
<option
|
<option
|
||||||
v-for="group in channelGroups"
|
v-for="group in channelGroups"
|
||||||
@@ -39,14 +44,24 @@
|
|||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<span class="flex gap-2">
|
<span class="flex gap-2">
|
||||||
<router-link v-t="'titles.subscriptions'" class="btn" to="/subscriptions" />
|
<router-link
|
||||||
<a :href="getRssUrl" class="btn">
|
v-t="'titles.subscriptions'"
|
||||||
<i class="i-fa6-solid:rss" />
|
class="inline-block w-auto cursor-pointer rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
||||||
|
to="/subscriptions"
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
:href="getRssUrl"
|
||||||
|
class="inline-block w-auto cursor-pointer rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
||||||
|
>
|
||||||
|
<i-fa6-solid-rss />
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<LoadingIndicatorPage :show-content="videosStore != null" class="video-grid">
|
<LoadingIndicatorPage
|
||||||
|
:show-content="videosStore != null"
|
||||||
|
class="mx-2 grid grid-cols-1 gap-y-5 max-md:gap-x-3 sm:mx-0 sm:grid-cols-2 md:grid-cols-3 md:gap-x-6 lg:grid-cols-4 xl:grid-cols-5"
|
||||||
|
>
|
||||||
<template v-for="video in filteredVideos" :key="video.url">
|
<template v-for="video in filteredVideos" :key="video.url">
|
||||||
<VideoItem v-if="shouldShowVideo(video)" :is-feed="true" :item="video" @update:watched="onUpdateWatched" />
|
<VideoItem v-if="shouldShowVideo(video)" :is-feed="true" :item="video" @update:watched="onUpdateWatched" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,27 +1,41 @@
|
|||||||
<template>
|
<template>
|
||||||
<footer class="mt-10 w-full rounded-xl py-4 text-center children:(mx-3)">
|
<footer class="mt-10 flex w-full flex-wrap items-center justify-center gap-x-6 gap-y-4 rounded-xl py-4 text-center">
|
||||||
<a aria-label="GitHub" href="https://github.com/TeamPiped/Piped" target="_blank">
|
<a
|
||||||
<i class="i-fa6-brands:github" />
|
aria-label="GitHub"
|
||||||
|
href="https://github.com/TeamPiped/Piped"
|
||||||
|
target="_blank"
|
||||||
|
class="inline-flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<i-fa6-brands-github />
|
||||||
<span v-t="'actions.source_code'" class="ml-2 hover:underline" />
|
<span v-t="'actions.source_code'" class="ml-2 hover:underline" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://docs.piped.video/" target="_blank">
|
<a href="https://docs.piped.video/" target="_blank" class="inline-flex items-center justify-center">
|
||||||
<i class="i-fa6-solid:book" />
|
<i-fa6-solid-book />
|
||||||
<span v-t="'actions.documentation'" class="ml-2 hover:underline" />
|
<span v-t="'actions.documentation'" class="ml-2 hover:underline" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/TeamPiped/Piped#donations" target="_blank">
|
<a
|
||||||
<i class="i-fa6-brands:bitcoin" />
|
href="https://github.com/TeamPiped/Piped#donations"
|
||||||
|
target="_blank"
|
||||||
|
class="inline-flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<i-fa6-brands-bitcoin />
|
||||||
<span v-t="'actions.donations'" class="ml-2 hover:underline" />
|
<span v-t="'actions.donations'" class="ml-2 hover:underline" />
|
||||||
</a>
|
</a>
|
||||||
<a v-if="statusPageHref" :href="statusPageHref">
|
<a v-if="statusPageHref" :href="statusPageHref" class="inline-flex items-center justify-center">
|
||||||
<i class="i-fa6-solid:server" />
|
<i-fa6-solid-server />
|
||||||
<span v-t="'actions.status_page'" class="ml-2 hover:underline" />
|
<span v-t="'actions.status_page'" class="ml-2 hover:underline" />
|
||||||
</a>
|
</a>
|
||||||
<a v-if="donationHref" :href="donationHref">
|
<a v-if="donationHref" :href="donationHref" class="inline-flex items-center justify-center">
|
||||||
<i class="i-fa6-solid:money-check" />
|
<i-fa6-solid-money-check />
|
||||||
<span v-t="'actions.instance_donations'" class="ml-2 hover:underline" />
|
<span v-t="'actions.instance_donations'" class="ml-2 hover:underline" />
|
||||||
</a>
|
</a>
|
||||||
<a v-if="privacyPolicyHref" :href="privacyPolicyHref" target="_blank">
|
<a
|
||||||
<i class="i-fa6-solid:eye" />
|
v-if="privacyPolicyHref"
|
||||||
|
:href="privacyPolicyHref"
|
||||||
|
target="_blank"
|
||||||
|
class="inline-flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<i-fa6-solid-eye />
|
||||||
<span v-t="'actions.instance_privacy_policy'" class="ml-2 hover:underline" />
|
<span v-t="'actions.instance_privacy_policy'" class="ml-2 hover:underline" />
|
||||||
</a>
|
</a>
|
||||||
</footer>
|
</footer>
|
||||||
@@ -45,10 +59,14 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@reference "../app.css";
|
||||||
|
|
||||||
|
@layer components {
|
||||||
footer {
|
footer {
|
||||||
@apply bg-light-900;
|
background-color: #cacaca;
|
||||||
}
|
}
|
||||||
.dark footer {
|
.dark footer {
|
||||||
@apply bg-dark-800;
|
background-color: var(--color-dark-800);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,10 +3,22 @@
|
|||||||
|
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<div class="flex flex-col gap-2 md:flex-row md:items-center">
|
<div class="flex flex-col gap-2 md:flex-row md:items-center">
|
||||||
<button v-t="'actions.clear_history'" class="btn" @click="clearHistory" />
|
<button
|
||||||
|
v-t="'actions.clear_history'"
|
||||||
|
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="clearHistory"
|
||||||
|
/>
|
||||||
|
|
||||||
<button v-t="'actions.export_history'" class="btn" @click="showExportModal = !showExportModal" />
|
<button
|
||||||
<button v-t="'actions.import_history'" class="btn" @click="showImportModal = !showImportModal" />
|
v-t="'actions.export_history'"
|
||||||
|
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="showExportModal = !showExportModal"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-t="'actions.import_history'"
|
||||||
|
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="showImportModal = !showImportModal"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
@@ -16,7 +28,11 @@
|
|||||||
<div class="ml-4 flex items-center">
|
<div class="ml-4 flex items-center">
|
||||||
<input id="autoDelete" v-model="autoDeleteHistory" type="checkbox" @change="onChange" />
|
<input id="autoDelete" v-model="autoDeleteHistory" type="checkbox" @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 v-model="autoDeleteDelayHours" class="select ml-3 pl-3" @change="onChange">
|
<select
|
||||||
|
v-model="autoDeleteDelayHours"
|
||||||
|
class="ml-3 h-8 rounded-md bg-gray-300 pl-3 text-gray-600 dark:bg-dark-400 dark:text-gray-400"
|
||||||
|
@change="onChange"
|
||||||
|
>
|
||||||
<option v-t="{ path: 'info.hours', args: { amount: '1' } }" value="1" />
|
<option v-t="{ path: 'info.hours', args: { amount: '1' } }" value="1" />
|
||||||
<option v-t="{ path: 'info.hours', args: { amount: '3' } }" value="3" />
|
<option v-t="{ path: 'info.hours', args: { amount: '3' } }" value="3" />
|
||||||
<option v-t="{ path: 'info.hours', args: { amount: '6' } }" value="6" />
|
<option v-t="{ path: 'info.hours', args: { amount: '6' } }" value="6" />
|
||||||
@@ -33,7 +49,9 @@
|
|||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div class="video-grid">
|
<div
|
||||||
|
class="mx-2 grid grid-cols-1 gap-y-5 max-md:gap-x-3 sm:mx-0 sm:grid-cols-2 md:grid-cols-3 md:gap-x-6 lg:grid-cols-4 xl:grid-cols-5"
|
||||||
|
>
|
||||||
<VideoItem v-for="video in videos" :key="video.url" :item="video" />
|
<VideoItem v-for="video in videos" :key="video.url" :item="video" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,12 @@
|
|||||||
<form>
|
<form>
|
||||||
<br />
|
<br />
|
||||||
<div>
|
<div>
|
||||||
<input ref="fileSelector" class="btn mb-2 ml-2" type="file" @change="fileChange" />
|
<input
|
||||||
|
ref="fileSelector"
|
||||||
|
class="mb-2 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"
|
||||||
|
type="file"
|
||||||
|
@change="fileChange"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong
|
<strong
|
||||||
@@ -14,7 +19,7 @@
|
|||||||
</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="checkbox" type="checkbox" />
|
<span v-t="'actions.override'" />: <input v-model="override" class="size-4" type="checkbox" />
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
@@ -22,13 +27,19 @@
|
|||||||
<progress :value="index" :max="itemsLength" />
|
<progress :value="index" :max="itemsLength" />
|
||||||
<div
|
<div
|
||||||
v-text="
|
v-text="
|
||||||
`${$t('info.success')}: ${success} ${$t('info.error')}: ${error} ${$t('info.skipped')}: ${skipped}`
|
`${$t('info.success')}: ${success} ${$t('info.error')}: ${error} ${$t(
|
||||||
|
'info.skipped',
|
||||||
|
)}: ${skipped}`
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div>
|
<div>
|
||||||
<a class="btn w-auto" @click="handleImport" v-text="$t('actions.import')" />
|
<a
|
||||||
|
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"
|
||||||
|
v-text="$t('actions.import')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,11 +9,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong
|
<strong
|
||||||
><span v-t="'actions.override'" />: <input v-model="override" class="checkbox" type="checkbox"
|
><span v-t="'actions.override'" />: <input v-model="override" class="size-4" type="checkbox"
|
||||||
/></strong>
|
/></strong>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a v-t="'actions.import'" class="btn w-auto" @click="handleImport" />
|
<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"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="!showContent" class="min-h-[75vh] w-full flex items-center justify-center">
|
<div v-if="!showContent" class="grid min-h-[75vh] w-full place-items-center">
|
||||||
<span id="spinner" />
|
<span id="spinner" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else v-bind="$attrs">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
});
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
showContent: {
|
showContent: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -29,6 +33,7 @@ defineProps({
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 70px;
|
width: 70px;
|
||||||
height: 70px;
|
height: 70px;
|
||||||
|
margin-inline: auto;
|
||||||
}
|
}
|
||||||
#spinner:after {
|
#spinner:after {
|
||||||
content: " ";
|
content: " ";
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<h1 v-t="'titles.login'" class="my-4 text-center font-bold" />
|
<h1 v-t="'titles.login'" class="my-4 text-center font-bold" />
|
||||||
<i class="i-fa6-solid:circle-info ml-2 mt-6 cursor-pointer" :title="$t('info.login_note')" />
|
<i-fa6-solid-circle-info class="mt-6 ml-2 cursor-pointer" :title="$t('info.login_note')" />
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div class="w-full flex items-center justify-center text-center">
|
<div class="flex w-full items-center justify-center text-center">
|
||||||
<form class="w-min children:pb-3">
|
<form class="w-min *:pb-3">
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
v-model="username"
|
v-model="username"
|
||||||
class="input"
|
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"
|
||||||
type="text"
|
type="text"
|
||||||
autocomplete="username"
|
autocomplete="username"
|
||||||
:placeholder="$t('login.username')"
|
:placeholder="$t('login.username')"
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
v-model="password"
|
v-model="password"
|
||||||
class="input"
|
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"
|
||||||
type="password"
|
type="password"
|
||||||
autocomplete="password"
|
autocomplete="password"
|
||||||
:placeholder="$t('login.password')"
|
:placeholder="$t('login.password')"
|
||||||
@@ -29,7 +29,11 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a v-t="'titles.login'" class="btn w-auto" @click="login" />
|
<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"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal">
|
<div class="fixed top-0 left-0 z-50 table size-full bg-gray-500/80 transition-opacity dark:bg-dark-900/80">
|
||||||
<div @click="handleClick">
|
<div class="table-cell align-middle" @click="handleClick">
|
||||||
<div class="modal-container">
|
<div class="relative m-auto w-min min-w-[20vw] rounded-xl bg-white p-5 dark:bg-dark-700">
|
||||||
<button @click="$emit('close')"><i class="i-fa6-solid:xmark" /></button>
|
<button class="absolute top-1 right-2.5" @click="$emit('close')"><i-fa6-solid-xmark /></button>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,27 +34,3 @@ onUnmounted(() => {
|
|||||||
window.removeEventListener("keydown", handleKeyDown);
|
window.removeEventListener("keydown", handleKeyDown);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.modal {
|
|
||||||
@apply fixed z-50 top-0 left-0 w-full h-full bg-gray bg-opacity-80 transition-opacity table;
|
|
||||||
}
|
|
||||||
.dark .modal {
|
|
||||||
@apply bg-dark-900 bg-opacity-80;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal > div {
|
|
||||||
@apply table-cell align-middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-container {
|
|
||||||
@apply w-min m-auto bg-white p-5 rounded-xl min-w-[20vw] relative;
|
|
||||||
}
|
|
||||||
.dark .modal-container {
|
|
||||||
@apply bg-dark-700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-container > button {
|
|
||||||
@apply absolute right-2.5 top-1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<nav class="relative w-full flex flex-wrap items-center justify-center px-2 pb-2.5 sm:px-4">
|
<nav class="relative flex w-full flex-wrap items-center justify-center px-2 pb-2.5 sm:px-4">
|
||||||
<div class="flex flex-1 justify-start">
|
<div class="flex flex-1 justify-start">
|
||||||
<router-link class="flex items-center text-3xl font-bold font-sans" :to="homePagePath"
|
<router-link class="flex items-center font-sans text-3xl font-bold" :to="homePagePath"
|
||||||
><img
|
><img
|
||||||
alt="logo"
|
alt="logo"
|
||||||
src="/img/icons/logo.svg"
|
src="/img/icons/logo.svg"
|
||||||
@@ -11,11 +11,11 @@
|
|||||||
/>iped</router-link
|
/>iped</router-link
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="search-container lt-md:hidden">
|
<div class="relative inline-flex items-center max-md:hidden">
|
||||||
<input
|
<input
|
||||||
ref="videoSearch"
|
ref="videoSearch"
|
||||||
v-model="searchText"
|
v-model="searchText"
|
||||||
class="input h-10 w-72 pr-20"
|
class="h-10 w-72 rounded-md bg-gray-300 px-2.5 pr-20 text-gray-600 focus:shadow-red-400 focus:outline-2 focus:outline-red-500 dark:bg-dark-400 dark:text-gray-400"
|
||||||
type="text"
|
type="text"
|
||||||
role="search"
|
role="search"
|
||||||
:title="$t('actions.search')"
|
:title="$t('actions.search')"
|
||||||
@@ -25,81 +25,106 @@
|
|||||||
@focus="onInputFocus"
|
@focus="onInputFocus"
|
||||||
@blur="onInputBlur"
|
@blur="onInputBlur"
|
||||||
/>
|
/>
|
||||||
<span v-if="searchText" class="delete-search" @click="searchText = ''">⨉</span>
|
<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
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<button id="search-btn" class="input btn mx-1 h-10" @click="onSearchClick">
|
<button
|
||||||
<div class="i-fa6-solid:magnifying-glass"></div>
|
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"
|
||||||
|
@click="onSearchClick"
|
||||||
|
>
|
||||||
|
<i-fa6-solid-magnifying-glass />
|
||||||
</button>
|
</button>
|
||||||
<!-- three vertical lines for toggling the hamburger menu on mobile -->
|
<!-- three vertical lines for toggling the hamburger menu on mobile -->
|
||||||
<button class="mr-3 flex flex-col justify-end md:hidden" @click="showTopNav = !showTopNav">
|
<button class="mr-3 flex flex-col justify-end md:hidden" @click="showTopNav = !showTopNav">
|
||||||
<span class="line"></span>
|
<span class="my-[0.1125rem] rounded-xl bg-dark-900 px-2.5 py-px dark:bg-white"></span>
|
||||||
<span class="line"></span>
|
<span class="my-[0.1125rem] rounded-xl bg-dark-900 px-2.5 py-px dark:bg-white"></span>
|
||||||
<span class="line"></span>
|
<span class="my-[0.1125rem] rounded-xl bg-dark-900 px-2.5 py-px dark:bg-white"></span>
|
||||||
</button>
|
</button>
|
||||||
<!-- navigation bar for large screen devices -->
|
<!-- navigation bar for large screen devices -->
|
||||||
<ul class="md:text-1xl hidden list-none md:(flex flex flex-1 justify-end children:pl-3)">
|
<ul class="hidden list-none *:pl-3 md:flex md:flex-1 md:justify-end">
|
||||||
<li v-if="shouldShowTrending">
|
<li v-if="shouldShowTrending">
|
||||||
<router-link v-t="'titles.trending'" to="/trending" class="nav-link" />
|
<router-link
|
||||||
|
v-t="'titles.trending'"
|
||||||
|
to="/trending"
|
||||||
|
class="hover:text-red-500 dark:hover:text-red-400"
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<router-link v-t="'titles.preferences'" to="/preferences" class="nav-link" />
|
<router-link
|
||||||
|
v-t="'titles.preferences'"
|
||||||
|
to="/preferences"
|
||||||
|
class="hover:text-red-500 dark:hover:text-red-400"
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="shouldShowLogin">
|
<li v-if="shouldShowLogin">
|
||||||
<router-link v-t="'titles.login'" to="/login" class="nav-link" />
|
<router-link v-t="'titles.login'" to="/login" class="hover:text-red-500 dark:hover:text-red-400" />
|
||||||
</li>
|
</li>
|
||||||
<li v-if="shouldShowRegister">
|
<li v-if="shouldShowRegister">
|
||||||
<router-link v-t="'titles.register'" to="/register" class="nav-link" />
|
<router-link
|
||||||
|
v-t="'titles.register'"
|
||||||
|
to="/register"
|
||||||
|
class="hover:text-red-500 dark:hover:text-red-400"
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="shouldShowHistory">
|
<li v-if="shouldShowHistory">
|
||||||
<router-link v-t="'titles.history'" to="/history" class="nav-link" />
|
<router-link v-t="'titles.history'" to="/history" class="hover:text-red-500 dark:hover:text-red-400" />
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<router-link v-t="'titles.playlists'" to="/playlists" class="nav-link" />
|
<router-link
|
||||||
|
v-t="'titles.playlists'"
|
||||||
|
to="/playlists"
|
||||||
|
class="hover:text-red-500 dark:hover:text-red-400"
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="!shouldShowTrending">
|
<li v-if="!shouldShowTrending">
|
||||||
<router-link v-t="'titles.feed'" to="/feed" class="nav-link" />
|
<router-link v-t="'titles.feed'" to="/feed" class="hover:text-red-500 dark:hover:text-red-400" />
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<!-- navigation bar for mobile devices -->
|
<!-- navigation bar for mobile devices -->
|
||||||
<div
|
<div
|
||||||
v-if="showTopNav"
|
v-if="showTopNav"
|
||||||
class="mobile-nav mb-4 flex flex-col children:(w-full flex items-center gap-1 border-b border-dark-100 p-1)"
|
class="mb-4 flex flex-col *:flex *:w-full *:items-center *:gap-1 *:border-b *:border-dark-100 *:p-1"
|
||||||
>
|
>
|
||||||
<router-link v-if="shouldShowTrending" to="/trending">
|
<router-link v-if="shouldShowTrending" to="/trending">
|
||||||
<div class="i-fa6-solid:fire"></div>
|
<i-fa6-solid-fire />
|
||||||
<i18n-t keypath="titles.trending"></i18n-t>
|
<i18n-t keypath="titles.trending"></i18n-t>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link to="/preferences">
|
<router-link to="/preferences">
|
||||||
<div class="i-fa6-solid:gear"></div>
|
<i-fa6-solid-gear />
|
||||||
<i18n-t keypath="titles.preferences"></i18n-t>
|
<i18n-t keypath="titles.preferences"></i18n-t>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link v-if="shouldShowLogin" to="/login">
|
<router-link v-if="shouldShowLogin" to="/login">
|
||||||
<div class="i-fa6-solid:user"></div>
|
<i-fa6-solid-user />
|
||||||
<i18n-t keypath="titles.login"></i18n-t>
|
<i18n-t keypath="titles.login"></i18n-t>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link v-if="shouldShowLogin" to="/register">
|
<router-link v-if="shouldShowLogin" to="/register">
|
||||||
<div class="i-fa6-solid:user-plus"></div>
|
<i-fa6-solid-user-plus />
|
||||||
<i18n-t keypath="titles.register"></i18n-t>
|
<i18n-t keypath="titles.register"></i18n-t>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link v-if="shouldShowHistory" to="/history">
|
<router-link v-if="shouldShowHistory" to="/history">
|
||||||
<div class="i-fa6-solid:clock-rotate-left"></div>
|
<i-fa6-solid-clock-rotate-left />
|
||||||
<i18n-t keypath="titles.history"></i18n-t>
|
<i18n-t keypath="titles.history"></i18n-t>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link to="/playlists">
|
<router-link to="/playlists">
|
||||||
<div class="i-fa6-solid:list"></div>
|
<i-fa6-solid-list />
|
||||||
<i18n-t keypath="titles.playlists"></i18n-t>
|
<i18n-t keypath="titles.playlists"></i18n-t>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link v-if="!shouldShowTrending" to="/feed">
|
<router-link v-if="!shouldShowTrending" to="/feed">
|
||||||
<div class="i-fa6-solid:play"></div>
|
<i-fa6-solid-play />
|
||||||
<i18n-t keypath="titles.feed"></i18n-t>
|
<i18n-t keypath="titles.feed"></i18n-t>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<!-- search suggestions for mobile devices -->
|
<!-- search suggestions for mobile devices -->
|
||||||
<div class="search-container mb-2 w-full md:hidden">
|
<div class="relative mb-2 inline-flex w-full items-center md:hidden">
|
||||||
<input
|
<input
|
||||||
v-model="searchText"
|
v-model="searchText"
|
||||||
class="input h-10 w-full"
|
class="h-10 w-full 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"
|
||||||
type="text"
|
type="text"
|
||||||
role="search"
|
role="search"
|
||||||
:title="$t('actions.search')"
|
:title="$t('actions.search')"
|
||||||
@@ -109,7 +134,12 @@
|
|||||||
@focus="onInputFocus"
|
@focus="onInputFocus"
|
||||||
@blur="onInputBlur"
|
@blur="onInputBlur"
|
||||||
/>
|
/>
|
||||||
<span v-if="searchText" class="delete-search" @click="searchText = ''">⨉</span>
|
<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
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<SearchSuggestions
|
<SearchSuggestions
|
||||||
v-show="(searchText || showSearchHistory) && suggestionsVisible"
|
v-show="(searchText || showSearchHistory) && suggestionsVisible"
|
||||||
@@ -237,32 +267,6 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.search-container {
|
|
||||||
@apply relative inline-flex items-center;
|
|
||||||
}
|
|
||||||
.delete-search {
|
|
||||||
@apply absolute right-3 cursor-pointer rounded-full bg-[#ccc] w-4 h-4 text-center text-black opacity-50 hover:(opacity-70) text-size-[10px];
|
|
||||||
}
|
|
||||||
.mobile-nav div {
|
|
||||||
@apply mx-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-btn:hover {
|
|
||||||
@apply bg-red-400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark #search-btn:hover {
|
|
||||||
@apply bg-dark-100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link {
|
|
||||||
@apply hover:text-red-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .nav-link {
|
|
||||||
@apply hover:text-red-400;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 848px) {
|
@media screen and (max-width: 848px) {
|
||||||
#search-btn {
|
#search-btn {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -3,9 +3,13 @@ const homeUrl = import.meta.env.BASE_URL;
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-[88vh] flex flex-col items-center justify-center">
|
<div class="flex min-h-[88vh] flex-col items-center justify-center">
|
||||||
<h1 class="font-bold !text-9xl">404</h1>
|
<h1 class="text-9xl! font-bold">404</h1>
|
||||||
<h2 v-t="'info.page_not_found'" class="!text-2xl" />
|
<h2 v-t="'info.page_not_found'" class="text-2xl!" />
|
||||||
<a v-t="'actions.back_to_home'" class="btn mt-16" :href="homeUrl" />
|
<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"
|
||||||
|
:href="homeUrl"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<ModalComponent @close="$emit('close')">
|
<ModalComponent @close="$emit('close')">
|
||||||
<span v-t="'actions.select_playlist'" class="inline-block w-max text-2xl" />
|
<span v-t="'actions.select_playlist'" class="inline-block w-max text-2xl" />
|
||||||
<select v-model="selectedPlaylist" class="select mt-3 w-full">
|
<select
|
||||||
|
v-model="selectedPlaylist"
|
||||||
|
class="mt-3 h-8 w-full rounded-md bg-gray-300 px-2.5 text-gray-600 dark:bg-dark-400 dark:text-gray-400"
|
||||||
|
>
|
||||||
<option v-for="playlist in playlists" :key="playlist.id" :value="playlist.id" v-text="playlist.name" />
|
<option v-for="playlist in playlists" :key="playlist.id" :value="playlist.id" v-text="playlist.name" />
|
||||||
</select>
|
</select>
|
||||||
<div class="mt-3 w-full flex justify-between">
|
<div class="mt-3 flex w-full justify-between">
|
||||||
<button
|
<button
|
||||||
ref="addButton"
|
ref="addButton"
|
||||||
v-t="'actions.create_playlist'"
|
v-t="'actions.create_playlist'"
|
||||||
class="btn"
|
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="showCreatePlaylistModal = true"
|
@click="showCreatePlaylistModal = true"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
ref="addButton"
|
ref="addButton"
|
||||||
v-t="'actions.add_to_playlist'"
|
v-t="'actions.add_to_playlist'"
|
||||||
class="btn"
|
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="handleClick(selectedPlaylist)"
|
@click="handleClick(selectedPlaylist)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,27 +1,46 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col flex-justify-between">
|
<div class="flex flex-col justify-between">
|
||||||
<router-link :to="props.item.url" class="link inline-block">
|
<router-link
|
||||||
|
:to="props.item.url"
|
||||||
|
class="inline-block hover:text-red-500 focus:text-red-500 dark:hover:text-red-400 dark:focus:text-red-400"
|
||||||
|
>
|
||||||
<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 class="link pt-2 font-bold" :title="props.item.name" v-text="props.item.name" />
|
<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"
|
||||||
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
<p v-if="props.item.description" v-text="props.item.description" />
|
<p
|
||||||
|
v-if="props.item.description"
|
||||||
|
class="mt-1 line-clamp-2 text-sm text-gray-600 dark:text-gray-400"
|
||||||
|
v-text="props.item.description"
|
||||||
|
/>
|
||||||
|
|
||||||
<router-link v-if="props.item.uploaderUrl" class="link-secondary text-sm" :to="props.item.uploaderUrl">
|
<router-link
|
||||||
<p>
|
v-if="props.item.uploaderUrl"
|
||||||
|
class="mt-1 line-clamp-1 block text-sm underline decoration-dark-400 hover:text-dark-400 focus:text-dark-400 dark:text-gray-300 dark:decoration-dark-100 dark:hover:text-gray-400"
|
||||||
|
:to="props.item.uploaderUrl"
|
||||||
|
>
|
||||||
|
<p class="leading-tight">
|
||||||
<span v-text="props.item.uploaderName" />
|
<span v-text="props.item.uploaderName" />
|
||||||
<i v-if="props.item.uploaderVerified" class="i-fa6-solid:check ml-1.5" />
|
<i-fa6-solid-check v-if="props.item.uploaderVerified" class="ml-1.5" />
|
||||||
</p>
|
</p>
|
||||||
</router-link>
|
</router-link>
|
||||||
<a v-else-if="props.item.uploaderName" class="link" v-text="props.item.uploaderName" />
|
<a
|
||||||
|
v-else-if="props.item.uploaderName"
|
||||||
|
class="mt-1 line-clamp-1 block text-sm hover:text-red-500 focus:text-red-500 dark:text-gray-300 dark:hover:text-red-400 dark:focus:text-red-400"
|
||||||
|
v-text="props.item.uploaderName"
|
||||||
|
/>
|
||||||
|
|
||||||
<template v-if="props.item.videos >= 0">
|
<template v-if="props.item.videos >= 0">
|
||||||
<br v-if="props.item.uploaderName" />
|
<span
|
||||||
<span class="text-sm" v-text="`${props.item.videos} ${$t('video.videos')}`" />
|
class="mt-1 text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
|
v-text="`${props.item.videos} ${$t('video.videos')}`"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<br />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,16 @@
|
|||||||
<ErrorHandler v-if="playlist && playlist.error" :message="playlist.message" :error="playlist.error" />
|
<ErrorHandler v-if="playlist && playlist.error" :message="playlist.message" :error="playlist.error" />
|
||||||
|
|
||||||
<LoadingIndicatorPage v-show="!playlist?.error" :show-content="playlist != null">
|
<LoadingIndicatorPage v-show="!playlist?.error" :show-content="playlist != null">
|
||||||
<h1 class="mb-1 ml-1 mt-4 text-3xl!" v-text="playlist.name" />
|
<h1 class="mt-4 mb-1 ml-1 text-3xl!" v-text="playlist.name" />
|
||||||
|
|
||||||
<CollapsableText v-if="playlist?.description" :text="playlist.description" />
|
<CollapsableText v-if="playlist?.description" :text="playlist.description" />
|
||||||
|
|
||||||
<div class="mt-1 flex justify-between <md:flex-col md:items-center">
|
<div class="mt-1 flex justify-between max-md:flex-col md:items-center">
|
||||||
<div>
|
<div>
|
||||||
<router-link class="link flex items-center gap-3" :to="playlist.uploaderUrl || '/'">
|
<router-link
|
||||||
|
class="flex items-center gap-3 hover:text-red-500 focus:text-red-500 dark:hover:text-red-400 dark:focus:text-red-400"
|
||||||
|
:to="playlist.uploaderUrl || '/'"
|
||||||
|
>
|
||||||
<img loading="lazy" :src="playlist.uploaderAvatar" class="h-12 rounded-full" />
|
<img loading="lazy" :src="playlist.uploaderAvatar" class="h-12 rounded-full" />
|
||||||
<strong v-text="playlist.uploader" />
|
<strong v-text="playlist.uploader" />
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -21,18 +24,32 @@
|
|||||||
}`
|
}`
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<button v-if="!isPipedPlaylist" class="btn mx-1" @click="bookmarkPlaylist">
|
<button
|
||||||
|
v-if="!isPipedPlaylist"
|
||||||
|
class="mx-1 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="bookmarkPlaylist"
|
||||||
|
>
|
||||||
{{ $t(`actions.${isBookmarked ? "playlist_bookmarked" : "bookmark_playlist"}`)
|
{{ $t(`actions.${isBookmarked ? "playlist_bookmarked" : "bookmark_playlist"}`)
|
||||||
}}<i class="i-fa6-solid:bookmark ml-3" />
|
}}<i-fa6-solid-bookmark class="ml-3" />
|
||||||
</button>
|
</button>
|
||||||
<button v-if="authenticated && !isPipedPlaylist" class="btn mr-1" @click="clonePlaylist">
|
<button
|
||||||
{{ $t("actions.clone_playlist") }}<i class="i-fa6-solid:clone ml-3" />
|
v-if="authenticated && !isPipedPlaylist"
|
||||||
|
class="mr-1 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="clonePlaylist"
|
||||||
|
>
|
||||||
|
{{ $t("actions.clone_playlist") }}<i-fa6-solid-clone class="ml-3" />
|
||||||
</button>
|
</button>
|
||||||
<button class="btn mr-1" @click="downloadPlaylistAsTxt">
|
<button
|
||||||
|
class="mr-1 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="downloadPlaylistAsTxt"
|
||||||
|
>
|
||||||
{{ $t("actions.download_as_txt") }}
|
{{ $t("actions.download_as_txt") }}
|
||||||
</button>
|
</button>
|
||||||
<a class="btn mr-1" :href="getRssUrl">
|
<a
|
||||||
<i class="i-fa6-solid:rss" />
|
class="mr-1 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"
|
||||||
|
:href="getRssUrl"
|
||||||
|
>
|
||||||
|
<i-fa6-solid-rss />
|
||||||
</a>
|
</a>
|
||||||
<WatchOnButton :link="`https://www.youtube.com/playlist?list=${$route.query.list}`" />
|
<WatchOnButton :link="`https://www.youtube.com/playlist?list=${$route.query.list}`" />
|
||||||
</div>
|
</div>
|
||||||
@@ -40,7 +57,9 @@
|
|||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div class="video-grid">
|
<div
|
||||||
|
class="mx-2 grid grid-cols-1 gap-y-5 max-md:gap-x-3 sm:mx-0 sm:grid-cols-2 md:grid-cols-3 md:gap-x-6 lg:grid-cols-4 xl:grid-cols-5"
|
||||||
|
>
|
||||||
<VideoItem
|
<VideoItem
|
||||||
v-for="(video, index) in playlist.relatedStreams"
|
v-for="(video, index) in playlist.relatedStreams"
|
||||||
:key="video.url"
|
:key="video.url"
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<router-link :to="{ path: '/playlist', query: { list: playlistId } }"
|
<router-link :to="{ path: '/playlist', query: { list: playlistId } }"
|
||||||
><h1 class="font-bold !text-lg hover:underline" v-text="playlist.name"
|
><h1 class="text-lg! font-bold hover:underline" v-text="playlist.name"
|
||||||
/></router-link>
|
/></router-link>
|
||||||
<span class="text-sm">
|
<span class="text-sm">
|
||||||
<template v-if="playlist.uploader">
|
<template v-if="playlist.uploader">
|
||||||
<router-link class="link-secondary" :to="playlist.uploaderUrl" :title="playlist.uploader">
|
<router-link
|
||||||
|
class="underline decoration-dark-400 hover:text-dark-400 focus:text-dark-400 dark:text-gray-300 dark:decoration-dark-100 dark:hover:text-gray-400"
|
||||||
|
:to="playlist.uploaderUrl"
|
||||||
|
:title="playlist.uploader"
|
||||||
|
>
|
||||||
{{ playlist.uploader }}
|
{{ playlist.uploader }}
|
||||||
</router-link>
|
</router-link>
|
||||||
-
|
-
|
||||||
@@ -13,7 +17,7 @@
|
|||||||
{{ selectedIndex }} / {{ playlist.videos }}
|
{{ selectedIndex }} / {{ playlist.videos }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div ref="scrollable" class="mt-4 max-h-screen-sm overflow-y-auto">
|
<div ref="scrollable" class="mt-4 max-h-160 overflow-y-auto">
|
||||||
<div
|
<div
|
||||||
v-for="(related, index) in playlist.relatedStreams"
|
v-for="(related, index) in playlist.relatedStreams"
|
||||||
:key="related.url"
|
:key="related.url"
|
||||||
@@ -22,8 +26,8 @@
|
|||||||
:prefer-listen="preferListen"
|
:prefer-listen="preferListen"
|
||||||
>
|
>
|
||||||
<router-link
|
<router-link
|
||||||
class="flex rounded px-2 py-1 hover:bg-gray-50 .dark:hover:bg-neutral-800"
|
class="flex rounded-sm px-2 py-1 hover:bg-gray-50 dark:hover:bg-neutral-800"
|
||||||
:class="{ 'bg-gray-200 .dark:bg-neutral-700': index === selectedIndex - 1 }"
|
:class="{ 'bg-gray-200 dark:bg-neutral-700': index === selectedIndex - 1 }"
|
||||||
:to="{
|
:to="{
|
||||||
path: '/watch',
|
path: '/watch',
|
||||||
query: {
|
query: {
|
||||||
@@ -40,16 +44,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ml-2 flex flex-col">
|
<div class="ml-2 flex flex-col">
|
||||||
<span class="link" :title="related.title" v-text="related.title" />
|
<span
|
||||||
|
class="hover:text-red-500 focus:text-red-500 dark:hover:text-red-400 dark:focus:text-red-400"
|
||||||
|
:title="related.title"
|
||||||
|
v-text="related.title"
|
||||||
|
/>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<router-link
|
<router-link
|
||||||
v-if="related.uploaderUrl && related.uploaderName && !hideChannel"
|
v-if="related.uploaderUrl && related.uploaderName && !hideChannel"
|
||||||
class="link-secondary block overflow-hidden text-xs"
|
class="block overflow-hidden text-xs underline decoration-dark-400 hover:text-dark-400 focus:text-dark-400 dark:text-gray-300 dark:decoration-dark-100 dark:hover:text-gray-400"
|
||||||
:to="related.uploaderUrl"
|
:to="related.uploaderUrl"
|
||||||
:title="related.uploaderName"
|
:title="related.uploaderName"
|
||||||
>
|
>
|
||||||
<span v-text="related.uploaderName" />
|
<span v-text="related.uploaderName" />
|
||||||
<i v-if="related.uploaderVerified" class="i-fa6-solid:check ml-1.5" />
|
<i-fa6-solid-check v-if="related.uploaderVerified" class="ml-1.5" />
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,9 +2,18 @@
|
|||||||
<h2 v-t="'titles.playlists'" class="my-4 font-bold" />
|
<h2 v-t="'titles.playlists'" class="my-4 font-bold" />
|
||||||
|
|
||||||
<div class="mb-3 flex justify-between">
|
<div class="mb-3 flex justify-between">
|
||||||
<button v-t="'actions.create_playlist'" class="btn" @click="showCreatePlaylistModal = true" />
|
<button
|
||||||
|
v-t="'actions.create_playlist'"
|
||||||
|
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="showCreatePlaylistModal = true"
|
||||||
|
/>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<button v-if="playlists.length > 0" v-t="'actions.export_to_json'" class="btn" @click="exportPlaylists" />
|
<button
|
||||||
|
v-if="playlists.length > 0"
|
||||||
|
v-t="'actions.export_to_json'"
|
||||||
|
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="exportPlaylists"
|
||||||
|
/>
|
||||||
<input
|
<input
|
||||||
id="fileSelector"
|
id="fileSelector"
|
||||||
ref="fileSelector"
|
ref="fileSelector"
|
||||||
@@ -13,45 +22,63 @@
|
|||||||
multiple="multiple"
|
multiple="multiple"
|
||||||
@change="importPlaylists"
|
@change="importPlaylists"
|
||||||
/>
|
/>
|
||||||
<label v-t="'actions.import_from_json_csv'" for="fileSelector" class="btn ml-2" />
|
<label
|
||||||
|
v-t="'actions.import_from_json_csv'"
|
||||||
|
for="fileSelector"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="video-grid">
|
<div
|
||||||
|
class="mx-2 grid grid-cols-1 gap-y-5 max-md:gap-x-3 sm:mx-0 sm:grid-cols-2 md:grid-cols-3 md:gap-x-6 lg:grid-cols-4 xl:grid-cols-5"
|
||||||
|
>
|
||||||
<div v-for="playlist in playlists" :key="playlist.id">
|
<div v-for="playlist in playlists" :key="playlist.id">
|
||||||
<router-link :to="`/playlist?list=${playlist.id}`">
|
<router-link :to="`/playlist?list=${playlist.id}`">
|
||||||
<img class="w-full" :src="playlist.thumbnail" alt="thumbnail" />
|
<img class="w-full" :src="playlist.thumbnail" alt="thumbnail" />
|
||||||
<div class="relative text-sm">
|
<div class="relative text-sm">
|
||||||
<span
|
<span
|
||||||
class="thumbnail-overlay thumbnail-right"
|
class="absolute right-1.25 bottom-1.25 rounded-md bg-black/75 px-1.25 text-white"
|
||||||
v-text="`${playlist.videos} ${$t('video.videos')}`"
|
v-text="`${playlist.videos} ${$t('video.videos')}`"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical"
|
style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical"
|
||||||
class="link my-2 flex overflow-hidden"
|
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"
|
||||||
/>
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
<button v-t="'actions.edit_playlist'" class="btn h-auto" @click="showPlaylistEditModal(playlist)" />
|
<button
|
||||||
<button v-t="'actions.delete_playlist'" class="btn ml-2 h-auto" @click="playlistToDelete = playlist.id" />
|
v-t="'actions.edit_playlist'"
|
||||||
|
class="inline-block size-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="showPlaylistEditModal(playlist)"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-t="'actions.delete_playlist'"
|
||||||
|
class="ml-2 inline-block size-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="playlistToDelete = playlist.id"
|
||||||
|
/>
|
||||||
<ModalComponent v-if="playlist.id == playlistToEdit" @close="playlistToEdit = null">
|
<ModalComponent v-if="playlist.id == playlistToEdit" @close="playlistToEdit = null">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<h2 v-t="'actions.edit_playlist'" />
|
<h2 v-t="'actions.edit_playlist'" />
|
||||||
<input
|
<input
|
||||||
v-model="newPlaylistName"
|
v-model="newPlaylistName"
|
||||||
class="input"
|
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"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="$t('actions.playlist_name')"
|
:placeholder="$t('actions.playlist_name')"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
v-model="newPlaylistDescription"
|
v-model="newPlaylistDescription"
|
||||||
class="input"
|
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"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="$t('actions.playlist_description')"
|
:placeholder="$t('actions.playlist_description')"
|
||||||
/>
|
/>
|
||||||
<button v-t="'actions.okay'" class="btn ml-auto" @click="editPlaylist(playlist)" />
|
<button
|
||||||
|
v-t="'actions.okay'"
|
||||||
|
class="ml-auto 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="editPlaylist(playlist)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ModalComponent>
|
</ModalComponent>
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
@@ -66,7 +93,10 @@
|
|||||||
|
|
||||||
<h2 v-t="'titles.bookmarks'" class="my-4 font-bold" />
|
<h2 v-t="'titles.bookmarks'" class="my-4 font-bold" />
|
||||||
|
|
||||||
<div v-if="bookmarks" class="video-grid">
|
<div
|
||||||
|
v-if="bookmarks"
|
||||||
|
class="mx-2 grid grid-cols-1 gap-y-5 max-md:gap-x-3 sm:mx-0 sm:grid-cols-2 md:grid-cols-3 md:gap-x-6 lg:grid-cols-4 xl:grid-cols-5"
|
||||||
|
>
|
||||||
<router-link
|
<router-link
|
||||||
v-for="(playlist, index) in bookmarks"
|
v-for="(playlist, index) in bookmarks"
|
||||||
:key="playlist.playlistId"
|
:key="playlist.playlistId"
|
||||||
@@ -74,19 +104,22 @@
|
|||||||
>
|
>
|
||||||
<img class="w-full" :src="playlist.thumbnail" alt="thumbnail" />
|
<img class="w-full" :src="playlist.thumbnail" alt="thumbnail" />
|
||||||
<div class="relative text-sm">
|
<div class="relative text-sm">
|
||||||
<span class="thumbnail-overlay thumbnail-right" v-text="`${playlist.videos} ${$t('video.videos')}`" />
|
<span
|
||||||
<div class="absolute bottom-100px right-5px z-100 px-5px" @click.prevent="removeBookmark(index)">
|
class="absolute right-1.25 bottom-1.25 rounded-md bg-black/75 px-1.25 text-white"
|
||||||
<i class="i-fa6-solid:bookmark ml-3" />
|
v-text="`${playlist.videos} ${$t('video.videos')}`"
|
||||||
|
/>
|
||||||
|
<div class="absolute right-1.25 bottom-25 z-100 px-1.25" @click.prevent="removeBookmark(index)">
|
||||||
|
<i-fa6-solid-bookmark class="ml-3" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical"
|
style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical"
|
||||||
class="link my-2 flex overflow-hidden"
|
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"
|
||||||
/>
|
/>
|
||||||
<a :href="playlist.uploaderUrl" class="flex items-center">
|
<a :href="playlist.uploaderUrl" class="flex items-center">
|
||||||
<img class="h-32px w-32px rounded-full" :src="playlist.uploaderAvatar" />
|
<img class="size-8 rounded-full" :src="playlist.uploaderAvatar" />
|
||||||
<span class="ml-3 hover:underline" v-text="playlist.uploader" />
|
<span class="ml-3 hover:underline" v-text="playlist.uploader" />
|
||||||
</a>
|
</a>
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -95,7 +128,7 @@
|
|||||||
<CreatePlaylistModal
|
<CreatePlaylistModal
|
||||||
v-if="showCreatePlaylistModal"
|
v-if="showCreatePlaylistModal"
|
||||||
@close="showCreatePlaylistModal = false"
|
@close="showCreatePlaylistModal = false"
|
||||||
@created="fetchPlaylists"
|
@created="fetchPlaylistsList"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<h1 v-t="'titles.register'" class="my-4 text-center font-bold" />
|
<h1 v-t="'titles.register'" class="my-4 text-center font-bold" />
|
||||||
<i class="i-fa6-solid:circle-info ml-2 mt-6 cursor-pointer" :title="$t('info.register_note')" />
|
<i-fa6-solid-circle-info class="mt-6 ml-2 cursor-pointer" :title="$t('info.register_note')" />
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div class="flex flex-col items-center justify-center text-center">
|
<div class="flex flex-col items-center justify-center text-center">
|
||||||
<form class="w-max items-center px-3 children:pb-3">
|
<form class="w-max items-center px-3 *:pb-3">
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
v-model="username"
|
v-model="username"
|
||||||
class="input w-full"
|
class="h-8 w-full 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"
|
||||||
type="text"
|
type="text"
|
||||||
autocomplete="username"
|
autocomplete="username"
|
||||||
:placeholder="$t('login.username')"
|
:placeholder="$t('login.username')"
|
||||||
@@ -20,33 +20,45 @@
|
|||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<input
|
<input
|
||||||
v-model="password"
|
v-model="password"
|
||||||
class="input h-auto w-full"
|
class="h-auto w-full 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"
|
||||||
:type="showPassword ? 'text' : 'password'"
|
:type="showPassword ? 'text' : 'password'"
|
||||||
autocomplete="password"
|
autocomplete="password"
|
||||||
:placeholder="$t('login.password')"
|
:placeholder="$t('login.password')"
|
||||||
:aria-label="$t('login.password')"
|
:aria-label="$t('login.password')"
|
||||||
@keyup.enter="register"
|
@keyup.enter="register"
|
||||||
/>
|
/>
|
||||||
<button type="button" class="btn ml-2" @click="showPassword = !showPassword">
|
<button
|
||||||
<div class="i-fa6-solid:eye" />
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<input
|
<input
|
||||||
v-model="passwordConfirm"
|
v-model="passwordConfirm"
|
||||||
class="input h-auto w-full"
|
class="h-auto w-full 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"
|
||||||
:type="showConfirmPassword ? 'text' : 'password'"
|
:type="showConfirmPassword ? 'text' : 'password'"
|
||||||
autocomplete="password"
|
autocomplete="password"
|
||||||
:placeholder="$t('login.password_confirm')"
|
:placeholder="$t('login.password_confirm')"
|
||||||
:aria-label="$t('login.password_confirm')"
|
:aria-label="$t('login.password_confirm')"
|
||||||
@keyup.enter="register"
|
@keyup.enter="register"
|
||||||
/>
|
/>
|
||||||
<button type="button" class="btn ml-2" @click="showConfirmPassword = !showConfirmPassword">
|
<button
|
||||||
<div class="i-fa6-solid:eye" />
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a v-t="'titles.register'" class="btn w-auto" @click="register" />
|
<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"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,13 @@
|
|||||||
<label for="ddlSearchFilters">
|
<label for="ddlSearchFilters">
|
||||||
<strong v-text="`${$t('actions.filter')}:`" />
|
<strong v-text="`${$t('actions.filter')}:`" />
|
||||||
</label>
|
</label>
|
||||||
<select id="ddlSearchFilters" v-model="selectedFilter" default="all" class="select w-auto" @change="updateFilter()">
|
<select
|
||||||
|
id="ddlSearchFilters"
|
||||||
|
v-model="selectedFilter"
|
||||||
|
default="all"
|
||||||
|
class="h-8 w-auto rounded-md bg-gray-300 px-2.5 text-gray-600 dark:bg-dark-400 dark:text-gray-400"
|
||||||
|
@change="updateFilter()"
|
||||||
|
>
|
||||||
<option v-for="filter in availableFilters" :key="filter" v-t="`search.${filter}`" :value="filter" />
|
<option v-for="filter in availableFilters" :key="filter" v-t="`search.${filter}`" :value="filter" />
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
@@ -18,7 +24,10 @@
|
|||||||
</i18n-t>
|
</i18n-t>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LoadingIndicatorPage :show-content="Boolean(results != null && results.items?.length)" class="video-grid">
|
<LoadingIndicatorPage
|
||||||
|
:show-content="Boolean(results != null && results.items?.length)"
|
||||||
|
class="mx-2 grid grid-cols-1 gap-y-5 max-md:gap-x-3 sm:mx-0 sm:grid-cols-2 md:grid-cols-3 md:gap-x-6 lg:grid-cols-4 xl:grid-cols-5"
|
||||||
|
>
|
||||||
<template v-for="result in results.items" :key="result.url">
|
<template v-for="result in results.items" :key="result.url">
|
||||||
<ContentItem :item="result" height="94" width="168" />
|
<ContentItem :item="result" height="94" width="168" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="suggestions-container absolute">
|
<div
|
||||||
|
class="absolute left-1/2 z-10 box-border w-full max-w-3xl -translate-x-1/2 transform-gpu bg-gray-300 max-md:max-w-[calc(100%-0.5rem)] dark:bg-dark-400"
|
||||||
|
>
|
||||||
<ul>
|
<ul>
|
||||||
<li
|
<li
|
||||||
v-for="(suggestion, i) in searchSuggestions"
|
v-for="(suggestion, i) in searchSuggestions"
|
||||||
@@ -8,8 +10,8 @@
|
|||||||
@click="setSelected(i)"
|
@click="setSelected(i)"
|
||||||
>
|
>
|
||||||
<router-link
|
<router-link
|
||||||
class="suggestion"
|
class="block w-full p-1"
|
||||||
:class="{ 'suggestion-selected': selected === i }"
|
:class="{ 'bg-gray-200 dark:bg-dark-100': selected === i }"
|
||||||
:to="`/results?search_query=${encodeURIComponent(suggestion)}`"
|
:to="`/results?search_query=${encodeURIComponent(suggestion)}`"
|
||||||
>{{ suggestion }}</router-link
|
>{{ suggestion }}</router-link
|
||||||
>
|
>
|
||||||
@@ -84,25 +86,3 @@ function setSelected(val) {
|
|||||||
|
|
||||||
defineExpose({ onKeyUp, refreshSuggestions });
|
defineExpose({ onKeyUp, refreshSuggestions });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.suggestions-container {
|
|
||||||
@apply left-1/2 translate-x-[-50%] transform-gpu max-w-3xl w-full box-border z-10 lt-md:max-w-[calc(100%-0.5rem)] bg-gray-300;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .suggestions-container {
|
|
||||||
@apply bg-dark-400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestion-selected {
|
|
||||||
@apply bg-gray-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .suggestion-selected {
|
|
||||||
@apply bg-dark-100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestion {
|
|
||||||
@apply block w-full p-1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -16,7 +16,12 @@
|
|||||||
</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'" />
|
||||||
<input v-model="timeStamp" class="input w-12" type="text" @change="onChange" />
|
<input
|
||||||
|
v-model="timeStamp"
|
||||||
|
class="h-8 w-12 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"
|
||||||
|
type="text"
|
||||||
|
@change="onChange"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<a :href="generatedLink" target="_blank">
|
<a :href="generatedLink" target="_blank">
|
||||||
@@ -24,9 +29,21 @@
|
|||||||
</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 v-t="'actions.generate_qrcode'" class="btn" @click="showQrCode = !showQrCode" />
|
<button
|
||||||
<button v-t="'actions.follow_link'" class="btn ml-3" @click="followLink()" />
|
v-t="'actions.generate_qrcode'"
|
||||||
<button v-t="'actions.copy_link'" class="btn ml-3" @click="copyLink()" />
|
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()"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ModalComponent>
|
</ModalComponent>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<label v-t="'actions.sort_by'" for="ddlSortBy" />
|
<label v-t="'actions.sort_by'" for="ddlSortBy" />
|
||||||
<select id="ddlSortBy" v-model="selectedSort" class="select flex-grow">
|
<select
|
||||||
|
id="ddlSortBy"
|
||||||
|
v-model="selectedSort"
|
||||||
|
class="h-8 grow rounded-md bg-gray-300 px-2.5 text-gray-600 dark:bg-dark-400 dark:text-gray-400"
|
||||||
|
>
|
||||||
<option v-for="(value, key) in options" :key="key" v-t="`actions.${key}`" :value="value" />
|
<option v-for="(value, key) in options" :key="key" v-t="`actions.${key}`" :value="value" />
|
||||||
</select>
|
</select>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -4,18 +4,24 @@
|
|||||||
<div class="flex flex-wrap justify-between">
|
<div class="flex flex-wrap justify-between">
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<!-- import json/csv -->
|
<!-- import json/csv -->
|
||||||
<button class="btn">
|
<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"
|
||||||
|
>
|
||||||
<router-link v-t="'actions.import_from_json_csv'" to="/import" />
|
<router-link v-t="'actions.import_from_json_csv'" to="/import" />
|
||||||
</button>
|
</button>
|
||||||
<!-- export to json -->
|
<!-- export to json -->
|
||||||
<button v-t="'actions.export_to_json'" class="btn" @click="exportHandler" />
|
<button
|
||||||
|
v-t="'actions.export_to_json'"
|
||||||
|
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="exportHandler"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="m-1 flex flex-wrap gap-1">
|
<div class="m-1 flex flex-wrap gap-1">
|
||||||
<!-- import channel groups to json-->
|
<!-- import channel groups to json-->
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
for="fileSelector"
|
for="fileSelector"
|
||||||
class="btn"
|
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"
|
||||||
v-text="`${$t('actions.import_from_json')} (${$t('titles.channel_groups')})`"
|
v-text="`${$t('actions.import_from_json')} (${$t('titles.channel_groups')})`"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
@@ -30,30 +36,30 @@
|
|||||||
|
|
||||||
<!-- export channel groups to json -->
|
<!-- export channel groups to json -->
|
||||||
<button
|
<button
|
||||||
class="btn"
|
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="exportGroupsHandler"
|
@click="exportGroupsHandler"
|
||||||
v-text="`${$t('actions.export_to_json')} (${$t('titles.channel_groups')})`"
|
v-text="`${$t('actions.export_to_json')} (${$t('titles.channel_groups')})`"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- subscriptions count, only shown if there are any -->
|
<!-- subscriptions count, only shown if there are any -->
|
||||||
<div v-if="subscriptions.length > 0" class="flex self-center gap-1">
|
<div v-if="subscriptions.length > 0" class="flex gap-1 self-center">
|
||||||
<i18n-t keypath="subscriptions.subscribed_channels_count">{{ subscriptions.length }}</i18n-t>
|
<i18n-t keypath="subscriptions.subscribed_channels_count">{{ subscriptions.length }}</i18n-t>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<hr />
|
<hr />
|
||||||
<div class="w-full flex flex-wrap">
|
<div class="flex w-full flex-wrap">
|
||||||
<button
|
<button
|
||||||
v-for="group in channelGroups"
|
v-for="group in channelGroups"
|
||||||
:key="group.groupName"
|
:key="group.groupName"
|
||||||
class="btn mx-1 w-max"
|
class="mx-1 inline-block w-max 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="{ selected: selectedGroup === group }"
|
:class="{ 'border-2 border-red-500': selectedGroup === group }"
|
||||||
@click="selectGroup(group)"
|
@click="selectGroup(group)"
|
||||||
>
|
>
|
||||||
<span v-text="group.groupName !== '' ? group.groupName : $t('video.all')" />
|
<span v-text="group.groupName !== '' ? group.groupName : $t('video.all')" />
|
||||||
<div v-if="group.groupName != '' && selectedGroup == group">
|
<div v-if="group.groupName != '' && selectedGroup == group">
|
||||||
<i class="i-fa6-solid:pen mx-2" @click="showEditGroupModal = true" />
|
<i-fa6-solid-pen class="mx-2" @click="showEditGroupModal = true" />
|
||||||
<i class="i-fa6-solid:circle-minus mx-2" @click="groupToDelete = group.groupName" />
|
<i-fa6-solid-circle-minus class="mx-2" @click="groupToDelete = group.groupName" />
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
@@ -62,28 +68,31 @@
|
|||||||
@close="groupToDelete = null"
|
@close="groupToDelete = null"
|
||||||
@confirm="deleteGroup(groupToDelete)"
|
@confirm="deleteGroup(groupToDelete)"
|
||||||
/>
|
/>
|
||||||
<button class="btn mx-1" @click="showCreateGroupModal = true">
|
<button
|
||||||
<i class="i-fa6-solid:circle-plus" />
|
class="mx-1 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="showCreateGroupModal = true"
|
||||||
|
>
|
||||||
|
<i-fa6-solid-circle-plus />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<hr />
|
<hr />
|
||||||
<!-- Subscriptions card list -->
|
<!-- Subscriptions card list -->
|
||||||
<div class="xl:grid xl:grid-cols-5 <md:flex-wrap">
|
<div class="max-md:flex-wrap xl:grid xl:grid-cols-5">
|
||||||
<!-- channel info card -->
|
<!-- channel info card -->
|
||||||
<div
|
<div
|
||||||
v-for="subscription in filteredSubscriptions"
|
v-for="subscription in filteredSubscriptions"
|
||||||
:key="subscription.url"
|
:key="subscription.url"
|
||||||
class="col m-2 border border-gray-500 rounded-lg p-1"
|
class="m-2 rounded-lg border border-gray-500 p-1"
|
||||||
>
|
>
|
||||||
<router-link :to="subscription.url" class="text-4x4 flex p-2 font-bold">
|
<router-link :to="subscription.url" class="flex p-2 text-4xl font-bold">
|
||||||
<img :src="subscription.avatar" class="h-[fit-content] rounded-full" width="48" height="48" />
|
<img :src="subscription.avatar" class="h-fit rounded-full" width="48" height="48" />
|
||||||
<span class="mx-2 self-center" v-text="subscription.name" />
|
<span class="mx-2 self-center" v-text="subscription.name" />
|
||||||
</router-link>
|
</router-link>
|
||||||
<!-- subscribe / unsubscribe btn -->
|
<!-- subscribe / unsubscribe btn -->
|
||||||
<button
|
<button
|
||||||
v-t="`actions.${subscription.subscribed ? 'unsubscribe' : 'subscribe'}`"
|
v-t="`actions.${subscription.subscribed ? 'unsubscribe' : 'subscribe'}`"
|
||||||
class="btn mt-2 w-full"
|
class="mt-2 inline-block w-full 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="handleButton(subscription)"
|
@click="handleButton(subscription)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -97,20 +106,29 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<ModalComponent v-if="showEditGroupModal" @close="showEditGroupModal = false">
|
<ModalComponent v-if="showEditGroupModal" @close="showEditGroupModal = false">
|
||||||
<div class="mb-5 mt-3 flex justify-between">
|
<div class="mt-3 mb-5 flex justify-between">
|
||||||
<input v-model="editedGroupName" type="text" class="input" />
|
<input
|
||||||
<button v-t="'actions.okay'" class="btn" :placeholder="$t('actions.group_name')" @click="editGroupName()" />
|
v-model="editedGroupName"
|
||||||
|
type="text"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-t="'actions.okay'"
|
||||||
|
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"
|
||||||
|
:placeholder="$t('actions.group_name')"
|
||||||
|
@click="editGroupName()"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2 mt-3 h-[80vh] flex flex-col overflow-y-scroll pr-2">
|
<div class="mt-3 mb-2 flex h-[80vh] flex-col overflow-y-scroll pr-2">
|
||||||
<div v-for="subscription in subscriptions" :key="subscription.name">
|
<div v-for="subscription in subscriptions" :key="subscription.name">
|
||||||
<div class="mr-3 flex items-center justify-between">
|
<div class="mr-3 flex items-center justify-between">
|
||||||
<a :href="subscription.url" target="_blank" class="flex items-center overflow-hidden">
|
<a :href="subscription.url" target="_blank" class="flex items-center overflow-hidden">
|
||||||
<img :src="subscription.avatar" class="h-8 w-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
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="checkbox"
|
class="size-4"
|
||||||
:checked="selectedGroup.channels.includes(subscription.url.substr(-24))"
|
:checked="selectedGroup.channels.includes(subscription.url.substr(-24))"
|
||||||
@change="checkedChange(subscription)"
|
@change="checkedChange(subscription)"
|
||||||
/>
|
/>
|
||||||
@@ -276,9 +294,3 @@ onActivated(() => {
|
|||||||
document.title = "Subscriptions - Piped";
|
document.title = "Subscriptions - Piped";
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.selected {
|
|
||||||
border: 0.1rem outset red;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="toast">
|
<div
|
||||||
|
class="fixed top-12 right-12 z-9999 flex min-w-max flex-col justify-center rounded-sm bg-white/80 p-4 text-black shadow-sm duration-200 dark:bg-dark-900/80 dark:text-white"
|
||||||
|
>
|
||||||
<slot />
|
<slot />
|
||||||
<button v-t="'actions.dismiss'" @click="dismiss" />
|
<button v-t="'actions.dismiss'" class="underline" @click="dismiss" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -12,15 +14,3 @@ function dismiss() {
|
|||||||
emit("dismissed");
|
emit("dismissed");
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.toast {
|
|
||||||
@apply bg-white/80 text-black flex flex-col justify-center fixed top-12 right-12 p-4 min-w-max shadow rounded duration-200 z-9999;
|
|
||||||
}
|
|
||||||
.dark .toast {
|
|
||||||
@apply bg-dark-900/80 text-white;
|
|
||||||
}
|
|
||||||
.toast button {
|
|
||||||
@apply underline;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<LoadingIndicatorPage :show-content="videos.length != 0" class="video-grid">
|
<LoadingIndicatorPage
|
||||||
|
:show-content="videos.length != 0"
|
||||||
|
class="mx-2 grid grid-cols-1 gap-y-5 max-md:gap-x-3 sm:mx-0 sm:grid-cols-2 md:grid-cols-3 md:gap-x-6 lg:grid-cols-4 xl:grid-cols-5"
|
||||||
|
>
|
||||||
<VideoItem v-for="video in videos" :key="video.url" :item="video" height="118" width="210" />
|
<VideoItem v-for="video in videos" :key="video.url" :item="video" height="118" width="210" />
|
||||||
</LoadingIndicatorPage>
|
</LoadingIndicatorPage>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="showVideo" class="flex flex-col flex-justify-between">
|
<div v-if="showVideo" class="flex flex-col justify-between">
|
||||||
<router-link
|
<router-link
|
||||||
class="link inline-block w-full"
|
class="inline-block w-full hover:text-red-500 focus:text-red-500 dark:hover:text-red-400 dark:focus:text-red-400"
|
||||||
:to="{
|
:to="{
|
||||||
path: '/watch',
|
path: '/watch',
|
||||||
query: {
|
query: {
|
||||||
@@ -17,52 +17,54 @@
|
|||||||
<div>
|
<div>
|
||||||
<p
|
<p
|
||||||
style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical"
|
style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical"
|
||||||
class="link flex overflow-hidden pt-2 font-bold"
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<div class="flex">
|
<div class="flex items-start pt-1">
|
||||||
<router-link :to="item.uploaderUrl">
|
<router-link :to="item.uploaderUrl">
|
||||||
<img
|
<img
|
||||||
v-if="item.uploaderAvatar"
|
v-if="item.uploaderAvatar"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
:src="item.uploaderAvatar"
|
:src="item.uploaderAvatar"
|
||||||
class="mr-0.5 mt-0.5 h-32px w-32px rounded-full"
|
class="mt-0.5 mr-0.5 size-8 shrink-0 rounded-full"
|
||||||
width="68"
|
width="68"
|
||||||
height="68"
|
height="68"
|
||||||
/>
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<div class="flex-1 px-2">
|
<div class="min-w-0 flex-1 px-2">
|
||||||
<router-link
|
<router-link
|
||||||
v-if="item.uploaderUrl && item.uploaderName && !hideChannel"
|
v-if="item.uploaderUrl && item.uploaderName && !hideChannel"
|
||||||
class="link-secondary block overflow-hidden text-sm"
|
class="inline-flex max-w-full items-center gap-1 text-sm/tight underline decoration-dark-400 hover:text-dark-400 focus:text-dark-400 dark:text-gray-300 dark:decoration-dark-100 dark:hover:text-gray-400 dark:hover:underline dark:hover:decoration-gray-400"
|
||||||
:to="item.uploaderUrl"
|
:to="item.uploaderUrl"
|
||||||
:title="item.uploaderName"
|
:title="item.uploaderName"
|
||||||
>
|
>
|
||||||
<span v-text="item.uploaderName" />
|
<span class="truncate" v-text="item.uploaderName" />
|
||||||
<i v-if="item.uploaderVerified" class="i-fa6-solid:check ml-1.5" />
|
<i-fa6-solid-check v-if="item.uploaderVerified" class="shrink-0" />
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<div v-if="item.views >= 0 || item.uploadedDate" class="video-info">
|
<div
|
||||||
<span v-if="item.views >= 0">
|
v-if="item.views >= 0 || item.uploadedDate"
|
||||||
<i class="i-fa6-solid:eye" />
|
class="mt-1 flex flex-wrap items-center gap-x-1 text-xs font-normal text-gray-600 dark:text-gray-400"
|
||||||
<span class="pl-1" v-text="`${numberFormat(item.views)} •`" />
|
>
|
||||||
|
<span v-if="item.views >= 0" class="inline-flex items-center gap-1">
|
||||||
|
<i-fa6-solid-eye class="shrink-0" />
|
||||||
|
<span v-text="`${numberFormat(item.views)} •`" />
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="item.uploaded > 0"
|
v-if="item.uploaded > 0"
|
||||||
class="pl-0.5"
|
|
||||||
:title="new Date(item.uploaded).toLocaleString()"
|
:title="new Date(item.uploaded).toLocaleString()"
|
||||||
v-text="timeAgo(item.uploaded)"
|
v-text="timeAgo(item.uploaded)"
|
||||||
/>
|
/>
|
||||||
<span v-else-if="item.uploadedDate" class="pl-0.5" v-text="item.uploadedDate" />
|
<span v-else-if="item.uploadedDate" v-text="item.uploadedDate" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2.5">
|
<div class="ml-1 flex shrink-0 items-center gap-2.5 pt-0.5">
|
||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
path: '/watch',
|
path: '/watch',
|
||||||
@@ -76,13 +78,14 @@
|
|||||||
:aria-label="preferListen ? title : 'Listen to ' + title"
|
:aria-label="preferListen ? title : 'Listen to ' + title"
|
||||||
:title="preferListen ? title : 'Listen to ' + title"
|
:title="preferListen ? title : 'Listen to ' + title"
|
||||||
>
|
>
|
||||||
<i :class="preferListen ? 'i-fa6-solid:tv' : 'i-fa6-solid:headphones'" />
|
<i-fa6-solid-tv v-if="preferListen" />
|
||||||
|
<i-fa6-solid-headphones v-else />
|
||||||
</router-link>
|
</router-link>
|
||||||
<button :title="$t('actions.add_to_playlist')" @click="showPlaylistModal = !showPlaylistModal">
|
<button :title="$t('actions.add_to_playlist')" @click="showPlaylistModal = !showPlaylistModal">
|
||||||
<i class="i-fa6-solid:circle-plus" />
|
<i-fa6-solid-circle-plus />
|
||||||
</button>
|
</button>
|
||||||
<button :title="$t('actions.share')" @click="showShareModal = !showShareModal">
|
<button :title="$t('actions.share')" @click="showShareModal = !showShareModal">
|
||||||
<i class="i-fa6-solid:share" />
|
<i-fa6-solid-share />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="admin"
|
v-if="admin"
|
||||||
@@ -90,19 +93,18 @@
|
|||||||
:title="$t('actions.remove_from_playlist')"
|
:title="$t('actions.remove_from_playlist')"
|
||||||
@click="showConfirmRemove = true"
|
@click="showConfirmRemove = true"
|
||||||
>
|
>
|
||||||
<i class="i-fa6-solid:circle-minus" />
|
<i-fa6-solid-circle-minus />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="showMarkOnWatched && isFeed"
|
v-if="showMarkOnWatched && isFeed"
|
||||||
ref="watchButton"
|
ref="watchButton"
|
||||||
@click="toggleWatched(item.url.substr(-11))"
|
@click="toggleWatched(item.url.substr(-11))"
|
||||||
>
|
>
|
||||||
<i
|
<i-fa6-solid-eye-slash
|
||||||
v-if="item.watched && item.currentTime > item.duration * 0.9"
|
v-if="item.watched && item.currentTime > item.duration * 0.9"
|
||||||
:title="$t('actions.mark_as_unwatched')"
|
:title="$t('actions.mark_as_unwatched')"
|
||||||
class="i-fa6-solid:eye-slash"
|
|
||||||
/>
|
/>
|
||||||
<i v-else :title="$t('actions.mark_as_watched')" class="i-fa6-solid:eye" />
|
<i-fa6-solid-eye v-else :title="$t('actions.mark_as_watched')" />
|
||||||
</button>
|
</button>
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
v-if="showConfirmRemove"
|
v-if="showConfirmRemove"
|
||||||
@@ -229,13 +231,3 @@ onMounted(() => {
|
|||||||
shouldShowMarkOnWatched();
|
shouldShowMarkOnWatched();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.video-info {
|
|
||||||
@apply mt-1 text-xs text-gray-600 font-normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .video-info {
|
|
||||||
@apply text-gray-400;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
<div
|
<div
|
||||||
ref="container"
|
ref="container"
|
||||||
data-shaka-player-container
|
data-shaka-player-container
|
||||||
class="relative max-h-screen w-full flex justify-center"
|
class="relative flex max-h-screen w-full justify-center"
|
||||||
:class="{ 'player-container': !isEmbed }"
|
:class="{ 'max-h-[75vh] min-h-64 bg-black': !isEmbed }"
|
||||||
>
|
>
|
||||||
<video
|
<video
|
||||||
ref="videoEl"
|
ref="videoEl"
|
||||||
@@ -22,26 +22,26 @@
|
|||||||
@click="onClickSkipSegment"
|
@click="onClickSkipSegment"
|
||||||
>
|
>
|
||||||
<span v-t="'actions.skip_segment'" />
|
<span v-t="'actions.skip_segment'" />
|
||||||
<i class="skip-segment-icon i-fa6-solid:forward-step" aria-hidden="true" />
|
<i-fa6-solid-forward-step class="skip-segment-icon" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<span
|
<span
|
||||||
v-if="error > 0"
|
v-if="error > 0"
|
||||||
v-t="{ path: 'player.failed', args: [error] }"
|
v-t="{ path: 'player.failed', args: [error] }"
|
||||||
class="absolute top-8 rounded bg-black/80 p-2 text-lg backdrop-blur-sm"
|
class="absolute top-8 rounded-sm bg-black/80 p-2 text-lg backdrop-blur-sm"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="showCurrentSpeed"
|
v-if="showCurrentSpeed"
|
||||||
class="text-l absolute left-1/2 top-1/2 flex flex-col transform items-center gap-6 rounded-8 bg-white/80 px-8 py-4 -translate-x-1/2 -translate-y-1/2 .dark:bg-dark-700/80"
|
class="absolute top-1/2 left-1/2 flex -translate-1/2 transform flex-col items-center gap-6 rounded-4xl bg-white/80 px-8 py-4 text-lg dark:bg-dark-700/80"
|
||||||
>
|
>
|
||||||
<i class="i-fa6-solid:gauge-high h-25 w-25 p-5" />
|
<i-fa6-solid-gauge-high class="size-25 p-5" />
|
||||||
<span v-text="videoEl.playbackRate" />
|
<span v-text="videoEl.playbackRate" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="showCurrentVolume"
|
v-if="showCurrentVolume"
|
||||||
class="text-l absolute left-1/2 top-1/2 flex flex-col transform items-center gap-6 rounded-8 bg-white/80 px-8 py-4 -translate-x-1/2 -translate-y-1/2 .dark:bg-dark-700/80"
|
class="absolute top-1/2 left-1/2 flex -translate-1/2 transform flex-col items-center gap-6 rounded-4xl bg-white/80 px-8 py-4 text-lg dark:bg-dark-700/80"
|
||||||
>
|
>
|
||||||
<i v-if="videoEl.volume > 0" class="i-fa6-solid:volume-high h-25 w-25 p-5" />
|
<i-fa6-solid-volume-high v-if="videoEl.volume > 0" class="size-25 p-5" />
|
||||||
<i v-else class="i-fa6-solid:volume-xmark h-25 w-25 p-5" />
|
<i-fa6-solid-volume-xmark v-else class="size-25 p-5" />
|
||||||
<span v-text="Math.round(videoEl.volume * 100) / 100" />
|
<span v-text="Math.round(videoEl.volume * 100) / 100" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,12 +51,16 @@
|
|||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<input
|
<input
|
||||||
v-model="playbackSpeedInput"
|
v-model="playbackSpeedInput"
|
||||||
class="input my-3"
|
class="my-3 h-8 w-auto 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"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="$t('actions.playback_speed')"
|
:placeholder="$t('actions.playback_speed')"
|
||||||
@keyup.enter="setSpeedFromInput()"
|
@keyup.enter="setSpeedFromInput()"
|
||||||
/>
|
/>
|
||||||
<button v-t="'actions.okay'" class="btn ml-auto w-min" @click="setSpeedFromInput()" />
|
<button
|
||||||
|
v-t="'actions.okay'"
|
||||||
|
class="ml-auto inline-block w-min cursor-pointer rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
||||||
|
@click="setSpeedFromInput()"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ModalComponent>
|
</ModalComponent>
|
||||||
</template>
|
</template>
|
||||||
@@ -917,6 +921,7 @@ defineExpose({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--player-base: rgba(255, 255, 255, 0.3);
|
--player-base: rgba(255, 255, 255, 0.3);
|
||||||
--player-buffered: rgba(255, 255, 255, 0.54);
|
--player-buffered: rgba(255, 255, 255, 0.54);
|
||||||
@@ -933,13 +938,12 @@ defineExpose({
|
|||||||
--spon-seg-music_offtopic: #ff9900;
|
--spon-seg-music_offtopic: #ff9900;
|
||||||
--spon-seg-default: white;
|
--spon-seg-default: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-container {
|
|
||||||
@apply max-h-75vh min-h-64 bg-black;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
.shaka-video-container .material-icons-round {
|
.shaka-video-container .material-icons-round {
|
||||||
@apply !text-xl;
|
font-size: 1.25rem !important;
|
||||||
|
line-height: 1.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shaka-video-container:-webkit-full-screen {
|
.shaka-video-container:-webkit-full-screen {
|
||||||
@@ -961,42 +965,11 @@ defineExpose({
|
|||||||
padding: 0.09em 0;
|
padding: 0.09em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skip-segment-button {
|
|
||||||
/* position button above player overlay */
|
|
||||||
z-index: 1000;
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
transform: translate(0, -50%);
|
|
||||||
top: 50%;
|
|
||||||
right: 0;
|
|
||||||
|
|
||||||
background-color: rgb(0 0 0 / 0.5);
|
|
||||||
border: 2px rgba(255, 255, 255, 0.75) solid;
|
|
||||||
border-right: 0;
|
|
||||||
border-radius: 0.75em;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
padding: 0.5em;
|
|
||||||
|
|
||||||
/* center text vertically */
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
color: #fff;
|
|
||||||
line-height: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skip-segment-button .skip-segment-icon {
|
|
||||||
font-size: 1.6em !important;
|
|
||||||
line-height: inherit !important;
|
|
||||||
margin-left: 0.4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Override Tailwind preflight's `img { max-width: 100% }` which clamps
|
/* Override Tailwind preflight's `img { max-width: 100% }` which clamps
|
||||||
the sprite-sheet image to the container width and breaks Shaka's
|
the sprite-sheet image to the container width and breaks Shaka's
|
||||||
transform-based thumbnail cropping. */
|
transform-based thumbnail cropping. */
|
||||||
.shaka-player-ui-thumbnail-image {
|
.shaka-player-ui-thumbnail-image {
|
||||||
max-width: none !important;
|
max-width: none !important;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full">
|
<div class="relative w-full">
|
||||||
<img
|
<img
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
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="{ 'shorts-img': item.isShort, 'opacity-75': item.watched }"
|
:class="{ 'w-full object-contain': item.isShort, 'opacity-75': item.watched }"
|
||||||
/>
|
/>
|
||||||
<!-- progress bar -->
|
<!-- progress bar -->
|
||||||
<div class="relative h-1 w-full">
|
<div class="relative h-1 w-full">
|
||||||
@@ -15,20 +15,33 @@
|
|||||||
:style="{ width: `clamp(0%, ${(item.currentTime / item.duration) * 100}%, 100%` }"
|
:style="{ width: `clamp(0%, ${(item.currentTime / item.duration) * 100}%, 100%` }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="relative" :class="small ? 'text-xs' : 'text-sm'">
|
|
||||||
<!-- shorts thumbnail -->
|
<!-- shorts thumbnail -->
|
||||||
<span v-if="item.isShort" v-t="'video.shorts'" class="thumbnail-overlay thumbnail-left" />
|
<span
|
||||||
|
v-if="item.isShort"
|
||||||
|
v-t="'video.shorts'"
|
||||||
|
class="absolute bottom-1.25 left-1.25 rounded-md bg-red-600 px-1.25 text-xs font-bold text-white uppercase"
|
||||||
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="item.duration > 0 || (item.duration == 0 && item.isShort)"
|
v-if="item.duration > 0 || (item.duration == 0 && item.isShort)"
|
||||||
class="thumbnail-overlay thumbnail-right px-0.5"
|
class="absolute right-1.25 bottom-1.25 rounded-md bg-black/75 px-0.5 text-white"
|
||||||
|
:class="small ? 'text-xs' : 'text-sm'"
|
||||||
v-text="timeFormat(item.duration)"
|
v-text="timeFormat(item.duration)"
|
||||||
/>
|
/>
|
||||||
<i18n-t v-else keypath="video.live" class="thumbnail-overlay thumbnail-right !bg-red-600" tag="div">
|
<i18n-t
|
||||||
<i class="i-fa6-solid:tower-broadcast w-3" />
|
v-else
|
||||||
|
keypath="video.live"
|
||||||
|
class="absolute right-1.25 bottom-1.25 inline-flex items-center gap-1 rounded-md bg-red-600 px-1.25 text-white"
|
||||||
|
:class="small ? 'text-xs' : 'text-sm'"
|
||||||
|
tag="div"
|
||||||
|
>
|
||||||
|
<i-fa6-solid-tower-broadcast class="w-3" />
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
<span v-if="item.watched" v-t="'video.watched'" class="thumbnail-overlay bottom-5px left-5px" />
|
<span
|
||||||
|
v-if="item.watched"
|
||||||
|
v-t="'video.watched'"
|
||||||
|
class="absolute bottom-1.25 left-1.25 rounded-md bg-black/75 px-1.25 text-white"
|
||||||
|
:class="small ? 'text-xs' : 'text-sm'"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -49,9 +62,3 @@ defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.shorts-img {
|
|
||||||
@apply w-full object-contain;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -17,15 +17,21 @@ defineProps({
|
|||||||
<template>
|
<template>
|
||||||
<template v-if="getPreferenceBoolean('showWatchOnYouTube', false)">
|
<template v-if="getPreferenceBoolean('showWatchOnYouTube', false)">
|
||||||
<!-- For large screens -->
|
<!-- For large screens -->
|
||||||
<a :href="link" class="btn flex items-center lt-lg:hidden">
|
<a
|
||||||
|
:href="link"
|
||||||
|
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-lg:hidden max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
||||||
|
>
|
||||||
<i18n-t keypath="player.watch_on" tag="strong">{{ platform }}</i18n-t>
|
<i18n-t keypath="player.watch_on" tag="strong">{{ platform }}</i18n-t>
|
||||||
<i v-if="platform == 'YouTube'" class="i-fa6-brands:youtube mx-1.5" />
|
<i-fa6-brands-youtube v-if="platform == 'YouTube'" class="mx-1.5" />
|
||||||
<i v-else-if="platform == 'Odysee'" class="i-fa6-brands:odysee mx-1.5" />
|
<i-fa6-brands-odysee v-else-if="platform == 'Odysee'" class="mx-1.5" />
|
||||||
</a>
|
</a>
|
||||||
<!-- For small screens -->
|
<!-- For small screens -->
|
||||||
<a :href="link" class="btn flex items-center lg:hidden">
|
<a
|
||||||
<i v-if="platform == 'YouTube'" class="i-fa6-brands:youtube mx-1.5" />
|
:href="link"
|
||||||
<i v-else-if="platform == 'Odysee'" class="i-fa6-brands:odysee mx-1.5" />
|
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 lg:hidden dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
||||||
|
>
|
||||||
|
<i-fa6-brands-youtube v-if="platform == 'YouTube'" class="mx-1.5" />
|
||||||
|
<i-fa6-brands-odysee v-else-if="platform == 'Odysee'" class="mx-1.5" />
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="video && isEmbed" class="absolute left-0 top-0 z-50 h-full w-full bg-black">
|
<div v-if="video && isEmbed" class="absolute top-0 left-0 z-50 size-full bg-black">
|
||||||
<VideoPlayer
|
<VideoPlayer
|
||||||
ref="videoPlayer"
|
ref="videoPlayer"
|
||||||
:video="video"
|
:video="video"
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
:is-embed="isEmbed"
|
:is-embed="isEmbed"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div id="theaterModeSpot" class="-mx-1vw"></div>
|
<div id="theaterModeSpot" class="-mx-[1vw]"></div>
|
||||||
<LoadingIndicatorPage :show-content="video != null && !isEmbed" class="w-full">
|
<LoadingIndicatorPage :show-content="video != null && !isEmbed" class="w-full">
|
||||||
<ErrorHandler v-if="video && video.error" :message="video.message" :error="video.error" />
|
<ErrorHandler v-if="video && video.error" :message="video.message" :error="video.error" />
|
||||||
<Transition>
|
<Transition>
|
||||||
@@ -43,9 +43,10 @@
|
|||||||
setPreference('theaterMode', theaterMode);
|
setPreference('theaterMode', theaterMode);
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div
|
<div>
|
||||||
:class="theaterMode ? 'i-fa6-solid:chevron-left' : 'i-fa6-solid:chevron-right'"
|
<i-fa6-solid-chevron-left v-if="theaterMode" />
|
||||||
></div>
|
<i-fa6-solid-chevron-right v-else />
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@@ -66,8 +67,8 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- video title -->
|
<!-- video title -->
|
||||||
<div class="mt-2 break-words text-2xl font-bold" v-text="video.title" />
|
<div class="mt-2 text-2xl font-bold wrap-break-word" v-text="video.title" />
|
||||||
<div class="mb-3 mt-3 flex flex-wrap">
|
<div class="my-3 flex flex-wrap">
|
||||||
<!-- views / date -->
|
<!-- views / date -->
|
||||||
<div class="flex flex-auto gap-2">
|
<div class="flex flex-auto gap-2">
|
||||||
<span v-t="{ path: 'video.views', args: { views: addCommas(video.views) } }" />
|
<span v-t="{ path: 'video.views', args: { views: addCommas(video.views) } }" />
|
||||||
@@ -78,11 +79,11 @@
|
|||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<template v-if="video.likes >= 0">
|
<template v-if="video.likes >= 0">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="i-fa6-solid:thumbs-up" />
|
<i-fa6-solid-thumbs-up />
|
||||||
<strong class="ml-1" v-text="addCommas(video.likes)" />
|
<strong class="ml-1" v-text="addCommas(video.likes)" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="i-fa6-solid:thumbs-down" />
|
<i-fa6-solid-thumbs-down />
|
||||||
<strong
|
<strong
|
||||||
class="ml-1"
|
class="ml-1"
|
||||||
v-text="video.dislikes >= 0 ? addCommas(video.dislikes) : '?'"
|
v-text="video.dislikes >= 0 ? addCommas(video.dislikes) : '?'"
|
||||||
@@ -108,11 +109,14 @@
|
|||||||
alt=""
|
alt=""
|
||||||
class="rounded-full"
|
class="rounded-full"
|
||||||
/>
|
/>
|
||||||
<router-link v-if="video.uploaderUrl" class="link ml-1.5" :to="video.uploaderUrl">{{
|
<router-link
|
||||||
video.uploader
|
v-if="video.uploaderUrl"
|
||||||
}}</router-link>
|
class="ml-1.5 hover:text-red-500 focus:text-red-500 dark:hover:text-red-400 dark:focus:text-red-400"
|
||||||
|
:to="video.uploaderUrl"
|
||||||
|
>{{ video.uploader }}</router-link
|
||||||
|
>
|
||||||
<!-- Verified Badge -->
|
<!-- Verified Badge -->
|
||||||
<i v-if="video.uploaderVerified" class="i-fa6-solid:check ml-1" />
|
<i-fa6-solid-check v-if="video.uploaderVerified" class="ml-1" />
|
||||||
</div>
|
</div>
|
||||||
<PlaylistAddModal
|
<PlaylistAddModal
|
||||||
v-if="showModal"
|
v-if="showModal"
|
||||||
@@ -130,14 +134,20 @@
|
|||||||
/>
|
/>
|
||||||
<div class="ml-auto flex flex-wrap gap-1">
|
<div class="ml-auto flex flex-wrap gap-1">
|
||||||
<!-- Subscribe Button -->
|
<!-- Subscribe Button -->
|
||||||
<button class="btn flex items-center gap-1 <md:hidden" @click="downloadCurrentFrame">
|
<button
|
||||||
{{ $t("actions.download_frame") }}<i class="i-fa6-solid:download" />
|
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"
|
||||||
</button>
|
@click="downloadCurrentFrame"
|
||||||
<button class="btn flex items-center" @click="showModal = !showModal">
|
>
|
||||||
{{ $t("actions.add_to_playlist") }}<i class="i-fa6-solid:circle-plus ml-1" />
|
{{ $t("actions.download_frame") }}<i-fa6-solid-download />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn"
|
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="showModal = !showModal"
|
||||||
|
>
|
||||||
|
{{ $t("actions.add_to_playlist") }}<i-fa6-solid-circle-plus 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"
|
||||||
@click="subscribeHandler"
|
@click="subscribeHandler"
|
||||||
v-text="
|
v-text="
|
||||||
$t('actions.' + (subscribed ? 'unsubscribe' : 'subscribe')) +
|
$t('actions.' + (subscribed ? 'unsubscribe' : 'subscribe')) +
|
||||||
@@ -156,14 +166,17 @@
|
|||||||
video.uploaderUrl.split('/')[2]
|
video.uploaderUrl.split('/')[2]
|
||||||
}`"
|
}`"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="btn flex items-center"
|
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"
|
||||||
>
|
>
|
||||||
<i class="i-fa6-solid:rss mx-1.5" />
|
<i-fa6-solid-rss class="mx-1.5" />
|
||||||
</a>
|
</a>
|
||||||
<!-- Share Dialog -->
|
<!-- Share Dialog -->
|
||||||
<button class="btn flex items-center" @click="showShareModal = !showShareModal">
|
<button
|
||||||
<i18n-t class="lt-lg:hidden" keypath="actions.share" tag="strong"></i18n-t>
|
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"
|
||||||
<i class="i-fa6-solid:share mx-1.5" />
|
@click="showShareModal = !showShareModal"
|
||||||
|
>
|
||||||
|
<i18n-t class="max-lg:hidden" keypath="actions.share" tag="strong"></i18n-t>
|
||||||
|
<i-fa6-solid-share class="mx-1.5" />
|
||||||
</button>
|
</button>
|
||||||
<!-- YouTube -->
|
<!-- YouTube -->
|
||||||
<WatchOnButton :link="youtubeVideoHref" />
|
<WatchOnButton :link="youtubeVideoHref" />
|
||||||
@@ -178,12 +191,12 @@
|
|||||||
:to="toggleListenUrl"
|
:to="toggleListenUrl"
|
||||||
:aria-label="(isListening ? 'Watch ' : 'Listen to ') + video.title"
|
:aria-label="(isListening ? 'Watch ' : 'Listen to ') + video.title"
|
||||||
:title="(isListening ? 'Watch ' : 'Listen to ') + video.title"
|
:title="(isListening ? 'Watch ' : 'Listen to ') + video.title"
|
||||||
class="btn flex items-center"
|
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"
|
||||||
>
|
>
|
||||||
<i
|
<div>
|
||||||
:class="isListening ? 'i-fa6-solid:tv' : 'i-fa6-solid:headphones'"
|
<i-fa6-solid-tv v-if="isListening" class="mx-1.5" />
|
||||||
class="mx-1.5"
|
<i-fa6-solid-headphones v-else class="mx-1.5" />
|
||||||
/>
|
</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -194,7 +207,7 @@
|
|||||||
<div
|
<div
|
||||||
v-for="metaInfo in video?.metaInfo ?? []"
|
v-for="metaInfo in video?.metaInfo ?? []"
|
||||||
:key="metaInfo.title"
|
:key="metaInfo.title"
|
||||||
class="btn my-3 flex flex-wrap cursor-default gap-2 px-4 py-2"
|
class="my-3 inline-block w-auto cursor-default rounded-sm bg-gray-300 px-4 py-2 text-gray-600 dark:bg-dark-400 dark:text-gray-400"
|
||||||
>
|
>
|
||||||
<span>{{ metaInfo.description ?? metaInfo.title }}</span>
|
<span>{{ metaInfo.description ?? metaInfo.title }}</span>
|
||||||
<a v-for="(link, linkIndex) in metaInfo.urls" :key="linkIndex" :href="link" class="underline">{{
|
<a v-for="(link, linkIndex) in metaInfo.urls" :key="linkIndex" :href="link" class="underline">{{
|
||||||
@@ -205,18 +218,21 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
v-t="`actions.${showDesc ? 'minimize_description' : 'show_description'}`"
|
v-t="`actions.${showDesc ? 'minimize_description' : 'show_description'}`"
|
||||||
class="btn mb-2"
|
class="mb-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="showDesc = !showDesc"
|
@click="showDesc = !showDesc"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span v-show="video?.chapters?.length > 0" class="btn ml-2">
|
<span
|
||||||
|
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" />
|
<input id="showChapters" v-model="showChapters" type="checkbox" />
|
||||||
<label v-t="'actions.show_chapters'" class="ml-2" for="showChapters" />
|
<label v-t="'actions.show_chapters'" class="ml-2" for="showChapters" />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<template v-if="showDesc">
|
<template v-if="showDesc">
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
<div class="description break-words" v-html="purifiedDescription" />
|
<div class="wrap-break-word [&_a]:underline [&_a]:brightness-75" v-html="purifiedDescription" />
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -231,7 +247,7 @@
|
|||||||
<router-link
|
<router-link
|
||||||
v-for="tag in video.tags"
|
v-for="tag in video.tags"
|
||||||
:key="tag"
|
:key="tag"
|
||||||
class="btn line-clamp-1 rounded-s px-2 py-1"
|
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"
|
||||||
:to="`/results?search_query=${encodeURIComponent(tag)}`"
|
:to="`/results?search_query=${encodeURIComponent(tag)}`"
|
||||||
>{{ tag }}</router-link
|
>{{ tag }}</router-link
|
||||||
>
|
>
|
||||||
@@ -254,7 +270,7 @@
|
|||||||
<select
|
<select
|
||||||
id="chkAutoPlay"
|
id="chkAutoPlay"
|
||||||
v-model.number="selectedAutoPlay"
|
v-model.number="selectedAutoPlay"
|
||||||
class="select ml-1.5"
|
class="ml-1.5 h-8 rounded-md bg-gray-300 px-2.5 text-gray-600 dark:bg-dark-400 dark:text-gray-400"
|
||||||
@change="onChange($event)"
|
@change="onChange($event)"
|
||||||
>
|
>
|
||||||
<option v-t="'actions.never'" value="0" />
|
<option v-t="'actions.never'" value="0" />
|
||||||
@@ -266,7 +282,7 @@
|
|||||||
<div v-if="isMobile">
|
<div v-if="isMobile">
|
||||||
<a
|
<a
|
||||||
v-t="`actions.${showRecs ? 'minimize_recommendations' : 'show_recommendations'}`"
|
v-t="`actions.${showRecs ? 'minimize_recommendations' : 'show_recommendations'}`"
|
||||||
class="btn mb-2"
|
class="mb-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="showRecs = !showRecs"
|
@click="showRecs = !showRecs"
|
||||||
/>
|
/>
|
||||||
<hr v-show="showRecs" />
|
<hr v-show="showRecs" />
|
||||||
@@ -288,7 +304,7 @@
|
|||||||
<div class="">
|
<div class="">
|
||||||
<button
|
<button
|
||||||
v-if="!comments?.disabled"
|
v-if="!comments?.disabled"
|
||||||
class="btn mb-2"
|
class="mb-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="toggleCommentsVisibility"
|
@click="toggleCommentsVisibility"
|
||||||
v-text="
|
v-text="
|
||||||
`${$t(
|
`${$t(
|
||||||
@@ -333,7 +349,7 @@
|
|||||||
/>
|
/>
|
||||||
<a
|
<a
|
||||||
v-t="`actions.${showRecs ? 'minimize_recommendations' : 'show_recommendations'}`"
|
v-t="`actions.${showRecs ? 'minimize_recommendations' : 'show_recommendations'}`"
|
||||||
class="btn mb-2"
|
class="mb-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="showRecs = !showRecs"
|
@click="showRecs = !showRecs"
|
||||||
/>
|
/>
|
||||||
<hr v-show="showRecs" />
|
<hr v-show="showRecs" />
|
||||||
@@ -803,9 +819,4 @@ onUnmounted(() => {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(100%) scale(0.5);
|
transform: translateX(100%) scale(0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.description a {
|
|
||||||
text-decoration: underline;
|
|
||||||
filter: brightness(0.75);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
47
src/components/ui/PreferenceListbox.vue
Normal file
47
src/components/ui/PreferenceListbox.vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<ListboxRoot
|
||||||
|
:id="id"
|
||||||
|
:model-value="modelValue"
|
||||||
|
multiple
|
||||||
|
class="w-52 rounded-md bg-gray-300 text-gray-700 dark:bg-dark-400 dark:text-gray-300"
|
||||||
|
@update:model-value="handleUpdate"
|
||||||
|
>
|
||||||
|
<ListboxContent class="max-h-28 overflow-y-auto rounded-md p-1">
|
||||||
|
<ListboxItem
|
||||||
|
v-for="option in options"
|
||||||
|
:key="option.value"
|
||||||
|
:value="option.value"
|
||||||
|
class="flex cursor-pointer items-center justify-between rounded-sm px-2 py-1 text-sm transition-colors outline-none data-highlighted:bg-gray-500 data-highlighted:text-white data-[state=checked]:bg-red-100 data-[state=checked]:text-red-700 dark:data-highlighted:bg-dark-300 dark:data-[state=checked]:bg-red-950/40 dark:data-[state=checked]:text-red-200"
|
||||||
|
>
|
||||||
|
<span v-text="option.label" />
|
||||||
|
<ListboxItemIndicator class="ml-2 text-red-500 dark:text-red-400">✓</ListboxItemIndicator>
|
||||||
|
</ListboxItem>
|
||||||
|
</ListboxContent>
|
||||||
|
</ListboxRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ListboxContent, ListboxItem, ListboxItemIndicator, ListboxRoot } from "reka-ui";
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:modelValue", "change"]);
|
||||||
|
|
||||||
|
function handleUpdate(value) {
|
||||||
|
emit("update:modelValue", value);
|
||||||
|
emit("change", value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
61
src/components/ui/PreferenceNumberField.vue
Normal file
61
src/components/ui/PreferenceNumberField.vue
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<template>
|
||||||
|
<NumberFieldRoot
|
||||||
|
:id="id"
|
||||||
|
:model-value="modelValue"
|
||||||
|
:min="min"
|
||||||
|
:max="max"
|
||||||
|
:step="step"
|
||||||
|
class="inline-flex h-8 items-center rounded-md bg-gray-300 text-gray-700 dark:bg-dark-400 dark:text-gray-300"
|
||||||
|
@update:model-value="handleUpdate"
|
||||||
|
>
|
||||||
|
<NumberFieldDecrement
|
||||||
|
class="inline-flex h-full w-8 items-center justify-center rounded-l-md border-r border-gray-400 text-base transition-colors hover:bg-gray-500 hover:text-white focus-visible:outline-2 focus-visible:outline-red-500 disabled:cursor-not-allowed disabled:opacity-50 dark:border-dark-300 dark:hover:bg-dark-300"
|
||||||
|
aria-label="Decrease value"
|
||||||
|
>
|
||||||
|
−
|
||||||
|
</NumberFieldDecrement>
|
||||||
|
<NumberFieldInput
|
||||||
|
class="h-full w-16 bg-transparent px-2 text-center text-gray-700 outline-none focus-visible:outline-2 focus-visible:outline-red-500 dark:text-gray-300"
|
||||||
|
/>
|
||||||
|
<NumberFieldIncrement
|
||||||
|
class="inline-flex h-full w-8 items-center justify-center rounded-r-md border-l border-gray-400 text-base transition-colors hover:bg-gray-500 hover:text-white focus-visible:outline-2 focus-visible:outline-red-500 disabled:cursor-not-allowed disabled:opacity-50 dark:border-dark-300 dark:hover:bg-dark-300"
|
||||||
|
aria-label="Increase value"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</NumberFieldIncrement>
|
||||||
|
</NumberFieldRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { NumberFieldDecrement, NumberFieldIncrement, NumberFieldInput, NumberFieldRoot } from "reka-ui";
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
min: {
|
||||||
|
type: Number,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
max: {
|
||||||
|
type: Number,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
step: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:modelValue", "change"]);
|
||||||
|
|
||||||
|
function handleUpdate(value) {
|
||||||
|
emit("update:modelValue", value ?? 0);
|
||||||
|
emit("change", value ?? 0);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
18
src/components/ui/PreferenceRow.vue
Normal file
18
src/components/ui/PreferenceRow.vue
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<label
|
||||||
|
class="mx-[15vw] my-2 flex items-center justify-between odd:bg-gray-200 max-md:mx-[2vw] dark:odd:bg-dark-800"
|
||||||
|
:for="forId"
|
||||||
|
>
|
||||||
|
<slot name="label" />
|
||||||
|
<slot />
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
forId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
34
src/components/ui/PreferenceSwitch.vue
Normal file
34
src/components/ui/PreferenceSwitch.vue
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<SwitchRoot
|
||||||
|
:id="id"
|
||||||
|
:model-value="modelValue"
|
||||||
|
class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent bg-gray-300 transition-colors focus-visible:outline-2 focus-visible:outline-red-500 data-[state=checked]:bg-red-500 dark:bg-dark-400 dark:data-[state=checked]:bg-red-400"
|
||||||
|
@update:model-value="handleUpdate"
|
||||||
|
>
|
||||||
|
<SwitchThumb
|
||||||
|
class="pointer-events-none block size-5 translate-x-0 rounded-full bg-white shadow-sm transition-transform data-[state=checked]:translate-x-5"
|
||||||
|
/>
|
||||||
|
</SwitchRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { SwitchRoot, SwitchThumb } from "reka-ui";
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:modelValue", "change"]);
|
||||||
|
|
||||||
|
function handleUpdate(value) {
|
||||||
|
emit("update:modelValue", value);
|
||||||
|
emit("change", value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import { StorageSerializers, useLocalStorage } from "@vueuse/core";
|
||||||
|
|
||||||
|
const preferenceRefs = new Map();
|
||||||
|
|
||||||
export function testLocalStorage() {
|
export function testLocalStorage() {
|
||||||
try {
|
try {
|
||||||
if (window.localStorage !== undefined) localStorage;
|
if (window.localStorage !== undefined) localStorage;
|
||||||
@@ -7,20 +11,83 @@ export function testLocalStorage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getQueryPreference(key) {
|
||||||
|
return new URLSearchParams(window.location.search).get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOrCreatePreferenceRef(key, createRef) {
|
||||||
|
if (preferenceRefs.has(key)) return preferenceRefs.get(key);
|
||||||
|
|
||||||
|
const preferenceRef = createRef();
|
||||||
|
preferenceRefs.set(key, preferenceRef);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePreferenceString(key, defaultVal) {
|
||||||
|
return getOrCreatePreferenceRef(key, () =>
|
||||||
|
useLocalStorage(key, defaultVal ?? null, {
|
||||||
|
serializer: StorageSerializers.any,
|
||||||
|
writeDefaults: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePreferenceBoolean(key, defaultVal = false) {
|
||||||
|
return getOrCreatePreferenceRef(key, () =>
|
||||||
|
useLocalStorage(key, defaultVal, {
|
||||||
|
writeDefaults: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePreferenceNumber(key, defaultVal = 0) {
|
||||||
|
return getOrCreatePreferenceRef(key, () =>
|
||||||
|
useLocalStorage(key, defaultVal, {
|
||||||
|
writeDefaults: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePreferenceJSON(key, defaultVal) {
|
||||||
|
return getOrCreatePreferenceRef(key, () =>
|
||||||
|
useLocalStorage(key, defaultVal ?? null, {
|
||||||
|
serializer: StorageSerializers.object,
|
||||||
|
writeDefaults: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function setPreference(key, value, disableAlert = false) {
|
export function setPreference(key, value, disableAlert = false) {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(key, value);
|
createPreferenceRefForValue(key, value).value = value;
|
||||||
} catch {
|
} catch {
|
||||||
if (!disableAlert) alert("Could not save preference to local storage.");
|
if (!disableAlert) alert("Could not save preference to local storage.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPreferenceBoolean(key, defaultVal) {
|
export function getPreferenceBoolean(key, defaultVal) {
|
||||||
var value;
|
const queryValue = getQueryPreference(key);
|
||||||
if (
|
if (queryValue !== null) {
|
||||||
(value = new URLSearchParams(window.location.search).get(key)) !== null ||
|
switch (String(queryValue).toLowerCase()) {
|
||||||
(testLocalStorage() && (value = localStorage.getItem(key)) !== null)
|
case "true":
|
||||||
) {
|
case "1":
|
||||||
|
case "on":
|
||||||
|
case "yes":
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testLocalStorage()) {
|
||||||
|
const value = usePreferenceBoolean(key, defaultVal).value;
|
||||||
switch (String(value).toLowerCase()) {
|
switch (String(value).toLowerCase()) {
|
||||||
case "true":
|
case "true":
|
||||||
case "1":
|
case "1":
|
||||||
@@ -30,36 +97,47 @@ export function getPreferenceBoolean(key, defaultVal) {
|
|||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else return defaultVal;
|
}
|
||||||
|
|
||||||
|
return defaultVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPreferenceString(key, defaultVal) {
|
export function getPreferenceString(key, defaultVal) {
|
||||||
var value;
|
const queryValue = getQueryPreference(key);
|
||||||
if (
|
if (queryValue !== null) return queryValue;
|
||||||
(value = new URLSearchParams(window.location.search).get(key)) !== null ||
|
|
||||||
(testLocalStorage() && (value = localStorage.getItem(key)) !== null)
|
if (testLocalStorage()) {
|
||||||
) {
|
const value = usePreferenceString(key, defaultVal).value;
|
||||||
return value;
|
return value ?? defaultVal;
|
||||||
} else return defaultVal;
|
}
|
||||||
|
|
||||||
|
return defaultVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPreferenceNumber(key, defaultVal) {
|
export function getPreferenceNumber(key, defaultVal) {
|
||||||
var value;
|
const queryValue = getQueryPreference(key);
|
||||||
if (
|
if (queryValue !== null) {
|
||||||
(value = new URLSearchParams(window.location.search).get(key)) !== null ||
|
const num = Number(queryValue);
|
||||||
(testLocalStorage() && (value = localStorage.getItem(key)) !== null)
|
return isNaN(num) ? defaultVal : num;
|
||||||
) {
|
}
|
||||||
|
|
||||||
|
if (testLocalStorage()) {
|
||||||
|
const value = usePreferenceNumber(key, defaultVal).value;
|
||||||
const num = Number(value);
|
const num = Number(value);
|
||||||
return isNaN(num) ? defaultVal : num;
|
return isNaN(num) ? defaultVal : num;
|
||||||
} else return defaultVal;
|
}
|
||||||
|
|
||||||
|
return defaultVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPreferenceJSON(key, defaultVal) {
|
export function getPreferenceJSON(key, defaultVal) {
|
||||||
var value;
|
const queryValue = getQueryPreference(key);
|
||||||
if (
|
if (queryValue !== null) return JSON.parse(queryValue);
|
||||||
(value = new URLSearchParams(window.location.search).get(key)) !== null ||
|
|
||||||
(testLocalStorage() && (value = localStorage.getItem(key)) !== null)
|
if (testLocalStorage()) {
|
||||||
) {
|
const value = usePreferenceJSON(key, defaultVal).value;
|
||||||
return JSON.parse(value);
|
return value ?? defaultVal;
|
||||||
} else return defaultVal;
|
}
|
||||||
|
|
||||||
|
return defaultVal;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import App from "./App.vue";
|
|||||||
|
|
||||||
import { createI18n } from "vue-i18n";
|
import { createI18n } from "vue-i18n";
|
||||||
import enLocale from "@/locales/en.json";
|
import enLocale from "@/locales/en.json";
|
||||||
import "@unocss/reset/tailwind.css";
|
import "./app.css";
|
||||||
import "uno.css";
|
|
||||||
|
|
||||||
import("./registerServiceWorker");
|
import("./registerServiceWorker");
|
||||||
|
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
import { defineConfig } from "unocss";
|
|
||||||
import transformerDirective from "@unocss/transformer-directives";
|
|
||||||
import transformerVariantGroup from "@unocss/transformer-variant-group";
|
|
||||||
|
|
||||||
import presetUno from "@unocss/preset-uno";
|
|
||||||
import presetIcons from "@unocss/preset-icons";
|
|
||||||
import presetWebFonts from "@unocss/preset-web-fonts";
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
transformers: [transformerDirective(), transformerVariantGroup()],
|
|
||||||
presets: [
|
|
||||||
presetUno({
|
|
||||||
dark: "media",
|
|
||||||
}),
|
|
||||||
presetIcons({
|
|
||||||
extraProperties: {
|
|
||||||
display: "inline-block",
|
|
||||||
"vertical-align": "middle",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
presetWebFonts({
|
|
||||||
provider: "none",
|
|
||||||
fonts: {
|
|
||||||
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",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import vue from "@vitejs/plugin-vue";
|
import vue from "@vitejs/plugin-vue";
|
||||||
import Unocss from "unocss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import Icons from "unplugin-icons/vite";
|
||||||
|
import Components from "unplugin-vue-components/vite";
|
||||||
|
import IconsResolver from "unplugin-icons/resolver";
|
||||||
import legacy from "@vitejs/plugin-legacy";
|
import legacy from "@vitejs/plugin-legacy";
|
||||||
import VueI18nPlugin from "@intlify/unplugin-vue-i18n/vite";
|
import VueI18nPlugin from "@intlify/unplugin-vue-i18n/vite";
|
||||||
import { VitePWA } from "vite-plugin-pwa";
|
import { VitePWA } from "vite-plugin-pwa";
|
||||||
@@ -11,7 +14,12 @@ import eslintPlugin from "vite-plugin-eslint";
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
Unocss(),
|
tailwindcss(),
|
||||||
|
Icons({ compiler: "vue3", scale: 1 }),
|
||||||
|
Components({
|
||||||
|
resolvers: [IconsResolver()],
|
||||||
|
dts: false,
|
||||||
|
}),
|
||||||
VueI18nPlugin({
|
VueI18nPlugin({
|
||||||
include: path.resolve(__dirname, "./src/locales/**"),
|
include: path.resolve(__dirname, "./src/locales/**"),
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user