Updated History WebUI to include information about each event.
This commit is contained in:
@@ -3,40 +3,336 @@
|
|||||||
<div class="column is-12 is-clearfix">
|
<div class="column is-12 is-clearfix">
|
||||||
<span class="title is-4">
|
<span class="title is-4">
|
||||||
<NuxtLink href="/history">History</NuxtLink>
|
<NuxtLink href="/history">History</NuxtLink>
|
||||||
: {{ id }}
|
: {{ data?.full_title ?? data?.title ?? id }}
|
||||||
</span>
|
</span>
|
||||||
<div class="is-pulled-right">
|
<div class="is-pulled-right" v-if="data?.via">
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
<p class="control">
|
<p class="control">
|
||||||
<button class="button is-info" @click="loadContent(id)">
|
<button class="button is-info" @click="loadContent(id)" :class="{'is-loading':isLoading}">
|
||||||
<span class="icon"><i class="fas fa-sync"></i></span>
|
<span class="icon"><i class="fas fa-sync"></i></span>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="subtitle">
|
<div class="subtitle" v-if="data?.via && getTitle !== data.title">
|
||||||
This page still not done, and will be updated at later stages.
|
{{ getTitle }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-12">
|
<div class="column is-12" v-if="!data?.via && isLoading">
|
||||||
<pre><code>{{ data }}</code></pre>
|
<Message>
|
||||||
|
<span class="icon"><i class="fas fa-spinner fa-pulse"></i></span>
|
||||||
|
<span>Loading data. Please wait...</span>
|
||||||
|
</Message>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-12" v-if="data?.via">
|
||||||
|
<div class="card" :class="{ 'is-success': parseInt(data.watched), 'is-danger': !data.watched }">
|
||||||
|
<header class="card-header">
|
||||||
|
<div class="card-header-title">Latest metadata info</div>
|
||||||
|
<div class="card-header-icon">
|
||||||
|
<button class="button is-small" @click="toggleWatched"
|
||||||
|
:class="{ 'is-success': !data.watched, 'is-danger': data.watched }">
|
||||||
|
<span class="icon" v-if="data.watched"><i class="fas fa-eye-slash"></i></span>
|
||||||
|
<span class="icon" v-else><i class="fas fa-eye"></i></span>
|
||||||
|
<span>Mark as <span v-if="data.watched">Unplayed</span><span v-else>played</span></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="columns is-multiline is-mobile">
|
||||||
|
<div class="column is-6 has-text-left">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon"><i class="fas fa-server"></i></span>
|
||||||
|
<span>
|
||||||
|
<span class="is-hidden-mobile">Via: </span>
|
||||||
|
<NuxtLink :href="`/backend/${data.via}`" v-text="data.via"/>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="column is-6 has-text-right">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon"><i class="fas fa-passport"></i></span>
|
||||||
|
<span>
|
||||||
|
<span class="is-hidden-mobile">ID:</span>
|
||||||
|
{{ data.id }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="column is-6 has-text-left">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-eye-slash" v-if="!data.watched"></i>
|
||||||
|
<i class="fas fa-eye" v-else></i>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<span class="is-hidden-mobile">Status:</span>
|
||||||
|
{{ data.watched ? 'Played' : 'Unplayed' }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="column is-6 has-text-right">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon"><i class="fas fa-envelope"></i></span>
|
||||||
|
<span>
|
||||||
|
<span class="is-hidden-mobile">Event:</span>
|
||||||
|
{{ ag(data.extra, `${data.via}.event`, 'Unknown') }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="column is-6 has-text-left">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon"><i class="fas fa-calendar"></i></span>
|
||||||
|
<span>
|
||||||
|
<span class="is-hidden-mobile">Updated:</span>
|
||||||
|
{{ moment(data.updated).fromNow() }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-6 has-text-right">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon" v-if="'episode' === data.type"><i class="fas fa-tv"></i></span>
|
||||||
|
<span class="icon" v-else><i class="fas fa-film"></i></span>
|
||||||
|
<span>
|
||||||
|
<span class="is-hidden-mobile">Type:</span>
|
||||||
|
{{ ucFirst(data.type) }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-6 has-text-left" v-if="'episode' === data.type">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon"><i class="fas fa-tv"></i></span>
|
||||||
|
<span><span class="is-hidden-mobile">Season:</span> {{ data.season }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-6 has-text-right" v-if="'episode' === data.type">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon"><i class="fas fa-tv"></i></span>
|
||||||
|
<span><span class="is-hidden-mobile">Episode:</span> {{ data.episode }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-12" v-if="data.guids && Object.keys(data.guids).length>0">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon"><i class="fas fa-link"></i></span>
|
||||||
|
<span>
|
||||||
|
{{ ucFirst(data.type) }} GUIDs:
|
||||||
|
<span class="tag mr-1" v-for="(guid,source) in data.guids">
|
||||||
|
{{ source.split('guid_')[1] }} : {{ guid }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-12" v-if="data.parent && Object.keys(data.parent).length>0">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon"><i class="fas fa-link"></i></span>
|
||||||
|
<span>
|
||||||
|
Series GUIDs:
|
||||||
|
<span class="tag mr-1" v-for="(guid,source) in data.parent">
|
||||||
|
{{ source.split('guid_')[1] }} : {{ guid }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-12" v-if="data?.via && Object.keys(data.metadata).length>0">
|
||||||
|
<div class="card" v-for="(item, key) in data.metadata" :key="key"
|
||||||
|
:class="{ 'is-success': parseInt(item.watched), 'is-danger': !parseInt(item.watched) }">
|
||||||
|
<header class="card-header">
|
||||||
|
<div class="card-header-title">
|
||||||
|
Metadata from
|
||||||
|
</div>
|
||||||
|
<div class="card-header-icon">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon"><i class="fas fa-server"></i></span>
|
||||||
|
<span>
|
||||||
|
<NuxtLink :href="`/backend/${key}`" v-text="key"/>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="columns is-multiline is-mobile">
|
||||||
|
<div class="column is-12 has-text-left">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon"><i class="fas fa-passport"></i></span>
|
||||||
|
<span>
|
||||||
|
<span class="is-hidden-mobile">ID:</span>
|
||||||
|
{{ item.id }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="column is-6 has-text-left">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-eye-slash" v-if="!parseInt(item.watched)"></i>
|
||||||
|
<i class="fas fa-eye" v-else></i>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<span class="is-hidden-mobile">Status:</span>
|
||||||
|
{{ parseInt(item.watched) ? 'Played' : 'Unplayed' }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="column is-6 has-text-right">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon"><i class="fas fa-envelope"></i></span>
|
||||||
|
<span>
|
||||||
|
<span class="is-hidden-mobile">Event:</span>
|
||||||
|
{{ ag(data.extra, `${key}.event`, 'Unknown') }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="column is-6 has-text-left">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon"><i class="fas fa-calendar"></i></span>
|
||||||
|
<span>
|
||||||
|
<span class="is-hidden-mobile">Updated:</span>
|
||||||
|
{{ moment(ag(data.extra, `${key}.received_at`, data.updated)).fromNow() }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-6 has-text-right">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon" v-if="'episode' === item.type"><i class="fas fa-tv"></i></span>
|
||||||
|
<span class="icon" v-else><i class="fas fa-film"></i></span>
|
||||||
|
<span>
|
||||||
|
<span class="is-hidden-mobile">Type:</span>
|
||||||
|
{{ ucFirst(item.type) }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-6 has-text-left" v-if="'episode' === item.type">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon"><i class="fas fa-tv"></i></span>
|
||||||
|
<span><span class="is-hidden-mobile">Season:</span> {{ item.season }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-6 has-text-right" v-if="'episode' === item.type">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon"><i class="fas fa-tv"></i></span>
|
||||||
|
<span><span class="is-hidden-mobile">Episode:</span> {{ item.episode }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-12" v-if="item.guids && Object.keys(item.guids).length>0">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon"><i class="fas fa-link"></i></span>
|
||||||
|
<span>
|
||||||
|
{{ ucFirst(item.type) }} GUIDs:
|
||||||
|
<span class="tag mr-1" v-for="(guid,source) in item.guids">
|
||||||
|
{{ source.split('guid_')[1] }} : {{ guid }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-12" v-if="item.parent && Object.keys(item.parent).length>0">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon"><i class="fas fa-link"></i></span>
|
||||||
|
<span>
|
||||||
|
Series GUIDs:
|
||||||
|
<span class="tag mr-1" v-for="(guid,source) in item.parent">
|
||||||
|
{{ source.split('guid_')[1] }} : {{ guid }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=""></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import request from '~/utils/request.js'
|
import request from '~/utils/request.js'
|
||||||
|
import {ag, notification, ucFirst} from '~/utils/index.js'
|
||||||
|
import moment from 'moment'
|
||||||
|
|
||||||
const id = useRoute().params.id
|
const id = useRoute().params.id
|
||||||
|
|
||||||
useHead({title: `History : ${id}`})
|
useHead({title: `History : ${id}`})
|
||||||
|
|
||||||
const data = ref();
|
const isLoading = ref(false)
|
||||||
|
|
||||||
|
const data = ref({
|
||||||
|
id: id,
|
||||||
|
title: `${id}`,
|
||||||
|
via: null,
|
||||||
|
metadata: {},
|
||||||
|
guids: {},
|
||||||
|
parent: {},
|
||||||
|
});
|
||||||
|
|
||||||
const loadContent = async (id) => {
|
const loadContent = async (id) => {
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
const response = await request(`/history/${id}`)
|
const response = await request(`/history/${id}`)
|
||||||
data.value = await response.json();
|
const json = await response.json()
|
||||||
|
|
||||||
|
isLoading.value = false
|
||||||
|
|
||||||
|
if (200 !== response.status) {
|
||||||
|
notification('Error', 'Error loading data', `${json.error.code}: ${json.error.message}`);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.value = json
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
if (!data.value) {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.value?.via) {
|
||||||
|
return ag(data.value, `metadata.${data.value.via}.extra.title`, data.value.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.value.title
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleWatched = async () => {
|
||||||
|
if (!data.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!confirm(`Mark '${data.value.full_title}' as ${data.value.watched ? 'unplayed' : 'played'}?`)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await request(`/history/${data.value.id}/watch`, {
|
||||||
|
method: data.value.watched ? 'DELETE' : 'POST'
|
||||||
|
})
|
||||||
|
|
||||||
|
const json = await response.json()
|
||||||
|
|
||||||
|
if (200 !== response.status) {
|
||||||
|
notification('error', 'Error', `${json.error.code}: ${json.error.message}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.value = json
|
||||||
|
notification('success', '', `Marked '${data.value.full_title}' as ${data.value.watched ? 'played' : 'unplayed'}`)
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
notification('error', 'Error', `Failed to update watched status. ${e}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => loadContent(id))
|
onMounted(async () => loadContent(id))
|
||||||
|
|||||||
@@ -117,10 +117,12 @@
|
|||||||
<div class="column is-6-tablet" v-for="item in items" :key="item.id">
|
<div class="column is-6-tablet" v-for="item in items" :key="item.id">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<header class="card-header">
|
<header class="card-header">
|
||||||
<p class="card-header-title is-text-overflow is-justify-center pr-1">
|
<p class="card-header-title is-text-overflow pr-1">
|
||||||
<NuxtLink :to="'/history/'+item.id">
|
<span class="icon" v-if="!item.progress">
|
||||||
{{ item.full_title ?? item.title }}
|
<i class="fas fa-eye-slash" v-if="!item.watched"></i>
|
||||||
</NuxtLink>
|
<i class="fas fa-eye" v-else></i>
|
||||||
|
</span>
|
||||||
|
<NuxtLink :to="'/history/'+item.id" v-text="item.full_title ?? item.title"/>
|
||||||
</p>
|
</p>
|
||||||
<span class="card-header-icon">
|
<span class="card-header-icon">
|
||||||
<span class="icon" v-if="'episode' === item.type"><i class="fas fa-tv"></i></span>
|
<span class="icon" v-if="'episode' === item.type"><i class="fas fa-tv"></i></span>
|
||||||
@@ -129,29 +131,35 @@
|
|||||||
</header>
|
</header>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="columns is-multiline is-mobile has-text-centered">
|
<div class="columns is-multiline is-mobile has-text-centered">
|
||||||
<div class="column is-6-mobile">
|
<div class="column is-4-tablet is-6-mobile has-text-left">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon"><i class="fas fa-calendar"></i> </span>
|
||||||
{{ moment(item.updated).fromNow() }}
|
{{ moment(item.updated).fromNow() }}
|
||||||
</div>
|
|
||||||
<div class="column is-6-mobile">
|
|
||||||
<NuxtLink :href="'/backend/'+item.via">
|
|
||||||
{{ item.via }}
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
<div class="column is-6-mobile" v-if="item.event">
|
|
||||||
<span v-tooltip="'The event which triggered the update.'" class="has-tooltip">
|
|
||||||
{{ item.event }}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-6-mobile">
|
|
||||||
|
<div class="column is-4-tablet is-6-mobile has-text-right-mobile">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon"><i class="fas fa-server"></i></span>
|
||||||
|
<span>
|
||||||
|
<NuxtLink :href="'/backend/'+item.via" v-text="item.via"/>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="column is-4-tablet is-12-mobile has-text-left-mobile">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon"><i class="fas fa-envelope"></i></span>
|
||||||
|
<span>{{ item.event }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer" v-if="item.progress">
|
||||||
|
<div class="card-footer-item">
|
||||||
<span class="has-text-success" v-if="item.watched">Played</span>
|
<span class="has-text-success" v-if="item.watched">Played</span>
|
||||||
<span class="has-text-danger" v-else>Unplayed</span>
|
<span class="has-text-danger" v-else>Unplayed</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-6-mobile" v-if="item.progress && !item.watched">
|
<div class="card-footer-item">{{ item.progress }}</div>
|
||||||
<span v-tooltip="'Play Progress'">
|
|
||||||
{{ item.progress }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,12 +9,14 @@
|
|||||||
<div class="column is-12">
|
<div class="column is-12">
|
||||||
<div class="columns is-multiline" v-if="lastHistory.length>0">
|
<div class="columns is-multiline" v-if="lastHistory.length>0">
|
||||||
<div class="column is-6-tablet" v-for="history in lastHistory" :key="history.id">
|
<div class="column is-6-tablet" v-for="history in lastHistory" :key="history.id">
|
||||||
<div class="card">
|
<div class="card" :class="{ 'is-success': history.watched, 'is-danger': !history.watched }">
|
||||||
<header class="card-header">
|
<header class="card-header">
|
||||||
<p class="card-header-title is-text-overflow is-justify-center pr-1">
|
<p class="card-header-title is-text-overflow pr-1">
|
||||||
<NuxtLink :href="`/history/${history.id}`">
|
<span class="icon" v-if="!history.progress">
|
||||||
{{ history.full_title ?? history.title }}
|
<i class="fas fa-eye-slash" v-if="!history.watched"></i>
|
||||||
</NuxtLink>
|
<i class="fas fa-eye" v-else></i>
|
||||||
|
</span>
|
||||||
|
<NuxtLink :href="`/history/${history.id}`" v-text="history.full_title ?? history.title"/>
|
||||||
</p>
|
</p>
|
||||||
<span class="card-header-icon">
|
<span class="card-header-icon">
|
||||||
<span class="icon" v-if="'episode' === history.type"><i class="fas fa-tv"></i></span>
|
<span class="icon" v-if="'episode' === history.type"><i class="fas fa-tv"></i></span>
|
||||||
@@ -22,30 +24,35 @@
|
|||||||
</span>
|
</span>
|
||||||
</header>
|
</header>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="columns is-multiline is-mobile has-text-centered">
|
<div class="columns is-multiline is-mobile">
|
||||||
<div class="column is-6-mobile">
|
<div class="column is-4-tablet is-6-mobile has-text-left">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon"><i class="fas fa-calendar"></i> </span>
|
||||||
{{ moment(history.updated).fromNow() }}
|
{{ moment(history.updated).fromNow() }}
|
||||||
</div>
|
|
||||||
<div class="column is-6-mobile">
|
|
||||||
<NuxtLink :href="'/backend/'+history.via">
|
|
||||||
{{ history.via }}
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
<div class="column is-6-mobile" v-if="history.event">
|
|
||||||
<span v-tooltip="'The event which triggered the update.'" class="has-tooltip">
|
|
||||||
{{ history.event }}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-6-mobile">
|
<div class="column is-4-tablet is-6-mobile has-text-right-mobile">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon"><i class="fas fa-server"></i></span>
|
||||||
|
<span>
|
||||||
|
<NuxtLink :href="'/backend/'+history.via" v-text="history.via"/>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="column is-4-tablet is-12-mobile has-text-left-mobile">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon"><i class="fas fa-envelope"></i></span>
|
||||||
|
<span>{{ history.event }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer" v-if="history.progress">
|
||||||
|
<div class="card-footer-item">
|
||||||
<span class="has-text-success" v-if="history.watched">Played</span>
|
<span class="has-text-success" v-if="history.watched">Played</span>
|
||||||
<span class="has-text-danger" v-else>Unplayed</span>
|
<span class="has-text-danger" v-else>Unplayed</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-6-mobile" v-if="history.progress && !history.watched">
|
<div class="card-footer-item">{{ history.progress }}</div>
|
||||||
<span v-tooltip="'Play Progress'">
|
|
||||||
{{ history.progress }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,23 +2,51 @@ import {useNotification} from '@kyvg/vue3-notification'
|
|||||||
|
|
||||||
const {notify} = useNotification();
|
const {notify} = useNotification();
|
||||||
|
|
||||||
const ag = (obj, path, defaultValue = null, separator = '.') => {
|
const AG_SEPARATOR = '.'
|
||||||
const keys = path.split(separator)
|
|
||||||
|
/**
|
||||||
|
* Get value from object or function
|
||||||
|
*
|
||||||
|
* @param {Function|*} obj
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
const getValue = (obj) => 'function' === typeof obj ? obj() : obj;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get value from object or function and return default value if it's undefined or null
|
||||||
|
*
|
||||||
|
* @param {Object|Array} obj The object to get the value from.
|
||||||
|
* @param {string} path The path to the value.
|
||||||
|
* @param {*} defaultValue The default value to return if the path is not found.
|
||||||
|
*
|
||||||
|
* @returns {*} The value at the path or the default value.
|
||||||
|
*/
|
||||||
|
const ag = (obj, path, defaultValue = null) => {
|
||||||
|
const keys = path.split(AG_SEPARATOR)
|
||||||
let at = obj
|
let at = obj
|
||||||
|
|
||||||
for (let key of keys) {
|
for (let key of keys) {
|
||||||
if (typeof at === 'object' && at !== null && key in at) {
|
if (typeof at === 'object' && at !== null && key in at) {
|
||||||
at = at[key]
|
at = at[key]
|
||||||
} else {
|
} else {
|
||||||
return defaultValue
|
return getValue(defaultValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return at
|
return getValue(at)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ag_set = (obj, path, value, separator = '.') => {
|
/**
|
||||||
const keys = path.split(separator)
|
* Set value in object by path
|
||||||
|
*
|
||||||
|
* @param {Object} obj The object to set the value in.
|
||||||
|
* @param {string} path The path to the value.
|
||||||
|
* @param {*} value The value to set.
|
||||||
|
*
|
||||||
|
* @returns {Object} The object with the value set.
|
||||||
|
*/
|
||||||
|
const ag_set = (obj, path, value) => {
|
||||||
|
const keys = path.split(AG_SEPARATOR)
|
||||||
let at = obj
|
let at = obj
|
||||||
|
|
||||||
while (keys.length > 0) {
|
while (keys.length > 0) {
|
||||||
@@ -39,33 +67,73 @@ const ag_set = (obj, path, value, separator = '.') => {
|
|||||||
|
|
||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert bytes to human-readable file size
|
||||||
|
*
|
||||||
|
* @param {number} bytes The number of bytes.
|
||||||
|
* @param {boolean} showUnit Whether to show the unit.
|
||||||
|
* @param {number} decimals The number of decimals.
|
||||||
|
* @param {number} mod The mod.
|
||||||
|
*
|
||||||
|
* @returns {string} The human-readable file size.
|
||||||
|
*/
|
||||||
const humanFileSize = (bytes = 0, showUnit = true, decimals = 2, mod = 1000) => {
|
const humanFileSize = (bytes = 0, showUnit = true, decimals = 2, mod = 1000) => {
|
||||||
const sz = 'BKMGTP'
|
const sz = 'BKMGTP'
|
||||||
const factor = Math.floor((bytes.toString().length - 1) / 3)
|
const factor = Math.floor((bytes.toString().length - 1) / 3)
|
||||||
return `${(bytes / (mod ** factor)).toFixed(decimals)}${showUnit ? sz[factor] : ''}`
|
return `${(bytes / (mod ** factor)).toFixed(decimals)}${showUnit ? sz[factor] : ''}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for an element to be loaded in the DOM
|
||||||
|
*
|
||||||
|
* @param {string} sel The selector of the element.
|
||||||
|
* @param {Function} callback The callback function.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
const awaitElement = (sel, callback) => {
|
const awaitElement = (sel, callback) => {
|
||||||
let interval = undefined;
|
let interval = undefined
|
||||||
|
|
||||||
let $elm = document.querySelector(sel)
|
let $elm = document.querySelector(sel)
|
||||||
|
|
||||||
if ($elm) {
|
if ($elm) {
|
||||||
return callback(sel, $elm)
|
callback(sel, $elm)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
interval = setInterval(() => {
|
interval = setInterval(() => {
|
||||||
let $elm = document.querySelector(sel);
|
let $elm = document.querySelector(sel)
|
||||||
if ($elm) {
|
if ($elm) {
|
||||||
clearInterval(interval);
|
clearInterval(interval)
|
||||||
callback(sel, $elm);
|
callback(sel, $elm)
|
||||||
}
|
}
|
||||||
}, 200)
|
}, 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uppercase the first letter of a string
|
||||||
|
*
|
||||||
|
* @param {string} str The string to uppercase.
|
||||||
|
*
|
||||||
|
* @returns {string} The string with the first letter uppercased.
|
||||||
|
*/
|
||||||
|
const ucFirst = (str) => {
|
||||||
|
if (typeof str !== 'string') {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
const ucFirst = (str) => str.charAt(0).toUpperCase() + str.slice(1);
|
/**
|
||||||
|
* Display a notification
|
||||||
|
*
|
||||||
|
* @param {string} type The type of the notification.
|
||||||
|
* @param {string} title The title of the notification.
|
||||||
|
* @param {string} text The text of the notification.
|
||||||
|
* @param {number} duration The duration of the notification.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
const notification = (type, title, text, duration = 3000) => {
|
const notification = (type, title, text, duration = 3000) => {
|
||||||
let classes = ''
|
let classes = ''
|
||||||
|
|
||||||
|
|||||||
@@ -750,7 +750,7 @@
|
|||||||
which "^3.0.1"
|
which "^3.0.1"
|
||||||
ws "^8.16.0"
|
ws "^8.16.0"
|
||||||
|
|
||||||
"@nuxt/kit@3.11.2", "@nuxt/kit@^3.10.2", "@nuxt/kit@^3.11.2":
|
"@nuxt/kit@3.11.2", "@nuxt/kit@^3.10.2", "@nuxt/kit@^3.11.2", "@nuxt/kit@^3.7.3":
|
||||||
version "3.11.2"
|
version "3.11.2"
|
||||||
resolved "https://registry.yarnpkg.com/@nuxt/kit/-/kit-3.11.2.tgz#dfc43c05992691bcd6aa58c14f88cf43e3abb788"
|
resolved "https://registry.yarnpkg.com/@nuxt/kit/-/kit-3.11.2.tgz#dfc43c05992691bcd6aa58c14f88cf43e3abb788"
|
||||||
integrity sha512-yiYKP0ZWMW7T3TCmsv4H8+jEsB/nFriRAR8bKoSqSV9bkVYWPE36sf7JDux30dQ91jSlQG6LQkB3vCHYTS2cIg==
|
integrity sha512-yiYKP0ZWMW7T3TCmsv4H8+jEsB/nFriRAR8bKoSqSV9bkVYWPE36sf7JDux30dQ91jSlQG6LQkB3vCHYTS2cIg==
|
||||||
@@ -859,6 +859,14 @@
|
|||||||
vite-plugin-checker "^0.6.4"
|
vite-plugin-checker "^0.6.4"
|
||||||
vue-bundle-renderer "^2.0.0"
|
vue-bundle-renderer "^2.0.0"
|
||||||
|
|
||||||
|
"@nuxtjs/device@^3.1.1":
|
||||||
|
version "3.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@nuxtjs/device/-/device-3.1.1.tgz#51fa2308c365323b9f3cce923dbebd40cdffe7ac"
|
||||||
|
integrity sha512-wHTziEevt1hdgePQwPhEedWW3COalhP0YGVB+sGLqSrKujX8vdz7lcBFB01KIftpaP8kY5H8pssibNaJbxGcYw==
|
||||||
|
dependencies:
|
||||||
|
"@nuxt/kit" "^3.7.3"
|
||||||
|
defu "^6.1.2"
|
||||||
|
|
||||||
"@parcel/watcher-android-arm64@2.4.1":
|
"@parcel/watcher-android-arm64@2.4.1":
|
||||||
version "2.4.1"
|
version "2.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz#c2c19a3c442313ff007d2d7a9c2c1dd3e1c9ca84"
|
resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz#c2c19a3c442313ff007d2d7a9c2c1dd3e1c9ca84"
|
||||||
@@ -2273,7 +2281,7 @@ define-lazy-prop@^3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f"
|
resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f"
|
||||||
integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==
|
integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==
|
||||||
|
|
||||||
defu@^6.0.0, defu@^6.1.3, defu@^6.1.4:
|
defu@^6.0.0, defu@^6.1.2, defu@^6.1.3, defu@^6.1.4:
|
||||||
version "6.1.4"
|
version "6.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.4.tgz#4e0c9cf9ff68fe5f3d7f2765cc1a012dfdcb0479"
|
resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.4.tgz#4e0c9cf9ff68fe5f3d7f2765cc1a012dfdcb0479"
|
||||||
integrity sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==
|
integrity sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ namespace App\API\History;
|
|||||||
|
|
||||||
use App\Commands\Database\ListCommand;
|
use App\Commands\Database\ListCommand;
|
||||||
use App\Libs\Attributes\Route\Get;
|
use App\Libs\Attributes\Route\Get;
|
||||||
|
use App\Libs\Attributes\Route\Route;
|
||||||
use App\Libs\Container;
|
use App\Libs\Container;
|
||||||
use App\Libs\Database\DatabaseInterface as iDB;
|
use App\Libs\Database\DatabaseInterface as iDB;
|
||||||
use App\Libs\DataUtil;
|
use App\Libs\DataUtil;
|
||||||
use App\Libs\Entity\StateInterface as iState;
|
use App\Libs\Entity\StateInterface as iState;
|
||||||
use App\Libs\Guid;
|
use App\Libs\Guid;
|
||||||
use App\Libs\HTTP_STATUS;
|
use App\Libs\HTTP_STATUS;
|
||||||
|
use App\Libs\Mappers\Import\DirectMapper;
|
||||||
use App\Libs\Uri;
|
use App\Libs\Uri;
|
||||||
use PDO;
|
use PDO;
|
||||||
use Psr\Http\Message\ResponseInterface as iResponse;
|
use Psr\Http\Message\ResponseInterface as iResponse;
|
||||||
@@ -22,7 +24,7 @@ final class Index
|
|||||||
public const string URL = '%{api.prefix}/history';
|
public const string URL = '%{api.prefix}/history';
|
||||||
private PDO $pdo;
|
private PDO $pdo;
|
||||||
|
|
||||||
public function __construct(private readonly iDB $db)
|
public function __construct(private readonly iDB $db, private DirectMapper $mapper)
|
||||||
{
|
{
|
||||||
$this->pdo = $this->db->getPDO();
|
$this->pdo = $this->db->getPDO();
|
||||||
}
|
}
|
||||||
@@ -394,12 +396,55 @@ final class Index
|
|||||||
return api_error('Not found', HTTP_STATUS::HTTP_NOT_FOUND);
|
return api_error('Not found', HTTP_STATUS::HTTP_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
$item = $item->getAll();
|
$response = $item->getAll();
|
||||||
|
$response['full_title'] = $item->getName();
|
||||||
|
$response[iState::COLUMN_WATCHED] = $item->isWatched();
|
||||||
|
$response[iState::COLUMN_UPDATED] = makeDate($item->updated);
|
||||||
|
|
||||||
$item[iState::COLUMN_WATCHED] = $entity->isWatched();
|
return api_response(HTTP_STATUS::HTTP_OK, $response);
|
||||||
$item[iState::COLUMN_UPDATED] = makeDate($entity->updated);
|
|
||||||
|
|
||||||
return api_response(HTTP_STATUS::HTTP_OK, $item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route(['GET', 'POST', 'DELETE'], self::URL . '/{id:\d+}/watch[/]', name: 'history.watch')]
|
||||||
|
public function historyPlayStatus(iRequest $request, array $args = []): iResponse
|
||||||
|
{
|
||||||
|
if (null === ($id = ag($args, 'id'))) {
|
||||||
|
return api_error('Invalid value for id path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
$entity = Container::get(iState::class)::fromArray([iState::COLUMN_ID => $id]);
|
||||||
|
|
||||||
|
if (null === ($item = $this->db->get($entity))) {
|
||||||
|
return api_error('Not found', HTTP_STATUS::HTTP_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('GET' === $request->getMethod()) {
|
||||||
|
return api_response(HTTP_STATUS::HTTP_OK, ['watched' => $item->isWatched()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('POST' === $request->getMethod() && true === $item->isWatched()) {
|
||||||
|
return api_error('Already watched', HTTP_STATUS::HTTP_CONFLICT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('DELETE' === $request->getMethod() && false === $item->isWatched()) {
|
||||||
|
return api_error('Already unwatched', HTTP_STATUS::HTTP_CONFLICT);
|
||||||
|
}
|
||||||
|
|
||||||
|
$item->watched = 'POST' === $request->getMethod() ? 1 : 0;
|
||||||
|
$item->updated = time();
|
||||||
|
$item->extra = ag_set($item->getExtra(), $item->via, [
|
||||||
|
iState::COLUMN_EXTRA_EVENT => 'webui.mark' . ($item->isWatched() ? 'played' : 'unplayed'),
|
||||||
|
iState::COLUMN_EXTRA_DATE => (string)makeDate('now'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->mapper->add($item)->commit();
|
||||||
|
|
||||||
|
$response = $item->getAll();
|
||||||
|
$response['full_title'] = $item->getName();
|
||||||
|
$response[iState::COLUMN_WATCHED] = $item->isWatched();
|
||||||
|
$response[iState::COLUMN_UPDATED] = makeDate($item->updated);
|
||||||
|
|
||||||
|
queuePush($item);
|
||||||
|
|
||||||
|
return api_response(HTTP_STATUS::HTTP_OK, $response);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -488,7 +488,7 @@ final class ListCommand extends Command
|
|||||||
$changeState = strtolower($changeState);
|
$changeState = strtolower($changeState);
|
||||||
if (!$input->getOption('no-interaction')) {
|
if (!$input->getOption('no-interaction')) {
|
||||||
$text = r(
|
$text = r(
|
||||||
'<question>Are you sure you want to mark [<notce>{total}</notce>] items as [<notice>{state}</notice>]</question> ? [<value>Y|N</value>] [<value>Default: No</value>]',
|
'<question>Are you sure you want to mark [<notice>{total}</notice>] items as [<notice>{state}</notice>]</question> ? [<value>Y|N</value>] [<value>Default: No</value>]',
|
||||||
[
|
[
|
||||||
'total' => count($rows),
|
'total' => count($rows),
|
||||||
'state' => 'played' === $changeState ? 'Played' : 'Unplayed',
|
'state' => 'played' === $changeState ? 'Played' : 'Unplayed',
|
||||||
|
|||||||
Reference in New Issue
Block a user