Merge pull request #683 from TeamPiped/windicss

Replace UIKit with Windicss
This commit is contained in:
Kavin 2022-01-12 22:19:14 +00:00 committed by GitHub
commit 46ad4d1d27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 2418 additions and 8388 deletions

View File

@ -1,18 +1,18 @@
name: Build and Lint name: Build and Lint
on: on:
pull_request: pull_request:
push: push:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2.4.0 - uses: actions/checkout@v2.4.0
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v2.5.1 uses: actions/setup-node@v2.5.1
with: with:
cache: "yarn" cache: "yarn"
- run: yarn install --prefer-offline - run: yarn install --prefer-offline
- run: yarn build - run: yarn build
- run: yarn lint --no-fix - run: yarn lint --no-fix

View File

@ -1,38 +1,38 @@
name: Docker Multi-Architecture Build name: Docker Multi-Architecture Build
on: on:
push: push:
paths-ignore: paths-ignore:
- "**.md" - "**.md"
branches: branches:
- master - master
jobs: jobs:
build-docker-image: build-docker-image:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2.4.0 - uses: actions/checkout@v2.4.0
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1.2.0 uses: docker/setup-qemu-action@v1.2.0
with: with:
platforms: all platforms: all
- name: Set up Docker Buildx - name: Set up Docker Buildx
id: buildx id: buildx
uses: docker/setup-buildx-action@v1.6.0 uses: docker/setup-buildx-action@v1.6.0
with: with:
version: latest version: latest
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1.12.0 uses: docker/login-action@v1.12.0
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v2.7.0 uses: docker/build-push-action@v2.7.0
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: true push: true
tags: 1337kavin/piped-frontend:latest tags: 1337kavin/piped-frontend:latest

View File

