mirror of
https://github.com/TeamPiped/Piped.git
synced 2024-11-26 23:47:25 +00:00
Merge pull request #683 from TeamPiped/windicss
Replace UIKit with Windicss
This commit is contained in:
commit
46ad4d1d27
26
.github/workflows/ci.yml
vendored
26
.github/workflows/ci.yml
vendored
@ -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
|
||||
|
66
.github/workflows/docker-build.yml
vendored
66
.github/workflows/docker-build.yml
vendored
@ -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
|
||||
|
@ -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:
|
@ -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
28
index.html
Normal 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>
|
26
package.json
26
package.json
@ -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,
|
||||
|
@ -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>
|
143
src/App.vue
143
src/App.vue
@ -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>
|
||||
|
||||
<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>
|
||||
|
@ -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>
|
||||
|
@ -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> {{ $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
|
||||
> <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>
|
||||
|
||||
<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'" />
|
||||
|
||||
<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'" />
|
||||
|
||||
<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'" />
|
||||
|
||||
<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>
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -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");
|
||||
|
@ -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.
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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" /> {{ $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);
|
||||
|
@ -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>
|
||||
|
@ -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 }} <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 }} <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>
|
||||
|
@ -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>
|
||||
|
44
src/components/Sorting.vue
Normal file
44
src/components/Sorting.vue
Normal 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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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 }} <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: {
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>{{ $t("actions.loading") }}</div>
|
||||
<div v-text="$t('actions.loading')" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -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") }} </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
|
||||
> <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
|
||||
>
|
||||
<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
|
||||
>
|
||||
<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();
|
||||
|
40
src/main.js
40
src/main.js
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
@ -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
55
vite.config.js
Normal 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,
|
||||
},
|
||||
});
|
@ -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
23
windi.config.js
Normal 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",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
Loading…
Reference in New Issue
Block a user