Authors can flag a review's comment as containing spoilers; the comment is blurred behind a reveal control for every viewer except the author, omitted from RSS, and federates as containsSpoilers plus an AP summary content warning (ADR-0011) so Mastodon-class clients also collapse it. Flag follows the general-edit path (ADR-0001) across domain, both DB backends, REST, HTML templates, SPA (shadcn), TUI fixtures, and exports.
209 lines
5.0 KiB
Rust
209 lines
5.0 KiB
Rust
use chrono::{NaiveDateTime, Utc};
|
|
|
|
use crate::{
|
|
errors::DomainError,
|
|
value_objects::{Comment, MovieId, Rating, ReviewId, UserId, WatchMedium},
|
|
};
|
|
|
|
use super::movie::Movie;
|
|
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
|
pub enum ReviewSource {
|
|
#[default]
|
|
Local,
|
|
Remote {
|
|
actor_url: String,
|
|
},
|
|
}
|
|
|
|
pub struct PersistedReview {
|
|
pub id: ReviewId,
|
|
pub movie_id: MovieId,
|
|
pub user_id: UserId,
|
|
pub rating: Rating,
|
|
pub comment: Option<Comment>,
|
|
pub watched_at: NaiveDateTime,
|
|
pub created_at: NaiveDateTime,
|
|
pub source: ReviewSource,
|
|
pub watch_medium: Option<WatchMedium>,
|
|
pub spoilers: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct ReviewEdit {
|
|
pub rating: Option<Rating>,
|
|
pub comment: Option<Option<Comment>>,
|
|
pub watched_at: Option<NaiveDateTime>,
|
|
pub watch_medium: Option<Option<WatchMedium>>,
|
|
pub spoilers: Option<bool>,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct Review {
|
|
id: ReviewId,
|
|
movie_id: MovieId,
|
|
user_id: UserId,
|
|
rating: Rating,
|
|
comment: Option<Comment>,
|
|
watched_at: NaiveDateTime,
|
|
created_at: NaiveDateTime,
|
|
source: ReviewSource,
|
|
watch_medium: Option<WatchMedium>,
|
|
spoilers: bool,
|
|
}
|
|
|
|
impl Review {
|
|
pub fn new(
|
|
movie_id: MovieId,
|
|
user_id: UserId,
|
|
rating: Rating,
|
|
comment: Option<Comment>,
|
|
watched_at: NaiveDateTime,
|
|
watch_medium: Option<WatchMedium>,
|
|
spoilers: bool,
|
|
) -> Result<Self, DomainError> {
|
|
Ok(Self {
|
|
id: ReviewId::generate(),
|
|
movie_id,
|
|
user_id,
|
|
rating,
|
|
comment,
|
|
watched_at,
|
|
created_at: Utc::now().naive_utc(),
|
|
source: ReviewSource::Local,
|
|
watch_medium,
|
|
spoilers,
|
|
})
|
|
}
|
|
|
|
pub fn from_persistence(row: PersistedReview) -> Self {
|
|
Self {
|
|
id: row.id,
|
|
movie_id: row.movie_id,
|
|
user_id: row.user_id,
|
|
rating: row.rating,
|
|
comment: row.comment,
|
|
watched_at: row.watched_at,
|
|
created_at: row.created_at,
|
|
source: row.source,
|
|
watch_medium: row.watch_medium,
|
|
spoilers: row.spoilers,
|
|
}
|
|
}
|
|
|
|
pub fn apply_edit(&self, edit: ReviewEdit) -> Self {
|
|
Self {
|
|
id: self.id.clone(),
|
|
movie_id: self.movie_id.clone(),
|
|
user_id: self.user_id.clone(),
|
|
rating: edit.rating.unwrap_or_else(|| self.rating.clone()),
|
|
comment: match edit.comment {
|
|
Some(c) => c,
|
|
None => self.comment.clone(),
|
|
},
|
|
watched_at: edit.watched_at.unwrap_or(self.watched_at),
|
|
created_at: self.created_at,
|
|
source: self.source.clone(),
|
|
watch_medium: match edit.watch_medium {
|
|
Some(wm) => wm,
|
|
None => self.watch_medium,
|
|
},
|
|
spoilers: edit.spoilers.unwrap_or(self.spoilers),
|
|
}
|
|
}
|
|
|
|
pub fn id(&self) -> &ReviewId {
|
|
&self.id
|
|
}
|
|
pub fn movie_id(&self) -> &MovieId {
|
|
&self.movie_id
|
|
}
|
|
pub fn user_id(&self) -> &UserId {
|
|
&self.user_id
|
|
}
|
|
pub fn rating(&self) -> &Rating {
|
|
&self.rating
|
|
}
|
|
pub fn comment(&self) -> Option<&Comment> {
|
|
self.comment.as_ref()
|
|
}
|
|
pub fn watched_at(&self) -> &NaiveDateTime {
|
|
&self.watched_at
|
|
}
|
|
pub fn created_at(&self) -> &NaiveDateTime {
|
|
&self.created_at
|
|
}
|
|
pub fn source(&self) -> &ReviewSource {
|
|
&self.source
|
|
}
|
|
pub fn watch_medium(&self) -> Option<&WatchMedium> {
|
|
self.watch_medium.as_ref()
|
|
}
|
|
pub fn spoilers(&self) -> bool {
|
|
self.spoilers
|
|
}
|
|
pub fn stars(&self) -> [bool; 5] {
|
|
let r = self.rating.value();
|
|
[r >= 1, r >= 2, r >= 3, r >= 4, r >= 5]
|
|
}
|
|
|
|
pub fn is_remote(&self) -> bool {
|
|
matches!(self.source, ReviewSource::Remote { .. })
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct DiaryEntry {
|
|
movie: Movie,
|
|
review: Review,
|
|
}
|
|
|
|
impl DiaryEntry {
|
|
pub fn new(movie: Movie, review: Review) -> Self {
|
|
Self { movie, review }
|
|
}
|
|
|
|
pub fn movie(&self) -> &Movie {
|
|
&self.movie
|
|
}
|
|
pub fn review(&self) -> &Review {
|
|
&self.review
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct DiaryFilter {
|
|
pub sort_by: super::ReviewSortBy,
|
|
pub page: crate::models::collections::PageParams,
|
|
pub movie_id: Option<MovieId>,
|
|
pub user_id: Option<UserId>,
|
|
pub search: Option<String>,
|
|
pub include_remote: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct ReviewHistory {
|
|
movie: Movie,
|
|
viewings: Vec<Review>,
|
|
}
|
|
|
|
impl ReviewHistory {
|
|
pub fn new(movie: Movie, viewings: Vec<Review>) -> Self {
|
|
Self { movie, viewings }
|
|
}
|
|
|
|
pub fn movie(&self) -> &Movie {
|
|
&self.movie
|
|
}
|
|
pub fn viewings(&self) -> &[Review] {
|
|
&self.viewings
|
|
}
|
|
pub fn sort_by_date(&mut self) {
|
|
self.viewings.sort_by_key(|r| *r.watched_at());
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "tests/review.rs"]
|
|
mod tests;
|