mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-09-01 13:51:27 +02:00
Added Explicit user book rating + Community rating
This commit is contained in:
parent
3bfd9f419c
commit
dba575761e
@ -6,8 +6,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div role="tablist" class="absolute -top-10 left-0 z-10 w-full flex">
|
<div role="tablist" class="absolute -top-10 left-0 z-10 w-full flex">
|
||||||
<template v-for="tab in availableTabs">
|
<template v-for="tab in availableTabs" :key="tab.id">
|
||||||
<button :key="tab.id" role="tab" class="w-28 rounded-t-lg flex items-center justify-center mr-0.5 sm:mr-1 cursor-pointer hover:bg-bg border-t border-l border-r border-black-300 tab text-xs sm:text-base" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</button>
|
<button role="tab" class="w-28 rounded-t-lg flex items-center justify-center mr-0.5 sm:mr-1 cursor-pointer hover:bg-bg border-t border-l border-r border-black-300 tab text-xs sm:text-base" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -245,8 +245,22 @@ export default {
|
|||||||
this.processing = false
|
this.processing = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
libraryItemUpdated(expandedLibraryItem) {
|
libraryItemUpdated(updatedLibraryItem) {
|
||||||
this.libraryItem = expandedLibraryItem
|
if (this.libraryItem && this.libraryItem.id === updatedLibraryItem.id) {
|
||||||
|
// The updated item from the server doesn't contain rating data, so we preserve it from the client-side model.
|
||||||
|
if (this.libraryItem.media) {
|
||||||
|
updatedLibraryItem.media = {
|
||||||
|
...(updatedLibraryItem.media || {}),
|
||||||
|
myRating: this.libraryItem.media.myRating,
|
||||||
|
communityRating: this.libraryItem.media.communityRating,
|
||||||
|
myExplicitRating: this.libraryItem.media.myExplicitRating,
|
||||||
|
communityExplicitRating: this.libraryItem.media.communityExplicitRating,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.libraryItem = updatedLibraryItem
|
||||||
|
} else {
|
||||||
|
this.libraryItem = updatedLibraryItem
|
||||||
|
}
|
||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
this.fetchFull()
|
this.fetchFull()
|
||||||
|
@ -169,6 +169,7 @@ export default {
|
|||||||
if (updateResult) {
|
if (updateResult) {
|
||||||
if (updateResult.updated) {
|
if (updateResult.updated) {
|
||||||
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
|
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
|
||||||
|
this.$eventBus.$emit(`${this.libraryItemId}_updated`, updateResult.libraryItem)
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
||||||
|
32
client/components/ui/FlameIcon.vue
Normal file
32
client/components/ui/FlameIcon.vue
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<svg :viewBox="viewBox" :fill="empty ? 'transparent' : color" :stroke="empty ? '#d1d5db' : 'none'" stroke-width="1.5">
|
||||||
|
<path :d="path1" />
|
||||||
|
<path :d="path2" :fill="empty ? 'transparent' : color2" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
empty: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: '#EC6F59'
|
||||||
|
},
|
||||||
|
color2: {
|
||||||
|
type: String,
|
||||||
|
default: '#FAD15C'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
path1: 'M18.61,54.89C15.7,28.8,30.94,10.45,59.52,0C42.02,22.71,74.44,47.31,76.23,70.89 c4.19-7.15,6.57-16.69,7.04-29.45c21.43,33.62,3.66,88.57-43.5,80.67c-4.33-0.72-8.5-2.09-12.3-4.13C10.27,108.8,0,88.79,0,69.68 C0,57.5,5.21,46.63,11.95,37.99C12.85,46.45,14.77,52.76,18.61,54.89L18.61,54.89z',
|
||||||
|
path2: 'M33.87,92.58c-4.86-12.55-4.19-32.82,9.42-39.93c0.1,23.3,23.05,26.27,18.8,51.14 c3.92-4.44,5.9-11.54,6.25-17.15c6.22,14.24,1.34,25.63-7.53,31.43c-26.97,17.64-50.19-18.12-34.75-37.72 C26.53,84.73,31.89,91.49,33.87,92.58L33.87,92.58z',
|
||||||
|
viewBox: '0 0 92.27 122.88'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -1,32 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="rating-input-container">
|
<div class="rating-input flex items-center" :aria-label="label" :class="{ 'read-only': readOnly }">
|
||||||
<label v-if="label" class="px-1 text-sm font-semibold">{{ label }}</label>
|
|
||||||
<div
|
<div
|
||||||
class="flex items-center"
|
v-for="star in 5"
|
||||||
|
:key="star"
|
||||||
|
class="star-container relative"
|
||||||
|
:data-star="star"
|
||||||
@mouseleave="handleMouseleave"
|
@mouseleave="handleMouseleave"
|
||||||
|
@mousemove="handleMousemove"
|
||||||
|
@click="handleClick()"
|
||||||
>
|
>
|
||||||
<div
|
<template v-if="icon === 'star'">
|
||||||
v-for="star in 5"
|
|
||||||
:key="star"
|
|
||||||
class="star-container relative"
|
|
||||||
:data-star="star"
|
|
||||||
@mousemove="handleMousemove"
|
|
||||||
@click="handleClick()"
|
|
||||||
>
|
|
||||||
<svg class="star star-empty" viewBox="0 0 24 24">
|
<svg class="star star-empty" viewBox="0 0 24 24">
|
||||||
<path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" />
|
<path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" />
|
||||||
</svg>
|
</svg>
|
||||||
<svg class="star star-filled absolute top-0 left-0" :style="{ clipPath: getClipPath(star) }" viewBox="0 0 24 24">
|
<svg class="star star-filled absolute top-0 left-0" :style="{ clipPath: getClipPath(star), fill: color, stroke: color }" viewBox="0 0 24 24">
|
||||||
<path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" />
|
<path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</template>
|
||||||
<span class="ml-2 text-white/70">{{ displayValue }}/5</span>
|
<template v-if="icon === 'flame'">
|
||||||
|
<ui-flame-icon class="star" :empty="true" />
|
||||||
|
<ui-flame-icon class="star star-filled absolute top-0 left-0" :style="{ clipPath: getClipPath(star) }" />
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<span class="ml-2 text-white/70">{{ displayValue }}/5</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
name: 'RatingInput',
|
||||||
props: {
|
props: {
|
||||||
value: {
|
value: {
|
||||||
type: Number,
|
type: Number,
|
||||||
@ -34,11 +36,19 @@ export default {
|
|||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: 'Rating'
|
||||||
},
|
},
|
||||||
readOnly: {
|
readOnly: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
default: 'star'
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: '#f59e0b'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@ -97,11 +107,8 @@ export default {
|
|||||||
.star-empty {
|
.star-empty {
|
||||||
fill: transparent;
|
fill: transparent;
|
||||||
stroke: #d1d5db;
|
stroke: #d1d5db;
|
||||||
stroke-width: 1.5;
|
|
||||||
}
|
}
|
||||||
.star-filled {
|
.star-filled {
|
||||||
fill: #f59e0b;
|
|
||||||
stroke: #f59e0b;
|
|
||||||
stroke-width: 1.5;
|
stroke-width: 1.5;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@ -42,6 +42,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-4">
|
||||||
|
<h2 class="font-semibold">{{ $strings.HeaderSettingsRatings }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div role="article" :aria-label="$strings.LabelSettingsEnableRatingHelp" class="flex items-center py-2">
|
||||||
|
<ui-toggle-switch :label="$strings.LabelSettingsEnableRating" v-model="newServerSettings.enableRating" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('enableRating', val)" />
|
||||||
|
<ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsEnableRatingHelp">
|
||||||
|
<p class="pl-4">
|
||||||
|
<span id="settings-enable-rating">{{ $strings.LabelSettingsEnableRating }}</span>
|
||||||
|
<span class="material-symbols icon-text">info</span>
|
||||||
|
</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div role="article" :aria-label="$strings.LabelSettingsEnableCommunityRatingHelp" class="flex items-center py-2">
|
||||||
|
<ui-toggle-switch :label="$strings.LabelSettingsEnableCommunityRating" v-model="newServerSettings.enableCommunityRating" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('enableCommunityRating', val)" />
|
||||||
|
<ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsEnableCommunityRatingHelp">
|
||||||
|
<p class="pl-4">
|
||||||
|
<span id="settings-enable-community-rating">{{ $strings.LabelSettingsEnableCommunityRating }}</span>
|
||||||
|
<span class="material-symbols icon-text">info</span>
|
||||||
|
</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div role="article" :aria-label="$strings.LabelSettingsEnableExplicitRatingHelp" class="flex items-center py-2">
|
||||||
|
<ui-toggle-switch :label="$strings.LabelSettingsEnableExplicitRating" v-model="newServerSettings.enableExplicitRating" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('enableExplicitRating', val)" />
|
||||||
|
<ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsEnableExplicitRatingHelp">
|
||||||
|
<p class="pl-4">
|
||||||
|
<span id="settings-enable-explicit-rating">{{ $strings.LabelSettingsEnableExplicitRating }}</span>
|
||||||
|
<span class="material-symbols icon-text">info</span>
|
||||||
|
</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="pt-4">
|
<div class="pt-4">
|
||||||
<h2 class="font-semibold">{{ $strings.HeaderSettingsScanner }}</h2>
|
<h2 class="font-semibold">{{ $strings.HeaderSettingsScanner }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
@ -47,19 +47,34 @@
|
|||||||
</p>
|
</p>
|
||||||
<p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p>
|
<p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p>
|
||||||
|
|
||||||
<div class="flex items-center space-x-4 mt-2">
|
<!-- RATING SECTION -->
|
||||||
|
<div v-if="serverSettings.enableRating" class="flex items-center space-x-4 mt-2">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<ui-rating-input v-model="personalRating" :label="$strings.LabelYourRating" />
|
<ui-rating-input :value="myRating" :label="$strings.LabelYourRating" @input="updateRating" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="globalRating > 0" class="flex items-center">
|
<div v-if="serverSettings.enableCommunityRating && communityRating.count > 0" class="flex items-center bg-zinc-800 rounded-lg py-1.5 px-2 space-x-1.5 opacity-80">
|
||||||
<ui-rating-input :value="globalRating" label="Community Rating" :read-only="true" />
|
<span class="material-symbols text-lg -ml-0.5">groups</span>
|
||||||
|
<span class="font-semibold text-white">{{ communityRating.average.toFixed(1) }}/5</span>
|
||||||
|
<span class="text-xs text-white/50">({{ communityRating.count }})</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="provider === 'audible' && providerRating != null" class="flex items-center bg-zinc-800 rounded-lg p-1.5 space-x-1.5 opacity-80">
|
<div v-if="providerRating > 0 && provider === 'audible'" class="flex items-center bg-zinc-800 rounded-lg py-1.5 px-2 space-x-1.5 opacity-80">
|
||||||
<img src="~/assets/logos/audible.svg" alt="Audible Logo" class="w-12 h-auto" />
|
<img src="/audible.svg" alt="Audible Logo" class="h-5 w-auto" />
|
||||||
<span class="font-semibold text-white">{{ providerRating.toFixed(1) }}/5</span>
|
<span class="font-semibold text-white">{{ providerRating.toFixed(1) }}/5</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="providerRating > 0" class="flex items-center">
|
<div v-else-if="providerRating > 0" class="flex items-center">
|
||||||
<ui-rating-input :value="providerRating" :read-only="true" />
|
<ui-rating-input :value="providerRating" :label="provider" :read-only="true" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- EXPLICIT RATING SECTION -->
|
||||||
|
<div v-if="serverSettings.enableExplicitRating && isExplicit" class="flex items-center space-x-4 mt-3">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<ui-rating-input :value="myExplicitRating" :label="$strings.LabelExplicitRating" icon="flame" @input="updateExplicitRating" />
|
||||||
|
</div>
|
||||||
|
<div v-if="serverSettings.enableCommunityRating && communityExplicitRating.count > 0" class="flex items-center bg-zinc-800 rounded-lg py-1.5 px-2 space-x-1.5 opacity-80">
|
||||||
|
<ui-flame-icon class="h-5 w-5 -ml-0.5" />
|
||||||
|
<span class="font-semibold text-white">{{ communityExplicitRating.average.toFixed(1) }}/5</span>
|
||||||
|
<span class="text-xs text-white/50">({{ communityExplicitRating.count }})</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -206,7 +221,6 @@ export default {
|
|||||||
showBookmarksModal: false,
|
showBookmarksModal: false,
|
||||||
isDescriptionClamped: false,
|
isDescriptionClamped: false,
|
||||||
showFullDescription: false,
|
showFullDescription: false,
|
||||||
localPersonalRating: 0
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -216,6 +230,9 @@ export default {
|
|||||||
userToken() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
},
|
},
|
||||||
|
serverSettings() {
|
||||||
|
return this.$store.state.serverSettings
|
||||||
|
},
|
||||||
downloadUrl() {
|
downloadUrl() {
|
||||||
return `${process.env.serverUrl}/api/items/${this.libraryItemId}/download?token=${this.userToken}`
|
return `${process.env.serverUrl}/api/items/${this.libraryItemId}/download?token=${this.userToken}`
|
||||||
},
|
},
|
||||||
@ -336,8 +353,17 @@ export default {
|
|||||||
description() {
|
description() {
|
||||||
return this.mediaMetadata.description || ''
|
return this.mediaMetadata.description || ''
|
||||||
},
|
},
|
||||||
globalRating() {
|
myRating() {
|
||||||
return this.mediaMetadata.rating || 0
|
return this.media.myRating
|
||||||
|
},
|
||||||
|
communityRating() {
|
||||||
|
return this.media.communityRating || { average: 0, count: 0 }
|
||||||
|
},
|
||||||
|
myExplicitRating() {
|
||||||
|
return this.media.myExplicitRating
|
||||||
|
},
|
||||||
|
communityExplicitRating() {
|
||||||
|
return this.media.communityExplicitRating || { average: 0, count: 0 }
|
||||||
},
|
},
|
||||||
providerRating() {
|
providerRating() {
|
||||||
return this.media.providerRating || 0
|
return this.media.providerRating || 0
|
||||||
@ -345,14 +371,6 @@ export default {
|
|||||||
provider() {
|
provider() {
|
||||||
return this.media.provider || null
|
return this.media.provider || null
|
||||||
},
|
},
|
||||||
personalRating: {
|
|
||||||
get() {
|
|
||||||
return this.localPersonalRating
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
this.updatePersonalRating(val)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
userMediaProgress() {
|
userMediaProgress() {
|
||||||
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
||||||
},
|
},
|
||||||
@ -478,7 +496,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.localPersonalRating = this.libraryItem.personalRating || 0
|
|
||||||
this.$root.$on('progress-updated', this.progressUpdated)
|
this.$root.$on('progress-updated', this.progressUpdated)
|
||||||
this.$root.$on('libraryitem-updated', this.libraryItemUpdated)
|
this.$root.$on('libraryitem-updated', this.libraryItemUpdated)
|
||||||
this.$root.$on('rss-updated', this.rssUpdated)
|
this.$root.$on('rss-updated', this.rssUpdated)
|
||||||
@ -683,9 +700,7 @@ export default {
|
|||||||
},
|
},
|
||||||
libraryItemUpdated(libraryItem) {
|
libraryItemUpdated(libraryItem) {
|
||||||
if (libraryItem.id === this.libraryItemId) {
|
if (libraryItem.id === this.libraryItemId) {
|
||||||
libraryItem.personalRating = this.localPersonalRating
|
|
||||||
this.$store.commit('libraries/UPDATE_LIBRARY_ITEM', libraryItem)
|
this.$store.commit('libraries/UPDATE_LIBRARY_ITEM', libraryItem)
|
||||||
this.localPersonalRating = libraryItem.personalRating || 0
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clearProgressClick() {
|
clearProgressClick() {
|
||||||
@ -867,12 +882,26 @@ export default {
|
|||||||
this.$store.commit('globals/setShareModal', this.mediaItemShare)
|
this.$store.commit('globals/setShareModal', this.mediaItemShare)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async updatePersonalRating(rating) {
|
async updateRating(rating) {
|
||||||
this.localPersonalRating = rating
|
|
||||||
try {
|
try {
|
||||||
await this.$axios.post(`/api/items/${this.libraryItemId}/rate`, { rating })
|
const res = await this.$axios.$post(`/api/items/${this.libraryItemId}/rate`, { rating })
|
||||||
|
if (res.libraryItem) {
|
||||||
|
this.$store.commit('libraries/UPDATE_LIBRARY_ITEM', res.libraryItem)
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async updateExplicitRating(rating) {
|
||||||
|
try {
|
||||||
|
const res = await this.$axios.$post(`/api/items/${this.libraryItemId}/rate-explicit`, { rating })
|
||||||
|
if (res.libraryItem) {
|
||||||
|
this.$store.commit('libraries/UPDATE_LIBRARY_ITEM', res.libraryItem)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
progressUpdated(data) {
|
progressUpdated(data) {
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
<svg width="800px" height="800px" viewBox="0 0 192 192" xmlns="http://www.w3.org/2000/svg" style="enable-background:new 0 0 192 192" xml:space="preserve"><path fill="#FF9900" d="m129.8 96.1.2-.1c3.5-2.2 4.1-7.1 1.2-10.1-5.8-6.1-16.3-14.5-31.7-16.2-25.1-2.8-42 14.8-45.8 19.2-.4.5-.5 1.3 0 1.8.5.7 1.5.8 2.1.2 4.2-3.6 14.4-11.1 29-11.7 18.1-.7 30.9 9.8 36.7 15.8 2.2 2.3 5.6 2.8 8.3 1.1z"/><path fill="#FF9900" d="M148.8 82.8c3.7-2.2 4.4-7.2 1.5-10.4-8.5-9.5-27.3-26-55.6-26.4-34.4-.3-55.2 23.4-59.7 29-.4.6-.5 1.3-.1 1.9.6.8 1.8.9 2.5.2C42.9 71.5 62.2 54.3 91 56c25.7 1.6 42.1 17.2 49 25.3 2.2 2.7 5.9 3.3 8.8 1.5z"/><path d="m22 88.8 65.2 54.3c4.4 3.7 10.8 3.8 15.3.2L170 90.7" style="fill:none;stroke:#FF9900;stroke-width:12;stroke-linecap:round;stroke-miterlimit:10"/><path fill="#FF9900" d="M109.6 106.9c3.6-2.2 4-7.3.7-10-2.9-2.3-6.8-4.7-11.9-5.7-13.4-2.6-23.6 6.4-26.4 9.2-.5.5-.6 1.2-.3 1.7.4.8 1.4 1 2.1.6 2.4-1.5 6.4-3.4 11.5-3.5 7.8-.2 13.7 3.9 17 7 2 1.8 5 2.1 7.3.7z"/></svg>
|
<svg width="800px" height="800px" viewBox="0 0 192 192" xmlns="http://www.w3.org/2000/svg" style="enable-background:new 0 0 192 192" xml:space="preserve"><path fill="#FF9900" d="m129.8 96.1.2-.1c3.5-2.2 4.1-7.1 1.2-10.1-5.8-6.1-16.3-14.5-31.7-16.2-25.1-2.8-42 14.8-45.8 19.2-.4.5-.5 1.3 0 1.8.5.7 1.5.8 2.1.2 4.2-3.6 14.4-11.1 29-11.7 18.1-.7 30.9 9.8 36.7 15.8 2.2 2.3 5.6 2.8 8.3 1.1z"/><path fill="#FF9900" d="M148.8 82.8c3.7-2.2 4.4-7.2 1.5-10.4-8.5-9.5-27.3-26-55.6-26.4-34.4-.3-55.2 23.4-59.7 29-.4.6-.5 1.3-.1 1.9.6.8 1.8.9 2.5.2C42.9 71.5 62.2 54.3 91 56c25.7 1.6 42.1 17.2 49 25.3 2.2 2.7 5.9 3.3 8.8 1.5z"/><path d="m22 88.8 65.2 54.3c4.4 3.7 10.8 3.8 15.3.2L170 90.7" style="fill:none;stroke:#FF9900;stroke-width:12;stroke-linecap:round;stroke-miterlimit:10"/><path fill="#FF9900" d="M109.6 106.9c3.6-2.2 4-7.3.7-10-2.9-2.3-6.8-4.7-11.9-5.7-13.4-2.6-23.6 6.4-26.4 9.2-.5.5-.6 1.2-.3 1.7.4.8 1.4 1 2.1.6 2.4-1.5 6.4-3.4 11.5-3.5 7.8-.2 13.7 3.9 17 7 2 1.8 5 2.1 7.3.7z"/></svg>
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
1
client/static/flame-icon.svg
Normal file
1
client/static/flame-icon.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 92.27 122.88" style="enable-background:new 0 0 92.27 122.88" xml:space="preserve"><style type="text/css">.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#EC6F59;} .st1{fill-rule:evenodd;clip-rule:evenodd;fill:#FAD15C;}</style><g><path class="st0" d="M18.61,54.89C15.7,28.8,30.94,10.45,59.52,0C42.02,22.71,74.44,47.31,76.23,70.89 c4.19-7.15,6.57-16.69,7.04-29.45c21.43,33.62,3.66,88.57-43.5,80.67c-4.33-0.72-8.5-2.09-12.3-4.13C10.27,108.8,0,88.79,0,69.68 C0,57.5,5.21,46.63,11.95,37.99C12.85,46.45,14.77,52.76,18.61,54.89L18.61,54.89z"/><path class="st1" d="M33.87,92.58c-4.86-12.55-4.19-32.82,9.42-39.93c0.1,23.3,23.05,26.27,18.8,51.14 c3.92-4.44,5.9-11.54,6.25-17.15c6.22,14.24,1.34,25.63-7.53,31.43c-26.97,17.64-50.19-18.12-34.75-37.72 C26.53,84.73,31.89,91.49,33.87,92.58L33.87,92.58z"/></g></svg>
|
After Width: | Height: | Size: 975 B |
@ -173,7 +173,13 @@ export const actions = {
|
|||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
UPDATE_LIBRARY_ITEM(state, libraryItem) {
|
UPDATE_LIBRARY_ITEM(state, libraryItem) {
|
||||||
Vue.set(state.libraryItemsCache, libraryItem.id, libraryItem)
|
const existingItem = state.libraryItemsCache[libraryItem.id]
|
||||||
|
if (existingItem) {
|
||||||
|
const updatedItem = { ...existingItem, ...libraryItem }
|
||||||
|
Vue.set(state.libraryItemsCache, libraryItem.id, updatedItem)
|
||||||
|
} else {
|
||||||
|
Vue.set(state.libraryItemsCache, libraryItem.id, libraryItem)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
setFolders(state, folders) {
|
setFolders(state, folders) {
|
||||||
state.folders = folders
|
state.folders = folders
|
||||||
|
@ -194,6 +194,7 @@
|
|||||||
"HeaderSettingsDisplay": "Display",
|
"HeaderSettingsDisplay": "Display",
|
||||||
"HeaderSettingsExperimental": "Experimental Features",
|
"HeaderSettingsExperimental": "Experimental Features",
|
||||||
"HeaderSettingsGeneral": "General",
|
"HeaderSettingsGeneral": "General",
|
||||||
|
"HeaderSettingsRatings": "Ratings",
|
||||||
"HeaderSettingsScanner": "Scanner",
|
"HeaderSettingsScanner": "Scanner",
|
||||||
"HeaderSettingsWebClient": "Web Client",
|
"HeaderSettingsWebClient": "Web Client",
|
||||||
"HeaderSleepTimer": "Sleep Timer",
|
"HeaderSleepTimer": "Sleep Timer",
|
||||||
@ -349,6 +350,7 @@
|
|||||||
"LabelExplicit": "Explicit",
|
"LabelExplicit": "Explicit",
|
||||||
"LabelExplicitChecked": "Explicit (checked)",
|
"LabelExplicitChecked": "Explicit (checked)",
|
||||||
"LabelExplicitUnchecked": "Not Explicit (unchecked)",
|
"LabelExplicitUnchecked": "Not Explicit (unchecked)",
|
||||||
|
"LabelExplicitRating": "Explicit Rating",
|
||||||
"LabelExportOPML": "Export OPML",
|
"LabelExportOPML": "Export OPML",
|
||||||
"LabelFeedURL": "Feed URL",
|
"LabelFeedURL": "Feed URL",
|
||||||
"LabelFetchingMetadata": "Fetching Metadata",
|
"LabelFetchingMetadata": "Fetching Metadata",
|
||||||
@ -562,7 +564,13 @@
|
|||||||
"LabelSettingsBookshelfViewHelp": "Skeumorphic design with wooden shelves",
|
"LabelSettingsBookshelfViewHelp": "Skeumorphic design with wooden shelves",
|
||||||
"LabelSettingsChromecastSupport": "Chromecast support",
|
"LabelSettingsChromecastSupport": "Chromecast support",
|
||||||
"LabelSettingsDateFormat": "Date Format",
|
"LabelSettingsDateFormat": "Date Format",
|
||||||
"LabelSettingsEnableWatcher": "Automatically scan libraries for changes",
|
"LabelSettingsEnableCommunityRating": "Enable Community Rating",
|
||||||
|
"LabelSettingsEnableCommunityRatingHelp": "Shows the community rating next to the user's personal rating.",
|
||||||
|
"LabelSettingsEnableExplicitRating": "Enable Explicit Rating",
|
||||||
|
"LabelSettingsEnableExplicitRatingHelp": "Enables a separate rating system for explicit books.",
|
||||||
|
"LabelSettingsEnableRating": "Enable Rating",
|
||||||
|
"LabelSettingsEnableRatingHelp": "Enables the rating system for all users.",
|
||||||
|
"LabelSettingsEnableWatcher": "Enable Watcher",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "Automatically scan library for changes",
|
"LabelSettingsEnableWatcherForLibrary": "Automatically scan library for changes",
|
||||||
"LabelSettingsEnableWatcherHelp": "Enables the automatic adding/updating of items when file changes are detected. *Requires server restart",
|
"LabelSettingsEnableWatcherHelp": "Enables the automatic adding/updating of items when file changes are detected. *Requires server restart",
|
||||||
"LabelSettingsEpubsAllowScriptedContent": "Allow scripted content in epubs",
|
"LabelSettingsEpubsAllowScriptedContent": "Allow scripted content in epubs",
|
||||||
|
@ -147,11 +147,21 @@ class Database {
|
|||||||
return this.models.mediaItemShare
|
return this.models.mediaItemShare
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @type {typeof import('./models/UserBookRating')} */
|
||||||
|
get userBookRatingModel() {
|
||||||
|
return this.models.userBookRating
|
||||||
|
}
|
||||||
|
|
||||||
/** @type {typeof import('./models/Device')} */
|
/** @type {typeof import('./models/Device')} */
|
||||||
get deviceModel() {
|
get deviceModel() {
|
||||||
return this.models.device
|
return this.models.device
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @type {typeof import('./models/UserBookExplicitRating')} */
|
||||||
|
get userBookExplicitRatingModel() {
|
||||||
|
return this.models.userBookExplicitRating
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if db file exists
|
* Check if db file exists
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
@ -334,6 +344,7 @@ class Database {
|
|||||||
require('./models/CustomMetadataProvider').init(this.sequelize)
|
require('./models/CustomMetadataProvider').init(this.sequelize)
|
||||||
require('./models/MediaItemShare').init(this.sequelize)
|
require('./models/MediaItemShare').init(this.sequelize)
|
||||||
require('./models/UserBookRating').init(this.sequelize)
|
require('./models/UserBookRating').init(this.sequelize)
|
||||||
|
require('./models/UserBookExplicitRating').init(this.sequelize)
|
||||||
|
|
||||||
return this.sequelize.sync({ force, alter: false })
|
return this.sequelize.sync({ force, alter: false })
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
const { Request, Response, NextFunction } = require('express')
|
const { Request, Response, NextFunction } = require('express')
|
||||||
|
const { Op } = require('sequelize')
|
||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
const uaParserJs = require('../libs/uaParser')
|
const uaParserJs = require('../libs/uaParser')
|
||||||
@ -39,6 +40,62 @@ const ShareManager = require('../managers/ShareManager')
|
|||||||
class LibraryItemController {
|
class LibraryItemController {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
|
async _getExpandedItemWithRatings(libraryItem, user) {
|
||||||
|
const item = libraryItem.toOldJSONExpanded()
|
||||||
|
|
||||||
|
if (libraryItem.isBook) {
|
||||||
|
// Include users personal rating
|
||||||
|
const userBookRating = await Database.userBookRatingModel.findOne({
|
||||||
|
where: { userId: user.id, bookId: libraryItem.media.id }
|
||||||
|
})
|
||||||
|
if (userBookRating) {
|
||||||
|
item.media.myRating = userBookRating.rating
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include all users ratings for community rating
|
||||||
|
const allBookRatings = await Database.userBookRatingModel.findAll({
|
||||||
|
where: {
|
||||||
|
bookId: libraryItem.media.id,
|
||||||
|
userId: { [Op.ne]: user.id }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (allBookRatings.length > 0) {
|
||||||
|
const totalRating = allBookRatings.reduce((acc, cur) => acc + cur.rating, 0)
|
||||||
|
item.media.communityRating = {
|
||||||
|
average: totalRating / allBookRatings.length,
|
||||||
|
count: allBookRatings.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include users personal explicit rating
|
||||||
|
const userBookExplicitRating = await Database.userBookExplicitRatingModel.findOne({
|
||||||
|
where: { userId: user.id, bookId: libraryItem.media.id }
|
||||||
|
})
|
||||||
|
if (userBookExplicitRating) {
|
||||||
|
item.media.myExplicitRating = userBookExplicitRating.rating
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include all users explicit ratings for community explicit rating
|
||||||
|
const allBookExplicitRatings = await Database.userBookExplicitRatingModel.findAll({
|
||||||
|
where: {
|
||||||
|
bookId: libraryItem.media.id,
|
||||||
|
userId: { [Op.ne]: user.id }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (allBookExplicitRatings.length > 0) {
|
||||||
|
const totalExplicitRating = allBookExplicitRatings.reduce((acc, cur) => acc + cur.rating, 0)
|
||||||
|
item.media.communityExplicitRating = {
|
||||||
|
average: totalExplicitRating / allBookExplicitRatings.length,
|
||||||
|
count: allBookExplicitRatings.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET: /api/items/:id
|
* GET: /api/items/:id
|
||||||
* Optional query params:
|
* Optional query params:
|
||||||
@ -51,15 +108,7 @@ class LibraryItemController {
|
|||||||
async findOne(req, res) {
|
async findOne(req, res) {
|
||||||
const includeEntities = (req.query.include || '').split(',')
|
const includeEntities = (req.query.include || '').split(',')
|
||||||
if (req.query.expanded == 1) {
|
if (req.query.expanded == 1) {
|
||||||
const item = req.libraryItem.toOldJSONExpanded()
|
const item = await this._getExpandedItemWithRatings(req.libraryItem, req.user)
|
||||||
|
|
||||||
// Include users personal rating
|
|
||||||
const userBookRating = await Database.models.userBookRating.findOne({
|
|
||||||
where: { userId: req.user.id, bookId: req.libraryItem.media.id }
|
|
||||||
})
|
|
||||||
if (userBookRating) {
|
|
||||||
item.personalRating = userBookRating.rating
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include users media progress
|
// Include users media progress
|
||||||
if (includeEntities.includes('progress')) {
|
if (includeEntities.includes('progress')) {
|
||||||
@ -263,9 +312,13 @@ class LibraryItemController {
|
|||||||
Logger.debug(`[LibraryItemController] Updated library item media ${req.libraryItem.media.title}`)
|
Logger.debug(`[LibraryItemController] Updated library item media ${req.libraryItem.media.title}`)
|
||||||
SocketAuthority.libraryItemEmitter('item_updated', req.libraryItem)
|
SocketAuthority.libraryItemEmitter('item_updated', req.libraryItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updatedLibraryItem = await Database.libraryItemModel.getExpandedById(req.libraryItem.id)
|
||||||
|
const itemWithRatings = await this._getExpandedItemWithRatings(updatedLibraryItem, req.user)
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
updated: hasUpdates,
|
updated: hasUpdates,
|
||||||
libraryItem: req.libraryItem.toOldJSON()
|
libraryItem: itemWithRatings
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1189,8 +1242,8 @@ class LibraryItemController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.path.includes('/play') || req.path.includes('/rate')) {
|
if (req.path.includes('/play') || req.path.includes('/rate') || req.path.includes('/rate-explicit')) {
|
||||||
// allow POST requests using /play and /play/:episodeId OR /rate
|
// allow POST requests using /play and /play/:episodeId OR /rate and /rate-explicit
|
||||||
} else if (req.method == 'DELETE' && !req.user.canDelete) {
|
} else if (req.method == 'DELETE' && !req.user.canDelete) {
|
||||||
Logger.warn(`[LibraryItemController] User "${req.user.username}" attempted to delete without permission`)
|
Logger.warn(`[LibraryItemController] User "${req.user.username}" attempted to delete without permission`)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
@ -1218,14 +1271,54 @@ class LibraryItemController {
|
|||||||
const bookId = req.libraryItem.media.id
|
const bookId = req.libraryItem.media.id
|
||||||
const userId = req.user.id
|
const userId = req.user.id
|
||||||
|
|
||||||
await Database.models.userBookRating.upsert({ userId, bookId, rating })
|
await Database.userBookRatingModel.upsert({ userId, bookId, rating })
|
||||||
|
|
||||||
res.status(200).json({ success: true })
|
const updatedLibraryItem = await Database.libraryItemModel.getExpandedById(req.libraryItem.id)
|
||||||
|
const itemWithRatings = await this._getExpandedItemWithRatings(updatedLibraryItem, req.user)
|
||||||
|
|
||||||
|
res.status(200).json({ success: true, libraryItem: itemWithRatings })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Logger.error(err)
|
Logger.error(err)
|
||||||
res.status(500).json({ error: 'An error occurred while saving the rating' })
|
res.status(500).json({ error: 'An error occurred while saving the rating' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST: /api/items/:id/rate-explicit
|
||||||
|
*
|
||||||
|
* @param {LibraryItemControllerRequest} req
|
||||||
|
* @param {Response} res
|
||||||
|
*/
|
||||||
|
async rateExplicit(req, res) {
|
||||||
|
try {
|
||||||
|
const { rating } = req.body
|
||||||
|
if (rating === null || typeof rating !== 'number' || rating < 0 || rating > 5) {
|
||||||
|
return res.status(400).json({ error: 'Invalid rating' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const bookId = req.libraryItem.media.id
|
||||||
|
const userId = req.user.id
|
||||||
|
|
||||||
|
await Database.userBookExplicitRatingModel.upsert({ userId, bookId, rating })
|
||||||
|
|
||||||
|
const updatedLibraryItem = await Database.libraryItemModel.getExpandedById(req.libraryItem.id)
|
||||||
|
const itemWithRatings = await this._getExpandedItemWithRatings(updatedLibraryItem, req.user)
|
||||||
|
|
||||||
|
res.status(200).json({ success: true, libraryItem: itemWithRatings })
|
||||||
|
} catch (err) {
|
||||||
|
Logger.error(err)
|
||||||
|
res.status(500).json({ error: 'An error occurred while saving the explicit rating' })
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new LibraryItemController()
|
const controller = new LibraryItemController()
|
||||||
|
|
||||||
|
// Manually bind 'this' for all methods
|
||||||
|
for (const methodName of Object.getOwnPropertyNames(LibraryItemController.prototype)) {
|
||||||
|
if (methodName !== 'constructor' && typeof controller[methodName] === 'function') {
|
||||||
|
controller[methodName] = controller[methodName].bind(controller)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = controller
|
||||||
|
59
server/migrations/v2.25.4-add-user-book-explicit-ratings.js
Normal file
59
server/migrations/v2.25.4-add-user-book-explicit-ratings.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
const { DataTypes } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: async ({ context: queryInterface }) => {
|
||||||
|
const transaction = await queryInterface.sequelize.transaction()
|
||||||
|
try {
|
||||||
|
await queryInterface.createTable(
|
||||||
|
'userBookExplicitRatings',
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
references: { model: 'users', key: 'id' },
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
onDelete: 'CASCADE'
|
||||||
|
},
|
||||||
|
bookId: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
references: { model: 'books', key: 'id' },
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
onDelete: 'CASCADE'
|
||||||
|
},
|
||||||
|
rating: {
|
||||||
|
type: DataTypes.FLOAT,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ transaction }
|
||||||
|
)
|
||||||
|
await queryInterface.addConstraint('userBookExplicitRatings', {
|
||||||
|
fields: ['userId', 'bookId'],
|
||||||
|
type: 'unique',
|
||||||
|
name: 'user_book_explicit_ratings_unique_constraint',
|
||||||
|
transaction
|
||||||
|
})
|
||||||
|
await transaction.commit()
|
||||||
|
} catch (err) {
|
||||||
|
await transaction.rollback()
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
},
|
||||||
|
down: async ({ context: queryInterface }) => {
|
||||||
|
await queryInterface.dropTable('userBookExplicitRatings')
|
||||||
|
}
|
||||||
|
}
|
54
server/models/UserBookExplicitRating.js
Normal file
54
server/models/UserBookExplicitRating.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
class UserBookExplicitRating extends Model {
|
||||||
|
static init(sequelize) {
|
||||||
|
super.init(
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
bookId: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
rating: {
|
||||||
|
type: DataTypes.FLOAT,
|
||||||
|
allowNull: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize,
|
||||||
|
modelName: 'userBookExplicitRating',
|
||||||
|
tableName: 'userBookExplicitRatings',
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
unique: true,
|
||||||
|
fields: ['userId', 'bookId']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const { user, book } = sequelize.models
|
||||||
|
|
||||||
|
user.hasMany(UserBookExplicitRating, {
|
||||||
|
foreignKey: 'userId'
|
||||||
|
})
|
||||||
|
|
||||||
|
this.belongsTo(user, { foreignKey: 'userId' })
|
||||||
|
|
||||||
|
book.hasMany(this, {
|
||||||
|
foreignKey: 'bookId'
|
||||||
|
})
|
||||||
|
|
||||||
|
this.belongsTo(book, { foreignKey: 'bookId' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = UserBookExplicitRating
|
@ -58,6 +58,11 @@ class ServerSettings {
|
|||||||
this.version = packageJson.version
|
this.version = packageJson.version
|
||||||
this.buildNumber = packageJson.buildNumber
|
this.buildNumber = packageJson.buildNumber
|
||||||
|
|
||||||
|
// Ratings
|
||||||
|
this.enableRating = true
|
||||||
|
this.enableCommunityRating = false
|
||||||
|
this.enableExplicitRating = false
|
||||||
|
|
||||||
// Auth settings
|
// Auth settings
|
||||||
this.authLoginCustomMessage = null
|
this.authLoginCustomMessage = null
|
||||||
this.authActiveAuthMethods = ['local']
|
this.authActiveAuthMethods = ['local']
|
||||||
@ -123,6 +128,10 @@ class ServerSettings {
|
|||||||
this.version = settings.version || null
|
this.version = settings.version || null
|
||||||
this.buildNumber = settings.buildNumber || 0 // Added v2.4.5
|
this.buildNumber = settings.buildNumber || 0 // Added v2.4.5
|
||||||
|
|
||||||
|
this.enableRating = settings.enableRating !== false
|
||||||
|
this.enableCommunityRating = !!settings.enableCommunityRating
|
||||||
|
this.enableExplicitRating = !!settings.enableExplicitRating
|
||||||
|
|
||||||
this.authLoginCustomMessage = settings.authLoginCustomMessage || null // Added v2.8.0
|
this.authLoginCustomMessage = settings.authLoginCustomMessage || null // Added v2.8.0
|
||||||
this.authActiveAuthMethods = settings.authActiveAuthMethods || ['local']
|
this.authActiveAuthMethods = settings.authActiveAuthMethods || ['local']
|
||||||
|
|
||||||
@ -233,6 +242,9 @@ class ServerSettings {
|
|||||||
logLevel: this.logLevel,
|
logLevel: this.logLevel,
|
||||||
version: this.version,
|
version: this.version,
|
||||||
buildNumber: this.buildNumber,
|
buildNumber: this.buildNumber,
|
||||||
|
enableRating: this.enableRating,
|
||||||
|
enableCommunityRating: this.enableCommunityRating,
|
||||||
|
enableExplicitRating: this.enableExplicitRating,
|
||||||
authLoginCustomMessage: this.authLoginCustomMessage,
|
authLoginCustomMessage: this.authLoginCustomMessage,
|
||||||
authActiveAuthMethods: this.authActiveAuthMethods,
|
authActiveAuthMethods: this.authActiveAuthMethods,
|
||||||
authOpenIDIssuerURL: this.authOpenIDIssuerURL,
|
authOpenIDIssuerURL: this.authOpenIDIssuerURL,
|
||||||
|
@ -126,6 +126,7 @@ class ApiRouter {
|
|||||||
this.router.get('/items/:id/ebook/:fileid?', LibraryItemController.middleware.bind(this), LibraryItemController.getEBookFile.bind(this))
|
this.router.get('/items/:id/ebook/:fileid?', LibraryItemController.middleware.bind(this), LibraryItemController.getEBookFile.bind(this))
|
||||||
this.router.patch('/items/:id/ebook/:fileid/status', LibraryItemController.middleware.bind(this), LibraryItemController.updateEbookFileStatus.bind(this))
|
this.router.patch('/items/:id/ebook/:fileid/status', LibraryItemController.middleware.bind(this), LibraryItemController.updateEbookFileStatus.bind(this))
|
||||||
this.router.post('/items/:id/rate', LibraryItemController.middleware.bind(this), LibraryItemController.rate.bind(this))
|
this.router.post('/items/:id/rate', LibraryItemController.middleware.bind(this), LibraryItemController.rate.bind(this))
|
||||||
|
this.router.post('/items/:id/rate-explicit', LibraryItemController.middleware.bind(this), LibraryItemController.rateExplicit.bind(this))
|
||||||
|
|
||||||
//
|
//
|
||||||
// User Routes
|
// User Routes
|
||||||
|
Loading…
Reference in New Issue
Block a user