Implement PWA, client-side Watch History and cleanup

Squashed commit of the following:

commit a1c18c7903
Author: root <root@git.maharshi.ninja>
Date:   Sun Aug 22 21:46:45 2021 +0530

    Minor UI change

commit 82783a4fb2
Author: root <root@git.maharshi.ninja>
Date:   Sun Aug 22 21:23:15 2021 +0530

    Format date and time the Right Way (™️)

commit 1b0d87e81d
Author: root <root@git.maharshi.ninja>
Date:   Sun Aug 22 21:17:41 2021 +0530

    A few fixes and a basic implementation

commit 7e5d057d37
Author: root <root@git.maharshi.ninja>
Date:   Sun Aug 22 19:58:10 2021 +0530

    Add PouchDB and add the basic design

commit 16a07c10ff
Author: root <root@git.maharshi.ninja>
Date:   Sun Aug 22 18:53:34 2021 +0530

    Delete robots.txt

commit 83e8690dc5
Author: root <root@git.maharshi.ninja>
Date:   Sun Aug 22 18:52:00 2021 +0530

    Add PWA configuration
This commit is contained in:
root
2021-08-28 03:27:49 +05:30
parent 0b9fded2ac
commit bbd1ce30ec
27 changed files with 752 additions and 17 deletions
+5
View File
@@ -56,6 +56,11 @@ export default {
id: 'trending',
name: 'Trending',
to: '/'
},
{
id: 'watch-history',
name: 'Watch History',
to: '/watch-history'
}
]
}),
+4 -3
View File
@@ -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) }}
+1
View File
@@ -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
+3 -3
View File
@@ -15,7 +15,7 @@ export default new Vuetify({
warning: colors.deepOrange.base,
info: colors.blue.base,
success: colors.green.base
},
},
},
}
}
}
})
+32
View File
@@ -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)
}
})
}
+4
View File
@@ -42,6 +42,10 @@ const routes = [
{
path: '/:videoId([a-zA-Z0-9_-]{11})',
component: () => import('@/components/VideoRedirect')
},
{
path: '/watch-history',
component: () => import('@/routes/WatchHistory')
}
]
+82
View File
@@ -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>
+5 -2
View File
@@ -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 () {
+41
View File
@@ -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
})))
}
+9
View File
@@ -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 = [