302 lines
8.4 KiB
PHP
302 lines
8.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
|
|
class NovaCard extends Model
|
|
{
|
|
use HasFactory, SoftDeletes;
|
|
|
|
public const FORMAT_SQUARE = 'square';
|
|
public const FORMAT_PORTRAIT = 'portrait';
|
|
public const FORMAT_STORY = 'story';
|
|
public const FORMAT_LANDSCAPE = 'landscape';
|
|
|
|
public const VISIBILITY_PUBLIC = 'public';
|
|
public const VISIBILITY_UNLISTED = 'unlisted';
|
|
public const VISIBILITY_PRIVATE = 'private';
|
|
|
|
public const STATUS_DRAFT = 'draft';
|
|
public const STATUS_PROCESSING = 'processing';
|
|
public const STATUS_PUBLISHED = 'published';
|
|
public const STATUS_HIDDEN = 'hidden';
|
|
public const STATUS_REJECTED = 'rejected';
|
|
|
|
public const MOD_PENDING = 'pending';
|
|
public const MOD_APPROVED = 'approved';
|
|
public const MOD_FLAGGED = 'flagged';
|
|
public const MOD_REJECTED = 'rejected';
|
|
|
|
protected $fillable = [
|
|
'uuid',
|
|
'user_id',
|
|
'category_id',
|
|
'title',
|
|
'slug',
|
|
'quote_text',
|
|
'quote_author',
|
|
'quote_source',
|
|
'description',
|
|
'format',
|
|
'project_json',
|
|
'schema_version',
|
|
'render_version',
|
|
'preview_path',
|
|
'preview_width',
|
|
'preview_height',
|
|
'background_type',
|
|
'background_image_id',
|
|
'original_card_id',
|
|
'root_card_id',
|
|
'template_id',
|
|
'visibility',
|
|
'status',
|
|
'moderation_status',
|
|
'featured',
|
|
'allow_download',
|
|
'allow_remix',
|
|
'views_count',
|
|
'shares_count',
|
|
'downloads_count',
|
|
'likes_count',
|
|
'favorites_count',
|
|
'saves_count',
|
|
'remixes_count',
|
|
'comments_count',
|
|
'challenge_entries_count',
|
|
'trending_score',
|
|
'featured_score',
|
|
'style_family',
|
|
'palette_family',
|
|
'density_score',
|
|
'editor_mode_last_used',
|
|
'allow_background_reuse',
|
|
'allow_export',
|
|
'original_creator_id',
|
|
'published_at',
|
|
'last_engaged_at',
|
|
'last_ranked_at',
|
|
'last_rendered_at',
|
|
];
|
|
|
|
protected $casts = [
|
|
'project_json' => 'array',
|
|
'schema_version' => 'integer',
|
|
'render_version' => 'integer',
|
|
'preview_width' => 'integer',
|
|
'preview_height' => 'integer',
|
|
'featured' => 'boolean',
|
|
'allow_download' => 'boolean',
|
|
'allow_remix' => 'boolean',
|
|
'views_count' => 'integer',
|
|
'shares_count' => 'integer',
|
|
'downloads_count' => 'integer',
|
|
'likes_count' => 'integer',
|
|
'favorites_count' => 'integer',
|
|
'saves_count' => 'integer',
|
|
'remixes_count' => 'integer',
|
|
'comments_count' => 'integer',
|
|
'challenge_entries_count' => 'integer',
|
|
'trending_score' => 'float',
|
|
'featured_score' => 'float',
|
|
'density_score' => 'integer',
|
|
'allow_background_reuse' => 'boolean',
|
|
'allow_export' => 'boolean',
|
|
'published_at' => 'datetime',
|
|
'last_engaged_at' => 'datetime',
|
|
'last_ranked_at' => 'datetime',
|
|
'last_rendered_at' => 'datetime',
|
|
];
|
|
|
|
protected static function booted(): void
|
|
{
|
|
static::creating(function (self $card): void {
|
|
if (! $card->uuid) {
|
|
$card->uuid = (string) Str::uuid();
|
|
}
|
|
});
|
|
}
|
|
|
|
public function user(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class);
|
|
}
|
|
|
|
public function category(): BelongsTo
|
|
{
|
|
return $this->belongsTo(NovaCardCategory::class, 'category_id');
|
|
}
|
|
|
|
public function template(): BelongsTo
|
|
{
|
|
return $this->belongsTo(NovaCardTemplate::class, 'template_id');
|
|
}
|
|
|
|
public function backgroundImage(): BelongsTo
|
|
{
|
|
return $this->belongsTo(NovaCardBackground::class, 'background_image_id');
|
|
}
|
|
|
|
public function originalCard(): BelongsTo
|
|
{
|
|
return $this->belongsTo(self::class, 'original_card_id');
|
|
}
|
|
|
|
public function rootCard(): BelongsTo
|
|
{
|
|
return $this->belongsTo(self::class, 'root_card_id');
|
|
}
|
|
|
|
public function originalCreator(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class, 'original_creator_id');
|
|
}
|
|
|
|
public function remixes(): HasMany
|
|
{
|
|
return $this->hasMany(self::class, 'original_card_id');
|
|
}
|
|
|
|
public function tags(): BelongsToMany
|
|
{
|
|
return $this->belongsToMany(NovaCardTag::class, 'nova_card_tag_relation', 'card_id', 'tag_id')
|
|
->withTimestamps();
|
|
}
|
|
|
|
public function reactions(): HasMany
|
|
{
|
|
return $this->hasMany(NovaCardReaction::class, 'card_id');
|
|
}
|
|
|
|
public function versions(): HasMany
|
|
{
|
|
return $this->hasMany(NovaCardVersion::class, 'card_id');
|
|
}
|
|
|
|
public function collectionItems(): HasMany
|
|
{
|
|
return $this->hasMany(NovaCardCollectionItem::class, 'card_id');
|
|
}
|
|
|
|
public function challengeEntries(): HasMany
|
|
{
|
|
return $this->hasMany(NovaCardChallengeEntry::class, 'card_id');
|
|
}
|
|
|
|
public function comments(): HasMany
|
|
{
|
|
return $this->hasMany(NovaCardComment::class, 'card_id');
|
|
}
|
|
|
|
public function scopePublished(Builder $query): Builder
|
|
{
|
|
return $query
|
|
->where('status', self::STATUS_PUBLISHED)
|
|
->where('moderation_status', '!=', self::MOD_REJECTED)
|
|
->whereIn('visibility', [self::VISIBILITY_PUBLIC, self::VISIBILITY_UNLISTED])
|
|
->whereNotNull('published_at');
|
|
}
|
|
|
|
public function scopePubliclyVisible(Builder $query): Builder
|
|
{
|
|
return $query
|
|
->published()
|
|
->where('visibility', self::VISIBILITY_PUBLIC)
|
|
->whereNotIn('moderation_status', [self::MOD_FLAGGED, self::MOD_REJECTED])
|
|
->where('status', self::STATUS_PUBLISHED);
|
|
}
|
|
|
|
public function scopeRising(Builder $query): Builder
|
|
{
|
|
return $query
|
|
->publiclyVisible()
|
|
->where('published_at', '>=', now()->subHours(96))
|
|
->where(function (Builder $q): void {
|
|
$q->where('saves_count', '>', 0)
|
|
->orWhere('remixes_count', '>', 0)
|
|
->orWhere('likes_count', '>', 1);
|
|
});
|
|
}
|
|
|
|
public function scopeFeaturedEditorial(Builder $query): Builder
|
|
{
|
|
return $query
|
|
->publiclyVisible()
|
|
->where(function (Builder $q): void {
|
|
$q->whereNotNull('featured_score')
|
|
->orWhere('featured', true);
|
|
});
|
|
}
|
|
|
|
public function previewUrl(): ?string
|
|
{
|
|
if (! $this->preview_path) {
|
|
return null;
|
|
}
|
|
|
|
return Storage::disk((string) config('nova_cards.storage.public_disk', 'public'))->url($this->preview_path);
|
|
}
|
|
|
|
public function ogPreviewUrl(): ?string
|
|
{
|
|
if (! $this->preview_path) {
|
|
return null;
|
|
}
|
|
|
|
$ogPath = preg_replace('/\.webp$/', '-og.jpg', $this->preview_path) ?: null;
|
|
if (! $ogPath) {
|
|
return $this->previewUrl();
|
|
}
|
|
|
|
return Storage::disk((string) config('nova_cards.storage.public_disk', 'public'))->url($ogPath);
|
|
}
|
|
|
|
public function publicUrl(): string
|
|
{
|
|
return route('cards.show', ['slug' => $this->slug, 'id' => $this->id]);
|
|
}
|
|
|
|
public function isOwnedBy(?User $user): bool
|
|
{
|
|
return $user !== null && (int) $user->id === (int) $this->user_id;
|
|
}
|
|
|
|
public function canBeViewedBy(?User $user): bool
|
|
{
|
|
if ($this->isOwnedBy($user)) {
|
|
return true;
|
|
}
|
|
|
|
if ($this->status !== self::STATUS_PUBLISHED) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->moderation_status === self::MOD_REJECTED || $this->moderation_status === self::MOD_FLAGGED) {
|
|
return false;
|
|
}
|
|
|
|
return in_array($this->visibility, [self::VISIBILITY_PUBLIC, self::VISIBILITY_UNLISTED], true);
|
|
}
|
|
|
|
public function isRemix(): bool
|
|
{
|
|
return $this->original_card_id !== null;
|
|
}
|
|
|
|
public function canReceiveCommentsFrom(?User $user): bool
|
|
{
|
|
return $user !== null && $this->canBeViewedBy($user);
|
|
}
|
|
}
|