@ -15,9 +15,9 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v2.5.1 uses: actions/setup-node@v2.5.1
with: with:
cache: 'yarn' cache: "yarn"
- run: yarn install --prefer-offline - run: yarn install --prefer-offline
- run: yarn build && sed -i 's/fonts.gstatic.com/fonts.kavin.rocks/g' dist/css/*.css - run: yarn build && sed -i 's/fonts.gstatic.com/fonts.kavin.rocks/g' dist/assets/*.css
- uses: aquiladev/ipfs-action@v0.1.6 - uses: aquiladev/ipfs-action@v0.1.6
id: ipfs-add id: ipfs-add
with: with:

View File

@ -8,7 +8,7 @@ RUN yarn install --prefer-offline
COPY . . COPY . .
RUN yarn build && sed -i 's/fonts.gstatic.com/fonts.kavin.rocks/g' dist/css/*.css RUN yarn build && sed -i 's/fonts.gstatic.com/fonts.kavin.rocks/g' dist/assets/*.css
FROM nginx:alpine FROM nginx:alpine

28
index.html Normal file
View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html style="background: #0f0f0f" lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="/favicon.ico" />
<link title="Piped" type="application/opensearchdescription+xml" rel="search" href="/opensearch.xml" />
<title>Piped</title>
<meta property="og:title" content="Piped" />
<meta property="og:type" content="website" />
<meta property="og:image" content="/img/icons/favicon-32x32.png" />
<meta property="og:url" content="/" />
<meta
property="og:description"
content="An alternative privacy-friendly YouTube frontend which is efficient by design."
/>
</head>
<noscript>
<strong style="color: #fff"
>We're sorry but Piped doesn't work properly without JavaScript enabled. Please enable it to
continue.</strong
>
</noscript>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</html>

View File

@ -3,42 +3,42 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vite",
"build": "vue-cli-service build", "build": "vite build",
"lint": "vue-cli-service lint" "preview": "vite preview",
"lint": "eslint --fix --color --ignore-path .gitignore --ext .js,.vue ."
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.36", "@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.4", "@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/vue-fontawesome": "^3.0.0-5", "@fortawesome/vue-fontawesome": "^3.0.0-5",
"core-js": "3.20.1", "buffer": "^6.0.3",
"css-loader": "^6.5.1",
"dompurify": "^2.3.4", "dompurify": "^2.3.4",
"hotkeys-js": "^3.8.7", "hotkeys-js": "^3.8.7",
"javascript-time-ago": "^2.3.10", "javascript-time-ago": "^2.3.10",
"mux.js": "^6.0.1", "mux.js": "^6.0.1",
"register-service-worker": "^1.7.1",
"shaka-player": "3.3.0", "shaka-player": "3.3.0",
"uikit": "3.9.4", "stream": "^0.0.2",
"vue": "^3.2.26", "vue": "^3.2.26",
"vue-i18n": "^9.2.0-beta.25", "vue-i18n": "^9.2.0-beta.25",
"vue-router": "^4.0.12", "vue-router": "^4.0.12",
"xml-js": "^1.6.11" "xml-js": "^1.6.11"
}, },
"devDependencies": { "devDependencies": {
"@intlify/vue-i18n-loader": "^4.1.0", "@intlify/vite-plugin-vue-i18n": "^3.2.1",
"@vue/cli-plugin-babel": "^4.5.15", "@vitejs/plugin-vue": "^2.0.1",
"@vue/cli-plugin-eslint": "^4.5.15",
"@vue/cli-plugin-pwa": "^4.5.15",
"@vue/cli-service": "^4.5.15",
"@vue/compiler-sfc": "3.2.26", "@vue/compiler-sfc": "3.2.26",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^7.20.0", "eslint-plugin-vue": "^7.20.0",
"vue-cli-plugin-i18n": "^2.3.1" "prettier": "^2.5.1",
"vite": "^2.7.9",
"vite-plugin-eslint": "^1.3.0",
"vite-plugin-pwa": "^0.11.12",
"vite-plugin-windicss": "^1.6.1"
}, },
"eslintConfig": { "eslintConfig": {
"root": true, "root": true,

View File

@ -1,34 +0,0 @@
<!DOCTYPE html>
<html style="background: #0b0e0f" lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<link
title="Piped"
type="application/opensearchdescription+xml"
rel="search"
href="<%= BASE_URL %>opensearch.xml"
/>
<title><%= htmlWebpackPlugin.options.title %></title>
<meta property="og:title" content="Piped" />
<meta property="og:type" content="website" />
<meta property="og:image" content="<%= BASE_URL %>img/icons/favicon-32x32.png" />
<meta property="og:url" content="<%= BASE_URL %>" />
<meta
property="og:description"
content="An alternative privacy-friendly YouTube frontend which is efficient by design."
/>
</head>
<body>
<noscript>
<strong style="color: #fff"
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript
enabled. Please enable it to continue.</strong
>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@ -1,9 +1,5 @@
<template> <template>
<div <div class="w-full min-h-screen px-1vw reset" :class="[theme]">
class="uk-container uk-container-expand uk-height-viewport"
:style="[{ background: backgroundColor, colour: foregroundColor }]"
:class="{ 'uk-light': darkMode }"
>
<Navigation /> <Navigation />
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<keep-alive :max="5"> <keep-alive :max="5">
@ -11,21 +7,20 @@
</keep-alive> </keep-alive>
</router-view> </router-view>
<div style="text-align: center"> <footer class="text-center">
<a aria-label="GitHub" href="https://github.com/TeamPiped/Piped"> <a aria-label="GitHub" href="https://github.com/TeamPiped/Piped">
<font-awesome-icon :icon="['fab', 'github']"></font-awesome-icon> <font-awesome-icon :icon="['fab', 'github']" />
</a> </a>
&nbsp; <a class="ml-2" href="https://github.com/TeamPiped/Piped#donations">
<a href="https://github.com/TeamPiped/Piped#donations"> <font-awesome-icon :icon="['fab', 'bitcoin']" />
<font-awesome-icon :icon="['fab', 'bitcoin']"></font-awesome-icon> <span v-text="$t('actions.donations')" />
{{ $t("actions.donations") }}
</a> </a>
</div> </footer>
</div> </div>
</template> </template>
<script> <script>
import Navigation from "@/components/Navigation"; import Navigation from "@/components/Navigation.vue";
export default { export default {
components: { components: {
Navigation, Navigation,
@ -45,7 +40,7 @@ export default {
if (this.getPreferenceBoolean("watchHistory", false)) if (this.getPreferenceBoolean("watchHistory", false))
if ("indexedDB" in window) { if ("indexedDB" in window) {
const request = indexedDB.open("piped-db", 1); const request = indexedDB.open("piped-db", 1);
request.onupgradeneeded = function() { request.onupgradeneeded = function () {
const db = request.result; const db = request.result;
console.log("Upgrading object store."); console.log("Upgrading object store.");
if (!db.objectStoreNames.contains("watch_history")) { if (!db.objectStoreNames.contains("watch_history")) {
@ -61,18 +56,18 @@ export default {
const App = this; const App = this;
(async function() { (async function () {
const locale = App.getPreferenceString("hl", App.defaultLangage); const locale = App.getPreferenceString("hl", App.defaultLangage);
if (locale !== App.TimeAgoConfig.locale) { if (locale !== App.TimeAgoConfig.locale) {
const localeTime = await import("javascript-time-ago/locale/" + locale + ".json").then( const localeTime = await import(
module => module.default, "./../node_modules/javascript-time-ago/locale/" + locale + ".json"
); ).then(module => module.default);
App.TimeAgo.addLocale(localeTime); App.TimeAgo.addLocale(localeTime);
App.TimeAgoConfig.locale = locale; App.TimeAgoConfig.locale = locale;
} }
if (window.i18n.global.locale.value !== locale) { if (window.i18n.global.locale.value !== locale) {
if (!window.i18n.global.availableLocales.includes(locale)) { if (!window.i18n.global.availableLocales.includes(locale)) {
const messages = await import("@/locales/" + locale + ".json").then(module => module.default); const messages = await import(`./locales/${locale}.json`).then(module => module.default);
window.i18n.global.setLocaleMessage(locale, messages); window.i18n.global.setLocaleMessage(locale, messages);
} }
window.i18n.global.locale.value = locale; window.i18n.global.locale.value = locale;
@ -93,7 +88,6 @@ b {
::-webkit-scrollbar { ::-webkit-scrollbar {
background-color: #15191a; background-color: #15191a;
color: #c5bcae;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
@ -114,13 +108,114 @@ b {
* { * {
scrollbar-color: #15191a #444a4e; scrollbar-color: #15191a #444a4e;
@apply font-sans;
} }
.uk-grid > div { .video-grid {
padding-bottom: 1vh; @apply grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 col-auto <md:gap-x-2.5 md:gap-x-1vw gap-y-1.5 mx-3;
} }
.uk-button { .btn {
background: #222; @apply py-2 px-4 rounded;
}
.reset {
@apply text-black bg-white;
}
.auto {
@apply dark:(text-white bg-dark-900);
}
.dark {
@apply text-white bg-dark-900;
}
.input,
.select,
.btn {
@apply w-auto text-gray-600 bg-gray-300;
}
.input,
.select {
@apply h-8;
}
.btn {
@apply h-full;
}
.checkbox {
@apply h-4 w-4;
}
.dark .input,
.dark .select,
.dark .btn {
@apply text-gray-400 bg-dark-400;
}
.auto .input,
.auto .select,
.auto .btn {
@apply dark:(text-gray-400 bg-dark-400);
}
.input {
@apply pl-2.5;
}
hr {
@apply !mt-2 !mb-3 border-gray-300;
}
.dark hr {
@apply border-dark-100;
}
.auto hr {
@apply dark:border-dark-100;
}
h1,
h2 {
@apply m-0 font-bold;
}
h1 {
@apply !text-5xl;
}
h2 {
@apply !text-3xl;
}
.table {
@apply w-full text-lg text-left font-light border;
}
.link {
@apply hover:(text-dark-300 underline underline-dark-300);
}
.link-secondary {
@apply hover:(text-dark-400 underline underline-dark-400);
}
.dark .link {
@apply hover:(text-gray-300 underline underline-gray-300);
}
.auto .link {
@apply dark:hover:(text-gray-300 underline underline-gray-300);
}
.dark .link-secondary {
@apply text-gray-300 hover:(text-gray-400 underline underline-gray-400);
}
.auto .link-secondary {
@apply dark:(text-gray-300 hover:(text-gray-400 underline underline-gray-400));
} }
</style> </style>

View File

@ -2,27 +2,34 @@
<ErrorHandler v-if="channel && channel.error" :message="channel.message" :error="channel.error" /> <ErrorHandler v-if="channel && channel.error" :message="channel.message" :error="channel.error" />
<div v-if="channel" v-show="!channel.error"> <div v-if="channel" v-show="!channel.error">
<h1 class="uk-text-center"> <div class="flex justify-center place-items-center">
<img height="48" width="48" class="uk-border-circle" :src="channel.avatarUrl" />{{ channel.name }} <img height="48" width="48" class="rounded-full m-1" :src="channel.avatarUrl" />
</h1> <h1 v-text="channel.name" />
<img v-if="channel.bannerUrl" :src="channel.bannerUrl" style="width: 100%" loading="lazy" /> </div>
<img v-if="channel.bannerUrl" :src="channel.bannerUrl" class="w-full pb-1.5" loading="lazy" />
<!-- eslint-disable-next-line vue/no-v-html --> <!-- eslint-disable-next-line vue/no-v-html -->
<p style="white-space: pre-wrap"><span v-html="purifyHTML(urlify(channel.description))"></span></p> <p class="whitespace-pre-wrap">
<span v-html="purifyHTML(urlify(channel.description))" />
</p>
<button v-if="authenticated" class="uk-button uk-button-small" type="button" @click="subscribeHandler"> <button
{{ subscribed ? $t("actions.unsubscribe") : $t("actions.subscribe") }} v-if="authenticated"
</button> class="btn"
@click="subscribeHandler"
v-text="$t(`actions.${subscribed ? 'unsubscribe' : 'subscribe'}`)"
/>
<hr /> <hr />
<div class="uk-grid uk-grid-xl"> <div class="video-grid">
<div <VideoItem
v-for="video in channel.relatedStreams" v-for="video in channel.relatedStreams"
:key="video.url" :key="video.url"
class="uk-width-1-2 uk-width-1-3@m uk-width-1-4@l uk-width-1-5@xl" :video="video"
> height="94"
<VideoItem :video="video" height="94" width="168" hide-channel /> width="168"
</div> hide-channel
/>
</div> </div>
</div> </div>
</template> </template>

View File

@ -1,67 +1,53 @@
<template> <template>
<div class="comment uk-flex"> <div class="comment flex mt-1.5">
<img <img
:src="comment.thumbnail" :src="comment.thumbnail"
class="comment-avatar uk-border-circle uk-margin-right" class="comment-avatar rounded-full w-12 h-12"
height="48" height="48"
width="48" width="48"
style="width: 48px; height: 48px"
loading="lazy" loading="lazy"
alt="Avatar" alt="Avatar"
/> />
<div class="comment-content"> <div class="comment-content pl-2">
<div class="comment-header"> <div class="comment-header">
<div v-if="comment.pinned" class="comment-pinned uk-text-meta"> <div v-if="comment.pinned" class="comment-pinned">
<font-awesome-icon icon="thumbtack"></font-awesome-icon>&nbsp; {{ $t("comment.pinned_by") }} <font-awesome-icon icon="thumbtack" />
{{ uploader }} <span class="ml-1.5" v-text="$t('comment.pinned_by')" />
<span v-text="uploader" />
</div> </div>
<div class="comment-author"> <div class="comment-author">
<router-link class="uk-text-bold uk-text-small" :to="comment.commentorUrl"> <router-link class="font-bold link" :to="comment.commentorUrl" v-text="comment.author" />
{{ comment.author }} </router-link <font-awesome-icon class="ml-1.5" v-if="comment.verified" icon="check" />
>&thinsp;<font-awesome-icon v-if="comment.verified" icon="check"></font-awesome-icon>
</div>
<div class="comment-meta uk-text-meta uk-margin-small-bottom">
{{ comment.commentedTime }}
</div> </div>
<div class="comment-meta text-sm mb-1.5" v-text="comment.commentedTime" />
</div> </div>
<div class="comment-body" style="white-space: pre-wrap"> <div class="whitespace-pre-wrap" v-text="comment.commentText" />
{{ comment.commentText }} <div class="comment-footer mt-1">
</div> <font-awesome-icon icon="thumbs-up" />
<div class="comment-footer uk-margin-small-top uk-text-meta"> <span class="ml-1" v-text="numberFormat(comment.likeCount)" />
<font-awesome-icon icon="thumbs-up" style="margin-right: 4px"></font-awesome-icon> <font-awesome-icon class="ml-1" v-if="comment.hearted" icon="heart" />
<span>{{ numberFormat(comment.likeCount) }}</span>
&nbsp;
<font-awesome-icon v-if="comment.hearted" icon="heart"></font-awesome-icon>
</div> </div>
<template v-if="comment.repliesPage && (!loadingReplies || !showingReplies)"> <template v-if="comment.repliesPage && (!loadingReplies || !showingReplies)">
<div @click="loadReplies"> <div @click="loadReplies">
<a class="uk-link-text" v-t="'actions.show_replies'" /> <a v-t="'actions.show_replies'" />
&nbsp; <font-awesome-icon class="ml-1.5" icon="level-down-alt" />
<font-awesome-icon icon="level-down-alt" />
</div> </div>
</template> </template>
<template v-if="showingReplies"> <template v-if="showingReplies">
<div @click="hideReplies"> <div @click="hideReplies">
<a class="uk-link-text" v-t="'actions.hide_replies'" /> <a v-t="'actions.hide_replies'" />
&nbsp; <font-awesome-icon class="ml-1.5" icon="level-up-alt" />
<font-awesome-icon icon="level-up-alt" />
</div> </div>
</template> </template>
<div v-show="showingReplies" v-if="replies" class="replies uk-width-4-5@xl uk-width-3-4@s uk-width-1"> <div v-show="showingReplies" v-if="replies" class="replies">
<div <div v-for="reply in replies" :key="reply.commentId" class="w-full">
v-for="reply in replies"
:key="reply.commentId"
class="uk-tile-default uk-align-left uk-width-expand"
:style="[{ background: backgroundColor }]"
>
<Comment :comment="reply" :uploader="uploader" :video-id="videoId" /> <Comment :comment="reply" :uploader="uploader" :video-id="videoId" />
</div> </div>
<div v-if="nextpage" @click="loadReplies"> <div v-if="nextpage" @click="loadReplies">
<a class="uk-link-text" v-t="'actions.load_more_replies'" /> <a v-t="'actions.load_more_replies'" />
&nbsp; <font-awesome-icon class="ml-1.5" icon="level-down-alt" />
<font-awesome-icon icon="level-down-alt" />
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,9 +1,7 @@
<template> <template>
<p>{{ message }}</p> <p v-text="message" />
<button @click="toggleTrace" class="uk-button uk-button-small" type="button"> <button @click="toggleTrace" class="btn" v-text="$t('actions.show_more')" />
{{ $t("actions.show_more") }} <p ref="stacktrace" class="whitespace-pre-wrap" hidden v-text="error" />
</button>
<p ref="stacktrace" style="white-space: pre-wrap" hidden>{{ error }}</p>
</template> </template>
<script> <script>

View File

@ -1,50 +1,35 @@
<template> <template>
<h1 v-t="'titles.feed'" class="uk-text-bold uk-text-center" /> <h1 v-t="'titles.feed'" class="font-bold text-center" />
<button <button v-if="authenticated" class="btn mr-2" @click="exportHandler">
v-if="authenticated" <router-link to="/subscriptions">Subscriptions</router-link>
class="uk-button uk-button-small"
style="margin-right: 0.5rem"
type="button"
@click="exportHandler"
>
<router-link to="/subscriptions"> Subscriptions </router-link>
</button> </button>
<span> <span>
<a :href="getRssUrl"><font-awesome-icon icon="rss" style="padding-top: 0.2rem"></font-awesome-icon></a> <a :href="getRssUrl">
<font-awesome-icon icon="rss" />
</a>
</span> </span>
<span class="uk-align-right@m"> <span class="md:float-right">
<label for="ddlSortBy">{{ $t("actions.sort_by") }}</label> <Sorting by-key="uploaded" @apply="order => videos.sort(order)" />
<select id="ddlSortBy" v-model="selectedSort" class="uk-select uk-width-auto" @change="onChange()">
<option v-t="'actions.most_recent'" value="descending" />
<option v-t="'actions.least_recent'" value="ascending" />
<option v-t="'actions.channel_name_asc'" value="channel_ascending" />
<option v-t="'actions.channel_name_desc'" value="channel_descending" />
</select>
</span> </span>
<hr /> <hr />
<div class="uk-grid uk-grid-xl"> <div class="video-grid">
<div <VideoItem v-for="video in videos" :key="video.url" :video="video" />
v-for="video in videos"
:key="video.url"
:style="[{ background: backgroundColor }]"
class="uk-width-1-1 uk-width-1-3@s uk-width-1-4@m uk-width-1-5@l uk-width-1-6@xl"
>
<VideoItem :video="video" />
</div>
</div> </div>
</template> </template>
<script> <script>
import VideoItem from "@/components/VideoItem.vue"; import VideoItem from "@/components/VideoItem.vue";
import Sorting from "@/components/Sorting.vue";
export default { export default {
components: { components: {
VideoItem, VideoItem,
Sorting,
}, },
data() { data() {
return { return {
@ -52,7 +37,6 @@ export default {
videoStep: 100, videoStep: 100,
videosStore: [], videosStore: [],
videos: [], videos: [],
selectedSort: "descending",
}; };
}, },
computed: { computed: {
@ -84,22 +68,6 @@ export default {
authToken: this.getAuthToken(), authToken: this.getAuthToken(),
}); });
}, },
onChange() {
switch (this.selectedSort) {
case "ascending":
this.videos.sort((a, b) => a.uploaded - b.uploaded);
break;
case "descending":
this.videos.sort((a, b) => b.uploaded - a.uploaded);
break;
case "channel_ascending":
this.videos.sort((a, b) => a.uploaderName.localeCompare(b.uploaderName));
break;
case "channel_descending":
this.videos.sort((a, b) => b.uploaderName.localeCompare(a.uploaderName));
break;
}
},
loadMoreVideos() { loadMoreVideos() {
this.currentVideoCount = Math.min(this.currentVideoCount + this.videoStep, this.videosStore.length); this.currentVideoCount = Math.min(this.currentVideoCount + this.videoStep, this.videosStore.length);
if (this.videos.length != this.videosStore.length) if (this.videos.length != this.videosStore.length)

View File

@ -1,31 +1,20 @@
<template> <template>
<h1 class="uk-text-bold uk-text-center">{{ $t("titles.history") }}</h1> <h1 class="font-bold text-center" v-text="$t('titles.history')" />
<div style="text-align: left"> <div class="flex">
<button class="uk-button" v-t="'actions.clear_history'" @click="clearHistory"></button> <div>
</div> <button class="btn" v-t="'actions.clear_history'" @click="clearHistory" />
</div>
<div style="text-align: right"> <div class="right-1">
<label for="ddlSortBy">{{ $t("actions.sort_by") }}</label> <Sorting by-key="watchedAt" @apply="order => videos.sort(order)" />
<select id="ddlSortBy" v-model="selectedSort" class="uk-select uk-width-auto" @change="onChange()"> </div>
<option v-t="'actions.most_recent'" value="descending" />
<option v-t="'actions.least_recent'" value="ascending" />
<option v-t="'actions.channel_name_asc'" value="channel_ascending" />
<option v-t="'actions.channel_name_desc'" value="channel_descending" />
</select>
</div> </div>
<hr /> <hr />
<div class="uk-grid uk-grid-xl"> <div class="video-grid">
<div <VideoItem v-for="video in videos" :key="video.url" :video="video" />
v-for="video in videos"
:key="video.url"
:style="[{ background: backgroundColor }]"
class="uk-width-1-2 uk-width-1-3@s uk-width-1-4@m uk-width-1-5@l uk-width-1-6@xl"
>
<VideoItem :video="video" />
</div>
</div> </div>
<br /> <br />
@ -33,15 +22,16 @@
<script> <script>
import VideoItem from "@/components/VideoItem.vue"; import VideoItem from "@/components/VideoItem.vue";
import Sorting from "@/components/Sorting.vue";
export default { export default {
components: { components: {
VideoItem, VideoItem,
Sorting,
}, },
data() { data() {
return { return {
videos: [], videos: [],
selectedSort: "descending",
}; };
}, },
mounted() { mounted() {
@ -74,22 +64,6 @@ export default {
document.title = "Watch History - Piped"; document.title = "Watch History - Piped";
}, },
methods: { methods: {
onChange() {
switch (this.selectedSort) {
case "ascending":
this.videos.sort((a, b) => a.watchedAt - b.watchedAt);
break;
case "descending":
this.videos.sort((a, b) => b.watchedAt - a.watchedAt);
break;
case "channel_ascending":
this.videos.sort((a, b) => a.uploaderName.localeCompare(b.uploaderName));
break;
case "channel_descending":
this.videos.sort((a, b) => b.uploaderName.localeCompare(a.uploaderName));
break;
}
},
clearHistory() { clearHistory() {
if (window.db) { if (window.db) {
var tx = window.db.transaction("watch_history", "readwrite"); var tx = window.db.transaction("watch_history", "readwrite");

View File

@ -1,21 +1,21 @@
<template> <template>
<div class="uk-vertical-align uk-text-center uk-height-1-1"> <div class="text-center">
<form class="uk-panel uk-panel-box"> <form>
<div class="uk-form-row"> <div>
<input ref="fileSelector" type="file" @change="fileChange" /> <input ref="fileSelector" type="file" @change="fileChange" />
</div> </div>
<div class="uk-form-row"> <div>
<b>Selected Subscriptions: {{ selectedSubscriptions }}</b> <strong v-text="`Selected Subscriptions: ${selectedSubscriptions}`" />
</div> </div>
<div class="uk-form-row"> <div>
<b>Override: <input v-model="override" class="uk-checkbox" type="checkbox"/></b> <strong>Override: <input v-model="override" class="checkbox" type="checkbox" /></strong>
</div> </div>
<div class="uk-form-row"> <div>
<a class="uk-width-1-1 uk-button uk-button-large uk-width-auto" @click="handleImport">Import</a> <a class="btn w-auto" @click="handleImport">Import</a>
</div> </div>
</form> </form>
<br /> <br />
<b>Importing Subscriptions from YouTube</b> <strong>Importing Subscriptions from YouTube</strong>
<br /> <br />
<div> <div>
Open Open
@ -30,7 +30,7 @@
Select and import the file above. Select and import the file above.
</div> </div>
<br /> <br />
<b>Importing Subscriptions from Invidious</b> <strong>Importing Subscriptions from Invidious</strong>
<br /> <br />
<div> <div>
Open Open
@ -41,7 +41,7 @@
Select and import the file above. Select and import the file above.
</div> </div>
<br /> <br />
<b>Importing Subscriptions from NewPipe</b> <strong>Importing Subscriptions from NewPipe</strong>
<br /> <br />
<div> <div>
Go to the Feed tab. Go to the Feed tab.

View File

@ -1,30 +1,29 @@
<template> <template>
<div class="uk-vertical-align uk-text-center uk-height-1-1"> <div class="text-center">
<form class="uk-panel uk-panel-box"> <h1 v-t="'titles.login'" />
<div class="uk-form-row"> <form class="children:pb-3">
<div>
<input <input
v-model="username" v-model="username"
class="uk-width-1-1 uk-form-large uk-input uk-width-auto" class="input"
type="text" type="text"
autocomplete="username" autocomplete="username"
:placeholder="$t('login.username')" :placeholder="$t('login.username')"
:aria-label="$t('login.username')" :aria-label="$t('login.username')"
/> />
</div> </div>
<div class="uk-form-row"> <div>
<input <input
v-model="password" v-model="password"
class="uk-width-1-1 uk-form-large uk-input uk-width-auto" class="input"
type="password" type="password"
autocomplete="password" autocomplete="password"
:placeholder="$t('login.password')" :placeholder="$t('login.password')"
:aria-label="$t('login.password')" :aria-label="$t('login.password')"
/> />
</div> </div>
<div class="uk-form-row"> <div>
<a class="uk-width-1-1 uk-button uk-button-large uk-width-auto" @click="login"> <a class="btn w-auto" @click="login" v-text="$t('titles.login')" />
{{ $t("titles.login") }}
</a>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,24 +1,20 @@
<template> <template>
<nav <nav class="flex flex-wrap items-center justify-center px-2 sm:px-4 py-2.5 w-full relative">
class="uk-navbar uk-navbar-container uk-container-expand uk-position-relative" <div class="flex-1">
:style="[{ background: backgroundColor, colour: foregroundColor }]" <router-link class="flex font-bold text-3xl items-center font-sans font-bold" to="/"
uk-navbar
>
<div class="uk-navbar-left">
<router-link class="uk-navbar-item uk-logo uk-text-bold" :style="[{ colour: foregroundColor }]" to="/"
><img ><img
alt="logo" alt="logo"
src="/img/icons/logo.svg" src="/img/icons/logo.svg"
height="32" height="32"
width="32" width="32"
style="margin-bottom: 6px; margin-right: -13px" class="w-10 mr-[-0.6rem]"
/>iped</router-link />iped</router-link
> >
</div> </div>
<div class="uk-navbar-center uk-flex uk-visible@m"> <div class="<md:hidden">
<input <input
v-model="searchText" v-model="searchText"
class="uk-input uk-width-medium" class="input !w-72 !h-10"
type="text" type="text"
role="search" role="search"
:title="$t('actions.search')" :title="$t('actions.search')"
@ -29,8 +25,8 @@
@blur="onInputBlur" @blur="onInputBlur"
/> />
</div> </div>
<div class="uk-navbar-right"> <div class="flex-1 flex justify-end">
<ul class="uk-navbar-nav"> <ul class="flex text-1xl children:pl-3">
<li> <li>
<router-link v-t="'titles.preferences'" to="/preferences" /> <router-link v-t="'titles.preferences'" to="/preferences" />
</li> </li>
@ -49,10 +45,10 @@
</ul> </ul>
</div> </div>
</nav> </nav>
<div class="uk-container-expand uk-hidden@m"> <div class="w-full md:hidden">
<input <input
v-model="searchText" v-model="searchText"
class="uk-input" class="input !h-10 !w-full"
type="text" type="text"
role="search" role="search"
:title="$t('actions.search')" :title="$t('actions.search')"
@ -72,7 +68,7 @@
</template> </template>
<script> <script>
import SearchSuggestions from "@/components/SearchSuggestions"; import SearchSuggestions from "@/components/SearchSuggestions.vue";
export default { export default {
components: { components: {
@ -84,6 +80,10 @@ export default {
suggestionsVisible: false, suggestionsVisible: false,
}; };
}, },
mounted() {
const query = new URLSearchParams(window.location.search).get("search_query");
if (query) this.onSearchTextChange(query);
},
computed: { computed: {
shouldShowLogin(_this) { shouldShowLogin(_this) {
return _this.getAuthToken() == null; return _this.getAuthToken() == null;
@ -121,5 +121,3 @@ export default {
}, },
}; };
</script> </script>
<style></style>

View File

@ -1,18 +1,12 @@
<template> <template>
<div class="uk-container-expand"> <div class="w-full">
<div <div
ref="container" ref="container"
data-shaka-player-container data-shaka-player-container
style="width: 100%; height: 100%; background: #000" style="width: 100%; height: 100%; background: #000"
:style="!isEmbed ? { 'max-height': '75vh', 'min-height': '250px' } : {}" :style="!isEmbed ? { 'max-height': '75vh', 'min-height': '250px' } : {}"
> >
<video <video ref="videoEl" data-shaka-player class="w-full" :autoplay="shouldAutoPlay" :loop="selectedAutoLoop" />
ref="videoEl"
data-shaka-player
class="uk-width-expand"
:autoplay="shouldAutoPlay"
:loop="selectedAutoLoop"
></video>
</div> </div>
</div> </div>
</template> </template>
@ -81,103 +75,103 @@ export default {
.then(hotkeys => { .then(hotkeys => {
this.hotkeys = hotkeys; this.hotkeys = hotkeys;
var self = this; var self = this;
hotkeys("f,m,j,k,l,c,space,up,down,left,right,0,1,2,3,4,5,6,7,8,9,shift+,,shift+.", function( hotkeys(
e, "f,m,j,k,l,c,space,up,down,left,right,0,1,2,3,4,5,6,7,8,9,shift+,,shift+.",
handler, function (e, handler) {
) { const videoEl = self.$refs.videoEl;
const videoEl = self.$refs.videoEl; console.log(handler.key);
console.log(handler.key); switch (handler.key) {
switch (handler.key) { case "f":
case "f": self.$ui.getControls().toggleFullScreen();
self.$ui.getControls().toggleFullScreen(); e.preventDefault();
e.preventDefault(); break;
break; case "m":
case "m": videoEl.muted = !videoEl.muted;
videoEl.muted = !videoEl.muted; e.preventDefault();
e.preventDefault(); break;
break; case "j":
case "j": videoEl.currentTime = Math.max(videoEl.currentTime - 15, 0);
videoEl.currentTime = Math.max(videoEl.currentTime - 15, 0); e.preventDefault();
e.preventDefault(); break;
break; case "l":
case "l": videoEl.currentTime = videoEl.currentTime + 15;
videoEl.currentTime = videoEl.currentTime + 15; e.preventDefault();
e.preventDefault(); break;
break; case "c":
case "c": self.$player.setTextTrackVisibility(!self.$player.isTextTrackVisible());
self.$player.setTextTrackVisibility(!self.$player.isTextTrackVisible()); e.preventDefault();
e.preventDefault(); break;
break; case "k":
case "k": case "space":
case "space": if (videoEl.paused) videoEl.play();
if (videoEl.paused) videoEl.play(); else videoEl.pause();
else videoEl.pause(); e.preventDefault();
e.preventDefault(); break;
break; case "up":
case "up": videoEl.volume = Math.min(videoEl.volume + 0.05, 1);
videoEl.volume = Math.min(videoEl.volume + 0.05, 1); e.preventDefault();
e.preventDefault(); break;
break; case "down":
case "down": videoEl.volume = Math.max(videoEl.volume - 0.05, 0);
videoEl.volume = Math.max(videoEl.volume - 0.05, 0); e.preventDefault();
e.preventDefault(); break;
break; case "left":
case "left": videoEl.currentTime = Math.max(videoEl.currentTime - 5, 0);
videoEl.currentTime = Math.max(videoEl.currentTime - 5, 0); e.preventDefault();
e.preventDefault(); break;
break; case "right":
case "right": videoEl.currentTime = videoEl.currentTime + 5;
videoEl.currentTime = videoEl.currentTime + 5; e.preventDefault();
e.preventDefault(); break;
break; case "0":
case "0": videoEl.currentTime = 0;
videoEl.currentTime = 0; e.preventDefault();
e.preventDefault(); break;
break; case "1":
case "1": videoEl.currentTime = videoEl.duration * 0.1;
videoEl.currentTime = videoEl.duration * 0.1; e.preventDefault();
e.preventDefault(); break;
break; case "2":
case "2": videoEl.currentTime = videoEl.duration * 0.2;
videoEl.currentTime = videoEl.duration * 0.2; e.preventDefault();
e.preventDefault(); break;
break; case "3":
case "3": videoEl.currentTime = videoEl.duration * 0.3;
videoEl.currentTime = videoEl.duration * 0.3; e.preventDefault();
e.preventDefault(); break;
break; case "4":
case "4": videoEl.currentTime = videoEl.duration * 0.4;
videoEl.currentTime = videoEl.duration * 0.4; e.preventDefault();
e.preventDefault(); break;
break; case "5":
case "5": videoEl.currentTime = videoEl.duration * 0.5;
videoEl.currentTime = videoEl.duration * 0.5; e.preventDefault();
e.preventDefault(); break;
break; case "6":
case "6": videoEl.currentTime = videoEl.duration * 0.6;
videoEl.currentTime = videoEl.duration * 0.6; e.preventDefault();
e.preventDefault(); break;
break; case "7":
case "7": videoEl.currentTime = videoEl.duration * 0.7;
videoEl.currentTime = videoEl.duration * 0.7; e.preventDefault();
e.preventDefault(); break;
break; case "8":
case "8": videoEl.currentTime = videoEl.duration * 0.8;
videoEl.currentTime = videoEl.duration * 0.8; e.preventDefault();
e.preventDefault(); break;
break; case "9":
case "9": videoEl.currentTime = videoEl.duration * 0.9;
videoEl.currentTime = videoEl.duration * 0.9; e.preventDefault();
e.preventDefault(); break;
break; case "shift+,":
case "shift+,": self.$player.trickPlay(Math.max(videoEl.playbackRate - 0.25, 0.25));
self.$player.trickPlay(Math.max(videoEl.playbackRate - 0.25, 0.25)); break;
break; case "shift+.":
case "shift+.": self.$player.trickPlay(Math.min(videoEl.playbackRate + 0.25, 2));
self.$player.trickPlay(Math.min(videoEl.playbackRate + 0.25, 2)); break;
break; }
} },
}); );
}); });
}, },
deactivated() { deactivated() {
@ -194,12 +188,30 @@ export default {
videoEl.setAttribute("poster", this.video.thumbnailUrl); videoEl.setAttribute("poster", this.video.thumbnailUrl);
if (this.$route.query.t) { if (this.$route.query.t) {
videoEl.currentTime = this.$route.query.t; const time = this.$route.query.t;
let start = 0;
if (/^[\d]*$/g.test(time)) {
start = time;
} else {
const hours = /([\d]*)h/gi.exec(time)?.[1];
const minutes = /([\d]*)m/gi.exec(time)?.[1];
const seconds = /([\d]*)s/gi.exec(time)?.[1];
if (hours) {
start += parseInt(hours) * 60 * 60;
}
if (minutes) {
start += parseInt(minutes) * 60;
}
if (seconds) {
start += parseInt(seconds);
}
}
videoEl.currentTime = start;
} else if (window.db) { } else if (window.db) {
var tx = window.db.transaction("watch_history", "readonly"); var tx = window.db.transaction("watch_history", "readonly");
var store = tx.objectStore("watch_history"); var store = tx.objectStore("watch_history");
var request = store.get(this.video.id); var request = store.get(this.video.id);
request.onsuccess = function(event) { request.onsuccess = function (event) {
var video = event.target.result; var video = event.target.result;
if (video && video.currentTime) { if (video && video.currentTime) {
videoEl.currentTime = video.currentTime; videoEl.currentTime = video.currentTime;
@ -228,10 +240,9 @@ export default {
mime = "application/x-mpegURL"; mime = "application/x-mpegURL";
} else if (this.video.audioStreams.length > 0 && !lbry && MseSupport) { } else if (this.video.audioStreams.length > 0 && !lbry && MseSupport) {
if (!this.video.dash) { if (!this.video.dash) {
const dash = require("@/utils/DashUtils.js").default.generate_dash_file_from_formats( const dash = (
streams, await import("@/utils/DashUtils.js").then(mod => mod.default)
this.video.duration, ).generate_dash_file_from_formats(streams, this.video.duration);
);
uri = "data:application/dash+xml;charset=utf-8;base64," + btoa(dash); uri = "data:application/dash+xml;charset=utf-8;base64," + btoa(dash);
} else uri = this.video.dash; } else uri = this.video.dash;
@ -484,7 +495,7 @@ export default {
var tx = window.db.transaction("watch_history", "readwrite"); var tx = window.db.transaction("watch_history", "readwrite");
var store = tx.objectStore("watch_history"); var store = tx.objectStore("watch_history");
var request = store.get(this.video.id); var request = store.get(this.video.id);
request.onsuccess = function(event) { request.onsuccess = function (event) {
var video = event.target.result; var video = event.target.result;
if (video) { if (video) {
video.currentTime = time; video.currentTime = time;

View File

@ -2,34 +2,36 @@
<ErrorHandler v-if="playlist && playlist.error" :message="playlist.message" :error="playlist.error" /> <ErrorHandler v-if="playlist && playlist.error" :message="playlist.message" :error="playlist.error" />
<div v-if="playlist" v-show="!playlist.error"> <div v-if="playlist" v-show="!playlist.error">
<h1 class="uk-text-center"> <h1 class="text-center" v-text="playlist.name" />
<img :src="playlist.avatarUrl" height="48" width="48" loading="lazy" />
{{ playlist.name }}
</h1>
<b <div class="grid grid-cols-2">
><router-link class="uk-text-justify" :to="playlist.uploaderUrl || '/'"> <div>
<img :src="playlist.uploaderAvatar" loading="lazy" class="uk-border-circle" /> <router-link class="link" :to="playlist.uploaderUrl || '/'">
{{ playlist.uploader }}</router-link <img :src="playlist.uploaderAvatar" loading="lazy" class="rounded-full" />
></b <strong v-text="playlist.uploader" />
> </router-link>
</div>
<div class="uk-align-right"> <div>
<b>{{ playlist.videos }} {{ $t("video.videos") }}</b> <div class="right-2vw absolute">
<br /> <strong v-text="`${playlist.videos} ${$t('video.videos')}`" />
<a :href="getRssUrl"><font-awesome-icon icon="rss"></font-awesome-icon></a> <br />
<a :href="getRssUrl">
<font-awesome-icon icon="rss" />
</a>
</div>
</div>
</div> </div>
<hr /> <hr />
<div class="uk-grid uk-grid-xl"> <div class="video-grid">
<div <VideoItem
v-for="video in playlist.relatedStreams" v-for="video in playlist.relatedStreams"
:key="video.url" :key="video.url"
class="uk-width-1-2 uk-width-1-3@m uk-width-1-4@l uk-width-1-5@xl" :video="video"
> height="94"
<VideoItem :video="video" height="94" width="168" /> width="168"
</div> />
</div> </div>
</div> </div>
</template> </template>

View File

@ -1,205 +1,169 @@
<template> <template>
<div class="uk-flex uk-flex-between uk-flex-middle"> <div class="flex">
<button class="uk-button uk-button-text" @click="$router.go(-1) || $router.push('/')"> <button @click="$router.go(-1) || $router.push('/')">
<font-awesome-icon icon="chevron-left" /> &nbsp;{{ $t("actions.back") }} <font-awesome-icon icon="chevron-left" /><span class="ml-1.5" v-text="$t('actions.back')" />
</button> </button>
<span><h1 v-t="'titles.preferences'" class="uk-text-bold uk-text-center"/></span>
<span />
</div> </div>
<h1 v-t="'titles.preferences'" class="font-bold text-center" />
<hr /> <hr />
<h2>SponsorBlock</h2> <h2>SponsorBlock</h2>
<p>{{ $t("actions.uses_api_from") }}<a href="https://sponsor.ajay.app/">sponsor.ajay.app</a></p> <p>
<label for="chkEnableSponsorblock"><b v-t="'actions.enable_sponsorblock'"/></label> <span v-text="$t('actions.uses_api_from')" /><a class="link" href="https://sponsor.ajay.app/"
>sponsor.ajay.app</a
>
</p>
<label for="chkEnableSponsorblock"><strong v-t="'actions.enable_sponsorblock'" /></label>
<br /> <br />
<input <input
id="chkEnableSponsorblock" id="chkEnableSponsorblock"
v-model="sponsorBlock" v-model="sponsorBlock"
class="uk-checkbox" class="checkbox"
type="checkbox" type="checkbox"
@change="onChange($event)" @change="onChange($event)"
/> />
<br /> <br />
<label for="chkSkipSponsors"><b v-t="'actions.skip_sponsors'"/></label> <label for="chkSkipSponsors"><strong v-t="'actions.skip_sponsors'" /></label>
<br /> <br />
<input id="chkSkipSponsors" v-model="skipSponsor" class="uk-checkbox" type="checkbox" @change="onChange($event)" /> <input id="chkSkipSponsors" v-model="skipSponsor" class="checkbox" type="checkbox" @change="onChange($event)" />
<br /> <br />
<label for="chkSkipIntro"><b v-t="'actions.skip_intro'"/></label> <label for="chkSkipIntro"><strong v-t="'actions.skip_intro'" /></label>
<br /> <br />
<input id="chkSkipIntro" v-model="skipIntro" class="uk-checkbox" type="checkbox" @change="onChange($event)" /> <input id="chkSkipIntro" v-model="skipIntro" class="checkbox" type="checkbox" @change="onChange($event)" />
<br /> <br />
<label for="chkSkipOutro"><b v-t="'actions.skip_outro'"/></label> <label for="chkSkipOutro"><strong v-t="'actions.skip_outro'" /></label>
<br /> <br />
<input id="chkSkipOutro" v-model="skipOutro" class="uk-checkbox" type="checkbox" @change="onChange($event)" /> <input id="chkSkipOutro" v-model="skipOutro" class="checkbox" type="checkbox" @change="onChange($event)" />
<br /> <br />
<label for="chkSkipPreview"><b v-t="'actions.skip_preview'"/></label> <label for="chkSkipPreview"><strong v-t="'actions.skip_preview'" /></label>
<br /> <br />
<input id="chkSkipPreview" v-model="skipPreview" class="uk-checkbox" type="checkbox" @change="onChange($event)" /> <input id="chkSkipPreview" v-model="skipPreview" class="checkbox" type="checkbox" @change="onChange($event)" />
<br /> <br />
<label for="chkSkipInteraction"><b v-t="'actions.skip_interaction'"/></label> <label for="chkSkipInteraction"><strong v-t="'actions.skip_interaction'" /></label>
<br /> <br />
<input <input
id="chkSkipInteraction" id="chkSkipInteraction"
v-model="skipInteraction" v-model="skipInteraction"
class="uk-checkbox" class="checkbox"
type="checkbox" type="checkbox"
@change="onChange($event)" @change="onChange($event)"
/> />
<br /> <br />
<label for="chkSkipSelfPromo"><b v-t="'actions.skip_self_promo'"/></label> <label for="chkSkipSelfPromo"><strong v-t="'actions.skip_self_promo'" /></label>
<br /> <br />
<input <input id="chkSkipSelfPromo" v-model="skipSelfPromo" class="checkbox" type="checkbox" @change="onChange($event)" />
id="chkSkipSelfPromo"
v-model="skipSelfPromo"
class="uk-checkbox"
type="checkbox"
@change="onChange($event)"
/>
<br /> <br />
<label for="chkSkipNonMusic"><b v-t="'actions.skip_non_music'"/></label> <label for="chkSkipNonMusic"><strong v-t="'actions.skip_non_music'" /></label>
<br /> <br />
<input <input
id="chkSkipNonMusic" id="chkSkipNonMusic"
v-model="skipMusicOffTopic" v-model="skipMusicOffTopic"
class="uk-checkbox" class="checkbox"
type="checkbox" type="checkbox"
@change="onChange($event)" @change="onChange($event)"
/> />
<br /> <br />
<label for="ddlTheme"><b v-t="'actions.theme'"/></label> <label for="ddlTheme"><strong v-t="'actions.theme'" /></label>
<br /> <br />
<select id="ddlTheme" v-model="selectedTheme" class="uk-select uk-width-auto" @change="onChange($event)"> <select id="ddlTheme" v-model="selectedTheme" class="select w-auto" @change="onChange($event)">
<option v-t="'actions.auto'" value="auto" /> <option v-t="'actions.auto'" value="auto" />
<option v-t="'actions.dark'" value="dark" /> <option v-t="'actions.dark'" value="dark" />
<option v-t="'actions.light'" value="light" /> <option v-t="'actions.light'" value="light" />
</select> </select>
<br /> <br />
<label for="chkAutoPlayVideo"><b v-t="'actions.autoplay_video'"/></label> <label for="chkAutoPlayVideo"><strong v-t="'actions.autoplay_video'" /></label>
<br /> <br />
<input <input id="chkAutoPlayVideo" v-model="autoPlayVideo" class="checkbox" type="checkbox" @change="onChange($event)" />
id="chkAutoPlayVideo"
v-model="autoPlayVideo"
class="uk-checkbox"
type="checkbox"
@change="onChange($event)"
/>
<br /> <br />
<label for="chkAudioOnly"><b v-t="'actions.audio_only'"/></label> <label for="chkAudioOnly"><strong v-t="'actions.audio_only'" /></label>
<br /> <br />
<input id="chkAudioOnly" v-model="listen" class="uk-checkbox" type="checkbox" @change="onChange($event)" /> <input id="chkAudioOnly" v-model="listen" class="checkbox" type="checkbox" @change="onChange($event)" />
<br /> <br />
<label for="ddlDefaultQuality"><b v-t="'actions.default_quality'"/></label> <label for="ddlDefaultQuality"><strong v-t="'actions.default_quality'" /></label>
<br /> <br />
<select id="ddlDefaultQuality" v-model="defaultQuality" class="uk-select uk-width-auto" @change="onChange($event)"> <select id="ddlDefaultQuality" v-model="defaultQuality" class="select w-auto" @change="onChange($event)">
<option v-t="'actions.auto'" value="0" /> <option v-t="'actions.auto'" value="0" />
<option v-for="resolution in resolutions" :key="resolution" :value="resolution">{{ resolution }}p</option> <option v-for="resolution in resolutions" :key="resolution" :value="resolution" v-text="`${resolution}p`" />
</select> </select>
<br /> <br />
<label for="txtBufferingGoal"><b v-t="'actions.buffering_goal'"/></label> <label for="txtBufferingGoal"><strong v-t="'actions.buffering_goal'" /></label>
<br /> <br />
<input <input id="txtBufferingGoal" v-model="bufferingGoal" class="input w-auto" type="text" @change="onChange($event)" />
id="txtBufferingGoal"
v-model="bufferingGoal"
class="uk-input uk-width-auto"
type="text"
@change="onChange($event)"
/>
<br /> <br />
<label for="ddlCountrySelection"><b v-t="'actions.country_selection'"/></label> <label for="ddlCountrySelection"><strong v-t="'actions.country_selection'" /></label>
<br /> <br />
<select <select id="ddlCountrySelection" v-model="countrySelected" class="select w-auto" @change="onChange($event)">
id="ddlCountrySelection" <option v-for="country in countryMap" :key="country.code" :value="country.code" v-text="country.name" />
v-model="countrySelected"
class="uk-select uk-width-auto"
@change="onChange($event)"
>
<option v-for="country in countryMap" :key="country.code" :value="country.code">{{ country.name }}</option>
</select> </select>
<br /> <br />
<label for="ddlDefaultHomepage"><b v-t="'actions.default_homepage'"/></label> <label for="ddlDefaultHomepage"><strong v-t="'actions.default_homepage'" /></label>
<br /> <br />
<select <select id="ddlDefaultHomepage" v-model="defaultHomepage" class="select w-auto" @change="onChange($event)">
id="ddlDefaultHomepage"
v-model="defaultHomepage"
class="uk-select uk-width-auto"
@change="onChange($event)"
>
<option v-t="'titles.trending'" value="trending" /> <option v-t="'titles.trending'" value="trending" />
<option v-t="'titles.feed'" value="feed" /> <option v-t="'titles.feed'" value="feed" />
</select> </select>
<br /> <br />
<label for="chkShowComments"><b v-t="'actions.show_comments'"/></label> <label for="chkShowComments"><strong v-t="'actions.show_comments'" /></label>
<br /> <br />
<input id="chkShowComments" v-model="showComments" class="uk-checkbox" type="checkbox" @change="onChange($event)" /> <input id="chkShowComments" v-model="showComments" class="checkbox" type="checkbox" @change="onChange($event)" />
<br /> <br />
<label for="chkMinimizeDescription"><b v-t="'actions.minimize_description_default'"/></label> <label for="chkMinimizeDescription"><strong v-t="'actions.minimize_description_default'" /></label>
<br /> <br />
<input <input
id="chkMinimizeDescription" id="chkMinimizeDescription"
v-model="minimizeDescription" v-model="minimizeDescription"
class="uk-checkbox" class="checkbox"
type="checkbox" type="checkbox"
@change="onChange($event)" @change="onChange($event)"
/> />
<br /> <br />
<label for="chkStoreWatchHistory"><b v-t="'actions.store_watch_history'"/></label> <label for="chkStoreWatchHistory"><strong v-t="'actions.store_watch_history'" /></label>
<br /> <br />
<input <input
id="chkStoreWatchHistory" id="chkStoreWatchHistory"
v-model="watchHistory" v-model="watchHistory"
class="uk-checkbox" class="checkbox"
type="checkbox" type="checkbox"
@change="onChange($event)" @change="onChange($event)"
/> />
<br /> <br />
<label for="ddlLanguageSelection"><b v-t="'actions.language_selection'"/></label> <label for="ddlLanguageSelection"><strong v-t="'actions.language_selection'" /></label>
<br /> <br />
<select <select id="ddlLanguageSelection" v-model="selectedLanguage" class="select w-auto" @change="onChange($event)">
id="ddlLanguageSelection" <option v-for="language in languages" :key="language.code" :value="language.code" v-text="language.name" />
v-model="selectedLanguage"
class="uk-select uk-width-auto"
@change="onChange($event)"
>
<option v-for="language in languages" :key="language.code" :value="language.code">{{ language.name }}</option>
</select> </select>
<br /> <br />
<label for="ddlEnabledCodecs"><b v-t="'actions.enabled_codecs'"/></label> <label for="ddlEnabledCodecs"><strong v-t="'actions.enabled_codecs'" /></label>
<br /> <br />
<select <select id="ddlEnabledCodecs" v-model="enabledCodecs" class="select w-auto" multiple @change="onChange($event)">
id="ddlEnabledCodecs"
v-model="enabledCodecs"
class="uk-select uk-width-auto"
multiple
@change="onChange($event)"
>
<option value="av1">AV1</option> <option value="av1">AV1</option>
<option value="vp9">VP9</option> <option value="vp9">VP9</option>
<option value="avc">AVC (h.264)</option> <option value="avc">AVC (h.264)</option>
</select> </select>
<br /> <br />
<label for="chkDisableLBRY"><b v-t="'actions.disable_lbry'"/></label> <label for="chkDisableLBRY"><strong v-t="'actions.disable_lbry'" /></label>
<br /> <br />
<input id="chkDisableLBRY" v-model="disableLBRY" class="uk-checkbox" type="checkbox" @change="onChange($event)" /> <input id="chkDisableLBRY" v-model="disableLBRY" class="checkbox" type="checkbox" @change="onChange($event)" />
<br /> <br />
<label for="chkEnableLBRYProxy"><b v-t="'actions.enable_lbry_proxy'"/></label> <label for="chkEnableLBRYProxy"><strong v-t="'actions.enable_lbry_proxy'" /></label>
<br /> <br />
<input id="chkEnableLBRYProxy" v-model="proxyLBRY" class="uk-checkbox" type="checkbox" @change="onChange($event)" /> <input id="chkEnableLBRYProxy" v-model="proxyLBRY" class="checkbox" type="checkbox" @change="onChange($event)" />
<h2 v-t="'actions.instances_list'" /> <h2 v-t="'actions.instances_list'" />
<table class="uk-table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th>{{ $t("preferences.instance_name") }}</th> <th v-text="$t('preferences.instance_name')" />
<th>{{ $t("preferences.instance_locations") }}</th> <th v-text="$t('preferences.instance_locations')" />
<th>{{ $t("preferences.has_cdn") }}</th> <th v-text="$t('preferences.has_cdn')" />
<th>{{ $t("preferences.ssl_score") }}</th> <th v-text="$t('preferences.ssl_score')" />
</tr> </tr>
</thead> </thead>
<tbody v-for="instance in instances" :key="instance.name"> <tbody v-for="instance in instances" :key="instance.name">
<tr> <tr>
<td>{{ instance.name }}</td> <td v-text="instance.name" />
<td>{{ instance.locations }}</td> <td v-text="instance.locations" />
<td>{{ instance.cdn == "Yes" ? $t("actions.yes") : $t("actions.no") }}</td> <td v-text="$t(`actions.${instance.cdn === 'Yes' ? 'yes' : 'no'}`)" />
<td> <td>
<a :href="sslScore(instance.apiurl)" target="_blank"> {{ $t("actions.view_ssl_score") }}</a> <a :href="sslScore(instance.apiurl)" target="_blank" v-text="$t('actions.view_ssl_score')" />
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -207,19 +171,10 @@
<hr /> <hr />
<label for="ddlInstanceSelection" <label for="ddlInstanceSelection"><strong v-text="`${$t('actions.instance_selection')}:`" /></label>
><b>{{ $t("actions.instance_selection") }}:</b></label
>
<br /> <br />
<select <select id="ddlInstanceSelection" v-model="selectedInstance" class="select w-auto" @change="onChange($event)">
id="ddlInstanceSelection" <option v-for="instance in instances" :key="instance.name" :value="instance.apiurl" v-text="instance.name" />
v-model="selectedInstance"
class="uk-select uk-width-auto"
@change="onChange($event)"
>
<option v-for="instance in instances" :key="instance.name" :value="instance.apiurl">
{{ instance.name }}
</option>
</select> </select>
</template> </template>
@ -326,7 +281,14 @@ export default {
this.sponsorBlock = this.getPreferenceBoolean("sponsorblock", true); this.sponsorBlock = this.getPreferenceBoolean("sponsorblock", true);
if (localStorage.getItem("selectedSkip") !== null) { if (localStorage.getItem("selectedSkip") !== null) {
var skipList = localStorage.getItem("selectedSkip").split(","); var skipList = localStorage.getItem("selectedSkip").split(",");
this.skipSponsor = this.skipIntro = this.skipOutro = this.skipPreview = this.skipInteraction = this.skipSelfPromo = this.skipMusicOffTopic = false; this.skipSponsor =
this.skipIntro =
this.skipOutro =
this.skipPreview =
this.skipInteraction =
this.skipSelfPromo =
this.skipMusicOffTopic =
false;
skipList.forEach(skip => { skipList.forEach(skip => {
switch (skip) { switch (skip) {
case "sponsor": case "sponsor":
@ -373,10 +335,8 @@ export default {
this.proxyLBRY = this.getPreferenceBoolean("proxyLBRY", false); this.proxyLBRY = this.getPreferenceBoolean("proxyLBRY", false);
if (this.selectedLanguage != "en") { if (this.selectedLanguage != "en") {
try { try {
this.CountryMap = await import("@/utils/CountryMaps/" + this.selectedLanguage + ".json").then( this.CountryMap = await import(`../utils/CountryMaps/${this.selectedLanguage}.json`).then(
val => { val => val.default,
this.countryMap = val;
},
); );
} catch (e) { } catch (e) {
console.error("Countries not translated into " + this.selectedLanguage); console.error("Countries not translated into " + this.selectedLanguage);

View File

@ -1,30 +1,28 @@
<template> <template>
<div class="uk-vertical-align uk-text-center uk-height-1-1"> <div class="text-center">
<form class="uk-panel uk-panel-box"> <form class="children:pb-3">
<div class="uk-form-row"> <div>
<input <input
v-model="username" v-model="username"
class="uk-width-1-1 uk-form-large uk-input uk-width-auto" class="input"
type="text" type="text"
autocomplete="username" autocomplete="username"
:placeholder="$t('login.username')" :placeholder="$t('login.username')"
:aria-label="$t('login.username')" :aria-label="$t('login.username')"
/> />
</div> </div>
<div class="uk-form-row"> <div>
<input <input
v-model="password" v-model="password"
class="uk-width-1-1 uk-form-large uk-input uk-width-auto" class="input"
type="password" type="password"
autocomplete="password" autocomplete="password"
:placeholder="$t('login.password')" :placeholder="$t('login.password')"
:aria-label="$t('login.password')" :aria-label="$t('login.password')"
/> />
</div> </div>
<div class="uk-form-row"> <div>
<a class="uk-width-1-1 uk-button uk-button-large uk-width-auto" @click="register"> <a class="btn w-auto" @click="register" v-text="$t('titles.register')" />
{{ $t("titles.register") }}</a
>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,69 +1,55 @@
<template> <template>
<h1 class="uk-text-center"> <h1 class="text-center" v-text="$route.query.search_query" />
{{ $route.query.search_query }}
</h1>
<label for="ddlSearchFilters" <label for="ddlSearchFilters">
><b>{{ $t("actions.filter") }}: </b></label <strong v-text="`${$t('actions.filter')}:`" />
> </label>
<select <select
id="ddlSearchFilters" id="ddlSearchFilters"
v-model="selectedFilter" v-model="selectedFilter"
default="all" default="all"
class="uk-select uk-width-auto" class="select w-auto"
style="height: 100%"
@change="updateResults()" @change="updateResults()"
> >
<option v-for="filter in availableFilters" :key="filter" :value="filter"> <option v-for="filter in availableFilters" :key="filter" :value="filter" v-text="filter.replace('_', ' ')" />
{{ filter.replace("_", " ") }}
</option>
</select> </select>
<hr /> <hr />
<div v-if="results && results.corrected" style="height: 7vh"> <div v-if="results && results.corrected" style="height: 7vh">
{{ $t("search.did_you_mean") }} <span v-text="$t('search.did_you_mean')" />
<i>
<router-link :to="{ name: 'SearchResults', query: { search_query: results.suggestion } }"> <router-link :to="{ name: 'SearchResults', query: { search_query: results.suggestion } }">
{{ results.suggestion }} <em v-text="results.suggestion" />
</router-link> </router-link>
</i>
</div> </div>
<div v-if="results" class="uk-grid uk-grid-xl"> <div v-if="results" class="video-grid">
<div <div v-for="result in results.items" :key="result.url">
v-for="result in results.items"
:key="result.url"
:style="[{ background: backgroundColor }]"
class="uk-width-1-2 uk-width-1-3@s uk-width-1-4@m uk-width-1-5@l uk-width-1-6@xl"
>
<VideoItem v-if="shouldUseVideoItem(result)" :video="result" height="94" width="168" /> <VideoItem v-if="shouldUseVideoItem(result)" :video="result" height="94" width="168" />
<div v-if="!shouldUseVideoItem(result)" class="uk-text-secondary"> <div v-if="!shouldUseVideoItem(result)">
<router-link class="uk-text-emphasis" :to="result.url"> <router-link :to="result.url">
<div class="uk-position-relative"> <div class="relative">
<img style="width: 100%" :src="result.thumbnail" loading="lazy" /> <img class="w-full" :src="result.thumbnail" loading="lazy" />
</div> </div>
<p> <p>
{{ result.name }}&thinsp;<font-awesome-icon <span v-text="result.name" />
v-if="result.verified" <font-awesome-icon class="ml-1.5" v-if="result.verified" icon="check" />
icon="check"
></font-awesome-icon>
</p> </p>
</router-link> </router-link>
<p v-if="result.description">{{ result.description }}</p> <p v-if="result.description" v-text="result.description" />
<router-link v-if="result.uploaderUrl" class="uk-link-muted" :to="result.uploaderUrl"> <router-link v-if="result.uploaderUrl" class="link" :to="result.uploaderUrl">
<p> <p>
{{ result.uploader }}&thinsp;<font-awesome-icon <span v-text="result.uploader" />
v-if="result.uploaderVerified" <font-awesome-icon class="ml-1.5" v-if="result.uploaderVerified" icon="check" />
icon="check"
></font-awesome-icon>
</p> </p>
</router-link> </router-link>
<a v-if="result.uploaderName" class="uk-text-muted">{{ result.uploaderName }}</a> <a v-if="result.uploaderName" class="link" v-text="result.uploaderName" />
<b v-if="result.videos >= 0" <template v-if="result.videos >= 0">
><br v-if="result.uploaderName" />{{ result.videos }} {{ $t("video.videos") }}</b <br v-if="result.uploaderName" />
> <strong v-text="`${result.videos} ${$t('video.videos')}`" />
</template>
<br /> <br />
</div> </div>

View File

@ -1,19 +1,15 @@
<template> <template>
<div <div class="absolute suggestions-container">
class="uk-position-absolute uk-panel uk-box-shadow-large suggestions-container" <ul>
:style="[{ background: secondaryBackgroundColor }]"
>
<ul class="uk-list uk-margin-remove uk-text-secondary">
<li <li
v-for="(suggestion, i) in searchSuggestions" v-for="(suggestion, i) in searchSuggestions"
:key="i" :key="i"
:style="[selected === i ? { background: secondaryForegroundColor } : {}]" class="suggestion"
class="uk-margin-remove suggestion" :class="{ 'suggestion-selected': selected === i }"
@mouseover="onMouseOver(i)" @mouseover="onMouseOver(i)"
@mousedown.stop="onClick(i)" @mousedown.stop="onClick(i)"
> v-text="suggestion"
{{ suggestion }} />
</li>
</ul> </ul>
</div> </div>
</template> </template>
@ -79,25 +75,30 @@ export default {
<style> <style>
.suggestions-container { .suggestions-container {
left: 50%; @apply left-1/2 translate-x-[-50%] transform-gpu max-w-3xl w-full box-border p-y-1.25 z-10 <md:max-w-[calc(100%-0.5rem)] bg-gray-300;
transform: translateX(-50%);
max-width: 640px;
width: 100%;
box-sizing: border-box;
padding: 5px 0;
z-index: 10;
} }
.dark .suggestions-container {
@apply bg-dark-400;
}
.auto .suggestions-container {
@apply dark:bg-dark-400;
}
.suggestion-selected {
@apply bg-gray-200;
}
.dark .suggestion-selected {
@apply bg-dark-100;
}
.auto .suggestion-selected {
@apply dark:bg-dark-100;
}
.suggestion { .suggestion {
padding: 4px 15px; @apply p-y-1;
}
@media screen and (max-width: 959px) {
.suggestions-container {
max-width: calc(100% - 60px);
}
}
@media screen and (max-width: 639px) {
.suggestions-container {
max-width: calc(100% - 30px);
}
} }
</style> </style>

View File

@ -0,0 +1,44 @@
<template>
<label for="ddlSortBy" v-text="$t('actions.sort_by')" />
<select id="ddlSortBy" v-model="selectedSort" class="select w-auto">
<option v-for="(value, key) in options" v-t="`actions.${key}`" :key="key" :value="value" />
</select>
</template>
<script setup>
import { defineEmits, defineProps, ref, watch } from "vue";
const options = {
most_recent: "descending",
least_recent: "ascending",
channel_name_asc: "channel_ascending",
channel_name_desc: "channel_descending",
};
const selectedSort = ref("descending");
const props = defineProps({
byKey: String,
});
const emit = defineEmits(["apply"]);
watch(selectedSort, value => {
switch (value) {
case "ascending":
emit("apply", (a, b) => a[props.byKey] - b[props.byKey]);
break;
case "descending":
emit("apply", (a, b) => b[props.byKey] - a[props.byKey]);
break;
case "channel_ascending":
emit("apply", (a, b) => a.uploaderName.localeCompare(b.uploaderName));
break;
case "channel_descending":
emit("apply", (a, b) => b.uploaderName.localeCompare(a.uploaderName));
break;
default:
console.error("Unexpected sort value");
}
});
</script>

View File

@ -1,45 +1,31 @@
<template> <template>
<h1 class="uk-text-bold uk-text-center">{{ $t("titles.subscriptions") }}</h1> <h1 class="font-bold text-center" v-text="$t('titles.subscriptions')" />
<div style="text-align: center"> <div v-if="authenticated">
<button v-if="authenticated" class="uk-button uk-button-small" style=" margin-right: 0.5rem" type="button"> <button class="btn mr-0.5">
<router-link to="/import"> <router-link to="/import" v-text="$t('actions.import_from_json')" />
{{ $t("actions.import_from_json") }}
</router-link>
</button> </button>
<button <button class="btn" @click="exportHandler" v-text="$t('actions.export_to_json')" />
v-if="authenticated"
class="uk-button uk-button-small"
style="color: white"
type="button"
@click="exportHandler"
>
{{ $t("actions.export_to_json") }}
</button>
</div> </div>
<hr /> <hr />
<div v-for="subscription in subscriptions" :key="subscription.url" style="text-align: center"> <div class="grid">
<div class="uk-text-primary" :style="[{ background: backgroundColor }]"> <div class="mb-3" v-for="subscription in subscriptions" :key="subscription.url">
<a :href="subscription.url"> <div class="flex justify-center place-items-center">
<img :src="subscription.avatar" class="uk-margin-small-right uk-border-circle" width="96" height="96" /> <div class="w-full grid grid-cols-3">
<span <router-link :to="subscription.url" class="col-start-2 block flex text-center font-bold text-4xl">
class="uk-text-large" <img :src="subscription.avatar" class="rounded-full" width="48" height="48" />
style="width: 30rem; display: inline-block; text-align: center; margin-left: 6rem" <span v-text="subscription.name" />
>{{ subscription.name }}</span </router-link>
> <button
</a> class="btn !w-min"
<button @click="handleButton(subscription)"
class="uk-button uk-button-large" v-text="$t(`actions.${subscription.subscribed ? 'unsubscribe' : 'subscribe'}`)"
style="background: #222; margin-left: 0.5rem; width: 185px" />
type="button" </div>
@click="handleButton(subscription)" </div>
>
{{ subscription.subscribed ? $t("actions.unsubscribe") : $t("actions.subscribe") }}
</button>
</div> </div>
<br />
</div> </div>
<br /> <br />
</template> </template>

View File

@ -1,17 +1,10 @@
<template> <template>
<h1 v-t="'titles.trending'" class="uk-text-bold uk-text-center" /> <h1 v-t="'titles.trending'" class="font-bold text-center" />
<hr /> <hr />
<div class="uk-grid uk-grid-xl"> <div class="video-grid">
<div <VideoItem v-for="video in videos" :key="video.url" :video="video" height="118" width="210" />
v-for="video in videos"
:key="video.url"
:style="[{ background: backgroundColor }]"
class="uk-width-1-2 uk-width-1-3@s uk-width-1-4@m uk-width-1-5@l uk-width-1-6@xl"
>
<VideoItem :video="video" height="118" width="210" />
</div>
</div> </div>
</template> </template>

View File

@ -1,92 +1,83 @@
<template> <template>
<div class="uk-text-secondary" :style="[{ background: backgroundColor }]"> <div>
<router-link class="uk-text-emphasis" :to="video.url"> <router-link :to="video.url">
<img :height="height" :width="width" style="width: 100%" :src="video.thumbnail" alt="" loading="lazy" /> <img :height="height" :width="width" class="w-full" :src="video.thumbnail" alt="" loading="lazy" />
<div class="uk-position-relative"> <div class="relative text-sm">
<span <span
v-if="video.duration" v-if="video.duration"
class="uk-label uk-border-rounded uk-position-absolute video-duration" class="thumbnail-overlay bottom-5px right-5px px-5px"
style="bottom: 5px; right: 5px; background: rgba(0, 0, 0, 0.75); color: white; padding: 0 5px" v-text="timeFormat(video.duration)"
>{{ timeFormat(video.duration) }}</span />
>
<span <span
v-if="video.watched" v-if="video.watched"
class="uk-label uk-border-rounded uk-position-absolute video-duration" class="thumbnail-overlay bottom-5px left-5px px-5px"
style="bottom: 5px; left: 5px; background: rgba(0, 0, 0, 0.75); color: white; padding: 0 5px" v-text="$t('video.watched')"
>{{ $t("video.watched") }}</span />
>
</div> </div>
<div> <div>
<p <p
style=" style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical"
padding-top: 0.5rem; class="my-2 overflow-hidden flex link"
margin-bottom: 0.5rem;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
"
:title="video.title" :title="video.title"
> v-text="video.title"
{{ video.title }} />
</p>
</div> </div>
</router-link> </router-link>
<div class="uk-align-right" style="margin-left: 0; margin-bottom: 0; display: inline-block; width: 10%"> <div class="float-right m-0 inline-block">
<router-link <router-link
:to="video.url + '&listen=1'" :to="video.url + '&listen=1'"
:aria-label="'Listen to ' + video.title" :aria-label="'Listen to ' + video.title"
:title="'Listen to ' + video.title" :title="'Listen to ' + video.title"
> >
<font-awesome-icon icon="headphones"></font-awesome-icon> <font-awesome-icon icon="headphones" />
</router-link> </router-link>
</div> </div>
<div style="display: flex; flex-flow: row; height: 15%"> <div class="flex">
<router-link class="uk-link-muted" :to="video.uploaderUrl"> <router-link :to="video.uploaderUrl">
<img <img
v-if="video.uploaderAvatar" v-if="video.uploaderAvatar"
:src="video.uploaderAvatar" :src="video.uploaderAvatar"
loading="lazy" loading="lazy"
:alt="video.uploaderName" :alt="video.uploaderName"
class="uk-border-circle" class="rounded-full mr-0.5 mt-0.5 w-32px h-32px"
style="margin-right: 0.5rem; margin-top: 0.5rem; width: 32px; height: 32px" width="68"
height="68"
/> />
</router-link> </router-link>
<div style="width: calc(100% - 32px - 8px)"> <div class="w-[calc(100%-32px-1rem)]">
<router-link <router-link
v-if="video.uploaderUrl && video.uploaderName && !hideChannel" v-if="video.uploaderUrl && video.uploaderName && !hideChannel"
class="uk-link-muted uk-overflow-hidden" class="link-secondary overflow-hidden block"
:to="video.uploaderUrl" :to="video.uploaderUrl"
:title="video.uploaderName" :title="video.uploaderName"
style="display: block; width: 90%"
> >
{{ video.uploaderName }}&thinsp;<font-awesome-icon <span v-text="video.uploaderName" />
v-if="video.uploaderVerified" <font-awesome-icon class="ml-1.5" v-if="video.uploaderVerified" icon="check" />
icon="check"
></font-awesome-icon>
</router-link> </router-link>
<b v-if="video.views >= 0 || video.uploadedDate" class="uk-text-small"> <strong v-if="video.views >= 0 || video.uploadedDate" class="text-sm">
<span v-if="video.views >= 0"> <span v-if="video.views >= 0">
<font-awesome-icon icon="eye"></font-awesome-icon> <font-awesome-icon icon="eye" />
{{ numberFormat(video.views) }} <span class="pl-0.5" v-text="`${numberFormat(video.views)} `" />
</span> </span>
<span v-if="video.uploadedDate"> <span v-if="video.uploadedDate" class="pl-0.5" v-text="video.uploadedDate" />
{{ video.uploadedDate }} <span v-if="video.uploaded" class="pl-0.5" v-text="timeAgo(video.uploaded)" />
</span> </strong>
<span v-if="video.uploaded">
{{ timeAgo(video.uploaded) }}
</span>
</b>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<style>
.thumbnail-overlay {
@apply rounded-md absolute bg-black bg-opacity-75;
}
</style>
<script> <script>
export default { export default {
props: { props: {

View File

@ -1,5 +1,5 @@
<template> <template>
<div>{{ $t("actions.loading") }}</div> <div v-text="$t('actions.loading')" />
</template> </template>
<script> <script>

View File

@ -1,5 +1,5 @@
<template> <template>
<div v-if="video && isEmbed" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 999"> <div v-if="video && isEmbed" class="absolute top-0 left-0 h-full w-full z-50">
<Player <Player
ref="videoPlayer" ref="videoPlayer"
:video="video" :video="video"
@ -10,7 +10,7 @@
/> />
</div> </div>
<div v-if="video && !isEmbed" class="uk-container uk-container-expand"> <div v-if="video && !isEmbed" class="w-full">
<ErrorHandler v-if="video && video.error" :message="video.message" :error="video.error" /> <ErrorHandler v-if="video && video.error" :message="video.message" :error="video.error" />
<div v-show="!video.error"> <div v-show="!video.error">
@ -21,125 +21,118 @@
:selected-auto-play="selectedAutoPlay" :selected-auto-play="selectedAutoPlay"
:selected-auto-loop="selectedAutoLoop" :selected-auto-loop="selectedAutoLoop"
/> />
<div class="uk-text-bold uk-margin-small-top uk-text-large uk-text-emphasis uk-text-break"> <div class="font-bold mt-2 text-2xl break-words" v-text="video.title" />
{{ video.title }}
<div class="flex mb-1.5">
<span v-text="`${addCommas(video.views)} views`" />
<span class="ml-2" v-text="uploadDate" />
<div class="flex items-center relative ml-auto children:ml-2">
<template v-if="video.likes >= 0">
<div>
<font-awesome-icon icon="thumbs-up" />
<strong class="ml-2" v-text="addCommas(video.likes)" />
</div>
<div>
<font-awesome-icon icon="thumbs-down" />
<strong class="ml-2" v-text="video.dislikes >= 0 ? addCommas(video.dislikes) : '?'" />
</div>
</template>
<template v-if="video.likes < 0">
<div>
<strong v-t="'video.ratings_disabled'" />
</div>
</template>
<a :href="`https://youtu.be/${getVideoId()}`" class="btn">
<strong v-text="$t('player.watch_on')" />
<font-awesome-icon class="ml-1.5" :icon="['fab', 'youtube']" />
</a>
<a v-if="video.lbryId" :href="'https://odysee.com/' + video.lbryId" class="btn">
<strong v-text="`${$t('player.watch_on')} LBRY`" />
</a>
<router-link
:to="toggleListenUrl"
:aria-label="(isListening ? 'Watch ' : 'Listen to ') + video.title"
:title="(isListening ? 'Watch ' : 'Listen to ') + video.title"
class="btn"
>
<font-awesome-icon :icon="isListening ? 'tv' : 'headphones'" />
</router-link>
</div>
</div> </div>
<div class="uk-flex uk-flex-middle"> <div class="flex">
<div class="uk-margin-small-right">{{ addCommas(video.views) }} views</div> <div class="flex items-center">
<div class="uk-margin-small-right">{{ uploadDate }}</div> <img :src="video.uploaderAvatar" alt="" loading="lazy" class="rounded-full" />
<div class="uk-flex-1"></div> <router-link
<template v-if="video.likes >= 0"> v-if="video.uploaderUrl"
<div class="uk-margin-small-left"> class="link ml-1.5"
<font-awesome-icon class="uk-margin-small-right" icon="thumbs-up"></font-awesome-icon> :to="video.uploaderUrl"
<b>{{ addCommas(video.likes) }}</b> v-text="video.uploader"
</div> />
<div class="uk-margin-small-left"> <font-awesome-icon class="ml-1" v-if="video.uploaderVerified" icon="check" />
<font-awesome-icon class="uk-margin-small-right" icon="thumbs-down"></font-awesome-icon> </div>
<b>{{ video.dislikes >= 0 ? addCommas(video.dislikes) : "?" }}</b> <button
</div> v-if="authenticated"
</template> class="btn relative ml-auto"
<template v-if="video.likes < 0"> @click="subscribeHandler"
<div class="uk-margin-small-left"> v-text="$t(`actions.${subscribed ? 'unsubscribe' : 'subscribe'}`)"
<b v-t="'video.ratings_disabled'" /> />
</div>
</template>
<a :href="'https://youtu.be/' + getVideoId()" class="uk-margin-small-left uk-button uk-button-small">
<b>{{ $t("player.watch_on") }}&nbsp;</b>
<font-awesome-icon class="uk-margin-small-right" :icon="['fab', 'youtube']"></font-awesome-icon>
</a>
<a
v-if="video.lbryId"
:href="'https://odysee.com/' + video.lbryId"
class="uk-margin-small-left uk-button uk-button-small"
>
<b>{{ $t("player.watch_on") }} LBRY</b>
</a>
<router-link
:to="toggleListenUrl"
:aria-label="(isListening ? 'Watch ' : 'Listen to ') + video.title"
:title="(isListening ? 'Watch ' : 'Listen to ') + video.title"
class="uk-margin-small-left uk-button uk-button-small"
>
<font-awesome-icon :icon="isListening ? 'tv' : 'headphones'"></font-awesome-icon>
</router-link>
</div>
<div class="uk-flex uk-flex-middle uk-margin-small-top">
<img :src="video.uploaderAvatar" alt="" loading="lazy" class="uk-border-circle" />
<router-link v-if="video.uploaderUrl" class="uk-link uk-margin-small-left" :to="video.uploaderUrl">
{{ video.uploader }} </router-link
>&thinsp;<font-awesome-icon v-if="video.uploaderVerified" icon="check"></font-awesome-icon>
<div class="uk-flex-1"></div>
<button v-if="authenticated" class="uk-button uk-button-small" type="button" @click="subscribeHandler">
{{ subscribed ? $t("actions.unsubscribe") : $t("actions.subscribe") }}
</button>
</div> </div>
<hr /> <hr />
<a class="uk-button uk-button-small" @click="showDesc = !showDesc"> <button
{{ showDesc ? $t("actions.minimize_description") : $t("actions.show_description") }} class="btn mb-2"
</a> @click="showDesc = !showDesc"
v-text="$t(`actions.${showDesc ? 'minimize_description' : 'show_description'}`)"
/>
<!-- eslint-disable-next-line vue/no-v-html --> <!-- eslint-disable-next-line vue/no-v-html -->
<p v-show="showDesc" :style="[{ colour: foregroundColor }]" v-html="purifyHTML(video.description)"></p> <p v-show="showDesc" class="break-words" v-html="purifyHTML(video.description)" />
<div v-if="showDesc && sponsors && sponsors.segments"> <div
{{ $t("video.sponsor_segments") }}: {{ sponsors.segments.length }} v-if="showDesc && sponsors && sponsors.segments"
</div> v-text="`${$t('video.sponsor_segments')}: ${sponsors.segments.length}`"
/>
</div> </div>
<hr /> <hr />
<label for="chkAutoLoop" <label for="chkAutoLoop"><strong v-text="`${$t('actions.loop_this_video')}:`" /></label>
><b>{{ $t("actions.loop_this_video") }}:</b></label <input id="chkAutoLoop" v-model="selectedAutoLoop" class="ml-1.5" type="checkbox" @change="onChange($event)" />
>&nbsp;
<input
id="chkAutoLoop"
v-model="selectedAutoLoop"
class="uk-checkbox"
type="checkbox"
@change="onChange($event)"
/>
<br /> <br />
<label for="chkAutoPlay" <label for="chkAutoPlay"><strong v-text="`${$t('actions.auto_play_next_video')}:`" /></label>
><b>{{ $t("actions.auto_play_next_video") }}:</b></label <input id="chkAutoPlay" v-model="selectedAutoPlay" class="ml-1.5" type="checkbox" @change="onChange($event)" />
>&nbsp;
<input
id="chkAutoPlay"
v-model="selectedAutoPlay"
class="uk-checkbox"
type="checkbox"
@change="onChange($event)"
/>
<hr /> <hr />
<div class="uk-grid"> <div class="grid xl:grid-cols-5 sm:grid-cols-4 grid-cols-1">
<div v-if="comments" ref="comments" class="uk-width-4-5@xl uk-width-3-4@s uk-width-1"> <div v-if="comments" ref="comments" class="xl:col-span-4 sm:col-span-3">
<div <Comment
v-for="comment in comments.comments" v-for="comment in comments.comments"
:key="comment.commentId" :key="comment.commentId"
class="uk-tile-default uk-align-left uk-width-expand" :comment="comment"
:style="[{ background: backgroundColor }]" :uploader="video.uploader"
> :video-id="getVideoId()"
<Comment :comment="comment" :uploader="video.uploader" :video-id="getVideoId()" /> />
</div>
</div> </div>
<div v-if="video" class="uk-width-1-5@xl uk-width-1-4@s uk-width-1 uk-flex-last@s uk-flex-first"> <div v-if="video" class="order-first sm:order-last">
<a class="uk-button uk-button-small uk-margin-small-bottom uk-hidden@s" @click="showRecs = !showRecs"> <a
{{ showRecs ? $t("actions.minimize_recommendations") : $t("actions.show_recommendations") }} class="btn mb-2 sm:hidden"
</a> @click="showRecs = !showRecs"
<div v-text="$t(`actions.${showRecs ? 'minimize_recommendations' : 'show_recommendations'}`)"
v-for="related in video.relatedStreams" />
v-show="showRecs || !smallView" <hr v-show="showRecs" />
:key="related.url" <div v-show="showRecs || !smallView">
class="uk-tile-default uk-width-auto" <VideoItem
:style="[{ background: backgroundColor }]" v-for="related in video.relatedStreams"
> :key="related.url"
<VideoItem :video="related" height="94" width="168" /> :video="related"
height="94"
width="168"
/>
</div> </div>
<hr class="uk-hidden@s" /> <hr class="sm:hidden" />
</div> </div>
</div> </div>
</div> </div>
@ -207,7 +200,7 @@ export default {
var tx = window.db.transaction("watch_history", "readwrite"); var tx = window.db.transaction("watch_history", "readwrite");
var store = tx.objectStore("watch_history"); var store = tx.objectStore("watch_history");
var request = store.get(videoId); var request = store.get(videoId);
request.onsuccess = function(event) { request.onsuccess = function (event) {
var video = event.target.result; var video = event.target.result;
if (video) { if (video) {
video.watchedAt = Date.now(); video.watchedAt = Date.now();

View File

@ -34,9 +34,7 @@ library.add(
faTv, faTv,
); );
import("uikit/dist/css/uikit-core.css"); import router from "@/router/router.js";
import router from "@/router/router";
import App from "./App.vue"; import App from "./App.vue";
import DOMPurify from "dompurify"; import DOMPurify from "dompurify";
@ -49,6 +47,7 @@ TimeAgo.addDefaultLocale(en);
import { createI18n } from "vue-i18n"; import { createI18n } from "vue-i18n";
import enLocale from "@/locales/en.json"; import enLocale from "@/locales/en.json";
import "windi.css";
const timeAgo = new TimeAgo("en-US"); const timeAgo = new TimeAgo("en-US");
@ -56,8 +55,8 @@ import("./registerServiceWorker");
const mixin = { const mixin = {
methods: { methods: {
timeFormat: function(duration) { timeFormat: function (duration) {
var pad = function(num, size) { var pad = function (num, size) {
return ("000" + num).slice(size * -1); return ("000" + num).slice(size * -1);
}; };
@ -94,7 +93,7 @@ const mixin = {
num = parseInt(num); num = parseInt(num);
return num.toLocaleString("en-US"); return num.toLocaleString("en-US");
}, },
fetchJson: function(url, params, options) { fetchJson: function (url, params, options) {
if (params) { if (params) {
url = new URL(url); url = new URL(url);
for (var param in params) url.searchParams.set(param, params[param]); for (var param in params) url.searchParams.set(param, params[param]);
@ -147,18 +146,11 @@ const mixin = {
apiUrl() { apiUrl() {
return this.getPreferenceString("instance", "https://pipedapi.kavin.rocks"); return this.getPreferenceString("instance", "https://pipedapi.kavin.rocks");
}, },
getEffectiveTheme() {
var theme = this.getPreferenceString("theme", "dark");
if (theme === "auto")
theme =
window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
return theme;
},
getAuthToken() { getAuthToken() {
return this.getPreferenceString("authToken" + this.hashCode(this.apiUrl())); return this.getPreferenceString("authToken" + this.hashCode(this.apiUrl()));
}, },
hashCode(s) { hashCode(s) {
return s.split("").reduce(function(a, b) { return s.split("").reduce(function (a, b) {
a = (a << 5) - a + b.charCodeAt(0); a = (a << 5) - a + b.charCodeAt(0);
return a & a; return a & a;
}, 0); }, 0);
@ -170,7 +162,7 @@ const mixin = {
const regex = /(((https?:\/\/)|(www\.))[^\s]+)/g; const regex = /(((https?:\/\/)|(www\.))[^\s]+)/g;
if (!string) return ""; if (!string) return "";
return string.replace(regex, url => { return string.replace(regex, url => {
return `<a class="uk-button uk-button-text" href="${url}" target="_blank">${url}</a>`; return `<a href="${url}" target="_blank">${url}</a>`;
}); });
}, },
async updateWatched(videos) { async updateWatched(videos) {
@ -179,7 +171,7 @@ const mixin = {
var store = tx.objectStore("watch_history"); var store = tx.objectStore("watch_history");
videos.map(async video => { videos.map(async video => {
var request = store.get(video.url.substr(-11)); var request = store.get(video.url.substr(-11));
request.onsuccess = function(event) { request.onsuccess = function (event) {
if (event.target.result) { if (event.target.result) {
video.watched = true; video.watched = true;
} }
@ -189,20 +181,8 @@ const mixin = {
}, },
}, },
computed: { computed: {
backgroundColor() { theme() {
return this.getEffectiveTheme() === "light" ? "#fff" : "#0b0e0f"; return this.getPreferenceString("theme", "dark");
},
secondaryBackgroundColor() {
return this.getEffectiveTheme() === "light" ? "#e5e5e5" : "#242727";
},
foregroundColor() {
return this.getEffectiveTheme() === "light" ? "#15191a" : "#0b0e0f";
},
secondaryForegroundColor() {
return this.getEffectiveTheme() === "light" ? "#666" : "#393d3d";
},
darkMode() {
return this.getEffectiveTheme() !== "light";
}, },
authenticated(_this) { authenticated(_this) {
return _this.getAuthToken() !== undefined; return _this.getAuthToken() !== undefined;

View File

@ -1,33 +1,7 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import { register } from "register-service-worker"; import { registerSW } from "virtual:pwa-register";
if (process.env.NODE_ENV === "production") { if (process.env.NODE_ENV === "production") {
register(`/service-worker.js`, { registerSW();
ready() {
console.log(
"App is being served from cache by a service worker.\n" +
"For more details, visit https://goo.gl/AFskqB",
);
},
registered() {
console.log("Service worker has been registered.");
},
cached() {
console.log("Content has been cached for offline use.");
},
updatefound() {
console.log("New content is downloading.");
},
updated() {
console.log("New content is available; please refresh.");
window.location.reload();
},
offline() {
console.log("No internet connection found. App is running in offline mode.");
},
error(error) {
console.error("Error during service worker registration:", error);
},
});
} }

View File

@ -70,7 +70,7 @@ const routes = [
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
routes, routes,
scrollBehavior: function(_to, _from, savedPosition) { scrollBehavior: function (_to, _from, savedPosition) {
return savedPosition ? savedPosition : window.scrollTo(0, 0); return savedPosition ? savedPosition : window.scrollTo(0, 0);
}, },
}); });

View File

@ -1,11 +1,13 @@
// Based of https://github.com/GilgusMaximus/yt-dash-manifest-generator/blob/master/src/DashGenerator.js // Based of https://github.com/GilgusMaximus/yt-dash-manifest-generator/blob/master/src/DashGenerator.js
const xml = require("xml-js"); import { Buffer } from "buffer";
window.Buffer = Buffer;
import { json2xml } from "xml-js";
const DashUtils = { const DashUtils = {
generate_dash_file_from_formats(VideoFormats, VideoLength) { generate_dash_file_from_formats(VideoFormats, VideoLength) {
const generatedJSON = this.generate_xmljs_json_from_data(VideoFormats, VideoLength); const generatedJSON = this.generate_xmljs_json_from_data(VideoFormats, VideoLength);
return xml.json2xml(generatedJSON); return json2xml(generatedJSON);
}, },
generate_xmljs_json_from_data(VideoFormatArray, VideoLength) { generate_xmljs_json_from_data(VideoFormatArray, VideoLength) {
const convertJSON = { const convertJSON = {

55
vite.config.js Normal file
View File

@ -0,0 +1,55 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import WindiCSS from "vite-plugin-windicss";
import vueI18n from "@intlify/vite-plugin-vue-i18n";
import { VitePWA } from "vite-plugin-pwa";
import path from "path";
import eslintPlugin from "vite-plugin-eslint";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
WindiCSS(),
vueI18n({
include: path.resolve(__dirname, "./src/locales/**"),
}),
VitePWA({
registerType: "autoUpdate",
workbox: {
globPatterns: ["**/*.{js,css,html,ico,svg,png}", "manifest.webmanifest"],
},
manifest: {
name: "Piped",
short_name: "Piped",
background_color: "#000000",
theme_color: "#fa4b4b",
icons: [
{ src: "./img/icons/android-chrome-192x192.png", sizes: "192x192", type: "image/png" },
{ src: "./img/icons/android-chrome-512x512.png", sizes: "512x512", type: "image/png" },
{
src: "./img/icons/android-chrome-maskable-192x192.png",
sizes: "192x192",
type: "image/png",
purpose: "maskable",
},
{
src: "./img/icons/android-chrome-maskable-512x512.png",
sizes: "512x512",
type: "image/png",
purpose: "maskable",
},
],
},
}),
eslintPlugin(),
],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
build: {
sourcemap: true,
},
});

