Skip to content

Commit

Permalink
Add:Create media item shares with expiration #1768
Browse files Browse the repository at this point in the history
  • Loading branch information
advplyr committed Jun 22, 2024
1 parent e52b695 commit d6eae9b
Show file tree
Hide file tree
Showing 12 changed files with 801 additions and 104 deletions.
195 changes: 195 additions & 0 deletions client/components/modals/ShareModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
<template>
<modals-modal ref="modal" v-model="show" name="share" :width="600" :height="'unset'" :processing="processing">
<template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
<p class="text-3xl text-white truncate">Share media item</p>
</div>
</template>
<div class="px-6 py-8 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
<template v-if="currentShare">
<div class="w-full py-2">
<label class="px-1 text-sm font-semibold block">Share URL</label>
<ui-text-input v-model="currentShareUrl" readonly class="text-base h-10" />
</div>
<div class="w-full py-2 px-1">
<p v-if="currentShare.expiresAt" class="text-base">Expires in {{ currentShareTimeRemaining }}</p>
<p v-else>Permanent</p>
</div>
</template>
<template v-else>
<div class="flex items-center justify-between space-x-4">
<div class="w-40">
<label class="px-1 text-sm font-semibold block">Slug</label>
<ui-text-input v-model="newShareSlug" class="text-base h-10" />
</div>
<div class="flex-grow" />
<div class="w-80">
<label class="px-1 text-sm font-semibold block">Share Duration</label>
<div class="inline-flex items-center space-x-2">
<div>
<ui-icon-btn icon="remove" :size="10" @click="clickMinus" />
</div>
<ui-text-input v-model="newShareDuration" type="number" text-center no-spinner class="text-center w-28 h-10 text-base" />
<div>
<ui-icon-btn icon="add" :size="10" @click="clickPlus" />
</div>
<ui-dropdown v-model="shareDurationUnit" :items="durationUnits" />
</div>
</div>
</div>
<p class="text-sm text-gray-300 py-4 px-1">
Share URL will be: <span class="">{{ demoShareUrl }}</span>
</p>
</template>
<div class="flex items-center pt-6">
<div class="flex-grow" />
<ui-btn v-if="currentShare" color="error" small @click="deleteShare">{{ $strings.ButtonDelete }}</ui-btn>
<ui-btn v-if="!currentShare" color="success" small @click="openShare">{{ $strings.ButtonShare }}</ui-btn>
</div>
</div>
</modals-modal>
</template>

<script>
export default {
props: {
value: Boolean,
libraryItem: {
type: Object,
default: () => null
},
mediaItemShare: {
type: Object,
default: () => null
}
},
data() {
return {
processing: false,
newShareSlug: '',
newShareDuration: 0,
currentShare: null,
shareDurationUnit: 'minutes',
durationUnits: [
{
text: 'Minutes',
value: 'minutes'
},
{
text: 'Hours',
value: 'hours'
},
{
text: 'Days',
value: 'days'
}
]
}
},
watch: {
show: {
handler(newVal) {
if (newVal) {
this.init()
}
}
}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
user() {
return this.$store.state.user.user
},
demoShareUrl() {
return `${window.origin}/share/${this.newShareSlug}`
},
currentShareUrl() {
if (!this.currentShare) return ''
return `${window.origin}/share/${this.currentShare.slug}`
},
currentShareTimeRemaining() {
if (!this.currentShare) return 'Error'
if (!this.currentShare.expiresAt) return 'Permanent'
const msRemaining = new Date(this.currentShare.expiresAt).valueOf() - Date.now()
if (msRemaining <= 0) return 'Expired'
return this.$elapsedPretty(msRemaining / 1000, true)
},
expireDurationSeconds() {
let shareDuration = Number(this.newShareDuration)
if (!shareDuration || isNaN(shareDuration)) return 0
return this.newShareDuration * (this.shareDurationUnit === 'minutes' ? 60 : this.shareDurationUnit === 'hours' ? 3600 : 86400)
}
},
methods: {
clickPlus() {
this.newShareDuration++
},
clickMinus() {
if (this.newShareDuration > 0) {
this.newShareDuration--
}
},
deleteShare() {
if (!this.currentShare) return
this.processing = true
this.$axios
.$delete(`/api/share/mediaitem/${this.currentShare.id}`)
.then(() => {
this.currentShare = null
this.$emit('removed')
})
.catch((error) => {
console.error('deleteShare', error)
let errorMsg = error.response?.data || 'Failed to delete share'
this.$toast.error(errorMsg)
})
.finally(() => {
this.processing = false
})
},
openShare() {
if (!this.newShareSlug) {
this.$toast.error('Slug is required')
return
}
const payload = {
slug: this.newShareSlug,
mediaItemType: 'book',
mediaItemId: this.libraryItem.media.id,
expiresAt: this.expireDurationSeconds ? Date.now() + this.expireDurationSeconds * 1000 : 0
}
this.processing = true
this.$axios
.$post(`/api/share/mediaitem`, payload)
.then((data) => {
this.currentShare = data
this.$emit('opened', data)
})
.catch((error) => {
console.error('openShare', error)
let errorMsg = error.response?.data || 'Failed to share item'
this.$toast.error(errorMsg)
})
.finally(() => {
this.processing = false
})
},
init() {
this.newShareSlug = this.$randomId(10)
if (this.mediaItemShare) {
this.currentShare = { ...this.mediaItemShare }
} else {
this.currentShare = null
}
}
},
mounted() {}
}
</script>
24 changes: 21 additions & 3 deletions client/pages/item/_id/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@
</div>
</div>

