Files
hstream/resources/views/livewire/partials/validation-badge.blade.php
T
w33b 30a6b080eb feat(admin): rebuild release upload form with Livewire and CDN validation
Replace the server-rendered release upload form with a Livewire component,
introducing an interactive admin release form that supports dynamic episode
management, real‑time CDN path validation, and direct download file checks.

Key changes:
- New `AdminReleaseForm` Livewire component with dynamic episode fields,
  Tagify integration, and upload handling via Livewire's file uploads.
- Added `CdnPathValidator` service to verify download and stream paths across
  all configured mirrors, caching results and capturing file sizes.
- Extended `EpisodeService` and `GalleryService` to support array‑based data
  and temporary uploaded files from Livewire.
- Persist validated download sizes and store the validation timestamp on the
  `downloads` table via a new migration; also add a unique constraint on
  `hentais.slug` to prevent duplicates.
- Remove the old POST `/admin/release/upload` route and controller logic,
  along with the legacy `upload.js` script.
- Add comprehensive feature and unit tests covering the new component and the
  CDN validation service.
2026-08-08 13:18:57 +02:00

42 lines
2.0 KiB
PHP

@props(['status' => null])
@php
$state = is_array($status) ? ($status['state'] ?? 'idle') : 'idle';
$message = is_array($status) ? ($status['message'] ?? '') : '';
$mirrors = is_array($status) ? ($status['mirrors'] ?? []) : [];
$missing = [];
foreach ($mirrors as $domain => $paths) {
foreach ((array) $paths as $ok) {
if (! $ok) {
$missing[] = preg_replace('#^https?://#', '', $domain);
break;
}
}
}
@endphp
@if ($state === 'checking')
<div class="flex items-center gap-2 text-xs text-blue-500">
<svg class="w-3.5 h-3.5 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>{{ $message }}</span>
</div>
@elseif ($state === 'valid')
<div class="flex items-center gap-2 text-xs text-green-600 dark:text-green-400">
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
<span>{{ $message }}</span>
</div>
@elseif ($state === 'invalid')
<div class="flex items-center gap-2 text-xs text-red-500">
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v4m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/></svg>
<div>
<span>{{ $message }}</span>
@if (! empty($missing))
<div class="mt-0.5 text-[10px] text-red-400">Missing: {{ implode(', ', $missing) }}</div>
@endif
</div>
</div>
@endif