mirror of
https://github.com/TeamPiped/Piped.git
synced 2024-12-22 13:33:37 +00:00
Fix eslint config and apply all fixes.
This commit is contained in:
parent
6c05f63bef
commit
301877e2e1
7
.eslintrc.cjs
Normal file
7
.eslintrc.cjs
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
extends: ["plugin:vue/vue3-recommended", "eslint:recommended", "plugin:prettier/recommended"],
|
||||
};
|
12
package.json
12
package.json
@ -51,18 +51,6 @@
|
||||
"vite-plugin-pwa": "0.16.4",
|
||||
"workbox-window": "7.0.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/vue3-essential",
|
||||
"plugin:prettier/recommended",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"rules": {}
|
||||
},
|
||||
"browserslist": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version"
|
||||
|
38
src/App.vue
38
src/App.vue
@ -29,25 +29,6 @@ export default {
|
||||
theme: "dark",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
setTheme() {
|
||||
let themePref = this.getPreferenceString("theme", "dark");
|
||||
if (themePref == "auto") this.theme = darkModePreference.matches ? "dark" : "light";
|
||||
else this.theme = themePref;
|
||||
|
||||
// Change title bar color based on user's theme
|
||||
const themeColor = document.querySelector("meta[name='theme-color']");
|
||||
if (this.theme === "light") {
|
||||
themeColor.setAttribute("content", "#FFF");
|
||||
} else {
|
||||
themeColor.setAttribute("content", "#0F0F0F");
|
||||
}
|
||||
|
||||
// Used for the scrollbar
|
||||
const root = document.querySelector(":root");
|
||||
this.theme == "dark" ? root.classList.add("dark") : root.classList.remove("dark");
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setTheme();
|
||||
darkModePreference.addEventListener("change", () => {
|
||||
@ -112,6 +93,25 @@ export default {
|
||||
}
|
||||
})();
|
||||
},
|
||||
methods: {
|
||||
setTheme() {
|
||||
let themePref = this.getPreferenceString("theme", "dark");
|
||||
if (themePref == "auto") this.theme = darkModePreference.matches ? "dark" : "light";
|
||||
else this.theme = themePref;
|
||||
|
||||
// Change title bar color based on user's theme
|
||||
const themeColor = document.querySelector("meta[name='theme-color']");
|
||||
if (this.theme === "light") {
|
||||
themeColor.setAttribute("content", "#FFF");
|
||||
} else {
|
||||
themeColor.setAttribute("content", "#0F0F0F");
|
||||
}
|
||||
|
||||
// Used for the scrollbar
|
||||
const root = document.querySelector(":root");
|
||||
this.theme == "dark" ? root.classList.add("dark") : root.classList.remove("dark");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -6,14 +6,14 @@
|
||||
</div>
|
||||
<p>
|
||||
<span v-text="props.item.name" />
|
||||
<font-awesome-icon class="ml-1.5" v-if="props.item.verified" icon="check" />
|
||||
<font-awesome-icon v-if="props.item.verified" class="ml-1.5" icon="check" />
|
||||
</p>
|
||||
</router-link>
|
||||
<p v-if="props.item.description" v-text="props.item.description" />
|
||||
<router-link v-if="props.item.uploaderUrl" class="link" :to="props.item.uploaderUrl">
|
||||
<p>
|
||||
<span v-text="props.item.uploader" />
|
||||
<font-awesome-icon class="ml-1.5" v-if="props.item.uploaderVerified" icon="check" />
|
||||
<font-awesome-icon v-if="props.item.uploaderVerified" class="ml-1.5" icon="check" />
|
||||
</p>
|
||||
</router-link>
|
||||
|
||||
@ -29,6 +29,9 @@
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
item: Object,
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
@ -12,27 +12,27 @@
|
||||
<div class="flex place-items-center">
|
||||
<img height="48" width="48" class="rounded-full m-1" :src="channel.avatarUrl" />
|
||||
<div class="flex gap-1 items-center">
|
||||
<h1 v-text="channel.name" class="!text-xl" />
|
||||
<font-awesome-icon class="!text-xl" v-if="channel.verified" icon="check" />
|
||||
<h1 class="!text-xl" v-text="channel.name" />
|
||||
<font-awesome-icon v-if="channel.verified" class="!text-xl" icon="check" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="btn"
|
||||
@click="subscribeHandler"
|
||||
v-t="{
|
||||
path: `actions.${subscribed ? 'unsubscribe' : 'subscribe'}`,
|
||||
args: { count: numberFormat(channel.subscriberCount) },
|
||||
}"
|
||||
class="btn"
|
||||
@click="subscribeHandler"
|
||||
></button>
|
||||
|
||||
<!-- RSS Feed button -->
|
||||
<a
|
||||
v-if="channel.id"
|
||||
aria-label="RSS feed"
|
||||
title="RSS feed"
|
||||
role="button"
|
||||
v-if="channel.id"
|
||||
:href="`${apiUrl()}/feed/unauthenticated/rss?channels=${channel.id}`"
|
||||
target="_blank"
|
||||
class="btn flex-col"
|
||||
@ -44,15 +44,15 @@
|
||||
|
||||
<CollapsableText :text="channel.description" />
|
||||
|
||||
<WatchOnButton :link="`https://youtube.com/channel/${this.channel.id}`" />
|
||||
<WatchOnButton :link="`https://youtube.com/channel/${channel.id}`" />
|
||||
|
||||
<div class="flex my-2 mx-1">
|
||||
<button
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="tab.name"
|
||||
class="btn mr-2"
|
||||
@click="loadTab(index)"
|
||||
:class="{ active: selectedTab == index }"
|
||||
@click="loadTab(index)"
|
||||
>
|
||||
<span v-text="tab.translatedName"></span>
|
||||
</button>
|
||||
|
@ -5,11 +5,11 @@
|
||||
{{ $t("video.chapters") }} ({{ chapters.length }})
|
||||
</h2>
|
||||
<div
|
||||
:key="chapter.start"
|
||||
v-for="(chapter, index) in chapters"
|
||||
@click="$emit('seek', chapter.start)"
|
||||
:key="chapter.start"
|
||||
class="chapter-vertical"
|
||||
:class="{ 'bg-red-500/50': isCurrentChapter(index) }"
|
||||
@click="$emit('seek', chapter.start)"
|
||||
>
|
||||
<div class="flex">
|
||||
<span class="mt-5 mr-2 text-current" v-text="index + 1" />
|
||||
@ -31,11 +31,11 @@
|
||||
{{ $t("video.chapters") }} ({{ chapters.length }})
|
||||
</h2>
|
||||
<div
|
||||
:key="chapter.start"
|
||||
v-for="(chapter, index) in chapters"
|
||||
@click="$emit('seek', chapter.start)"
|
||||
:key="chapter.start"
|
||||
class="chapter-vertical"
|
||||
:class="{ 'bg-red-500/50': isCurrentChapter(index) }"
|
||||
@click="$emit('seek', chapter.start)"
|
||||
>
|
||||
<div class="flex">
|
||||
<span class="mt-5 mr-2 text-current" v-text="index + 1" />
|
||||
@ -50,11 +50,11 @@
|
||||
<!-- mobile Horizontal view -->
|
||||
<div v-if="getPreferenceString('mobileChapterLayout') == 'Horizontal' && mobileLayout" class="flex overflow-x-auto">
|
||||
<div
|
||||
:key="chapter.start"
|
||||
v-for="(chapter, index) in chapters"
|
||||
@click="$emit('seek', chapter.start)"
|
||||
:key="chapter.start"
|
||||
class="chapter"
|
||||
:class="{ 'bg-red-500/50': isCurrentChapter(index) }"
|
||||
@click="$emit('seek', chapter.start)"
|
||||
>
|
||||
<img :src="chapter.image" :alt="chapter.title" />
|
||||
<div class="m-1 flex">
|
||||
@ -65,6 +65,32 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
chapters: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
mobileLayout: {
|
||||
type: Boolean,
|
||||
default: () => true,
|
||||
},
|
||||
playerPosition: {
|
||||
type: Number,
|
||||
default: () => 0,
|
||||
},
|
||||
});
|
||||
|
||||
const isCurrentChapter = index => {
|
||||
return (
|
||||
props.playerPosition >= props.chapters[index].start &&
|
||||
props.playerPosition < (props.chapters[index + 1]?.start ?? Infinity)
|
||||
);
|
||||
};
|
||||
|
||||
defineEmits(["seek"]);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
::-webkit-scrollbar {
|
||||
height: 5px;
|
||||
@ -89,26 +115,3 @@
|
||||
@apply truncate overflow-hidden inline-block w-10em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
chapters: Object,
|
||||
mobileLayout: {
|
||||
type: Boolean,
|
||||
default: () => true,
|
||||
},
|
||||
playerPosition: {
|
||||
type: Number,
|
||||
default: () => 0,
|
||||
},
|
||||
});
|
||||
|
||||
const isCurrentChapter = index => {
|
||||
return (
|
||||
props.playerPosition >= props.chapters[index].start &&
|
||||
props.playerPosition < (props.chapters[index + 1]?.start ?? Infinity)
|
||||
);
|
||||
};
|
||||
|
||||
defineEmits(["seek"]);
|
||||
</script>
|
||||
|
@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-if="text" class="whitespace-pre-wrap py-2 mx-1">
|
||||
<template v-if="text">
|
||||
<div class="whitespace-pre-wrap py-2 mx-1">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span v-if="showFullText" v-html="fullText()" />
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span v-else v-html="colapsedText()" />
|
||||
<span v-if="text.length > 100 && !showFullText">...</span>
|
||||
<button
|
||||
@ -19,7 +20,10 @@ import { purifyHTML, rewriteDescription } from "@/utils/HtmlUtils";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
text: String,
|
||||
text: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -14,34 +14,35 @@
|
||||
<div v-if="comment.pinned" class="comment-pinned">
|
||||
<font-awesome-icon icon="thumbtack" />
|
||||
<span
|
||||
class="ml-1.5"
|
||||
v-t="{
|
||||
path: 'comment.pinned_by',
|
||||
args: { author: uploader },
|
||||
}"
|
||||
class="ml-1.5"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="comment-author">
|
||||
<router-link class="font-bold link" :to="comment.commentorUrl">{{ comment.author }}</router-link>
|
||||
<font-awesome-icon class="ml-1.5" v-if="comment.verified" icon="check" />
|
||||
<font-awesome-icon v-if="comment.verified" class="ml-1.5" icon="check" />
|
||||
</div>
|
||||
<div class="comment-meta text-sm mb-1.5" v-text="comment.commentedTime" />
|
||||
</div>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div class="whitespace-pre-wrap" v-html="purifiedText" />
|
||||
<div class="comment-footer mt-1 flex items-center">
|
||||
<div class="i-fa6-solid:thumbs-up" />
|
||||
<span class="ml-1" v-text="numberFormat(comment.likeCount)" />
|
||||
<font-awesome-icon class="ml-1" v-if="comment.hearted" icon="heart" />
|
||||
<font-awesome-icon v-if="comment.hearted" class="ml-1" icon="heart" />
|
||||
</div>
|
||||
<template v-if="comment.repliesPage && (!loadingReplies || !showingReplies)">
|
||||
<div @click="loadReplies" class="cursor-pointer">
|
||||
<div class="cursor-pointer" @click="loadReplies">
|
||||
<a v-text="`${$t('actions.reply_count', comment.replyCount)}`" />
|
||||
<font-awesome-icon class="ml-1.5" icon="level-down-alt" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="showingReplies">
|
||||
<div @click="hideReplies" class="cursor-pointer">
|
||||
<div class="cursor-pointer" @click="hideReplies">
|
||||
<a v-t="'actions.hide_replies'" />
|
||||
<font-awesome-icon class="ml-1.5" icon="level-up-alt" />
|
||||
</div>
|
||||
@ -50,7 +51,7 @@
|
||||
<div v-for="reply in replies" :key="reply.commentId" class="w-full">
|
||||
<CommentItem :comment="reply" :uploader="uploader" :video-id="videoId" />
|
||||
</div>
|
||||
<div v-if="nextpage" @click="loadReplies" class="cursor-pointer">
|
||||
<div v-if="nextpage" class="cursor-pointer" @click="loadReplies">
|
||||
<a v-t="'actions.load_more_replies'" />
|
||||
<font-awesome-icon class="ml-1.5" icon="level-down-alt" />
|
||||
</div>
|
||||
|
@ -3,8 +3,8 @@
|
||||
<div>
|
||||
<h3 class="text-xl" v-text="message" />
|
||||
<div class="ml-auto mt-8 flex gap-2 w-min">
|
||||
<button class="btn" v-t="'actions.cancel'" @click="$emit('close')" />
|
||||
<button class="btn" v-t="'actions.okay'" @click="$emit('confirm')" />
|
||||
<button v-t="'actions.cancel'" class="btn" @click="$emit('close')" />
|
||||
<button v-t="'actions.okay'" class="btn" @click="$emit('confirm')" />
|
||||
</div>
|
||||
</div>
|
||||
</ModalComponent>
|
||||
@ -18,7 +18,10 @@ export default {
|
||||
ModalComponent,
|
||||
},
|
||||
props: {
|
||||
message: String,
|
||||
message: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["close", "confirm"],
|
||||
};
|
||||
|
@ -6,7 +6,10 @@
|
||||
import { defineAsyncComponent } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
item: Object,
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const VideoItem = defineAsyncComponent(() => import("./VideoItem.vue"));
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<p v-text="message" />
|
||||
<button @click="toggleTrace" class="btn" v-t="'actions.show_more'" />
|
||||
<button v-t="'actions.show_more'" class="btn" @click="toggleTrace" />
|
||||
<p ref="stacktrace" class="whitespace-pre-wrap" hidden v-text="error" />
|
||||
</template>
|
||||
|
||||
|
@ -13,7 +13,7 @@
|
||||
class="select flex-grow"
|
||||
@change="onFilterChange()"
|
||||
>
|
||||
<option v-for="filter in availableFilters" :key="filter" :value="filter" v-t="`video.${filter}`" />
|
||||
<option v-for="filter in availableFilters" :key="filter" v-t="`video.${filter}`" :value="filter" />
|
||||
</select>
|
||||
</span>
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
<strong v-text="`${$t('titles.channel_groups')}:`" />
|
||||
</label>
|
||||
<select id="group-selector" v-model="selectedGroupName" default="" class="select flex-grow">
|
||||
<option value="" v-t="`video.all`" />
|
||||
<option v-t="`video.all`" value="" />
|
||||
<option
|
||||
v-for="group in channelGroups"
|
||||
:key="group.groupName"
|
||||
|
@ -2,23 +2,23 @@
|
||||
<footer class="text-center py-4 rounded-xl children:(mx-3) w-full mt-10">
|
||||
<a aria-label="GitHub" href="https://github.com/TeamPiped/Piped" target="_blank">
|
||||
<font-awesome-icon :icon="['fab', 'github']" />
|
||||
<span class="ml-2" v-t="'actions.source_code'" />
|
||||
<span v-t="'actions.source_code'" class="ml-2" />
|
||||
</a>
|
||||
<a href="https://docs.piped.video/" target="_blank">
|
||||
<font-awesome-icon :icon="['fa', 'book']" />
|
||||
<span class="ml-2" v-t="'actions.documentation'" />
|
||||
<span v-t="'actions.documentation'" class="ml-2" />
|
||||
</a>
|
||||
<a href="https://github.com/TeamPiped/Piped#donations" target="_blank">
|
||||
<font-awesome-icon :icon="['fab', 'bitcoin']" />
|
||||
<span class="ml-2" v-t="'actions.donations'" />
|
||||
<span v-t="'actions.donations'" class="ml-2" />
|
||||
</a>
|
||||
<a v-if="statusPageHref" :href="statusPageHref">
|
||||
<font-awesome-icon :icon="['fa', 'server']" />
|
||||
<span class="ml-2" v-t="'actions.status_page'" />
|
||||
<span v-t="'actions.status_page'" class="ml-2" />
|
||||
</a>
|
||||
<a v-if="donationHref" :href="donationHref">
|
||||
<font-awesome-icon :icon="['fa', 'donate']" />
|
||||
<span class="ml-2" v-t="'actions.instance_donations'" />
|
||||
<span v-t="'actions.instance_donations'" class="ml-2" />
|
||||
</a>
|
||||
</footer>
|
||||
</template>
|
||||
|
@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<h1 class="font-bold text-center mb-3" v-t="'titles.history'" />
|
||||
<h1 v-t="'titles.history'" class="font-bold text-center mb-3" />
|
||||
|
||||
<div class="flex">
|
||||
<div class="flex md:items-center gap-2 flex-col md:flex-row">
|
||||
<button class="btn" v-t="'actions.clear_history'" @click="clearHistory" />
|
||||
<button v-t="'actions.clear_history'" class="btn" @click="clearHistory" />
|
||||
|
||||
<button class="btn" v-t="'actions.export_to_json'" @click="exportHistory" />
|
||||
<button v-t="'actions.export_to_json'" class="btn" @click="exportHistory" />
|
||||
|
||||
<div class="ml-auto flex gap-1 items-center">
|
||||
<SortingSelector by-key="watchedAt" @apply="order => videos.sort(order)" />
|
||||
@ -13,19 +13,19 @@
|
||||
</div>
|
||||
|
||||
<div class="flex ml-4 items-center">
|
||||
<input id="autoDelete" type="checkbox" v-model="autoDeleteHistory" @change="onChange" />
|
||||
<label class="ml-2" for="autoDelete" v-t="'actions.delete_automatically'" />
|
||||
<select class="pl-3 ml-3 select" v-model="autoDeleteDelayHours" @change="onChange">
|
||||
<option value="1" v-t="{ path: 'info.hours', args: { amount: '1' } }" />
|
||||
<option value="3" v-t="{ path: 'info.hours', args: { amount: '3' } }" />
|
||||
<option value="6" v-t="{ path: 'info.hours', args: { amount: '6' } }" />
|
||||
<option value="12" v-t="{ path: 'info.hours', args: { amount: '12' } }" />
|
||||
<option value="24" v-t="{ path: 'info.days', args: { amount: '1' } }" />
|
||||
<option value="72" v-t="{ path: 'info.days', args: { amount: '3' } }" />
|
||||
<option value="168" v-t="{ path: 'info.weeks', args: { amount: '1' } }" />
|
||||
<option value="336" v-t="{ path: 'info.weeks', args: { amount: '3' } }" />
|
||||
<option value="672" v-t="{ path: 'info.months', args: { amount: '1' } }" />
|
||||
<option value="1344" v-t="{ path: 'info.months', args: { amount: '2' } }" />
|
||||
<input id="autoDelete" v-model="autoDeleteHistory" type="checkbox" @change="onChange" />
|
||||
<label v-t="'actions.delete_automatically'" class="ml-2" for="autoDelete" />
|
||||
<select v-model="autoDeleteDelayHours" class="pl-3 ml-3 select" @change="onChange">
|
||||
<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: '6' } }" value="6" />
|
||||
<option v-t="{ path: 'info.hours', args: { amount: '12' } }" value="12" />
|
||||
<option v-t="{ path: 'info.days', args: { amount: '1' } }" value="24" />
|
||||
<option v-t="{ path: 'info.days', args: { amount: '3' } }" value="72" />
|
||||
<option v-t="{ path: 'info.weeks', args: { amount: '1' } }" value="168" />
|
||||
<option v-t="{ path: 'info.weeks', args: { amount: '3' } }" value="336" />
|
||||
<option v-t="{ path: 'info.months', args: { amount: '1' } }" value="672" />
|
||||
<option v-t="{ path: 'info.months', args: { amount: '2' } }" value="1344" />
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -7,6 +7,17 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
showContent: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#spinner:after {
|
||||
--spinner-color: #000;
|
||||
@ -42,14 +53,3 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
showContent: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -11,7 +11,7 @@
|
||||
autocomplete="username"
|
||||
:placeholder="$t('login.username')"
|
||||
:aria-label="$t('login.username')"
|
||||
v-on:keyup.enter="login"
|
||||
@keyup.enter="login"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@ -22,11 +22,11 @@
|
||||
autocomplete="password"
|
||||
:placeholder="$t('login.password')"
|
||||
:aria-label="$t('login.password')"
|
||||
v-on:keyup.enter="login"
|
||||
@keyup.enter="login"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<a class="btn w-auto" @click="login" v-t="'titles.login'" />
|
||||
<a v-t="'titles.login'" class="btn w-auto" @click="login" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
emits: ["close"],
|
||||
mounted() {
|
||||
window.addEventListener("keydown", this.handleKeyDown);
|
||||
},
|
||||
|
@ -13,11 +13,11 @@
|
||||
</div>
|
||||
<div class="lt-md:hidden search-container">
|
||||
<input
|
||||
ref="videoSearch"
|
||||
v-model="searchText"
|
||||
class="input w-72 h-10 pr-20"
|
||||
type="text"
|
||||
role="search"
|
||||
ref="videoSearch"
|
||||
:title="$t('actions.search')"
|
||||
:placeholder="$t('actions.search')"
|
||||
@keyup="onKeyUp"
|
||||
@ -27,7 +27,7 @@
|
||||
/>
|
||||
<span v-if="searchText" class="delete-search" @click="searchText = ''">⨉</span>
|
||||
</div>
|
||||
<button @click="onSearchClick" id="search-btn" class="input btn mx-1 h-10">
|
||||
<button id="search-btn" class="input btn mx-1 h-10" @click="onSearchClick">
|
||||
<div class="i-fa6-solid:magnifying-glass"></div>
|
||||
</button>
|
||||
<!-- three vertical lines for toggling the hamburger menu on mobile -->
|
||||
@ -135,13 +135,6 @@ export default {
|
||||
registrationDisabled: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.fetchAuthConfig();
|
||||
const query = new URLSearchParams(window.location.search).get("search_query");
|
||||
if (query) this.onSearchTextChange(query);
|
||||
this.focusOnSearchBar();
|
||||
this.homePagePath = this.getHomePage(this);
|
||||
},
|
||||
computed: {
|
||||
shouldShowLogin(_this) {
|
||||
return _this.getAuthToken() == null;
|
||||
@ -159,6 +152,13 @@ export default {
|
||||
return _this.getPreferenceBoolean("searchHistory", false) && localStorage.getItem("search_history");
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchAuthConfig();
|
||||
const query = new URLSearchParams(window.location.search).get("search_query");
|
||||
if (query) this.onSearchTextChange(query);
|
||||
this.focusOnSearchBar();
|
||||
this.homePagePath = this.getHomePage(this);
|
||||
},
|
||||
methods: {
|
||||
// focus on search bar when Ctrl+k is pressed
|
||||
focusOnSearchBar() {
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex flex-col justify-center items-center min-h-[88vh]">
|
||||
<h1 class="font-bold !text-9xl">404</h1>
|
||||
<h2 class="!text-2xl" v-t="'info.page_not_found'" />
|
||||
<a class="btn mt-16" href="/" v-t="'actions.back_to_home'" />
|
||||
<h2 v-t="'info.page_not_found'" class="!text-2xl" />
|
||||
<a v-t="'actions.back_to_home'" class="btn mt-16" href="/" />
|
||||
</div>
|
||||
</template>
|
||||
|
@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<ModalComponent>
|
||||
<span class="text-2xl w-max inline-block" v-t="'actions.select_playlist'" />
|
||||
<select class="select w-full mt-3" v-model="selectedPlaylist">
|
||||
<option v-for="playlist in playlists" :value="playlist.id" :key="playlist.id" v-text="playlist.name" />
|
||||
<span v-t="'actions.select_playlist'" class="text-2xl w-max inline-block" />
|
||||
<select v-model="selectedPlaylist" class="select w-full mt-3">
|
||||
<option v-for="playlist in playlists" :key="playlist.id" :value="playlist.id" v-text="playlist.name" />
|
||||
</select>
|
||||
<div class="flex justify-between w-full mt-3">
|
||||
<button class="btn" @click="onCreatePlaylist" ref="addButton" v-t="'actions.create_playlist'" />
|
||||
<button ref="addButton" v-t="'actions.create_playlist'" class="btn" @click="onCreatePlaylist" />
|
||||
<button
|
||||
class="btn"
|
||||
@click="handleClick(selectedPlaylist)"
|
||||
ref="addButton"
|
||||
v-t="'actions.add_to_playlist'"
|
||||
class="btn"
|
||||
@click="handleClick(selectedPlaylist)"
|
||||
/>
|
||||
</div>
|
||||
</ModalComponent>
|
||||
@ -33,6 +33,7 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["close"],
|
||||
data() {
|
||||
return {
|
||||
playlists: [],
|
||||
|
@ -6,7 +6,7 @@
|
||||
</div>
|
||||
<p>
|
||||
<span v-text="props.item.name" />
|
||||
<font-awesome-icon class="ml-1.5" v-if="props.item.verified" icon="check" />
|
||||
<font-awesome-icon v-if="props.item.verified" class="ml-1.5" icon="check" />
|
||||
</p>
|
||||
</router-link>
|
||||
<p v-if="props.item.description" v-text="props.item.description" />
|
||||
@ -14,7 +14,7 @@
|
||||
<router-link v-if="props.item.uploaderUrl" class="link" :to="props.item.uploaderUrl">
|
||||
<p>
|
||||
<span v-text="props.item.uploaderName" />
|
||||
<font-awesome-icon class="ml-1.5" v-if="props.item.uploaderVerified" icon="check" />
|
||||
<font-awesome-icon v-if="props.item.uploaderVerified" class="ml-1.5" icon="check" />
|
||||
</p>
|
||||
</router-link>
|
||||
<a v-else-if="props.item.uploaderName" class="link" v-text="props.item.uploaderName" />
|
||||
@ -30,6 +30,9 @@
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
item: Object,
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<ErrorHandler v-if="playlist && playlist.error" :message="playlist.message" :error="playlist.error" />
|
||||
|
||||
<LoadingIndicatorPage :show-content="playlist" v-show="!playlist?.error">
|
||||
<LoadingIndicatorPage v-show="!playlist?.error" :show-content="playlist">
|
||||
<h1 class="ml-1 mb-1 mt-4 text-3xl!" v-text="playlist.name" />
|
||||
|
||||
<CollapsableText :text="playlist.description" />
|
||||
@ -14,12 +14,12 @@
|
||||
</router-link>
|
||||
</div>
|
||||
<div>
|
||||
<strong v-text="`${playlist.videos} ${$t('video.videos')}`" class="mr-2" />
|
||||
<button class="btn mx-1" v-if="!isPipedPlaylist" @click="bookmarkPlaylist">
|
||||
<strong class="mr-2" v-text="`${playlist.videos} ${$t('video.videos')}`" />
|
||||
<button v-if="!isPipedPlaylist" class="btn mx-1" @click="bookmarkPlaylist">
|
||||
{{ $t(`actions.${isBookmarked ? "playlist_bookmarked" : "bookmark_playlist"}`)
|
||||
}}<font-awesome-icon class="ml-3" icon="bookmark" />
|
||||
</button>
|
||||
<button class="btn mr-1" v-if="authenticated && !isPipedPlaylist" @click="clonePlaylist">
|
||||
<button v-if="authenticated && !isPipedPlaylist" class="btn mr-1" @click="clonePlaylist">
|
||||
{{ $t("actions.clone_playlist") }}<font-awesome-icon class="ml-3" icon="clone" />
|
||||
</button>
|
||||
<button class="btn mr-1" @click="downloadPlaylistAsTxt">
|
||||
@ -28,7 +28,7 @@
|
||||
<a class="btn mr-1" :href="getRssUrl">
|
||||
<font-awesome-icon icon="rss" />
|
||||
</a>
|
||||
<WatchOnButton :link="`https://www.youtube.com/playlist?list=${this.$route.query.list}`" />
|
||||
<WatchOnButton :link="`https://www.youtube.com/playlist?list=${$route.query.list}`" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -42,9 +42,9 @@
|
||||
:index="index"
|
||||
:playlist-id="$route.query.list"
|
||||
:admin="admin"
|
||||
@remove="removeVideo(index)"
|
||||
height="94"
|
||||
width="168"
|
||||
@remove="removeVideo(index)"
|
||||
/>
|
||||
</div>
|
||||
</LoadingIndicatorPage>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="overflow-x-scroll h-screen-sm" ref="scrollable">
|
||||
<div ref="scrollable" class="overflow-x-scroll h-screen-sm">
|
||||
<VideoItem
|
||||
v-for="(related, index) in playlist.relatedStreams"
|
||||
:key="related.url"
|
||||
@ -28,6 +28,18 @@ export default {
|
||||
},
|
||||
selectedIndex: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
playlist: {
|
||||
handler() {
|
||||
if (this.selectedIndex - 1 < this.playlist.relatedStreams.length)
|
||||
nextTick(() => {
|
||||
this.updateScroll();
|
||||
});
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
@ -43,16 +55,5 @@ export default {
|
||||
elems[this.selectedIndex - 1].offsetTop - this.$refs.scrollable.offsetTop;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
playlist: {
|
||||
handler() {
|
||||
if (this.selectedIndex - 1 < this.playlist.relatedStreams.length)
|
||||
nextTick(() => {
|
||||
this.updateScroll();
|
||||
});
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -1,24 +1,19 @@
|
||||
<template>
|
||||
<h2 class="font-bold my-4" v-t="'titles.playlists'" />
|
||||
<h2 v-t="'titles.playlists'" class="font-bold my-4" />
|
||||
|
||||
<div class="flex justify-between mb-3">
|
||||
<button v-t="'actions.create_playlist'" class="btn" @click="onCreatePlaylist" />
|
||||
<div class="flex">
|
||||
<button
|
||||
v-if="this.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="btn" @click="exportPlaylists" />
|
||||
<input
|
||||
id="fileSelector"
|
||||
ref="fileSelector"
|
||||
type="file"
|
||||
class="display-none"
|
||||
@change="importPlaylists"
|
||||
multiple="multiple"
|
||||
@change="importPlaylists"
|
||||
/>
|
||||
<label for="fileSelector" v-t="'actions.import_from_json'" class="btn ml-2" />
|
||||
<label v-t="'actions.import_from_json'" for="fileSelector" class="btn ml-2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -39,24 +34,24 @@
|
||||
v-text="playlist.name"
|
||||
/>
|
||||
</router-link>
|
||||
<button class="btn h-auto" @click="showPlaylistEditModal(playlist)" v-t="'actions.edit_playlist'" />
|
||||
<button class="btn h-auto ml-2" @click="playlistToDelete = playlist.id" v-t="'actions.delete_playlist'" />
|
||||
<button v-t="'actions.edit_playlist'" class="btn h-auto" @click="showPlaylistEditModal(playlist)" />
|
||||
<button v-t="'actions.delete_playlist'" class="btn h-auto ml-2" @click="playlistToDelete = playlist.id" />
|
||||
<ModalComponent v-if="playlist.id == playlistToEdit" @close="playlistToEdit = null">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h2 v-t="'actions.edit_playlist'" />
|
||||
<input
|
||||
v-model="newPlaylistName"
|
||||
class="input"
|
||||
type="text"
|
||||
v-model="newPlaylistName"
|
||||
:placeholder="$t('actions.playlist_name')"
|
||||
/>
|
||||
<input
|
||||
v-model="newPlaylistDescription"
|
||||
class="input"
|
||||
type="text"
|
||||
v-model="newPlaylistDescription"
|
||||
:placeholder="$t('actions.playlist_description')"
|
||||
/>
|
||||
<button class="btn ml-auto" @click="editPlaylist(playlist)" v-t="'actions.okay'" />
|
||||
<button v-t="'actions.okay'" class="btn ml-auto" @click="editPlaylist(playlist)" />
|
||||
</div>
|
||||
</ModalComponent>
|
||||
<ConfirmModal
|
||||
@ -69,7 +64,7 @@
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<h2 class="font-bold my-4" v-t="'titles.bookmarks'" />
|
||||
<h2 v-t="'titles.bookmarks'" class="font-bold my-4" />
|
||||
|
||||
<div v-if="bookmarks" class="video-grid">
|
||||
<router-link
|
||||
@ -104,6 +99,7 @@ import ConfirmModal from "./ConfirmModal.vue";
|
||||
import ModalComponent from "./ModalComponent.vue";
|
||||
|
||||
export default {
|
||||
components: { ConfirmModal, ModalComponent },
|
||||
data() {
|
||||
return {
|
||||
playlists: [],
|
||||
@ -250,6 +246,5 @@ export default {
|
||||
this.bookmarks.splice(index, 1);
|
||||
},
|
||||
},
|
||||
components: { ConfirmModal, ModalComponent },
|
||||
};
|
||||
</script>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<button @click="$router.go(-1) || $router.push('/')">
|
||||
<font-awesome-icon icon="chevron-left" /><span class="ml-1.5" v-t="'actions.back'" />
|
||||
<font-awesome-icon icon="chevron-left" /><span v-t="'actions.back'" class="ml-1.5" />
|
||||
</button>
|
||||
</div>
|
||||
<h1 v-t="'titles.preferences'" class="font-bold text-center" />
|
||||
@ -34,7 +34,7 @@
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<h2 class="text-center" v-t="'titles.player'" />
|
||||
<h2 v-t="'titles.player'" class="text-center" />
|
||||
<label class="pref" for="chkAutoPlayVideo">
|
||||
<strong v-t="'actions.autoplay_video'" />
|
||||
<input
|
||||
@ -223,7 +223,7 @@
|
||||
/>
|
||||
</label>
|
||||
<div v-if="sponsorBlock">
|
||||
<label v-for="[name, item] in skipOptions" class="pref" :for="'ddlSkip_' + name" :key="name">
|
||||
<label v-for="[name, item] in skipOptions" :key="name" class="pref" :for="'ddlSkip_' + name">
|
||||
<strong v-t="item.label" />
|
||||
<select :id="'ddlSkip_' + name" v-model="item.value" class="select w-auto" @change="onChange($event)">
|
||||
<option v-t="'actions.no'" value="no" />
|
||||
@ -253,7 +253,7 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<h2 class="text-center" v-t="'titles.dearrow'" />
|
||||
<h2 v-t="'titles.dearrow'" class="text-center" />
|
||||
<p class="text-center">
|
||||
<span v-t="'actions.uses_api_from'" /><a class="link" href="https://sponsor.ajay.app/">sponsor.ajay.app</a>
|
||||
</p>
|
||||
@ -262,7 +262,7 @@
|
||||
<input id="chkDeArrow" v-model="dearrow" class="checkbox" type="checkbox" @change="onChange($event)" />
|
||||
</label>
|
||||
|
||||
<h2 class="text-center" v-t="'titles.instance'" />
|
||||
<h2 v-t="'titles.instance'" class="text-center" />
|
||||
<label class="pref" for="ddlInstanceSelection">
|
||||
<strong v-text="`${$t('actions.instance_selection')}:`" />
|
||||
<select id="ddlInstanceSelection" v-model="selectedInstance" class="select w-auto" @change="onChange($event)">
|
||||
@ -305,8 +305,8 @@
|
||||
<br />
|
||||
|
||||
<!-- options that are visible only when logged in -->
|
||||
<div v-if="this.authenticated">
|
||||
<h2 class="text-center" v-t="'titles.account'"></h2>
|
||||
<div v-if="authenticated">
|
||||
<h2 v-t="'titles.account'" class="text-center"></h2>
|
||||
<label class="pref" for="txtDeleteAccountPassword">
|
||||
<strong v-t="'actions.delete_account'" />
|
||||
<div class="flex items-center">
|
||||
@ -314,22 +314,22 @@
|
||||
id="txtDeleteAccountPassword"
|
||||
ref="txtDeleteAccountPassword"
|
||||
v-model="password"
|
||||
v-on:keyup.enter="deleteAccount"
|
||||
:placeholder="$t('login.password')"
|
||||
:aria-label="$t('login.password')"
|
||||
class="input w-auto mr-2"
|
||||
type="password"
|
||||
@keyup.enter="deleteAccount"
|
||||
/>
|
||||
<a class="btn w-auto" @click="deleteAccount" v-t="'actions.delete_account'" />
|
||||
<a v-t="'actions.delete_account'" class="btn w-auto" @click="deleteAccount" />
|
||||
</div>
|
||||
</label>
|
||||
<div class="pref">
|
||||
<a class="btn w-auto" @click="logout" v-t="'actions.logout'" />
|
||||
<a v-t="'actions.logout'" class="btn w-auto" @click="logout" />
|
||||
<a
|
||||
v-t="'actions.invalidate_session'"
|
||||
class="btn w-auto"
|
||||
style="margin-left: 0.5em"
|
||||
@click="invalidateSession"
|
||||
v-t="'actions.invalidate_session'"
|
||||
/>
|
||||
</div>
|
||||
<br />
|
||||
@ -342,7 +342,7 @@
|
||||
<th v-t="'preferences.instance_locations'" />
|
||||
<th v-t="'preferences.has_cdn'" />
|
||||
<th v-t="'preferences.registered_users'" />
|
||||
<th class="lt-md:hidden" v-t="'preferences.version'" />
|
||||
<th v-t="'preferences.version'" class="lt-md:hidden" />
|
||||
<th v-t="'preferences.up_to_date'" />
|
||||
<th v-t="'preferences.ssl_score'" />
|
||||
</tr>
|
||||
@ -356,7 +356,7 @@
|
||||
<td class="lt-md:hidden" v-text="instance.version" />
|
||||
<td v-text="`${instance.up_to_date ? '✅' : '❌'}`" />
|
||||
<td>
|
||||
<a :href="sslScore(instance.api_url)" target="_blank" v-t="'actions.view_ssl_score'" />
|
||||
<a v-t="'actions.view_ssl_score'" :href="sslScore(instance.api_url)" target="_blank" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@ -364,15 +364,15 @@
|
||||
<br />
|
||||
<p v-t="'info.preferences_note'" />
|
||||
<br />
|
||||
<button class="btn" v-t="'actions.reset_preferences'" @click="showConfirmResetPrefsDialog = true" />
|
||||
<button class="btn mx-4" v-t="'actions.backup_preferences'" @click="backupPreferences()" />
|
||||
<label for="fileSelector" class="btn" v-t="'actions.restore_preferences'" @click="restorePreferences()" />
|
||||
<input class="hidden" id="fileSelector" ref="fileSelector" type="file" @change="restorePreferences()" />
|
||||
<button v-t="'actions.reset_preferences'" class="btn" @click="showConfirmResetPrefsDialog = true" />
|
||||
<button v-t="'actions.backup_preferences'" class="btn mx-4" @click="backupPreferences()" />
|
||||
<label v-t="'actions.restore_preferences'" for="fileSelector" class="btn" @click="restorePreferences()" />
|
||||
<input id="fileSelector" ref="fileSelector" class="hidden" type="file" @change="restorePreferences()" />
|
||||
<ConfirmModal
|
||||
v-if="showConfirmResetPrefsDialog"
|
||||
:message="$t('actions.confirm_reset_preferences')"
|
||||
@close="showConfirmResetPrefsDialog = false"
|
||||
@confirm="resetPreferences()"
|
||||
:message="$t('actions.confirm_reset_preferences')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@ -380,6 +380,9 @@
|
||||
import CountryMap from "@/utils/CountryMaps/en.json";
|
||||
import ConfirmModal from "./ConfirmModal.vue";
|
||||
export default {
|
||||
components: {
|
||||
ConfirmModal,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
mobileChapterLayout: "Vertical",
|
||||
@ -670,9 +673,6 @@ export default {
|
||||
});
|
||||
},
|
||||
},
|
||||
components: {
|
||||
ConfirmModal,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -11,7 +11,7 @@
|
||||
autocomplete="username"
|
||||
:placeholder="$t('login.username')"
|
||||
:aria-label="$t('login.username')"
|
||||
v-on:keyup.enter="register"
|
||||
@keyup.enter="register"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@ -22,23 +22,23 @@
|
||||
autocomplete="password"
|
||||
:placeholder="$t('login.password')"
|
||||
:aria-label="$t('login.password')"
|
||||
v-on:keyup.enter="register"
|
||||
@keyup.enter="register"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<a class="btn w-auto" @click="register" v-t="'titles.register'" />
|
||||
<a v-t="'titles.register'" class="btn w-auto" @click="register" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<ConfirmModal
|
||||
v-if="showUnsecureRegisterDialog"
|
||||
:message="$t('info.register_no_email_note')"
|
||||
@close="showUnsecureRegisterDialog = false"
|
||||
@confirm="
|
||||
forceUnsecureRegister = true;
|
||||
showUnsecureRegisterDialog = false;
|
||||
register();
|
||||
"
|
||||
:message="$t('info.register_no_email_note')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@ -47,6 +47,7 @@ import { isEmail } from "../utils/Misc.js";
|
||||
import ConfirmModal from "./ConfirmModal.vue";
|
||||
|
||||
export default {
|
||||
components: { ConfirmModal },
|
||||
data() {
|
||||
return {
|
||||
username: null,
|
||||
@ -85,6 +86,5 @@ export default {
|
||||
});
|
||||
},
|
||||
},
|
||||
components: { ConfirmModal },
|
||||
};
|
||||
</script>
|
||||
|
@ -5,7 +5,7 @@
|
||||
<strong v-text="`${$t('actions.filter')}:`" />
|
||||
</label>
|
||||
<select id="ddlSearchFilters" v-model="selectedFilter" default="all" class="select w-auto" @change="updateFilter()">
|
||||
<option v-for="filter in availableFilters" :key="filter" :value="filter" v-t="`search.${filter}`" />
|
||||
<option v-for="filter in availableFilters" :key="filter" v-t="`search.${filter}`" :value="filter" />
|
||||
</select>
|
||||
|
||||
<hr />
|
||||
|
@ -3,26 +3,26 @@
|
||||
<h2 v-t="'actions.share'" />
|
||||
<div class="flex justify-between">
|
||||
<label v-t="'actions.piped_link'" />
|
||||
<input type="checkbox" v-model="pipedLink" @change="onChange" />
|
||||
<input v-model="pipedLink" type="checkbox" @change="onChange" />
|
||||
</div>
|
||||
<div v-if="this.hasPlaylist" class="flex justify-between">
|
||||
<div v-if="hasPlaylist" class="flex justify-between">
|
||||
<label v-t="'actions.with_playlist'" />
|
||||
<input type="checkbox" v-model="withPlaylist" @change="onChange" />
|
||||
<input v-model="withPlaylist" type="checkbox" @change="onChange" />
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<label v-t="'actions.with_timecode'" for="withTimeCode" />
|
||||
<input id="withTimeCode" type="checkbox" v-model="withTimeCode" @change="onChange" />
|
||||
<input id="withTimeCode" v-model="withTimeCode" type="checkbox" @change="onChange" />
|
||||
</div>
|
||||
<div v-if="this.withTimeCode" class="flex justify-between mt-2">
|
||||
<div v-if="withTimeCode" class="flex justify-between mt-2">
|
||||
<label v-t="'actions.time_code'" />
|
||||
<input class="input w-12" type="text" v-model="timeStamp" />
|
||||
<input v-model="timeStamp" class="input w-12" type="text" />
|
||||
</div>
|
||||
<a :href="generatedLink" target="_blank">
|
||||
<h3 class="mt-4" v-text="generatedLink" />
|
||||
</a>
|
||||
<div class="flex justify-end mt-4">
|
||||
<button class="btn" v-t="'actions.follow_link'" @click="followLink()" />
|
||||
<button class="btn ml-3" v-t="'actions.copy_link'" @click="copyLink()" />
|
||||
<button v-t="'actions.follow_link'" class="btn" @click="followLink()" />
|
||||
<button v-t="'actions.copy_link'" class="btn ml-3" @click="copyLink()" />
|
||||
</div>
|
||||
</ModalComponent>
|
||||
</template>
|
||||
@ -31,6 +31,9 @@
|
||||
import ModalComponent from "./ModalComponent.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ModalComponent,
|
||||
},
|
||||
props: {
|
||||
videoId: {
|
||||
type: String,
|
||||
@ -42,14 +45,13 @@ export default {
|
||||
},
|
||||
playlistId: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
playlistIndex: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
ModalComponent,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
withTimeCode: true,
|
||||
@ -59,6 +61,20 @@ export default {
|
||||
hasPlaylist: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
generatedLink() {
|
||||
var baseUrl = this.pipedLink
|
||||
? window.location.origin + "/watch?v=" + this.videoId
|
||||
: "https://youtu.be/" + this.videoId;
|
||||
var url = new URL(baseUrl);
|
||||
if (this.withTimeCode && this.timeStamp > 0) url.searchParams.append("t", this.timeStamp);
|
||||
if (this.hasPlaylist && this.withPlaylist) {
|
||||
url.searchParams.append("list", this.playlistId);
|
||||
url.searchParams.append("index", this.playlistIndex);
|
||||
}
|
||||
return url.href;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.timeStamp = parseInt(this.currentTime);
|
||||
this.withTimeCode = this.getPreferenceBoolean("shareWithTimeCode", true);
|
||||
@ -87,19 +103,5 @@ export default {
|
||||
this.setPreference("shareWithPlaylist", this.withPlaylist, true);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
generatedLink() {
|
||||
var baseUrl = this.pipedLink
|
||||
? window.location.origin + "/watch?v=" + this.videoId
|
||||
: "https://youtu.be/" + this.videoId;
|
||||
var url = new URL(baseUrl);
|
||||
if (this.withTimeCode && this.timeStamp > 0) url.searchParams.append("t", this.timeStamp);
|
||||
if (this.hasPlaylist && this.withPlaylist) {
|
||||
url.searchParams.append("list", this.playlistId);
|
||||
url.searchParams.append("index", this.playlistIndex);
|
||||
}
|
||||
return url.href;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<label for="ddlSortBy" v-t="'actions.sort_by'" />
|
||||
<label v-t="'actions.sort_by'" for="ddlSortBy" />
|
||||
<select id="ddlSortBy" v-model="selectedSort" class="select flex-grow">
|
||||
<option v-for="(value, key) in options" v-t="`actions.${key}`" :key="key" :value="value" />
|
||||
<option v-for="(value, key) in options" :key="key" v-t="`actions.${key}`" :value="value" />
|
||||
</select>
|
||||
</template>
|
||||
|
||||
@ -18,7 +18,10 @@ const options = {
|
||||
const selectedSort = ref("descending");
|
||||
|
||||
const props = defineProps({
|
||||
byKey: String,
|
||||
byKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["apply"]);
|
||||
|
@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<h1 class="font-bold text-center my-4" v-t="'titles.subscriptions'" />
|
||||
<h1 v-t="'titles.subscriptions'" class="font-bold text-center my-4" />
|
||||
<!-- import / export section -->
|
||||
<div class="flex justify-between w-full">
|
||||
<div class="flex">
|
||||
<button class="btn mx-1">
|
||||
<router-link to="/import" v-t="'actions.import_from_json'" />
|
||||
<router-link v-t="'actions.import_from_json'" to="/import" />
|
||||
</button>
|
||||
<button class="btn" @click="exportHandler" v-t="'actions.export_to_json'" />
|
||||
<button v-t="'actions.export_to_json'" class="btn" @click="exportHandler" />
|
||||
</div>
|
||||
<!-- subscriptions count, only shown if there are any -->
|
||||
<i18n-t v-if="subscriptions.length > 0" keypath="subscriptions.subscribed_channels_count">{{
|
||||
@ -18,9 +18,9 @@
|
||||
<div class="w-full flex flex-wrap">
|
||||
<button
|
||||
v-for="group in channelGroups"
|
||||
:key="group.groupName"
|
||||
class="btn mx-1 w-max"
|
||||
:class="{ selected: selectedGroup === group }"
|
||||
:key="group.groupName"
|
||||
@click="selectedGroup = group"
|
||||
>
|
||||
<span v-text="group.groupName !== '' ? group.groupName : $t('video.all')" />
|
||||
@ -39,9 +39,9 @@
|
||||
<div class="xl:grid xl:grid-cols-5 <md:flex-wrap">
|
||||
<!-- channel info card -->
|
||||
<div
|
||||
class="col m-2 p-1 border rounded-lg border-gray-500"
|
||||
v-for="subscription in filteredSubscriptions"
|
||||
:key="subscription.url"
|
||||
class="col m-2 p-1 border rounded-lg border-gray-500"
|
||||
>
|
||||
<router-link :to="subscription.url" class="flex p-2 font-bold text-4x4">
|
||||
<img :src="subscription.avatar" class="rounded-full h-[fit-content]" width="48" height="48" />
|
||||
@ -49,9 +49,9 @@
|
||||
</router-link>
|
||||
<!-- subscribe / unsubscribe btn -->
|
||||
<button
|
||||
v-t="`actions.${subscription.subscribed ? 'unsubscribe' : 'subscribe'}`"
|
||||
class="btn w-full mt-2"
|
||||
@click="handleButton(subscription)"
|
||||
v-t="`actions.${subscription.subscribed ? 'unsubscribe' : 'subscribe'}`"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -60,8 +60,8 @@
|
||||
<ModalComponent v-if="showCreateGroupModal" @close="showCreateGroupModal = !showCreateGroupModal">
|
||||
<h2 v-t="'actions.create_group'" />
|
||||
<div class="flex flex-col">
|
||||
<input class="input my-4" type="text" v-model="newGroupName" :placeholder="$t('actions.group_name')" />
|
||||
<button class="ml-auto btn w-max" v-t="'actions.create_group'" @click="createGroup()" />
|
||||
<input v-model="newGroupName" class="input my-4" type="text" :placeholder="$t('actions.group_name')" />
|
||||
<button v-t="'actions.create_group'" class="ml-auto btn w-max" @click="createGroup()" />
|
||||
</div>
|
||||
</ModalComponent>
|
||||
|
||||
@ -88,6 +88,7 @@
|
||||
import ModalComponent from "./ModalComponent.vue";
|
||||
|
||||
export default {
|
||||
components: { ModalComponent },
|
||||
data() {
|
||||
return {
|
||||
subscriptions: [],
|
||||
@ -101,6 +102,13 @@ export default {
|
||||
newGroupName: "",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filteredSubscriptions(_this) {
|
||||
return _this.selectedGroup.groupName == ""
|
||||
? _this.subscriptions
|
||||
: _this.subscriptions.filter(channel => _this.selectedGroup.channels.includes(channel.url.substr(-11)));
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchSubscriptions().then(json => {
|
||||
this.subscriptions = json;
|
||||
@ -201,14 +209,6 @@ export default {
|
||||
this.createOrUpdateChannelGroup(this.selectedGroup);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
filteredSubscriptions(_this) {
|
||||
return _this.selectedGroup.groupName == ""
|
||||
? _this.subscriptions
|
||||
: _this.subscriptions.filter(channel => _this.selectedGroup.channels.includes(channel.url.substr(-11)));
|
||||
},
|
||||
},
|
||||
components: { ModalComponent },
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -1,12 +1,13 @@
|
||||
<template>
|
||||
<div class="toast">
|
||||
<slot />
|
||||
<button @click="dismiss" v-t="'actions.dismiss'" />
|
||||
<button v-t="'actions.dismiss'" @click="dismiss" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
emits: ["dismissed"],
|
||||
methods: {
|
||||
dismiss() {
|
||||
this.$emit("dismissed");
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-justify-between" v-if="showVideo">
|
||||
<div v-if="showVideo" class="flex flex-col flex-justify-between">
|
||||
<router-link
|
||||
class="focus:underline hover:underline inline-block w-full"
|
||||
:to="{
|
||||
@ -22,8 +22,8 @@
|
||||
<!-- progress bar -->
|
||||
<div class="relative w-full h-1">
|
||||
<div
|
||||
class="absolute bottom-0 left-0 h-1 bg-red-600"
|
||||
v-if="item.watched && item.duration > 0"
|
||||
class="absolute bottom-0 left-0 h-1 bg-red-600"
|
||||
:style="{ width: `clamp(0%, ${(item.currentTime / item.duration) * 100}%, 100%` }"
|
||||
/>
|
||||
</div>
|
||||
@ -31,21 +31,21 @@
|
||||
|
||||
<div class="relative text-sm">
|
||||
<span
|
||||
class="thumbnail-overlay thumbnail-right"
|
||||
v-if="item.duration > 0"
|
||||
class="thumbnail-overlay thumbnail-right"
|
||||
v-text="timeFormat(item.duration)"
|
||||
/>
|
||||
<!-- shorts thumbnail -->
|
||||
<span class="thumbnail-overlay thumbnail-left" v-if="item.isShort" v-t="'video.shorts'" />
|
||||
<span v-if="item.isShort" v-t="'video.shorts'" class="thumbnail-overlay thumbnail-left" />
|
||||
<span
|
||||
class="thumbnail-overlay thumbnail-right"
|
||||
v-else-if="item.duration >= 0"
|
||||
class="thumbnail-overlay thumbnail-right"
|
||||
v-text="timeFormat(item.duration)"
|
||||
/>
|
||||
<i18n-t v-else keypath="video.live" class="thumbnail-overlay thumbnail-right !bg-red-600" tag="div">
|
||||
<font-awesome-icon class="w-3" :icon="['fas', 'broadcast-tower']" />
|
||||
</i18n-t>
|
||||
<span v-if="item.watched" class="thumbnail-overlay bottom-5px left-5px" v-t="'video.watched'" />
|
||||
<span v-if="item.watched" v-t="'video.watched'" class="thumbnail-overlay bottom-5px left-5px" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@ -78,7 +78,7 @@
|
||||
:title="item.uploaderName"
|
||||
>
|
||||
<span v-text="item.uploaderName" />
|
||||
<font-awesome-icon class="ml-1.5" v-if="item.uploaderVerified" icon="check" />
|
||||
<font-awesome-icon v-if="item.uploaderVerified" class="ml-1.5" icon="check" />
|
||||
</router-link>
|
||||
|
||||
<div v-if="item.views >= 0 || item.uploadedDate" class="text-xs font-normal text-gray-300 mt-1">
|
||||
@ -112,17 +112,17 @@
|
||||
</button>
|
||||
<button
|
||||
v-if="admin"
|
||||
:title="$t('actions.remove_from_playlist')"
|
||||
ref="removeButton"
|
||||
:title="$t('actions.remove_from_playlist')"
|
||||
@click="showConfirmRemove = true"
|
||||
>
|
||||
<font-awesome-icon icon="circle-minus" />
|
||||
</button>
|
||||
<ConfirmModal
|
||||
v-if="showConfirmRemove"
|
||||
:message="$t('actions.delete_playlist_video_confirm')"
|
||||
@close="showConfirmRemove = false"
|
||||
@confirm="removeVideo(item.url.substr(-11))"
|
||||
:message="$t('actions.delete_playlist_video_confirm')"
|
||||
/>
|
||||
<PlaylistAddModal
|
||||
v-if="showModal"
|
||||
@ -135,17 +135,12 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.shorts-img {
|
||||
@apply w-full object-contain;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import PlaylistAddModal from "./PlaylistAddModal.vue";
|
||||
import ConfirmModal from "./ConfirmModal.vue";
|
||||
|
||||
export default {
|
||||
components: { PlaylistAddModal, ConfirmModal },
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
@ -164,6 +159,7 @@ export default {
|
||||
playlistId: { type: String, default: null },
|
||||
admin: { type: Boolean, default: false },
|
||||
},
|
||||
emits: ["remove"],
|
||||
data() {
|
||||
return {
|
||||
showModal: false,
|
||||
@ -171,9 +167,6 @@ export default {
|
||||
showConfirmRemove: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.shouldShowVideo();
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return this.item.dearrow?.titles[0]?.title ?? this.item.title;
|
||||
@ -182,6 +175,9 @@ export default {
|
||||
return this.item.dearrow?.thumbnails[0]?.thumbnail ?? this.item.thumbnail;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.shouldShowVideo();
|
||||
},
|
||||
methods: {
|
||||
removeVideo() {
|
||||
this.$refs.removeButton.disabled = true;
|
||||
@ -204,6 +200,11 @@ export default {
|
||||
};
|
||||
},
|
||||
},
|
||||
components: { PlaylistAddModal, ConfirmModal },
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.shorts-img {
|
||||
@apply w-full object-contain;
|
||||
}
|
||||
</style>
|
||||
|
@ -7,12 +7,12 @@
|
||||
>
|
||||
<video ref="videoEl" class="w-full" data-shaka-player :autoplay="shouldAutoPlay" :loop="selectedAutoLoop" />
|
||||
<span
|
||||
ref="previewContainer"
|
||||
id="preview-container"
|
||||
ref="previewContainer"
|
||||
class="hidden flex-col absolute bottom-0 z-[2000] mb-[3.5%] items-center"
|
||||
>
|
||||
<canvas ref="preview" id="preview" class="rounded-sm" />
|
||||
<span v-text="timeFormat(currentTime)" class="text-sm mt-2 rounded-xl pb-1 pt-1.5 px-2 bg-dark-700 w-min" />
|
||||
<canvas id="preview" ref="preview" class="rounded-sm" />
|
||||
<span class="text-sm mt-2 rounded-xl pb-1 pt-1.5 px-2 bg-dark-700 w-min" v-text="timeFormat(currentTime)" />
|
||||
</span>
|
||||
<button
|
||||
v-if="inSegment"
|
||||
@ -57,7 +57,7 @@ export default {
|
||||
selectedAutoLoop: Boolean,
|
||||
isEmbed: Boolean,
|
||||
},
|
||||
emits: ["timeupdate"],
|
||||
emits: ["timeupdate", "ended"],
|
||||
data() {
|
||||
return {
|
||||
lastUpdate: new Date().getTime(),
|
||||
|
@ -1,7 +1,10 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
link: String,
|
||||
link: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
platform: {
|
||||
type: String,
|
||||
required: false,
|
||||
@ -12,7 +15,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="this.getPreferenceBoolean('showWatchOnYouTube', false)">
|
||||
<template v-if="getPreferenceBoolean('showWatchOnYouTube', false)">
|
||||
<!-- For large screens -->
|
||||
<a :href="link" class="btn lt-lg:hidden flex items-center">
|
||||
<i18n-t keypath="player.watch_on" tag="strong">{{ platform }}</i18n-t>
|
||||
|
@ -32,8 +32,8 @@
|
||||
/>
|
||||
</keep-alive>
|
||||
<ChaptersBar
|
||||
:mobileLayout="isMobile"
|
||||
v-if="video?.chapters?.length > 0 && showChapters"
|
||||
:mobile-layout="isMobile"
|
||||
:chapters="video.chapters"
|
||||
:player-position="currentTime"
|
||||
@seek="navigate"
|
||||
@ -76,7 +76,7 @@
|
||||
video.uploader
|
||||
}}</router-link>
|
||||
<!-- Verified Badge -->
|
||||
<font-awesome-icon class="ml-1" v-if="video.uploaderVerified" icon="check" />
|
||||
<font-awesome-icon v-if="video.uploaderVerified" class="ml-1" icon="check" />
|
||||
</div>
|
||||
<PlaylistAddModal
|
||||
v-if="showModal"
|
||||
@ -98,20 +98,20 @@
|
||||
{{ $t("actions.add_to_playlist") }}<font-awesome-icon class="ml-1" icon="circle-plus" />
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
@click="subscribeHandler"
|
||||
v-t="{
|
||||
path: `actions.${subscribed ? 'unsubscribe' : 'subscribe'}`,
|
||||
args: { count: numberFormat(video.uploaderSubscriberCount) },
|
||||
}"
|
||||
class="btn"
|
||||
@click="subscribeHandler"
|
||||
/>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<!-- RSS Feed button -->
|
||||
<a
|
||||
v-if="video.uploaderUrl"
|
||||
aria-label="RSS feed"
|
||||
title="RSS feed"
|
||||
role="button"
|
||||
v-if="video.uploaderUrl"
|
||||
:href="`${apiUrl()}/feed/unauthenticated/rss?channels=${video.uploaderUrl.split('/')[2]}`"
|
||||
target="_blank"
|
||||
class="btn flex items-center"
|
||||
@ -147,14 +147,14 @@
|
||||
<hr />
|
||||
|
||||
<button
|
||||
v-t="`actions.${showDesc ? 'minimize_description' : 'show_description'}`"
|
||||
class="btn mb-2"
|
||||
@click="showDesc = !showDesc"
|
||||
v-t="`actions.${showDesc ? 'minimize_description' : 'show_description'}`"
|
||||
/>
|
||||
|
||||
<span class="btn ml-2" v-show="video?.chapters?.length > 0">
|
||||
<input id="showChapters" type="checkbox" v-model="showChapters" />
|
||||
<label class="ml-2" for="showChapters" v-t="'actions.show_chapters'" />
|
||||
<span v-show="video?.chapters?.length > 0" class="btn ml-2">
|
||||
<input id="showChapters" v-model="showChapters" type="checkbox" />
|
||||
<label v-t="'actions.show_chapters'" class="ml-2" for="showChapters" />
|
||||
</span>
|
||||
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
@ -192,10 +192,10 @@
|
||||
</div>
|
||||
<div v-if="!showComments" class="xl:col-span-4 sm:col-span-3"></div>
|
||||
<div v-else-if="!comments" class="xl:col-span-4 sm:col-span-3">
|
||||
<p class="text-center mt-8" v-t="'comment.loading'"></p>
|
||||
<p v-t="'comment.loading'" class="text-center mt-8"></p>
|
||||
</div>
|
||||
<div v-else-if="comments.disabled" class="xl:col-span-4 sm:col-span-3">
|
||||
<p class="text-center mt-8" v-t="'comment.disabled'"></p>
|
||||
<p v-t="'comment.disabled'" class="text-center mt-8"></p>
|
||||
</div>
|
||||
<div v-else ref="comments" class="xl:col-span-4 sm:col-span-3">
|
||||
<CommentItem
|
||||
@ -215,9 +215,9 @@
|
||||
:selected-index="index"
|
||||
/>
|
||||
<a
|
||||
v-t="`actions.${showRecs ? 'minimize_recommendations' : 'show_recommendations'}`"
|
||||
class="btn mb-2"
|
||||
@click="showRecs = !showRecs"
|
||||
v-t="`actions.${showRecs ? 'minimize_recommendations' : 'show_recommendations'}`"
|
||||
/>
|
||||
<hr v-show="showRecs" />
|
||||
<div v-show="showRecs">
|
||||
|
Loading…
Reference in New Issue
Block a user