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
on:
pull_request:
push:
pull_request:
push:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2.4.0
- name: Setup Node.js
uses: actions/setup-node@v2.5.1
with:
cache: "yarn"
- run: yarn install --prefer-offline
- run: yarn build
- run: yarn lint --no-fix
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2.4.0
- name: Setup Node.js
uses: actions/setup-node@v2.5.1
with:
cache: "yarn"
- run: yarn install --prefer-offline
- run: yarn build
- run: yarn lint --no-fix

View File

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

View File

@ -15,9 +15,9 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v2.5.1
with:
cache: 'yarn'
cache: "yarn"
- 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
id: ipfs-add
with:

View File

@ -8,7 +8,7 @@ RUN yarn install --prefer-offline
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

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",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
"serve": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint --fix --color --ignore-path .gitignore --ext .js,.vue ."
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/vue-fontawesome": "^3.0.0-5",
"core-js": "3.20.1",
"css-loader": "^6.5.1",
"buffer": "^6.0.3",
"dompurify": "^2.3.4",
"hotkeys-js": "^3.8.7",
"javascript-time-ago": "^2.3.10",
"mux.js": "^6.0.1",
"register-service-worker": "^1.7.1",
"shaka-player": "3.3.0",
"uikit": "3.9.4",
"stream": "^0.0.2",
"vue": "^3.2.26",
"vue-i18n": "^9.2.0-beta.25",
"vue-router": "^4.0.12",
"xml-js": "^1.6.11"
},
"devDependencies": {
"@intlify/vue-i18n-loader": "^4.1.0",
"@vue/cli-plugin-babel": "^4.5.15",
"@vue/cli-plugin-eslint": "^4.5.15",
"@vue/cli-plugin-pwa": "^4.5.15",
"@vue/cli-service": "^4.5.15",
"@intlify/vite-plugin-vue-i18n": "^3.2.1",
"@vitejs/plugin-vue": "^2.0.1",
"@vue/compiler-sfc": "3.2.26",
"babel-eslint": "^10.1.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.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": {
"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>
<div
class="uk-container uk-container-expand uk-height-viewport"
:style="[{ background: backgroundColor, colour: foregroundColor }]"
:class="{ 'uk-light': darkMode }"
>
<div class="w-full min-h-screen px-1vw reset" :class="[theme]">
<Navigation />
<router-view v-slot="{ Component }">
<keep-alive :max="5">
@ -11,21 +7,20 @@
</keep-alive>
</router-view>
<div style="text-align: center">
<footer class="text-center">
<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>
&nbsp;
<a href="https://github.com/TeamPiped/Piped#donations">
<font-awesome-icon :icon="['fab', 'bitcoin']"></font-awesome-icon>
{{ $t("actions.donations") }}
<a class="ml-2" href="https://github.com/TeamPiped/Piped#donations">
<font-awesome-icon :icon="['fab', 'bitcoin']" />
<span v-text="$t('actions.donations')" />
</a>
</div>
</footer>
</div>
</template>
<script>
import Navigation from "@/components/Navigation";
import Navigation from "@/components/Navigation.vue";
export default {
components: {
Navigation,
@ -45,7 +40,7 @@ export default {
if (this.getPreferenceBoolean("watchHistory", false))
if ("indexedDB" in window) {
const request = indexedDB.open("piped-db", 1);
request.onupgradeneeded = function() {
request.onupgradeneeded = function () {
const db = request.result;
console.log("Upgrading object store.");
if (!db.objectStoreNames.contains("watch_history")) {
@ -61,18 +56,18 @@ export default {
const App = this;
(async function() {
(async function () {
const locale = App.getPreferenceString("hl", App.defaultLangage);
if (locale !== App.TimeAgoConfig.locale) {
const localeTime = await import("javascript-time-ago/locale/" + locale + ".json").then(
module => module.default,
);
const localeTime = await import(
"./../node_modules/javascript-time-ago/locale/" + locale + ".json"
).then(module => module.default);
App.TimeAgo.addLocale(localeTime);
App.TimeAgoConfig.locale = locale;
}
if (window.i18n.global.locale.value !== 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.locale.value = locale;
@ -93,7 +88,6 @@ b {
::-webkit-scrollbar {
background-color: #15191a;
color: #c5bcae;
}
::-webkit-scrollbar-thumb {
@ -114,13 +108,114 @@ b {
* {
scrollbar-color: #15191a #444a4e;
@apply font-sans;
}
.uk-grid > div {
padding-bottom: 1vh;
.video-grid {
@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 {
background: #222;
.btn {
@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>

View File

@ -2,27 +2,34 @@
<ErrorHandler v-if="channel && channel.error" :message="channel.message" :error="channel.error" />
<div v-if="channel" v-show="!channel.error">
<h1 class="uk-text-center">
<img height="48" width="48" class="uk-border-circle" :src="channel.avatarUrl" />{{ channel.name }}
</h1>
<img v-if="channel.bannerUrl" :src="channel.bannerUrl" style="width: 100%" loading="lazy" />
<div class="flex justify-center place-items-center">
<img height="48" width="48" class="rounded-full m-1" :src="channel.avatarUrl" />
<h1 v-text="channel.name" />
</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 -->
<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">
{{ subscribed ? $t("actions.unsubscribe") : $t("actions.subscribe") }}
</button>
<button
v-if="authenticated"
class="btn"
@click="subscribeHandler"
v-text="$t(`actions.${subscribed ? 'unsubscribe' : 'subscribe'}`)"
/>
<hr />
<div class="uk-grid uk-grid-xl">
<div
<div class="video-grid">
<VideoItem
v-for="video in channel.relatedStreams"
:key="video.url"
class="uk-width-1-2 uk-width-1-3@m uk-width-1-4@l uk-width-1-5@xl"
>
<VideoItem :video="video" height="94" width="168" hide-channel />
</div>
:video="video"
height="94"
width="168"
hide-channel
/>
</div>
</div>
</template>

View File

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

View File

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

View File

@ -1,50 +1,35 @@
<template>
<h1 v-t="'titles.feed'" class="uk-text-bold uk-text-center" />
<h1 v-t="'titles.feed'" class="font-bold text-center" />
<button
v-if="authenticated"
class="uk-button uk-button-small"
style="margin-right: 0.5rem"
type="button"
@click="exportHandler"
>
<router-link to="/subscriptions"> Subscriptions </router-link>
<button v-if="authenticated" class="btn mr-2" @click="exportHandler">
<router-link to="/subscriptions">Subscriptions</router-link>
</button>
<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 class="uk-align-right@m">
<label for="ddlSortBy">{{ $t("actions.sort_by") }}</label>
<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 class="md:float-right">
<Sorting by-key="uploaded" @apply="order => videos.sort(order)" />
</span>
<hr />
<div class="uk-grid uk-grid-xl">
<div
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 class="video-grid">
<VideoItem v-for="video in videos" :key="video.url" :video="video" />
</div>
</template>
<script>
import VideoItem from "@/components/VideoItem.vue";
import Sorting from "@/components/Sorting.vue";
export default {
components: {
VideoItem,
Sorting,
},
data() {
return {
@ -52,7 +37,6 @@ export default {
videoStep: 100,
videosStore: [],
videos: [],
selectedSort: "descending",
};
},
computed: {
@ -84,22 +68,6 @@ export default {
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() {
this.currentVideoCount = Math.min(this.currentVideoCount + this.videoStep, this.videosStore.length);
if (this.videos.length != this.videosStore.length)

View File

@ -1,31 +1,20 @@
<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">
<button class="uk-button" v-t="'actions.clear_history'" @click="clearHistory"></button>
</div>
<div class="flex">
<div>
<button class="btn" v-t="'actions.clear_history'" @click="clearHistory" />
</div>
<div style="text-align: right">
<label for="ddlSortBy">{{ $t("actions.sort_by") }}</label>
<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>
<div class="right-1">
<Sorting by-key="watchedAt" @apply="order => videos.sort(order)" />
</div>
</div>
<hr />
<div class="uk-grid uk-grid-xl">
<div
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 class="video-grid">
<VideoItem v-for="video in videos" :key="video.url" :video="video" />
</div>
<br />
@ -33,15 +22,16 @@
<script>
import VideoItem from "@/components/VideoItem.vue";
import Sorting from "@/components/Sorting.vue";
export default {
components: {
VideoItem,
Sorting,
},
data() {
return {
videos: [],
selectedSort: "descending",
};
},
mounted() {
@ -74,22 +64,6 @@ export default {
document.title = "Watch History - Piped";
},
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() {
if (window.db) {
var tx = window.db.transaction("watch_history", "readwrite");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,19 +1,15 @@
<template>
<div
class="uk-position-absolute uk-panel uk-box-shadow-large suggestions-container"
:style="[{ background: secondaryBackgroundColor }]"
>
<ul class="uk-list uk-margin-remove uk-text-secondary">
<div class="absolute suggestions-container">
<ul>
<li
v-for="(suggestion, i) in searchSuggestions"
:key="i"
:style="[selected === i ? { background: secondaryForegroundColor } : {}]"
class="uk-margin-remove suggestion"
class="suggestion"
:class="{ 'suggestion-selected': selected === i }"
@mouseover="onMouseOver(i)"
@mousedown.stop="onClick(i)"
>
{{ suggestion }}
</li>
v-text="suggestion"
/>
</ul>
</div>
</template>
@ -79,25 +75,30 @@ export default {
<style>
.suggestions-container {
left: 50%;
transform: translateX(-50%);
max-width: 640px;
width: 100%;
box-sizing: border-box;
padding: 5px 0;
z-index: 10;
@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;
}
.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 {
padding: 4px 15px;
}
@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);
}
@apply p-y-1;
}
</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>
<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">
<button v-if="authenticated" class="uk-button uk-button-small" style=" margin-right: 0.5rem" type="button">
<router-link to="/import">
{{ $t("actions.import_from_json") }}
</router-link>
<div v-if="authenticated">
<button class="btn mr-0.5">
<router-link to="/import" v-text="$t('actions.import_from_json')" />
</button>
<button
v-if="authenticated"
class="uk-button uk-button-small"
style="color: white"
type="button"
@click="exportHandler"
>
{{ $t("actions.export_to_json") }}
</button>
<button class="btn" @click="exportHandler" v-text="$t('actions.export_to_json')" />
</div>
<hr />
<div v-for="subscription in subscriptions" :key="subscription.url" style="text-align: center">
<div class="uk-text-primary" :style="[{ background: backgroundColor }]">
<a :href="subscription.url">
<img :src="subscription.avatar" class="uk-margin-small-right uk-border-circle" width="96" height="96" />
<span
class="uk-text-large"
style="width: 30rem; display: inline-block; text-align: center; margin-left: 6rem"
>{{ subscription.name }}</span
>
</a>
<button
class="uk-button uk-button-large"
style="background: #222; margin-left: 0.5rem; width: 185px"
type="button"
@click="handleButton(subscription)"
>
{{ subscription.subscribed ? $t("actions.unsubscribe") : $t("actions.subscribe") }}
</button>
<div class="grid">
<div class="mb-3" v-for="subscription in subscriptions" :key="subscription.url">
<div class="flex justify-center place-items-center">
<div class="w-full grid grid-cols-3">
<router-link :to="subscription.url" class="col-start-2 block flex text-center font-bold text-4xl">
<img :src="subscription.avatar" class="rounded-full" width="48" height="48" />
<span v-text="subscription.name" />
</router-link>
<button
class="btn !w-min"
@click="handleButton(subscription)"
v-text="$t(`actions.${subscription.subscribed ? 'unsubscribe' : 'subscribe'}`)"
/>
</div>
</div>
</div>
<br />
</div>
<br />
</template>

View File

@ -1,17 +1,10 @@
<template>
<h1 v-t="'titles.trending'" class="uk-text-bold uk-text-center" />
<h1 v-t="'titles.trending'" class="font-bold text-center" />
<hr />
<div class="uk-grid uk-grid-xl">
<div
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 class="video-grid">
<VideoItem v-for="video in videos" :key="video.url" :video="video" height="118" width="210" />
</div>
</template>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,33 +1,7 @@
/* eslint-disable no-console */
import { register } from "register-service-worker";
import { registerSW } from "virtual:pwa-register";
if (process.env.NODE_ENV === "production") {
register(`/service-worker.js`, {
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);
},
});
registerSW();
}

View File

@ -70,7 +70,7 @@ const routes = [
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior: function(_to, _from, savedPosition) {
scrollBehavior: function (_to, _from, savedPosition) {
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
const xml = require("xml-js");
import { Buffer } from "buffer";
window.Buffer = Buffer;
import { json2xml } from "xml-js";
const DashUtils = {
generate_dash_file_from_formats(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) {
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