<modals-share-modal v-model="showShareModal" :media-item-share="mediaItemShare" :library-item="libraryItem" @opened="openedShare" @removed="removedShare" />
<modals-podcast-episode-feed v-model="showPodcastEpisodeFeed" :library-item="libraryItem" :episodes="podcastFeedEpisodes" />
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :library-item-id="libraryItemId" hide-create @select="selectBookmark" />
</div>
Expand All @@ -160,7 +161,7 @@ export default {
}
// Include episode downloads for podcasts
var item = await app.$axios.$get(`/api/items/${params.id}?expanded=1&include=downloads,rssfeed`).catch((error) => {
var item = await app.$axios.$get(`/api/items/${params.id}?expanded=1&include=downloads,rssfeed,share`).catch((error) => {
console.error('Failed', error)
return false
})
Expand All @@ -170,7 +171,8 @@ export default {
}
return {
libraryItem: item,
rssFeed: item.rssFeed || null
rssFeed: item.rssFeed || null,
mediaItemShare: item.mediaItemShare || null
}
},
data() {
Expand All @@ -184,7 +186,8 @@ export default {
episodeDownloadsQueued: [],
showBookmarksModal: false,
isDescriptionClamped: false,
showFullDescription: false
showFullDescription: false,
showShareModal: false
}
},
computed: {
Expand Down Expand Up @@ -437,6 +440,13 @@ export default {
})
}
if (this.userIsAdminOrUp && !this.isPodcast) {
items.push({
text: 'Share',
action: 'share'
})
}
if (this.userCanDelete) {
items.push({
text: this.$strings.ButtonDelete,
Expand All @@ -448,6 +458,12 @@ export default {
}
},
methods: {
openedShare(mediaItemShare) {
this.mediaItemShare = mediaItemShare
},
removedShare() {
this.mediaItemShare = null
},
selectBookmark(bookmark) {
if (!bookmark) return
if (this.isStreaming) {
Expand Down Expand Up @@ -761,6 +777,8 @@ export default {
this.deleteLibraryItem()
} else if (action === 'sendToDevice') {
this.sendToDevice(data)
} else if (action === 'share') {
this.showShareModal = true
}
}
},
Expand Down
34 changes: 34 additions & 0 deletions client/pages/share/_slug.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<template>
<div id="page-wrapper" class="w-full h-screen overflow-y-auto">
<div class="w-full h-full flex items-center justify-center">
<p class="text-xl">{{ mediaItemShare.mediaItem.title }}</p>
</div>
</div>
</template>

<script>
export default {
layout: 'blank',
async asyncData({ params, error, app }) {
const mediaItemShare = await app.$axios.$get(`/public/share/${params.slug}`).catch((error) => {
console.error('Failed', error)
return null
})
if (!mediaItemShare) {
return error({ statusCode: 404, message: 'Not found' })
}
return {
mediaItemShare: mediaItemShare
}
},
data() {
return {}
},
computed: {},
methods: {},
mounted() {
console.log('Loaded media item share', this.mediaItemShare)
}
}
</script>
27 changes: 16 additions & 11 deletions client/plugins/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import Vue from 'vue'
import cronParser from 'cron-parser'
import { nanoid } from 'nanoid'

Vue.prototype.$randomId = () => nanoid()
Vue.prototype.$randomId = (len = null) => {
if (len && !isNaN(len)) return nanoid(len)
return nanoid()
}

Vue.prototype.$bytesPretty = (bytes, decimals = 2) => {
if (isNaN(bytes) || bytes == 0) {
Expand Down Expand Up @@ -119,7 +122,7 @@ Vue.prototype.$parseCronExpression = (expression) => {
value: '* * * * *'
}
]
const patternMatch = commonPatterns.find(p => p.value === expression)
const patternMatch = commonPatterns.find((p) => p.value === expression)
if (patternMatch) {
return {
description: patternMatch.text
Expand All @@ -132,21 +135,25 @@ Vue.prototype.$parseCronExpression = (expression) => {
if (pieces[2] !== '*' || pieces[3] !== '*') {
return null
}
if (pieces[4] !== '*' && pieces[4].split(',').some(p => isNaN(p))) {
if (pieces[4] !== '*' && pieces[4].split(',').some((p) => isNaN(p))) {
return null
}

const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
var weekdayText = 'day'
if (pieces[4] !== '*') weekdayText = pieces[4].split(',').map(p => weekdays[p]).join(', ')
if (pieces[4] !== '*')
weekdayText = pieces[4]
.split(',')
.map((p) => weekdays[p])
.join(', ')

return {
description: `Run every ${weekdayText} at ${pieces[1]}:${pieces[0].padStart(2, '0')}`
}
}

Vue.prototype.$getNextScheduledDate = (expression) => {
const interval = cronParser.parseExpression(expression);
const interval = cronParser.parseExpression(expression)
return interval.next().toDate()
}

Expand All @@ -171,10 +178,8 @@ Vue.prototype.$downloadFile = (url, filename = null, openInNewTab = false) => {

export function supplant(str, subs) {
// source: http://crockford.com/javascript/remedial.html
return str.replace(/{([^{}]*)}/g,
function (a, b) {
var r = subs[b]
return typeof r === 'string' || typeof r === 'number' ? r : a
}
)
return str.replace(/{([^{}]*)}/g, function (a, b) {
var r = subs[b]
return typeof r === 'string' || typeof r === 'number' ? r : a
})
}
Loading

0 comments on commit d6eae9b

Please sign in to comment.