mirror of
https://github.com/mmjee/Piped-Material.git
synced 2024-12-06 19:26:36 +01:00
Implement PWA, client-side Watch History and cleanup
Squashed commit of the following: commita1c18c7903Author: root <root@git.maharshi.ninja> Date: Sun Aug 22 21:46:45 2021 +0530 Minor UI change commit82783a4fb2Author: root <root@git.maharshi.ninja> Date: Sun Aug 22 21:23:15 2021 +0530 Format date and time the Right Way (™️) commit1b0d87e81dAuthor: root <root@git.maharshi.ninja> Date: Sun Aug 22 21:17:41 2021 +0530 A few fixes and a basic implementation commit7e5d057d37Author: root <root@git.maharshi.ninja> Date: Sun Aug 22 19:58:10 2021 +0530 Add PouchDB and add the basic design commit16a07c10ffAuthor: root <root@git.maharshi.ninja> Date: Sun Aug 22 18:53:34 2021 +0530 Delete robots.txt commit83e8690dc5Author: root <root@git.maharshi.ninja> Date: Sun Aug 22 18:52:00 2021 +0530 Add PWA configuration
This commit is contained in:
@@ -56,6 +56,11 @@ export default {
|
||||
id: 'trending',
|
||||
name: 'Trending',
|
||||
to: '/'
|
||||
},
|
||||
{
|
||||
id: 'watch-history',
|
||||
name: 'Watch History',
|
||||
to: '/watch-history'
|
||||
}
|
||||
]
|
||||
}),
|
||||
|
||||
@@ -3,15 +3,16 @@
|
||||
<v-img
|
||||
:height="height"
|
||||
:width="width"
|
||||
:src="video.thumbnail"
|
||||
:src="video.thumbnail || video.thumbnailUrl"
|
||||
alt="thumbnail"
|
||||
loading="lazy"
|
||||
/>
|
||||
<v-card-title class="subtitle-1">{{ video.title }}</v-card-title>
|
||||
<v-card-text>
|
||||
<router-link :to="video.uploaderUrl" class="subtitle-1 text-decoration-none" v-if="video.uploaderUrl && video.uploaderName && !hideChannel" custom v-slot="{ navigate }">
|
||||
<h5 @click="navigate" @keypress.enter="navigate" role="link">{{ video.uploaderName }}</h5>
|
||||
<router-link :to="video.uploaderUrl" class="subtitle-1 text-decoration-none" v-if="video.uploaderUrl && (video.uploaderName || video.uploader) && !hideChannel" custom v-slot="{ navigate }">
|
||||
<h5 @click="navigate" @keypress.enter="navigate" role="link">{{ video.uploaderName || video.uploader }}</h5>
|
||||
</router-link>
|
||||
<slot />
|
||||
{{ numberFormat(video.views) }} views <br />
|
||||
{{ video.uploadedDate }} <br />
|
||||
{{ timeFormat(video.duration) }}
|
||||
|
||||
@@ -3,6 +3,7 @@ import App from './App.vue'
|
||||
import vuetify from './plugins/vuetify'
|
||||
import store from './store'
|
||||
import router from './router'
|
||||
import './registerServiceWorker'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export default new Vuetify({
|
||||
warning: colors.deepOrange.base,
|
||||
info: colors.blue.base,
|
||||
success: colors.green.base
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import { register } from 'register-service-worker'
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
register(`${process.env.BASE_URL}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.')
|
||||
},
|
||||
offline () {
|
||||
console.log('No internet connection found. App is running in offline mode.')
|
||||
},
|
||||
error (error) {
|
||||
console.error('Error during service worker registration:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -42,6 +42,10 @@ const routes = [
|
||||
{
|
||||
path: '/:videoId([a-zA-Z0-9_-]{11})',
|
||||
component: () => import('@/components/VideoRedirect')
|
||||
},
|
||||
{
|
||||
path: '/watch-history',
|
||||
component: () => import('@/routes/WatchHistory')
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<v-progress-linear height="1vh" indeterminate v-if="loaded === false" />
|
||||
<v-container fluid v-else>
|
||||
<v-row>
|
||||
<v-col md="2">
|
||||
<v-btn outlined @click="deleteWatchHistory" x-large>Delete History</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-for="(chunk, chunkId) in chunkedByFour" :key="chunkId">
|
||||
<v-col md="3" v-for="doc in chunk" :key="doc._id">
|
||||
<VideoItem :video="doc.video">
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<span v-bind="attrs" v-on="on">
|
||||
Watched {{ doc.timeAgo }}
|
||||
</span>
|
||||
</template>
|
||||
<span>{{ doc.formattedDate }}</span>
|
||||
</v-tooltip><br />
|
||||
</VideoItem>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _chunk from 'lodash-es/chunk'
|
||||
|
||||
import { deleteWatchedVideos, getWatchedVideos } from '@/store/watched-videos-db'
|
||||
import { LibPiped } from '@/tools/libpiped'
|
||||
import VideoItem from '@/components/VideoItem'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
VideoItem
|
||||
},
|
||||
data: () => ({
|
||||
loaded: false,
|
||||
data: null,
|
||||
headers: [
|
||||
{
|
||||
text: 'Title',
|
||||
value: 'video.title'
|
||||
},
|
||||
{
|
||||
text: 'Date & Time',
|
||||
value: 'timestamp'
|
||||
},
|
||||
{
|
||||
text: 'URL',
|
||||
value: 'url'
|
||||
}
|
||||
]
|
||||
}),
|
||||
|
||||
methods: {
|
||||
async loadData () {
|
||||
this.data = (await getWatchedVideos()).map(doc => {
|
||||
doc.timeAgo = LibPiped.timeAgo(doc.timestamp)
|
||||
doc.formattedDate = LibPiped.formatFullDate(doc.timestamp)
|
||||
return doc
|
||||
})
|
||||
this.loaded = true
|
||||
},
|
||||
|
||||
async deleteWatchHistory () {
|
||||
await deleteWatchedVideos()
|
||||
await this.loadData()
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
chunkedByFour () {
|
||||
return _chunk(this.data, 4)
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.loadData().catch(e => console.error(e))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -95,6 +95,7 @@ import Player from '@/components/Player.vue'
|
||||
import VideoItem from '@/components/VideoItem.vue'
|
||||
import ErrorHandler from '@/components/ErrorHandler.vue'
|
||||
import VideoComment from '@/components/VideoComment'
|
||||
import { addWatchedVideo } from '@/store/watched-videos-db'
|
||||
|
||||
export default {
|
||||
name: 'WatchVideo',
|
||||
@@ -196,8 +197,8 @@ export default {
|
||||
})
|
||||
},
|
||||
|
||||
async getVideoData () {
|
||||
await this.fetchVideo()
|
||||
getVideoData () {
|
||||
return this.fetchVideo()
|
||||
.then(data => {
|
||||
this.video = data
|
||||
})
|
||||
@@ -213,6 +214,8 @@ export default {
|
||||
.replaceAll('\n', '<br>')
|
||||
)
|
||||
}
|
||||
}).then(() => {
|
||||
return addWatchedVideo(this.video, this.$route.fullPath)
|
||||
})
|
||||
},
|
||||
async getSponsors () {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import crypto from 'crypto'
|
||||
|
||||
import PouchDB from 'pouchdb'
|
||||
|
||||
function generateRandomID () {
|
||||
return crypto.randomBytes(16).toString('hex')
|
||||
}
|
||||
|
||||
export const WatchedVideosDB = new PouchDB('WatchedVideosDB', {
|
||||
auto_compaction: true
|
||||
})
|
||||
|
||||
export async function addWatchedVideo (videoObj, currentUrl) {
|
||||
return WatchedVideosDB.put({
|
||||
_id: generateRandomID(),
|
||||
video: videoObj,
|
||||
url: currentUrl,
|
||||
timestamp: new Date()
|
||||
})
|
||||
}
|
||||
|
||||
export async function getWatchedVideos () {
|
||||
const data = await WatchedVideosDB.allDocs({
|
||||
include_docs: true
|
||||
})
|
||||
return data.rows.map(row => row.doc).map(doc => {
|
||||
doc.timestamp = new Date(doc.timestamp)
|
||||
return doc
|
||||
}).sort((a, b) => {
|
||||
return b.timestamp - a.timestamp
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteWatchedVideos () {
|
||||
const docs = await getWatchedVideos()
|
||||
await WatchedVideosDB.bulkDocs(docs.map(doc => ({
|
||||
_id: doc._id,
|
||||
_rev: doc._rev,
|
||||
_deleted: true
|
||||
})))
|
||||
}
|
||||
@@ -7,6 +7,11 @@ TimeAgo.addDefaultLocale(en)
|
||||
const timeAgo = new TimeAgo('en-US')
|
||||
|
||||
class LibPiped {
|
||||
intlDTF = new Intl.DateTimeFormat([], {
|
||||
dateStyle: 'full',
|
||||
timeStyle: 'full'
|
||||
})
|
||||
|
||||
timeFormat (duration) {
|
||||
const pad = function (num, size) {
|
||||
return ('000' + num).slice(size * -1)
|
||||
@@ -26,6 +31,10 @@ class LibPiped {
|
||||
return str
|
||||
}
|
||||
|
||||
formatFullDate (date) {
|
||||
return this.intlDTF.format(date)
|
||||
}
|
||||
|
||||
numberFormat (num) {
|
||||
const digits = 2
|
||||
const si = [
|
||||
|
||||
Reference in New Issue
Block a user