Merge pull request #2717 from TeamPiped/fix-eslint

Fix eslint config and apply all fixes
This commit is contained in:
Kavin 2023-07-27 12:59:34 +01:00 committed by GitHub
commit 480efd14f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 333 additions and 285 deletions

7
.eslintrc.cjs Normal file
View File

@ -0,0 +1,7 @@
module.exports = {
root: true,
env: {
node: true,
},
extends: ["plugin:vue/vue3-recommended", "eslint:recommended", "plugin:prettier/recommended"],
};

25
.github/workflows/reviewdog.yml vendored Normal file
View File

@ -0,0 +1,25 @@
name: reviewdog
on: [pull_request]
jobs:
eslint:
name: runner / eslint
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v3
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: latest
- name: Setup Node.js
uses: actions/setup-node@v3
with:
cache: "pnpm"
- run: pnpm install
- uses: reviewdog/action-eslint@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
reporter: github-pr-review
eslint_flags: "--ignore-path .gitignore --ext .js,.vue ."

View File

@ -51,18 +51,6 @@
"vite-plugin-pwa": "0.16.4", "vite-plugin-pwa": "0.16.4",
"workbox-window": "7.0.0" "workbox-window": "7.0.0"
}, },
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"plugin:prettier/recommended",
"eslint:recommended"
],
"rules": {}
},
"browserslist": [ "browserslist": [
"last 1 chrome version", "last 1 chrome version",
"last 1 firefox version" "last 1 firefox version"

View File

@ -29,25 +29,6 @@ export default {
theme: "dark", 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() { mounted() {
this.setTheme(); this.setTheme();
darkModePreference.addEventListener("change", () => { 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> </script>

View File

@ -6,14 +6,14 @@
</div> </div>
<p> <p>
<span v-text="props.item.name" /> <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> </p>
</router-link> </router-link>
<p v-if="props.item.description" v-text="props.item.description" /> <p v-if="props.item.description" v-text="props.item.description" />
<router-link v-if="props.item.uploaderUrl" class="link" :to="props.item.uploaderUrl"> <router-link v-if="props.item.uploaderUrl" class="link" :to="props.item.uploaderUrl">
<p> <p>
<span v-text="props.item.uploader" /> <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> </p>
</router-link> </router-link>
@ -29,6 +29,9 @@
<script setup> <script setup>
const props = defineProps({ const props = defineProps({
item: Object, item: {
type: Object,
required: true,
},
}); });
</script> </script>

View File

@ -12,27 +12,27 @@
<div class="flex place-items-center"> <div class="flex place-items-center">
<img height="48" width="48" class="rounded-full m-1" :src="channel.avatarUrl" /> <img height="48" width="48" class="rounded-full m-1" :src="channel.avatarUrl" />
<div class="flex gap-1 items-center"> <div class="flex gap-1 items-center">
<h1 v-text="channel.name" class="!text-xl" /> <h1 class="!text-xl" v-text="channel.name" />
<font-awesome-icon class="!text-xl" v-if="channel.verified" icon="check" /> <font-awesome-icon v-if="channel.verified" class="!text-xl" icon="check" />
</div> </div>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<button <button
class="btn"
@click="subscribeHandler"
v-t="{ v-t="{
path: `actions.${subscribed ? 'unsubscribe' : 'subscribe'}`, path: `actions.${subscribed ? 'unsubscribe' : 'subscribe'}`,
args: { count: numberFormat(channel.subscriberCount) }, args: { count: numberFormat(channel.subscriberCount) },
}" }"
class="btn"
@click="subscribeHandler"
></button> ></button>
<!-- RSS Feed button --> <!-- RSS Feed button -->
<a <a
v-if="channel.id"
aria-label="RSS feed" aria-label="RSS feed"
title="RSS feed" title="RSS feed"
role="button" role="button"
v-if="channel.id"
: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="btn flex-col"
@ -44,15 +44,15 @@
<CollapsableText :text="channel.description" /> <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"> <div class="flex my-2 mx-1">
<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="btn mr-2"
@click="loadTab(index)"
:class="{ active: selectedTab == index }" :class="{ active: selectedTab == index }"
@click="loadTab(index)"
> >
<span v-text="tab.translatedName"></span> <span v-text="tab.translatedName"></span>
</button> </button>

View File

@ -5,11 +5,11 @@
{{ $t("video.chapters") }} ({{ chapters.length }}) {{ $t("video.chapters") }} ({{ chapters.length }})
</h2> </h2>
<div <div
:key="chapter.start"
v-for="(chapter, index) in chapters" v-for="(chapter, index) in chapters"
@click="$emit('seek', chapter.start)" :key="chapter.start"
class="chapter-vertical" class="chapter-vertical"
:class="{ 'bg-red-500/50': isCurrentChapter(index) }" :class="{ 'bg-red-500/50': isCurrentChapter(index) }"
@click="$emit('seek', chapter.start)"
> >
<div class="flex"> <div class="flex">
<span class="mt-5 mr-2 text-current" v-text="index + 1" /> <span class="mt-5 mr-2 text-current" v-text="index + 1" />
@ -31,11 +31,11 @@
{{ $t("video.chapters") }} ({{ chapters.length }}) {{ $t("video.chapters") }} ({{ chapters.length }})
</h2> </h2>
<div <div
:key="chapter.start"
v-for="(chapter, index) in chapters" v-for="(chapter, index) in chapters"
@click="$emit('seek', chapter.start)" :key="chapter.start"
class="chapter-vertical" class="chapter-vertical"
:class="{ 'bg-red-500/50': isCurrentChapter(index) }" :class="{ 'bg-red-500/50': isCurrentChapter(index) }"
@click="$emit('seek', chapter.start)"
> >
<div class="flex"> <div class="flex">
<span class="mt-5 mr-2 text-current" v-text="index + 1" /> <span class="mt-5 mr-2 text-current" v-text="index + 1" />
@ -50,11 +50,11 @@
<!-- mobile Horizontal view --> <!-- mobile Horizontal view -->
<div v-if="getPreferenceString('mobileChapterLayout') == 'Horizontal' && mobileLayout" class="flex overflow-x-auto"> <div v-if="getPreferenceString('mobileChapterLayout') == 'Horizontal' && mobileLayout" class="flex overflow-x-auto">
<div <div
:key="chapter.start"
v-for="(chapter, index) in chapters" v-for="(chapter, index) in chapters"
@click="$emit('seek', chapter.start)" :key="chapter.start"
class="chapter" class="chapter"
:class="{ 'bg-red-500/50': isCurrentChapter(index) }" :class="{ 'bg-red-500/50': isCurrentChapter(index) }"
@click="$emit('seek', chapter.start)"
> >
<img :src="chapter.image" :alt="chapter.title" /> <img :src="chapter.image" :alt="chapter.title" />
<div class="m-1 flex"> <div class="m-1 flex">
@ -65,6 +65,32 @@
</div> </div>
</template> </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> <style>
::-webkit-scrollbar { ::-webkit-scrollbar {
height: 5px; height: 5px;
@ -89,26 +115,3 @@
@apply truncate overflow-hidden inline-block w-10em; @apply truncate overflow-hidden inline-block w-10em;
} }
</style> </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>

View File

@ -1,7 +1,8 @@
<template> <template v-if="text">
<div class="whitespace-pre-wrap py-2 mx-1">
<!-- eslint-disable-next-line vue/no-v-html --> <!-- eslint-disable-next-line vue/no-v-html -->
<div v-if="text" class="whitespace-pre-wrap py-2 mx-1">
<span v-if="showFullText" v-html="fullText()" /> <span v-if="showFullText" v-html="fullText()" />
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-else v-html="colapsedText()" /> <span v-else v-html="colapsedText()" />
<span v-if="text.length > 100 && !showFullText">...</span> <span v-if="text.length > 100 && !showFullText">...</span>
<button <button
@ -19,7 +20,10 @@ import { purifyHTML, rewriteDescription } from "@/utils/HtmlUtils";
export default { export default {
props: { props: {
text: String, text: {
type: String,
default: null,
},
}, },
data() { data() {
return { return {

View File

@ -14,34 +14,35 @@
<div v-if="comment.pinned" class="comment-pinned"> <div v-if="comment.pinned" class="comment-pinned">
<font-awesome-icon icon="thumbtack" /> <font-awesome-icon icon="thumbtack" />
<span <span
class="ml-1.5"
v-t="{ v-t="{
path: 'comment.pinned_by', path: 'comment.pinned_by',
args: { author: uploader }, args: { author: uploader },
}" }"
class="ml-1.5"
/> />
</div> </div>
<div class="comment-author"> <div class="comment-author">
<router-link class="font-bold link" :to="comment.commentorUrl">{{ comment.author }}</router-link> <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>
<div class="comment-meta text-sm mb-1.5" v-text="comment.commentedTime" /> <div class="comment-meta text-sm mb-1.5" v-text="comment.commentedTime" />
</div> </div>
<!-- eslint-disable-next-line vue/no-v-html -->
<div class="whitespace-pre-wrap" v-html="purifiedText" /> <div class="whitespace-pre-wrap" v-html="purifiedText" />
<div class="comment-footer mt-1 flex items-center"> <div class="comment-footer mt-1 flex items-center">
<div class="i-fa6-solid:thumbs-up" /> <div class="i-fa6-solid:thumbs-up" />
<span class="ml-1" v-text="numberFormat(comment.likeCount)" /> <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> </div>
<template v-if="comment.repliesPage && (!loadingReplies || !showingReplies)"> <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)}`" /> <a v-text="`${$t('actions.reply_count', comment.replyCount)}`" />
<font-awesome-icon class="ml-1.5" icon="level-down-alt" /> <font-awesome-icon class="ml-1.5" icon="level-down-alt" />
</div> </div>
</template> </template>
<template v-if="showingReplies"> <template v-if="showingReplies">
<div @click="hideReplies" class="cursor-pointer"> <div class="cursor-pointer" @click="hideReplies">
<a v-t="'actions.hide_replies'" /> <a v-t="'actions.hide_replies'" />
<font-awesome-icon class="ml-1.5" icon="level-up-alt" /> <font-awesome-icon class="ml-1.5" icon="level-up-alt" />
</div> </div>
@ -50,7 +51,7 @@
<div v-for="reply in replies" :key="reply.commentId" class="w-full"> <div v-for="reply in replies" :key="reply.commentId" class="w-full">
<CommentItem :comment="reply" :uploader="uploader" :video-id="videoId" /> <CommentItem :comment="reply" :uploader="uploader" :video-id="videoId" />
</div> </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'" /> <a v-t="'actions.load_more_replies'" />
<font-awesome-icon class="ml-1.5" icon="level-down-alt" /> <font-awesome-icon class="ml-1.5" icon="level-down-alt" />
</div> </div>

View File

@ -3,8 +3,8 @@
<div> <div>
<h3 class="text-xl" v-text="message" /> <h3 class="text-xl" v-text="message" />
<div class="ml-auto mt-8 flex gap-2 w-min"> <div class="ml-auto mt-8 flex gap-2 w-min">
<button class="btn" v-t="'actions.cancel'" @click="$emit('close')" /> <button v-t="'actions.cancel'" class="btn" @click="$emit('close')" />
<button class="btn" v-t="'actions.okay'" @click="$emit('confirm')" /> <button v-t="'actions.okay'" class="btn" @click="$emit('confirm')" />
</div> </div>
</div> </div>
</ModalComponent> </ModalComponent>
@ -18,7 +18,10 @@ export default {
ModalComponent, ModalComponent,
}, },
props: { props: {
message: String, message: {
type: String,
required: true,
},
}, },
emits: ["close", "confirm"], emits: ["close", "confirm"],
}; };

View File

@ -6,7 +6,10 @@
import { defineAsyncComponent } from "vue"; import { defineAsyncComponent } from "vue";
const props = defineProps({ const props = defineProps({
item: Object, item: {
type: Object,
required: true,
},
}); });
const VideoItem = defineAsyncComponent(() => import("./VideoItem.vue")); const VideoItem = defineAsyncComponent(() => import("./VideoItem.vue"));

View File

@ -1,6 +1,6 @@
<template> <template>
<p v-text="message" /> <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" /> <p ref="stacktrace" class="whitespace-pre-wrap" hidden v-text="error" />
</template> </template>

View File

@ -13,7 +13,7 @@
class="select flex-grow" class="select flex-grow"
@change="onFilterChange()" @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> </select>
</span> </span>
@ -22,7 +22,7 @@
<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="select flex-grow">
<option value="" v-t="`video.all`" /> <option v-t="`video.all`" value="" />
<option <option
v-for="group in channelGroups" v-for="group in channelGroups"
:key="group.groupName" :key="group.groupName"

View File

@ -2,23 +2,23 @@
<footer class="text-center py-4 rounded-xl children:(mx-3) w-full mt-10"> <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"> <a aria-label="GitHub" href="https://github.com/TeamPiped/Piped" target="_blank">
<font-awesome-icon :icon="['fab', 'github']" /> <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>
<a href="https://docs.piped.video/" target="_blank"> <a href="https://docs.piped.video/" target="_blank">
<font-awesome-icon :icon="['fa', 'book']" /> <font-awesome-icon :icon="['fa', 'book']" />
<span class="ml-2" v-t="'actions.documentation'" /> <span v-t="'actions.documentation'" class="ml-2" />
</a> </a>
<a href="https://github.com/TeamPiped/Piped#donations" target="_blank"> <a href="https://github.com/TeamPiped/Piped#donations" target="_blank">
<font-awesome-icon :icon="['fab', 'bitcoin']" /> <font-awesome-icon :icon="['fab', 'bitcoin']" />
<span class="ml-2" v-t="'actions.donations'" /> <span v-t="'actions.donations'" class="ml-2" />
</a> </a>
<a v-if="statusPageHref" :href="statusPageHref"> <a v-if="statusPageHref" :href="statusPageHref">
<font-awesome-icon :icon="['fa', 'server']" /> <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>
<a v-if="donationHref" :href="donationHref"> <a v-if="donationHref" :href="donationHref">
<font-awesome-icon :icon="['fa', 'donate']" /> <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> </a>
</footer> </footer>
</template> </template>

View File

@ -1,11 +1,11 @@
<template> <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">
<div class="flex md:items-center gap-2 flex-col md:flex-row"> <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"> <div class="ml-auto flex gap-1 items-center">
<SortingSelector by-key="watchedAt" @apply="order => videos.sort(order)" /> <SortingSelector by-key="watchedAt" @apply="order => videos.sort(order)" />
@ -13,19 +13,19 @@
</div> </div>
<div class="flex ml-4 items-center"> <div class="flex ml-4 items-center">
<input id="autoDelete" type="checkbox" v-model="autoDeleteHistory" @change="onChange" /> <input id="autoDelete" v-model="autoDeleteHistory" type="checkbox" @change="onChange" />
<label class="ml-2" for="autoDelete" v-t="'actions.delete_automatically'" /> <label v-t="'actions.delete_automatically'" class="ml-2" for="autoDelete" />
<select class="pl-3 ml-3 select" v-model="autoDeleteDelayHours" @change="onChange"> <select v-model="autoDeleteDelayHours" class="pl-3 ml-3 select" @change="onChange">
<option value="1" v-t="{ path: 'info.hours', args: { amount: '1' } }" /> <option v-t="{ path: 'info.hours', args: { amount: '1' } }" value="1" />
<option value="3" v-t="{ path: 'info.hours', args: { amount: '3' } }" /> <option v-t="{ path: 'info.hours', args: { amount: '3' } }" value="3" />
<option value="6" v-t="{ path: 'info.hours', args: { amount: '6' } }" /> <option v-t="{ path: 'info.hours', args: { amount: '6' } }" value="6" />
<option value="12" v-t="{ path: 'info.hours', args: { amount: '12' } }" /> <option v-t="{ path: 'info.hours', args: { amount: '12' } }" value="12" />
<option value="24" v-t="{ path: 'info.days', args: { amount: '1' } }" /> <option v-t="{ path: 'info.days', args: { amount: '1' } }" value="24" />
<option value="72" v-t="{ path: 'info.days', args: { amount: '3' } }" /> <option v-t="{ path: 'info.days', args: { amount: '3' } }" value="72" />
<option value="168" v-t="{ path: 'info.weeks', args: { amount: '1' } }" /> <option v-t="{ path: 'info.weeks', args: { amount: '1' } }" value="168" />
<option value="336" v-t="{ path: 'info.weeks', args: { amount: '3' } }" /> <option v-t="{ path: 'info.weeks', args: { amount: '3' } }" value="336" />
<option value="672" v-t="{ path: 'info.months', args: { amount: '1' } }" /> <option v-t="{ path: 'info.months', args: { amount: '1' } }" value="672" />
<option value="1344" v-t="{ path: 'info.months', args: { amount: '2' } }" /> <option v-t="{ path: 'info.months', args: { amount: '2' } }" value="1344" />
</select> </select>
</div> </div>
</div> </div>

View File

@ -7,6 +7,17 @@
</div> </div>
</template> </template>
<script>
export default {
props: {
showContent: {
type: Boolean,
required: true,
},
},
};
</script>
<style> <style>
#spinner:after { #spinner:after {
--spinner-color: #000; --spinner-color: #000;
@ -42,14 +53,3 @@
} }
} }
</style> </style>
<script>
export default {
props: {
showContent: {
type: Boolean,
required: true,
},
},
};
</script>

View File

@ -11,7 +11,7 @@
autocomplete="username" autocomplete="username"
:placeholder="$t('login.username')" :placeholder="$t('login.username')"
:aria-label="$t('login.username')" :aria-label="$t('login.username')"
v-on:keyup.enter="login" @keyup.enter="login"
/> />
</div> </div>
<div> <div>
@ -22,11 +22,11 @@
autocomplete="password" autocomplete="password"
:placeholder="$t('login.password')" :placeholder="$t('login.password')"
:aria-label="$t('login.password')" :aria-label="$t('login.password')"
v-on:keyup.enter="login" @keyup.enter="login"
/> />
</div> </div>
<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> </div>
</form> </form>
</div> </div>

View File

@ -11,6 +11,7 @@
<script> <script>
export default { export default {
emits: ["close"],
mounted() { mounted() {
window.addEventListener("keydown", this.handleKeyDown); window.addEventListener("keydown", this.handleKeyDown);
}, },

View File

@ -13,11 +13,11 @@
</div> </div>
<div class="lt-md:hidden search-container"> <div class="lt-md:hidden search-container">
<input <input
ref="videoSearch"
v-model="searchText" v-model="searchText"
class="input w-72 h-10 pr-20" class="input w-72 h-10 pr-20"
type="text" type="text"
role="search" role="search"
ref="videoSearch"
:title="$t('actions.search')" :title="$t('actions.search')"
:placeholder="$t('actions.search')" :placeholder="$t('actions.search')"
@keyup="onKeyUp" @keyup="onKeyUp"
@ -27,7 +27,7 @@
/> />
<span v-if="searchText" class="delete-search" @click="searchText = ''"></span> <span v-if="searchText" class="delete-search" @click="searchText = ''"></span>
</div> </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> <div class="i-fa6-solid:magnifying-glass"></div>
</button> </button>
<!-- three vertical lines for toggling the hamburger menu on mobile --> <!-- three vertical lines for toggling the hamburger menu on mobile -->
@ -135,13 +135,6 @@ export default {
registrationDisabled: false, 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: { computed: {
shouldShowLogin(_this) { shouldShowLogin(_this) {
return _this.getAuthToken() == null; return _this.getAuthToken() == null;
@ -159,6 +152,13 @@ export default {
return _this.getPreferenceBoolean("searchHistory", false) && localStorage.getItem("search_history"); 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: { methods: {
// focus on search bar when Ctrl+k is pressed // focus on search bar when Ctrl+k is pressed
focusOnSearchBar() { focusOnSearchBar() {

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="flex flex-col justify-center items-center min-h-[88vh]"> <div class="flex flex-col justify-center items-center min-h-[88vh]">
<h1 class="font-bold !text-9xl">404</h1> <h1 class="font-bold !text-9xl">404</h1>
<h2 class="!text-2xl" v-t="'info.page_not_found'" /> <h2 v-t="'info.page_not_found'" class="!text-2xl" />
<a class="btn mt-16" href="/" v-t="'actions.back_to_home'" /> <a v-t="'actions.back_to_home'" class="btn mt-16" href="/" />
</div> </div>
</template> </template>

View File

@ -1,16 +1,16 @@
<template> <template>
<ModalComponent> <ModalComponent>
<span class="text-2xl w-max inline-block" v-t="'actions.select_playlist'" /> <span v-t="'actions.select_playlist'" class="text-2xl w-max inline-block" />
<select class="select w-full mt-3" v-model="selectedPlaylist"> <select v-model="selectedPlaylist" class="select w-full mt-3">
<option v-for="playlist in playlists" :value="playlist.id" :key="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="flex justify-between w-full mt-3"> <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 <button
class="btn"
@click="handleClick(selectedPlaylist)"
ref="addButton" ref="addButton"
v-t="'actions.add_to_playlist'" v-t="'actions.add_to_playlist'"
class="btn"
@click="handleClick(selectedPlaylist)"
/> />
</div> </div>
</ModalComponent> </ModalComponent>
@ -33,6 +33,7 @@ export default {
required: true, required: true,
}, },
}, },
emits: ["close"],
data() { data() {
return { return {
playlists: [], playlists: [],

View File

@ -6,7 +6,7 @@
</div> </div>
<p> <p>
<span v-text="props.item.name" /> <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> </p>
</router-link> </router-link>
<p v-if="props.item.description" v-text="props.item.description" /> <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"> <router-link v-if="props.item.uploaderUrl" class="link" :to="props.item.uploaderUrl">
<p> <p>
<span v-text="props.item.uploaderName" /> <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> </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="link" v-text="props.item.uploaderName" />
@ -30,6 +30,9 @@
<script setup> <script setup>
const props = defineProps({ const props = defineProps({
item: Object, item: {
type: Object,
required: true,
},
}); });
</script> </script>

View File

@ -1,7 +1,7 @@
<template> <template>
<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 :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" /> <h1 class="ml-1 mb-1 mt-4 text-3xl!" v-text="playlist.name" />
<CollapsableText :text="playlist.description" /> <CollapsableText :text="playlist.description" />
@ -14,12 +14,12 @@
</router-link> </router-link>
</div> </div>
<div> <div>
<strong v-text="`${playlist.videos} ${$t('video.videos')}`" class="mr-2" /> <strong class="mr-2" v-text="`${playlist.videos} ${$t('video.videos')}`" />
<button class="btn mx-1" v-if="!isPipedPlaylist" @click="bookmarkPlaylist"> <button v-if="!isPipedPlaylist" class="btn mx-1" @click="bookmarkPlaylist">
{{ $t(`actions.${isBookmarked ? "playlist_bookmarked" : "bookmark_playlist"}`) {{ $t(`actions.${isBookmarked ? "playlist_bookmarked" : "bookmark_playlist"}`)
}}<font-awesome-icon class="ml-3" icon="bookmark" /> }}<font-awesome-icon class="ml-3" icon="bookmark" />
</button> </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" /> {{ $t("actions.clone_playlist") }}<font-awesome-icon class="ml-3" icon="clone" />
</button> </button>
<button class="btn mr-1" @click="downloadPlaylistAsTxt"> <button class="btn mr-1" @click="downloadPlaylistAsTxt">
@ -28,7 +28,7 @@
<a class="btn mr-1" :href="getRssUrl"> <a class="btn mr-1" :href="getRssUrl">
<font-awesome-icon icon="rss" /> <font-awesome-icon icon="rss" />
</a> </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>
</div> </div>
@ -42,9 +42,9 @@
:index="index" :index="index"
:playlist-id="$route.query.list" :playlist-id="$route.query.list"
:admin="admin" :admin="admin"
@remove="removeVideo(index)"
height="94" height="94"
width="168" width="168"
@remove="removeVideo(index)"
/> />
</div> </div>
</LoadingIndicatorPage> </LoadingIndicatorPage>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="overflow-x-scroll h-screen-sm" ref="scrollable"> <div ref="scrollable" class="overflow-x-scroll h-screen-sm">
<VideoItem <VideoItem
v-for="(related, index) in playlist.relatedStreams" v-for="(related, index) in playlist.relatedStreams"
:key="related.url" :key="related.url"
@ -28,6 +28,18 @@ export default {
}, },
selectedIndex: { selectedIndex: {
type: Number, type: Number,
required: true,
},
},
watch: {
playlist: {
handler() {
if (this.selectedIndex - 1 < this.playlist.relatedStreams.length)
nextTick(() => {
this.updateScroll();
});
},
deep: true,
}, },
}, },
mounted() { mounted() {
@ -43,16 +55,5 @@ export default {
elems[this.selectedIndex - 1].offsetTop - this.$refs.scrollable.offsetTop; 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> </script>

View File

@ -1,24 +1,19 @@
<template> <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"> <div class="flex justify-between mb-3">
<button v-t="'actions.create_playlist'" class="btn" @click="onCreatePlaylist" /> <button v-t="'actions.create_playlist'" class="btn" @click="onCreatePlaylist" />
<div class="flex"> <div class="flex">
<button <button v-if="playlists.length > 0" v-t="'actions.export_to_json'" class="btn" @click="exportPlaylists" />
v-if="this.playlists.length > 0"
v-t="'actions.export_to_json'"
class="btn"
@click="exportPlaylists"
/>
<input <input
id="fileSelector" id="fileSelector"
ref="fileSelector" ref="fileSelector"
type="file" type="file"
class="display-none" class="display-none"
@change="importPlaylists"
multiple="multiple" 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>
</div> </div>
@ -39,24 +34,24 @@
v-text="playlist.name" v-text="playlist.name"
/> />
</router-link> </router-link>
<button class="btn h-auto" @click="showPlaylistEditModal(playlist)" v-t="'actions.edit_playlist'" /> <button v-t="'actions.edit_playlist'" class="btn h-auto" @click="showPlaylistEditModal(playlist)" />
<button class="btn h-auto ml-2" @click="playlistToDelete = playlist.id" v-t="'actions.delete_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"> <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"
class="input" class="input"
type="text" type="text"
v-model="newPlaylistName"
:placeholder="$t('actions.playlist_name')" :placeholder="$t('actions.playlist_name')"
/> />
<input <input
v-model="newPlaylistDescription"
class="input" class="input"
type="text" type="text"
v-model="newPlaylistDescription"
:placeholder="$t('actions.playlist_description')" :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> </div>
</ModalComponent> </ModalComponent>
<ConfirmModal <ConfirmModal
@ -69,7 +64,7 @@
</div> </div>
<hr /> <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"> <div v-if="bookmarks" class="video-grid">
<router-link <router-link
@ -104,6 +99,7 @@ import ConfirmModal from "./ConfirmModal.vue";
import ModalComponent from "./ModalComponent.vue"; import ModalComponent from "./ModalComponent.vue";
export default { export default {
components: { ConfirmModal, ModalComponent },
data() { data() {
return { return {
playlists: [], playlists: [],
@ -250,6 +246,5 @@ export default {
this.bookmarks.splice(index, 1); this.bookmarks.splice(index, 1);
}, },
}, },
components: { ConfirmModal, ModalComponent },
}; };
</script> </script>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="flex"> <div class="flex">
<button @click="$router.go(-1) || $router.push('/')"> <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> </button>
</div> </div>
<h1 v-t="'titles.preferences'" class="font-bold text-center" /> <h1 v-t="'titles.preferences'" class="font-bold text-center" />
@ -34,7 +34,7 @@
</select> </select>
</label> </label>
<h2 class="text-center" v-t="'titles.player'" /> <h2 v-t="'titles.player'" class="text-center" />
<label class="pref" for="chkAutoPlayVideo"> <label class="pref" for="chkAutoPlayVideo">
<strong v-t="'actions.autoplay_video'" /> <strong v-t="'actions.autoplay_video'" />
<input <input
@ -223,7 +223,7 @@
/> />
</label> </label>
<div v-if="sponsorBlock"> <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" /> <strong v-t="item.label" />
<select :id="'ddlSkip_' + name" v-model="item.value" class="select w-auto" @change="onChange($event)"> <select :id="'ddlSkip_' + name" v-model="item.value" class="select w-auto" @change="onChange($event)">
<option v-t="'actions.no'" value="no" /> <option v-t="'actions.no'" value="no" />
@ -253,7 +253,7 @@
</label> </label>
</div> </div>
<h2 class="text-center" v-t="'titles.dearrow'" /> <h2 v-t="'titles.dearrow'" class="text-center" />
<p 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> <span v-t="'actions.uses_api_from'" /><a class="link" href="https://sponsor.ajay.app/">sponsor.ajay.app</a>
</p> </p>
@ -262,7 +262,7 @@
<input id="chkDeArrow" v-model="dearrow" class="checkbox" type="checkbox" @change="onChange($event)" /> <input id="chkDeArrow" v-model="dearrow" class="checkbox" type="checkbox" @change="onChange($event)" />
</label> </label>
<h2 class="text-center" v-t="'titles.instance'" /> <h2 v-t="'titles.instance'" class="text-center" />
<label class="pref" for="ddlInstanceSelection"> <label class="pref" for="ddlInstanceSelection">
<strong v-text="`${$t('actions.instance_selection')}:`" /> <strong v-text="`${$t('actions.instance_selection')}:`" />
<select id="ddlInstanceSelection" v-model="selectedInstance" class="select w-auto" @change="onChange($event)"> <select id="ddlInstanceSelection" v-model="selectedInstance" class="select w-auto" @change="onChange($event)">
@ -305,8 +305,8 @@
<br /> <br />
<!-- options that are visible only when logged in --> <!-- options that are visible only when logged in -->
<div v-if="this.authenticated"> <div v-if="authenticated">
<h2 class="text-center" v-t="'titles.account'"></h2> <h2 v-t="'titles.account'" class="text-center"></h2>
<label class="pref" for="txtDeleteAccountPassword"> <label class="pref" for="txtDeleteAccountPassword">
<strong v-t="'actions.delete_account'" /> <strong v-t="'actions.delete_account'" />
<div class="flex items-center"> <div class="flex items-center">
@ -314,22 +314,22 @@
id="txtDeleteAccountPassword" id="txtDeleteAccountPassword"
ref="txtDeleteAccountPassword" ref="txtDeleteAccountPassword"
v-model="password" v-model="password"
v-on:keyup.enter="deleteAccount"
:placeholder="$t('login.password')" :placeholder="$t('login.password')"
:aria-label="$t('login.password')" :aria-label="$t('login.password')"
class="input w-auto mr-2" class="input w-auto mr-2"
type="password" 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> </div>
</label> </label>
<div class="pref"> <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 <a
v-t="'actions.invalidate_session'"
class="btn w-auto" class="btn w-auto"
style="margin-left: 0.5em" style="margin-left: 0.5em"
@click="invalidateSession" @click="invalidateSession"
v-t="'actions.invalidate_session'"
/> />
</div> </div>
<br /> <br />
@ -342,7 +342,7 @@
<th v-t="'preferences.instance_locations'" /> <th v-t="'preferences.instance_locations'" />
<th v-t="'preferences.has_cdn'" /> <th v-t="'preferences.has_cdn'" />
<th v-t="'preferences.registered_users'" /> <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.up_to_date'" />
<th v-t="'preferences.ssl_score'" /> <th v-t="'preferences.ssl_score'" />
</tr> </tr>
@ -356,7 +356,7 @@
<td class="lt-md:hidden" v-text="instance.version" /> <td class="lt-md:hidden" v-text="instance.version" />
<td v-text="`${instance.up_to_date ? '&#9989;' : '&#10060;'}`" /> <td v-text="`${instance.up_to_date ? '&#9989;' : '&#10060;'}`" />
<td> <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> </td>
</tr> </tr>
</tbody> </tbody>
@ -364,15 +364,15 @@
<br /> <br />
<p v-t="'info.preferences_note'" /> <p v-t="'info.preferences_note'" />
<br /> <br />
<button class="btn" v-t="'actions.reset_preferences'" @click="showConfirmResetPrefsDialog = true" /> <button v-t="'actions.reset_preferences'" class="btn" @click="showConfirmResetPrefsDialog = true" />
<button class="btn mx-4" v-t="'actions.backup_preferences'" @click="backupPreferences()" /> <button v-t="'actions.backup_preferences'" class="btn mx-4" @click="backupPreferences()" />
<label for="fileSelector" class="btn" v-t="'actions.restore_preferences'" @click="restorePreferences()" /> <label v-t="'actions.restore_preferences'" for="fileSelector" class="btn" @click="restorePreferences()" />
<input class="hidden" id="fileSelector" ref="fileSelector" type="file" @change="restorePreferences()" /> <input id="fileSelector" ref="fileSelector" class="hidden" type="file" @change="restorePreferences()" />
<ConfirmModal <ConfirmModal
v-if="showConfirmResetPrefsDialog" v-if="showConfirmResetPrefsDialog"
:message="$t('actions.confirm_reset_preferences')"
@close="showConfirmResetPrefsDialog = false" @close="showConfirmResetPrefsDialog = false"
@confirm="resetPreferences()" @confirm="resetPreferences()"
:message="$t('actions.confirm_reset_preferences')"
/> />
</template> </template>
@ -380,6 +380,9 @@
import CountryMap from "@/utils/CountryMaps/en.json"; import CountryMap from "@/utils/CountryMaps/en.json";
import ConfirmModal from "./ConfirmModal.vue"; import ConfirmModal from "./ConfirmModal.vue";
export default { export default {
components: {
ConfirmModal,
},
data() { data() {
return { return {
mobileChapterLayout: "Vertical", mobileChapterLayout: "Vertical",
@ -670,9 +673,6 @@ export default {
}); });
}, },
}, },
components: {
ConfirmModal,
},
}; };
</script> </script>

View File

@ -11,7 +11,7 @@
autocomplete="username" autocomplete="username"
:placeholder="$t('login.username')" :placeholder="$t('login.username')"
:aria-label="$t('login.username')" :aria-label="$t('login.username')"
v-on:keyup.enter="register" @keyup.enter="register"
/> />
</div> </div>
<div> <div>
@ -22,23 +22,23 @@
autocomplete="password" autocomplete="password"
:placeholder="$t('login.password')" :placeholder="$t('login.password')"
:aria-label="$t('login.password')" :aria-label="$t('login.password')"
v-on:keyup.enter="register" @keyup.enter="register"
/> />
</div> </div>
<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> </div>
</form> </form>
</div> </div>
<ConfirmModal <ConfirmModal
v-if="showUnsecureRegisterDialog" v-if="showUnsecureRegisterDialog"
:message="$t('info.register_no_email_note')"
@close="showUnsecureRegisterDialog = false" @close="showUnsecureRegisterDialog = false"
@confirm=" @confirm="
forceUnsecureRegister = true; forceUnsecureRegister = true;
showUnsecureRegisterDialog = false; showUnsecureRegisterDialog = false;
register(); register();
" "
:message="$t('info.register_no_email_note')"
/> />
</template> </template>
@ -47,6 +47,7 @@ import { isEmail } from "../utils/Misc.js";
import ConfirmModal from "./ConfirmModal.vue"; import ConfirmModal from "./ConfirmModal.vue";
export default { export default {
components: { ConfirmModal },
data() { data() {
return { return {
username: null, username: null,
@ -85,6 +86,5 @@ export default {
}); });
}, },
}, },
components: { ConfirmModal },
}; };
</script> </script>

View File

@ -5,7 +5,7 @@
<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="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> </select>
<hr /> <hr />

View File

@ -3,26 +3,26 @@
<h2 v-t="'actions.share'" /> <h2 v-t="'actions.share'" />
<div class="flex justify-between"> <div class="flex justify-between">
<label v-t="'actions.piped_link'" /> <label v-t="'actions.piped_link'" />
<input type="checkbox" v-model="pipedLink" @change="onChange" /> <input v-model="pipedLink" type="checkbox" @change="onChange" />
</div> </div>
<div v-if="this.hasPlaylist" class="flex justify-between"> <div v-if="hasPlaylist" class="flex justify-between">
<label v-t="'actions.with_playlist'" /> <label v-t="'actions.with_playlist'" />
<input type="checkbox" v-model="withPlaylist" @change="onChange" /> <input v-model="withPlaylist" type="checkbox" @change="onChange" />
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<label v-t="'actions.with_timecode'" for="withTimeCode" /> <label v-t="'actions.with_timecode'" for="withTimeCode" />
<input id="withTimeCode" type="checkbox" v-model="withTimeCode" @change="onChange" /> <input id="withTimeCode" v-model="withTimeCode" type="checkbox" @change="onChange" />
</div> </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'" /> <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> </div>
<a :href="generatedLink" target="_blank"> <a :href="generatedLink" target="_blank">
<h3 class="mt-4" v-text="generatedLink" /> <h3 class="mt-4" v-text="generatedLink" />
</a> </a>
<div class="flex justify-end mt-4"> <div class="flex justify-end mt-4">
<button class="btn" v-t="'actions.follow_link'" @click="followLink()" /> <button v-t="'actions.follow_link'" class="btn" @click="followLink()" />
<button class="btn ml-3" v-t="'actions.copy_link'" @click="copyLink()" /> <button v-t="'actions.copy_link'" class="btn ml-3" @click="copyLink()" />
</div> </div>
</ModalComponent> </ModalComponent>
</template> </template>
@ -31,6 +31,9 @@
import ModalComponent from "./ModalComponent.vue"; import ModalComponent from "./ModalComponent.vue";
export default { export default {
components: {
ModalComponent,
},
props: { props: {
videoId: { videoId: {
type: String, type: String,
@ -42,14 +45,13 @@ export default {
}, },
playlistId: { playlistId: {
type: String, type: String,
default: undefined,
}, },
playlistIndex: { playlistIndex: {
type: Number, type: Number,
default: undefined,
}, },
}, },
components: {
ModalComponent,
},
data() { data() {
return { return {
withTimeCode: true, withTimeCode: true,
@ -59,6 +61,20 @@ export default {
hasPlaylist: false, 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() { mounted() {
this.timeStamp = parseInt(this.currentTime); this.timeStamp = parseInt(this.currentTime);
this.withTimeCode = this.getPreferenceBoolean("shareWithTimeCode", true); this.withTimeCode = this.getPreferenceBoolean("shareWithTimeCode", true);
@ -87,19 +103,5 @@ export default {
this.setPreference("shareWithPlaylist", this.withPlaylist, true); 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> </script>

View File

@ -1,7 +1,7 @@
<template> <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"> <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> </select>
</template> </template>
@ -18,7 +18,10 @@ const options = {
const selectedSort = ref("descending"); const selectedSort = ref("descending");
const props = defineProps({ const props = defineProps({
byKey: String, byKey: {
type: String,
required: true,
},
}); });
const emit = defineEmits(["apply"]); const emit = defineEmits(["apply"]);

View File

@ -1,12 +1,12 @@
<template> <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 --> <!-- import / export section -->
<div class="flex justify-between w-full"> <div class="flex justify-between w-full">
<div class="flex"> <div class="flex">
<button class="btn mx-1"> <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>
<button class="btn" @click="exportHandler" v-t="'actions.export_to_json'" /> <button v-t="'actions.export_to_json'" class="btn" @click="exportHandler" />
</div> </div>
<!-- subscriptions count, only shown if there are any --> <!-- subscriptions count, only shown if there are any -->
<i18n-t v-if="subscriptions.length > 0" keypath="subscriptions.subscribed_channels_count">{{ <i18n-t v-if="subscriptions.length > 0" keypath="subscriptions.subscribed_channels_count">{{
@ -18,9 +18,9 @@
<div class="w-full flex flex-wrap"> <div class="w-full flex flex-wrap">
<button <button
v-for="group in channelGroups" v-for="group in channelGroups"
:key="group.groupName"
class="btn mx-1 w-max" class="btn mx-1 w-max"
:class="{ selected: selectedGroup === group }" :class="{ selected: selectedGroup === group }"
:key="group.groupName"
@click="selectedGroup = group" @click="selectedGroup = group"
> >
<span v-text="group.groupName !== '' ? group.groupName : $t('video.all')" /> <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"> <div class="xl:grid xl:grid-cols-5 <md:flex-wrap">
<!-- channel info card --> <!-- channel info card -->
<div <div
class="col m-2 p-1 border rounded-lg border-gray-500"
v-for="subscription in filteredSubscriptions" v-for="subscription in filteredSubscriptions"
:key="subscription.url" :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"> <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" /> <img :src="subscription.avatar" class="rounded-full h-[fit-content]" width="48" height="48" />
@ -49,9 +49,9 @@
</router-link> </router-link>
<!-- subscribe / unsubscribe btn --> <!-- subscribe / unsubscribe btn -->
<button <button
v-t="`actions.${subscription.subscribed ? 'unsubscribe' : 'subscribe'}`"
class="btn w-full mt-2" class="btn w-full mt-2"
@click="handleButton(subscription)" @click="handleButton(subscription)"
v-t="`actions.${subscription.subscribed ? 'unsubscribe' : 'subscribe'}`"
/> />
</div> </div>
</div> </div>
@ -60,8 +60,8 @@
<ModalComponent v-if="showCreateGroupModal" @close="showCreateGroupModal = !showCreateGroupModal"> <ModalComponent v-if="showCreateGroupModal" @close="showCreateGroupModal = !showCreateGroupModal">
<h2 v-t="'actions.create_group'" /> <h2 v-t="'actions.create_group'" />
<div class="flex flex-col"> <div class="flex flex-col">
<input class="input my-4" type="text" v-model="newGroupName" :placeholder="$t('actions.group_name')" /> <input v-model="newGroupName" class="input my-4" type="text" :placeholder="$t('actions.group_name')" />
<button class="ml-auto btn w-max" v-t="'actions.create_group'" @click="createGroup()" /> <button v-t="'actions.create_group'" class="ml-auto btn w-max" @click="createGroup()" />
</div> </div>
</ModalComponent> </ModalComponent>
@ -88,6 +88,7 @@
import ModalComponent from "./ModalComponent.vue"; import ModalComponent from "./ModalComponent.vue";
export default { export default {
components: { ModalComponent },
data() { data() {
return { return {
subscriptions: [], subscriptions: [],
@ -101,6 +102,13 @@ export default {
newGroupName: "", newGroupName: "",
}; };
}, },
computed: {
filteredSubscriptions(_this) {
return _this.selectedGroup.groupName == ""
? _this.subscriptions
: _this.subscriptions.filter(channel => _this.selectedGroup.channels.includes(channel.url.substr(-11)));
},
},
mounted() { mounted() {
this.fetchSubscriptions().then(json => { this.fetchSubscriptions().then(json => {
this.subscriptions = json; this.subscriptions = json;
@ -201,14 +209,6 @@ export default {
this.createOrUpdateChannelGroup(this.selectedGroup); 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> </script>

View File

@ -1,12 +1,13 @@
<template> <template>
<div class="toast"> <div class="toast">
<slot /> <slot />
<button @click="dismiss" v-t="'actions.dismiss'" /> <button v-t="'actions.dismiss'" @click="dismiss" />
</div> </div>
</template> </template>
<script> <script>
export default { export default {
emits: ["dismissed"],
methods: { methods: {
dismiss() { dismiss() {
this.$emit("dismissed"); this.$emit("dismissed");

View File

@ -1,5 +1,5 @@
<template> <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 <router-link
class="focus:underline hover:underline inline-block w-full" class="focus:underline hover:underline inline-block w-full"
:to="{ :to="{
@ -22,8 +22,8 @@
<!-- progress bar --> <!-- progress bar -->
<div class="relative w-full h-1"> <div class="relative w-full h-1">
<div <div
class="absolute bottom-0 left-0 h-1 bg-red-600"
v-if="item.watched && item.duration > 0" 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%` }" :style="{ width: `clamp(0%, ${(item.currentTime / item.duration) * 100}%, 100%` }"
/> />
</div> </div>
@ -31,21 +31,21 @@
<div class="relative text-sm"> <div class="relative text-sm">
<span <span
class="thumbnail-overlay thumbnail-right"
v-if="item.duration > 0" v-if="item.duration > 0"
class="thumbnail-overlay thumbnail-right"
v-text="timeFormat(item.duration)" v-text="timeFormat(item.duration)"
/> />
<!-- shorts thumbnail --> <!-- 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 <span
class="thumbnail-overlay thumbnail-right"
v-else-if="item.duration >= 0" v-else-if="item.duration >= 0"
class="thumbnail-overlay thumbnail-right"
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 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']" /> <font-awesome-icon class="w-3" :icon="['fas', 'broadcast-tower']" />
</i18n-t> </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>
<div> <div>
@ -78,7 +78,7 @@
:title="item.uploaderName" :title="item.uploaderName"
> >
<span v-text="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> </router-link>
<div v-if="item.views >= 0 || item.uploadedDate" class="text-xs font-normal text-gray-300 mt-1"> <div v-if="item.views >= 0 || item.uploadedDate" class="text-xs font-normal text-gray-300 mt-1">
@ -112,17 +112,17 @@
</button> </button>
<button <button
v-if="admin" v-if="admin"
:title="$t('actions.remove_from_playlist')"
ref="removeButton" ref="removeButton"
:title="$t('actions.remove_from_playlist')"
@click="showConfirmRemove = true" @click="showConfirmRemove = true"
> >
<font-awesome-icon icon="circle-minus" /> <font-awesome-icon icon="circle-minus" />
</button> </button>
<ConfirmModal <ConfirmModal
v-if="showConfirmRemove" v-if="showConfirmRemove"
:message="$t('actions.delete_playlist_video_confirm')"
@close="showConfirmRemove = false" @close="showConfirmRemove = false"
@confirm="removeVideo(item.url.substr(-11))" @confirm="removeVideo(item.url.substr(-11))"
:message="$t('actions.delete_playlist_video_confirm')"
/> />
<PlaylistAddModal <PlaylistAddModal
v-if="showModal" v-if="showModal"
@ -135,17 +135,12 @@
</div> </div>
</template> </template>
<style>
.shorts-img {
@apply w-full object-contain;
}
</style>
<script> <script>
import PlaylistAddModal from "./PlaylistAddModal.vue"; import PlaylistAddModal from "./PlaylistAddModal.vue";
import ConfirmModal from "./ConfirmModal.vue"; import ConfirmModal from "./ConfirmModal.vue";
export default { export default {
components: { PlaylistAddModal, ConfirmModal },
props: { props: {
item: { item: {
type: Object, type: Object,
@ -164,6 +159,7 @@ export default {
playlistId: { type: String, default: null }, playlistId: { type: String, default: null },
admin: { type: Boolean, default: false }, admin: { type: Boolean, default: false },
}, },
emits: ["remove"],
data() { data() {
return { return {
showModal: false, showModal: false,
@ -171,9 +167,6 @@ export default {
showConfirmRemove: false, showConfirmRemove: false,
}; };
}, },
mounted() {
this.shouldShowVideo();
},
computed: { computed: {
title() { title() {
return this.item.dearrow?.titles[0]?.title ?? this.item.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; return this.item.dearrow?.thumbnails[0]?.thumbnail ?? this.item.thumbnail;
}, },
}, },
mounted() {
this.shouldShowVideo();
},
methods: { methods: {
removeVideo() { removeVideo() {
this.$refs.removeButton.disabled = true; this.$refs.removeButton.disabled = true;
@ -204,6 +200,11 @@ export default {
}; };
}, },
}, },
components: { PlaylistAddModal, ConfirmModal },
}; };
</script> </script>
<style>
.shorts-img {
@apply w-full object-contain;
}
</style>

View File

@ -7,12 +7,12 @@
> >
<video ref="videoEl" class="w-full" data-shaka-player :autoplay="shouldAutoPlay" :loop="selectedAutoLoop" /> <video ref="videoEl" class="w-full" data-shaka-player :autoplay="shouldAutoPlay" :loop="selectedAutoLoop" />
<span <span
ref="previewContainer"
id="preview-container" id="preview-container"
ref="previewContainer"
class="hidden flex-col absolute bottom-0 z-[2000] mb-[3.5%] items-center" class="hidden flex-col absolute bottom-0 z-[2000] mb-[3.5%] items-center"
> >
<canvas ref="preview" id="preview" class="rounded-sm" /> <canvas id="preview" ref="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" /> <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> </span>
<button <button
v-if="inSegment" v-if="inSegment"
@ -57,7 +57,7 @@ export default {
selectedAutoLoop: Boolean, selectedAutoLoop: Boolean,
isEmbed: Boolean, isEmbed: Boolean,
}, },
emits: ["timeupdate"], emits: ["timeupdate", "ended"],
data() { data() {
return { return {
lastUpdate: new Date().getTime(), lastUpdate: new Date().getTime(),

View File

@ -1,7 +1,10 @@
<script> <script>
export default { export default {
props: { props: {
link: String, link: {
type: String,
required: true,
},
platform: { platform: {
type: String, type: String,
required: false, required: false,
@ -12,7 +15,7 @@ export default {
</script> </script>
<template> <template>
<template v-if="this.getPreferenceBoolean('showWatchOnYouTube', false)"> <template v-if="getPreferenceBoolean('showWatchOnYouTube', false)">
<!-- For large screens --> <!-- For large screens -->
<a :href="link" class="btn lt-lg:hidden flex items-center"> <a :href="link" class="btn lt-lg:hidden flex items-center">
<i18n-t keypath="player.watch_on" tag="strong">{{ platform }}</i18n-t> <i18n-t keypath="player.watch_on" tag="strong">{{ platform }}</i18n-t>

View File

@ -32,8 +32,8 @@
/> />
</keep-alive> </keep-alive>
<ChaptersBar <ChaptersBar
:mobileLayout="isMobile"
v-if="video?.chapters?.length > 0 && showChapters" v-if="video?.chapters?.length > 0 && showChapters"
:mobile-layout="isMobile"
:chapters="video.chapters" :chapters="video.chapters"
:player-position="currentTime" :player-position="currentTime"
@seek="navigate" @seek="navigate"
@ -76,7 +76,7 @@
video.uploader video.uploader
}}</router-link> }}</router-link>
<!-- Verified Badge --> <!-- 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> </div>
<PlaylistAddModal <PlaylistAddModal
v-if="showModal" v-if="showModal"
@ -98,20 +98,20 @@
{{ $t("actions.add_to_playlist") }}<font-awesome-icon class="ml-1" icon="circle-plus" /> {{ $t("actions.add_to_playlist") }}<font-awesome-icon class="ml-1" icon="circle-plus" />
</button> </button>
<button <button
class="btn"
@click="subscribeHandler"
v-t="{ v-t="{
path: `actions.${subscribed ? 'unsubscribe' : 'subscribe'}`, path: `actions.${subscribed ? 'unsubscribe' : 'subscribe'}`,
args: { count: numberFormat(video.uploaderSubscriberCount) }, args: { count: numberFormat(video.uploaderSubscriberCount) },
}" }"
class="btn"
@click="subscribeHandler"
/> />
<div class="flex flex-wrap gap-1"> <div class="flex flex-wrap gap-1">
<!-- RSS Feed button --> <!-- RSS Feed button -->
<a <a
v-if="video.uploaderUrl"
aria-label="RSS feed" aria-label="RSS feed"
title="RSS feed" title="RSS feed"
role="button" role="button"
v-if="video.uploaderUrl"
:href="`${apiUrl()}/feed/unauthenticated/rss?channels=${video.uploaderUrl.split('/')[2]}`" :href="`${apiUrl()}/feed/unauthenticated/rss?channels=${video.uploaderUrl.split('/')[2]}`"
target="_blank" target="_blank"
class="btn flex items-center" class="btn flex items-center"
@ -147,14 +147,14 @@
<hr /> <hr />
<button <button
v-t="`actions.${showDesc ? 'minimize_description' : 'show_description'}`"
class="btn mb-2" class="btn mb-2"
@click="showDesc = !showDesc" @click="showDesc = !showDesc"
v-t="`actions.${showDesc ? 'minimize_description' : 'show_description'}`"
/> />
<span class="btn ml-2" v-show="video?.chapters?.length > 0"> <span v-show="video?.chapters?.length > 0" class="btn ml-2">
<input id="showChapters" type="checkbox" v-model="showChapters" /> <input id="showChapters" v-model="showChapters" type="checkbox" />
<label class="ml-2" for="showChapters" v-t="'actions.show_chapters'" /> <label v-t="'actions.show_chapters'" class="ml-2" for="showChapters" />
</span> </span>
<!-- eslint-disable-next-line vue/no-v-html --> <!-- eslint-disable-next-line vue/no-v-html -->
@ -192,10 +192,10 @@
</div> </div>
<div v-if="!showComments" class="xl:col-span-4 sm:col-span-3"></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"> <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>
<div v-else-if="comments.disabled" class="xl:col-span-4 sm:col-span-3"> <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>
<div v-else ref="comments" class="xl:col-span-4 sm:col-span-3"> <div v-else ref="comments" class="xl:col-span-4 sm:col-span-3">
<CommentItem <CommentItem
@ -215,9 +215,9 @@
:selected-index="index" :selected-index="index"
/> />
<a <a
v-t="`actions.${showRecs ? 'minimize_recommendations' : 'show_recommendations'}`"
class="btn mb-2" class="btn mb-2"
@click="showRecs = !showRecs" @click="showRecs = !showRecs"
v-t="`actions.${showRecs ? 'minimize_recommendations' : 'show_recommendations'}`"
/> />
<hr v-show="showRecs" /> <hr v-show="showRecs" />
<div v-show="showRecs"> <div v-show="showRecs">