'boolean', 'is_approved' => 'boolean', 'published_at' => 'datetime', ]; /** * Thumbnail sizes and their options. * Keys are the size dir used in the CDN URL. */ protected const THUMB_SIZES = [ 'sm' => ['height' => 240, 'quality' => 78, 'dir' => 'sm'], 'md' => ['height' => 360, 'quality' => 82, 'dir' => 'md'], 'lg' => ['height' => 1200, 'quality' => 85, 'dir' => 'lg'], 'xl' => ['height' => 2400, 'quality' => 90, 'dir' => 'xl'], ]; /** * Build the thumbnail URL for this artwork. * Returns null when no hash or thumb_ext is available. */ public function thumbUrl(string $size = 'md'): ?string { if (empty($this->hash) || empty($this->thumb_ext)) { return null; } $size = array_key_exists($size, self::THUMB_SIZES) ? $size : 'md'; $h = $this->hash; $h1 = substr($h, 0, 2); $h2 = substr($h, 2, 2); $ext = $this->thumb_ext; return "https://files.skinbase.org/{$size}/{$h1}/{$h2}/{$h}.{$ext}"; } /** * Accessor for `$art->thumb` used in legacy views (default medium size). */ public function getThumbAttribute(): string { return $this->thumbUrl('md') ?? '/gfx/sb_join.jpg'; } /** * Accessor for `$art->thumb_url` used in some views. */ public function getThumbUrlAttribute(): ?string { return $this->thumbUrl('md'); } /** * Provide a responsive `srcset` for legacy views. */ public function getThumbSrcsetAttribute(): ?string { if (empty($this->hash) || empty($this->thumb_ext)) return null; $sm = $this->thumbUrl('sm'); $md = $this->thumbUrl('md'); if (!$sm || !$md) return null; return $sm . ' 320w, ' . $md . ' 600w'; } // Relations public function user(): BelongsTo { return $this->belongsTo(User::class); } public function translations(): HasMany { return $this->hasMany(ArtworkTranslation::class); } public function stats(): HasOne { return $this->hasOne(ArtworkStats::class, 'artwork_id'); } public function categories(): BelongsToMany { return $this->belongsToMany(Category::class, 'artwork_category', 'artwork_id', 'category_id'); } public function comments(): HasMany { return $this->hasMany(ArtworkComment::class); } public function downloads(): HasMany { return $this->hasMany(ArtworkDownload::class); } public function features(): HasMany { return $this->hasMany(ArtworkFeature::class, 'artwork_id'); } // Scopes public function scopePublic(Builder $query): Builder { // Compose approved() so behavior is consistent and composable $table = $this->getTable(); return $query->approved()->where("{$table}.is_public", true); } public function scopeApproved(Builder $query): Builder { // Respect soft deletes and mark approved content $table = $this->getTable(); return $query->whereNull("{$table}.deleted_at")->where("{$table}.is_approved", true); } public function scopePublished(Builder $query): Builder { // Respect soft deletes and only include published items up to now $table = $this->getTable(); return $query->whereNull("{$table}.deleted_at") ->whereNotNull("{$table}.published_at") ->where("{$table}.published_at", '<=', now()); } public function getRouteKeyName(): string { return 'slug'; } }