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

View File

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

View File

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

View File

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

View File

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

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 -->
<div v-if="text" class="whitespace-pre-wrap py-2 mx-1">
<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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: [],

View File

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

View File

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

View File

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

View File

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

View File

@ -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 ? '&#9989;' : '&#10060;'}`" />
<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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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