Reuse actions.okay string

This commit is contained in:
Bnyro
2023-05-31 09:50:59 +02:00
56 changed files with 1034 additions and 844 deletions

View File

@@ -42,19 +42,7 @@
</div>
</div>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-if="channel.description" class="whitespace-pre-wrap py-2 mx-1">
<span v-if="fullDescription" v-html="purifyHTML(rewriteDescription(channel.description))" />
<span v-html="purifyHTML(rewriteDescription(channel.description.slice(0, 100)))" v-else />
<span v-if="channel.description.length > 100 && !fullDescription">...</span>
<button
v-if="channel.description.length > 100"
class="hover:underline font-semibold text-neutral-500 block whitespace-normal"
@click="fullDescription = !fullDescription"
>
[{{ fullDescription ? $t("actions.show_less") : $t("actions.show_more") }}]
</button>
</div>
<CollapsableText :text="channel.description" />
<WatchOnYouTubeButton :link="`https://youtube.com/channel/${this.channel.id}`" />
@@ -90,6 +78,7 @@ import ErrorHandler from "./ErrorHandler.vue";
import ContentItem from "./ContentItem.vue";
import WatchOnYouTubeButton from "./WatchOnYouTubeButton.vue";
import LoadingIndicatorPage from "./LoadingIndicatorPage.vue";
import CollapsableText from "./CollapsableText.vue";
export default {
components: {
@@ -97,6 +86,7 @@ export default {
ContentItem,
WatchOnYouTubeButton,
LoadingIndicatorPage,
CollapsableText,
},
data() {
return {
@@ -105,7 +95,6 @@ export default {
tabs: [],
selectedTab: 0,
contentItems: [],
fullDescription: false,
};
},
mounted() {

View File

@@ -0,0 +1,28 @@
<template>
<!-- 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="purifyHTML(rewriteDescription(text))" />
<span v-else v-html="purifyHTML(rewriteDescription(text.slice(0, 100)))" />
<span v-if="text.length > 100 && !showFullText">...</span>
<button
v-if="text.length > 100"
class="hover:underline font-semibold text-neutral-500 block whitespace-normal"
@click="showFullText = !showFullText"
>
[{{ showFullText ? $t("actions.show_less") : $t("actions.show_more") }}]
</button>
</div>
</template>
<script>
export default {
props: {
text: String,
},
data() {
return {
showFullText: false,
};
},
};
</script>

View File

@@ -4,7 +4,7 @@
<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.confirm'" @click="$emit('confirm')" />
<button class="btn" v-t="'actions.okay'" @click="$emit('confirm')" />
</div>
</div>
</ModalComponent>

View File

@@ -1,40 +1,49 @@
<template>
<h1 v-t="'titles.feed'" class="font-bold text-center my-4" />
<button class="btn mr-2" @click="exportHandler">
<router-link to="/subscriptions">Subscriptions</router-link>
</button>
<div class="flex flex-wrap md:items-center flex-col md:flex-row gap-2 children:(flex gap-1 items-center)">
<span>
<label for="filters">
<strong v-text="`${$t('actions.filter')}:`" />
</label>
<select
id="filters"
v-model="selectedFilter"
default="all"
class="select flex-grow"
@change="onFilterChange()"
>
<option v-for="filter in availableFilters" :key="filter" :value="filter" v-t="`video.${filter}`" />
</select>
</span>
<span>
<a :href="getRssUrl">
<span>
<label for="group-selector">
<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-for="group in channelGroups"
:key="group.groupName"
:value="group.groupName"
v-text="group.groupName"
/>
</select>
</span>
<span class="md:ml-auto">
<SortingSelector by-key="uploaded" @apply="order => videos.sort(order)" />
</span>
</div>
<hr />
<span class="flex gap-2">
<router-link class="btn" to="/subscriptions">Subscriptions</router-link>
<a :href="getRssUrl" class="btn">
<font-awesome-icon icon="rss" />
</a>
</span>
<label for="filters" class="ml-10 mr-2">
<strong v-text="`${$t('actions.filter')}:`" />
</label>
<select id="filters" v-model="selectedFilter" default="all" class="select w-auto" @change="onFilterChange()">
<option v-for="filter in availableFilters" :key="filter" :value="filter" v-t="`video.${filter}`" />
</select>
<label for="group-selector" class="ml-10 mr-2">
<strong v-text="`${$t('titles.channel_groups')}:`" />
</label>
<select id="group-selector" v-model="selectedGroupName" default="" class="select w-auto">
<option value="" v-t="`video.all`" />
<option
v-for="group in channelGroups"
:key="group.groupName"
:value="group.groupName"
v-text="group.groupName"
/>
</select>
<span class="md:float-right">
<SortingSelector by-key="uploaded" @apply="order => videos.sort(order)" />
</span>
<hr />
<LoadingIndicatorPage :show-content="videosStore != null" class="video-grid">

View File

@@ -1,14 +1,12 @@
<template>
<h1 class="font-bold text-center" v-t="'titles.history'" />
<div class="flex">
<div>
<button class="btn" v-t="'actions.clear_history'" @click="clearHistory" />
<div class="flex md:items-center gap-2 flex-col md:flex-row">
<button class="btn" v-t="'actions.clear_history'" @click="clearHistory" />
<button class="btn mx-3" v-t="'actions.export_to_json'" @click="exportHistory" />
</div>
<button class="btn" v-t="'actions.export_to_json'" @click="exportHistory" />
<div class="right-1">
<div class="ml-auto flex gap-1 items-center">
<SortingSelector by-key="watchedAt" @apply="order => videos.sort(order)" />
</div>
</div>

View File

@@ -45,13 +45,13 @@ export default {
}
.modal-container {
@apply w-min m-auto px-8 bg-white p-6 rounded-xl min-w-[20vw] relative;
@apply w-min m-auto bg-white p-5 rounded-xl min-w-[20vw] relative;
}
.dark .modal-container {
@apply bg-dark-700;
}
.modal-container > button {
@apply absolute right-8 top-6;
@apply absolute right-2.5 top-1;
}
</style>

View File

@@ -59,35 +59,41 @@
</ul>
</nav>
<!-- navigation bar for mobile devices -->
<ul
<div
v-if="showTopNav"
class="flex flex-col justify-center items-end mb-4 children:(my-0.5 mr-5)"
@click="showTopNav = false"
class="mobile-nav flex flex-col mb-4 children:(p-1 w-full border-b border-dark-100 flex items-center gap-1)"
>
<li v-if="shouldShowTrending">
<router-link v-t="'titles.trending'" to="/trending" />
</li>
<li>
<router-link v-t="'titles.preferences'" to="/preferences" />
</li>
<li v-if="shouldShowLogin">
<router-link v-t="'titles.login'" to="/login" />
</li>
<li v-if="shouldShowLogin">
<router-link v-t="'titles.register'" to="/register" />
</li>
<li v-if="shouldShowHistory">
<router-link v-t="'titles.history'" to="/history" />
</li>
<li>
<router-link v-t="'titles.playlists'" to="/playlists" />
</li>
<li v-if="!shouldShowTrending">
<router-link v-t="'titles.feed'" to="/feed" />
</li>
</ul>
<router-link v-if="shouldShowTrending" to="/trending">
<div class="i-fa6-solid:fire"></div>
<i18n-t keypath="titles.trending"></i18n-t>
</router-link>
<router-link to="/preferences">
<div class="i-fa6-solid:gear"></div>
<i18n-t keypath="titles.preferences"></i18n-t>
</router-link>
<router-link v-if="shouldShowLogin" to="/login">
<div class="i-fa6-solid:user"></div>
<i18n-t keypath="titles.login"></i18n-t>
</router-link>
<router-link v-if="shouldShowLogin" to="/register">
<div class="i-fa6-solid:user-plus"></div>
<i18n-t keypath="titles.register"></i18n-t>
</router-link>
<router-link v-if="shouldShowHistory" to="/history">
<div class="i-fa6-solid:clock-rotate-left"></div>
<i18n-t keypath="titles.history"></i18n-t>
</router-link>
<router-link to="/playlists">
<div class="i-fa6-solid:list"></div>
<i18n-t keypath="titles.playlists"></i18n-t>
</router-link>
<router-link v-if="!shouldShowTrending" to="/feed">
<div class="i-fa6-solid:play"></div>
<i18n-t keypath="titles.feed"></i18n-t>
</router-link>
</div>
<!-- search suggestions for mobile devices -->
<div class="mobile-search md:hidden mx-2 search-container">
<div class="w-full mb-2 md:hidden search-container">
<input
v-model="searchText"
class="input h-10 w-full"
@@ -189,8 +195,7 @@ export default {
@apply absolute right-3 cursor-pointer rounded-full bg-[#ccc] w-4 h-4 text-center text-black opacity-50 hover:(opacity-70) text-size-[13px];
line-height: 1.05;
}
.mobile-search {
width: calc(100% - 1rem);
@apply mx-2;
.mobile-nav div {
@apply mx-1;
}
</style>

View File

@@ -1,20 +1,21 @@
<template>
<ErrorHandler v-if="playlist && playlist.error" :message="playlist.message" :error="playlist.error" />
<LoadingIndicatorPage :show-content="playlist" v-show="!playlist.error">
<h1 class="text-center my-4" v-text="playlist.name" />
<LoadingIndicatorPage :show-content="playlist" v-show="!playlist?.error">
<h1 class="ml-1 mb-1 mt-4 text-3xl!" v-text="playlist.name" />
<div class="flex justify-between items-center">
<CollapsableText :text="playlist.description" />
<div class="flex justify-between items-center mt-1">
<div>
<router-link class="link" :to="playlist.uploaderUrl || '/'">
<router-link class="link flex items-center gap-3" :to="playlist.uploaderUrl || '/'">
<img :src="playlist.uploaderAvatar" loading="lazy" class="rounded-full" />
<strong v-text="playlist.uploader" />
</router-link>
</div>
<div>
<strong v-text="`${playlist.videos} ${$t('video.videos')}`" />
<br />
<button class="btn mr-1" v-if="!isPipedPlaylist" @click="bookmarkPlaylist">
<strong v-text="`${playlist.videos} ${$t('video.videos')}`" class="mr-2" />
<button class="btn mx-1" v-if="!isPipedPlaylist" @click="bookmarkPlaylist">
{{ $t(`actions.${isBookmarked ? "playlist_bookmarked" : "bookmark_playlist"}`)
}}<font-awesome-icon class="ml-3" icon="bookmark" />
</button>
@@ -52,6 +53,7 @@
<script>
import ErrorHandler from "./ErrorHandler.vue";
import LoadingIndicatorPage from "./LoadingIndicatorPage.vue";
import CollapsableText from "./CollapsableText.vue";
import VideoItem from "./VideoItem.vue";
import WatchOnYouTubeButton from "./WatchOnYouTubeButton.vue";
@@ -61,6 +63,7 @@ export default {
VideoItem,
WatchOnYouTubeButton,
LoadingIndicatorPage,
CollapsableText,
},
data() {
return {

View File

@@ -39,8 +39,26 @@
v-text="playlist.name"
/>
</router-link>
<button class="btn h-auto" @click="renamePlaylist(playlist.id)" v-t="'actions.rename_playlist'" />
<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'" />
<ModalComponent v-if="playlist.id == playlistToEdit" @close="playlistToEdit = null">
<div class="flex flex-col gap-2">
<h2 v-t="'actions.edit_playlist'" />
<input
class="input"
type="text"
v-model="newPlaylistName"
:placeholder="$t('actions.playlist_name')"
/>
<input
class="input"
type="text"
v-model="newPlaylistDescription"
:placeholder="$t('actions.playlist_description')"
/>
<button class="btn ml-auto" @click="editPlaylist(playlist)" v-t="'actions.okay'" />
</div>
</ModalComponent>
<ConfirmModal
v-if="playlistToDelete == playlist.id"
:message="$t('actions.delete_playlist_confirm')"
@@ -83,6 +101,7 @@
<script>
import ConfirmModal from "./ConfirmModal.vue";
import ModalComponent from "./ModalComponent.vue";
export default {
data() {
@@ -90,6 +109,9 @@ export default {
playlists: [],
bookmarks: [],
playlistToDelete: null,
playlistToEdit: null,
newPlaylistName: "",
newPlaylistDescription: "",
};
},
mounted() {
@@ -109,30 +131,48 @@ export default {
this.playlists = json;
});
},
renamePlaylist(id) {
const newName = prompt(this.$t("actions.new_playlist_name"));
if (!newName) return;
this.fetchJson(this.authApiUrl() + "/user/playlists/rename", null, {
method: "POST",
body: JSON.stringify({
playlistId: id,
newName: newName,
}),
headers: {
Authorization: this.getAuthToken(),
"Content-Type": "application/json",
},
}).then(json => {
if (json.error) alert(json.error);
else {
this.playlists.forEach((playlist, index) => {
if (playlist.id == id) {
this.playlists[index].name = newName;
return;
}
});
}
});
showPlaylistEditModal(playlist) {
this.newPlaylistName = playlist.name;
this.newPlaylistDescription = playlist.description;
this.playlistToEdit = playlist.id;
},
editPlaylist(selectedPlaylist) {
// save the new name and description since they could be overwritten during the http request
const newName = this.newPlaylistName;
const newDescription = this.newPlaylistDescription;
if (newName != selectedPlaylist.name) {
this.fetchJson(this.authApiUrl() + "/user/playlists/rename", null, {
method: "POST",
body: JSON.stringify({
playlistId: selectedPlaylist.id,
newName: newName,
}),
headers: {
Authorization: this.getAuthToken(),
"Content-Type": "application/json",
},
}).then(json => {
if (json.error) alert(json.error);
else selectedPlaylist.name = newName;
});
}
if (newDescription != selectedPlaylist.description) {
this.fetchJson(this.authApiUrl() + "/user/playlists/description", null, {
method: "PATCH",
body: JSON.stringify({
playlistId: selectedPlaylist.id,
description: newDescription,
}),
headers: {
Authorization: this.getAuthToken(),
"Content-Type": "application/json",
},
}).then(json => {
if (json.error) alert(json.error);
else selectedPlaylist.description = newDescription;
});
}
this.playlistToEdit = null;
},
deletePlaylist(id) {
this.fetchJson(this.authApiUrl() + "/user/playlists/delete", null, {
@@ -271,6 +311,6 @@ export default {
this.bookmarks.splice(index, 1);
},
},
components: { ConfirmModal },
components: { ConfirmModal, ModalComponent },
};
</script>

View File

@@ -82,7 +82,7 @@ export default {
<style>
.suggestions-container {
@apply left-1/2 translate-x-[-50%] transform-gpu max-w-3xl w-full box-border p-y-1.25 z-10 lt-md:max-w-[calc(100%-0.5rem)] bg-gray-300;
@apply left-1/2 translate-x-[-50%] transform-gpu max-w-3xl w-full box-border z-10 lt-md:max-w-[calc(100%-0.5rem)] bg-gray-300;
}
.dark .suggestions-container {
@@ -98,6 +98,6 @@ export default {
}
.suggestion {
@apply p-y-1;
@apply p-1;
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<label for="ddlSortBy" v-t="'actions.sort_by'" />
<select id="ddlSortBy" v-model="selectedSort" class="select w-auto">
<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" />
</select>
</template>

View File

@@ -43,13 +43,13 @@
<div class="font-bold mt-2 text-2xl break-words" v-text="video.title" />
<div class="flex flex-wrap mt-3 mb-3">
<!-- views / date -->
<div class="flex flex-auto children:ml-2">
<div class="flex flex-auto gap-2">
<span v-t="{ path: 'video.views', args: { views: addCommas(video.views) } }" />
<span> | </span>
<span v-text="uploadDate" />
</div>
<!-- Likes/dilikes -->
<div class="flex children:mr-2">
<div class="flex gap-2">
<template v-if="video.likes >= 0">
<div class="flex items-center">
<div class="i-fa6-solid:thumbs-up" />
@@ -68,7 +68,7 @@
</div>
</div>
<!-- Channel info & options flex container -->
<div class="flex">
<div class="flex flex-wrap gap-1">
<!-- Channel Image & Info -->
<div class="flex items-center">
<img :src="video.uploaderAvatar" alt="" loading="lazy" class="rounded-full" />
@@ -78,19 +78,6 @@
<!-- Verified Badge -->
<font-awesome-icon class="ml-1" v-if="video.uploaderVerified" icon="check" />
</div>
<div class="flex relative ml-auto children:mr-1 items-center">
<button class="btn" v-if="authenticated" @click="showModal = !showModal">
{{ $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) },
}"
/>
</div>
<PlaylistAddModal v-if="showModal" :video-id="getVideoId()" @close="showModal = !showModal" />
<ShareModal
v-if="showShareModal"
@@ -100,8 +87,20 @@
:playlist-index="index"
@close="showShareModal = !showShareModal"
/>
<div class="flex">
<div class="self-center children:mr-1 my-1">
<div class="flex flex-wrap gap-1 ml-auto">
<!-- Subscribe Button -->
<button class="btn flex items-center" v-if="authenticated" @click="showModal = !showModal">
{{ $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) },
}"
/>
<div class="flex flex-wrap gap-1">
<!-- RSS Feed button -->
<a
aria-label="RSS feed"
@@ -110,18 +109,22 @@
v-if="video.uploaderUrl"
:href="`${apiUrl()}/feed/unauthenticated/rss?channels=${video.uploaderUrl.split('/')[2]}`"
target="_blank"
class="btn flex-col"
class="btn flex items-center"
>
<font-awesome-icon icon="rss" />
<font-awesome-icon class="mx-1.5" icon="rss" />
</a>
<WatchOnYouTubeButton :link="`https://youtu.be/${getVideoId()}`" />
<!-- Share Dialog -->
<button class="btn" @click="showShareModal = !showShareModal">
<button class="btn flex items-center" @click="showShareModal = !showShareModal">
<i18n-t class="lt-lg:hidden" keypath="actions.share" tag="strong"></i18n-t>
<font-awesome-icon class="mx-1.5" icon="fa-share" />
</button>
<!-- LBRY -->
<a v-if="video.lbryId" :href="'https://odysee.com/' + video.lbryId" class="btn">
<a
v-if="video.lbryId"
:href="'https://odysee.com/' + video.lbryId"
class="btn flex items-center"
>
<i18n-t keypath="player.watch_on" tag="strong">LBRY</i18n-t>
</a>
<!-- listen / watch toggle -->
@@ -129,9 +132,9 @@
:to="toggleListenUrl"
:aria-label="(isListening ? 'Watch ' : 'Listen to ') + video.title"
:title="(isListening ? 'Watch ' : 'Listen to ') + video.title"
class="btn flex-col"
class="btn flex items-center"
>
<font-awesome-icon :icon="isListening ? 'tv' : 'headphones'" />
<font-awesome-icon class="mx-1.5" :icon="isListening ? 'tv' : 'headphones'" />
</router-link>
</div>
</div>