View File

@ -1,37 +0,0 @@
module.exports = {
pwa: {
name: "Piped",
themeColor: "#fa4b4b",
msTileColor: "#000000",
appleMobileWebAppCapable: "yes",
appleMobileWebAppStatusBarStyle: "black",
workboxPluginMode: "GenerateSW",
workboxOptions: {
navigateFallback: "index.html",
skipWaiting: true,
importWorkboxFrom: "local",
runtimeCaching: [
{
urlPattern: /\.(?:png|svg|ico)$/,
handler: "CacheFirst",
},
],
},
},
configureWebpack: {
resolve: {
alias: {
"vue-i18n": "vue-i18n/dist/vue-i18n.runtime.esm-bundler.js",
},
},
},
pluginOptions: {
i18n: {
locale: "en",
fallbackLocale: "en",
localeDir: "locales",
fullInstall: true,
enableLegacy: false,
},
},
};

23
windi.config.js Normal file
View File

@ -0,0 +1,23 @@
module.exports = {
darkMode: "media",
theme: {
extend: {
fontFamily: {
sans: [
"-apple-system",
"BlinkMacSystemFont",
"Segoe UI",
"Roboto",
"Helvetica Neue",
"Arial",
"Noto Sans",
"sans-serif",
"Apple Color Emoji",
"Segoe UI Emoji",
"Segoe UI Symbol",
"Noto Color Emoji",
],
},
},
},
};

8987
yarn.lock

File diff suppressed because it is too large Load Diff