Compare commits
47 Commits
1bc505057f
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f8022b3f18 | |||
| 57d1ec34c3 | |||
| 81639aaabf | |||
| 5dc1bff60c | |||
| 2f3f0edc30 | |||
| a71b2976af | |||
| 2c016274ab | |||
| 5ba0a55316 | |||
| a6fe34a0d1 | |||
| bb53e06c69 | |||
| 5cae5dc658 | |||
| 356d07365f | |||
| 3574d20fae | |||
| 9fc9e8ed10 | |||
| f5c706b587 | |||
| cbea71d9ae | |||
| 64a621173c | |||
| 839779b82e | |||
| 112cf9433e | |||
| 26a6500fca | |||
| d8cf70e747 | |||
| 0d4545c2ab | |||
| 900103e1c2 | |||
| d4c90976f8 | |||
| 72263127df | |||
| 6d3de59929 | |||
| ddb1bc2d14 | |||
| 5f3874a233 | |||
| ba3650899e | |||
| 904604fcfb | |||
| b7b34b503c | |||
| 4928733383 | |||
| 6340302ac6 | |||
| c1829ba7bd | |||
| 0b155bbb80 | |||
| 9f959efa14 | |||
| 38e3346dc3 | |||
| 09c08f3fea | |||
| 75f631c3e6 | |||
| fdf26604f3 | |||
| 59cb39ca77 | |||
| 62647be75c | |||
| de6efb877c | |||
| 2151d69791 | |||
| 05d4ef1bdb | |||
| 8ae9eaaadb | |||
| 361b511c3e |
@@ -16,7 +16,7 @@ class GenerateSitemap extends Command
|
|||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $signature = 'sitemap:generate';
|
protected $signature = 'app:generate-sitemap';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The console command description.
|
* The console command description.
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Enums\UserRole;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Crypt;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Contracts\Encryption\DecryptException;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class SyncSubscriptionKeys extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'app:sync-subscription-keys';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Sync local users against active subscription keys';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$endpoint = config('services.subscription_service_host');
|
||||||
|
if (!$endpoint) {
|
||||||
|
$this->error('Missing endpoint.');
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = Http::asJson()
|
||||||
|
->acceptJson()
|
||||||
|
->timeout(20)
|
||||||
|
->retry(3, 1000)
|
||||||
|
->post($endpoint.'/api/membership/keys', [
|
||||||
|
'payload' => base64_encode(Crypt::encryptString('get-active-keys')),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (! $response->successful()) {
|
||||||
|
$this->error('Subscription API request failed: HTTP ' . $response->status());
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$decrypted = Crypt::decryptString(base64_decode($response->json('payload')));
|
||||||
|
$activeKeys = json_decode($decrypted, true, flags: JSON_THROW_ON_ERROR);
|
||||||
|
} catch (DecryptException $e) {
|
||||||
|
$this->error('Could not decrypt API response.');
|
||||||
|
return self::FAILURE;
|
||||||
|
} catch (\JsonException $e) {
|
||||||
|
$this->error('API returned invalid JSON.');
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! is_array($activeKeys)) {
|
||||||
|
$this->error('API response payload was not an array.');
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$activeKeys = collect($activeKeys)
|
||||||
|
->filter(fn ($key) => is_string($key) && $key !== '')
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$this->markInactiveUsers($activeKeys);
|
||||||
|
$this->markActiveUsers($activeKeys);
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function markInactiveUsers(array $activeKeys)
|
||||||
|
{
|
||||||
|
User::query()
|
||||||
|
->whereNotNull('subscription_key')
|
||||||
|
->whereNotIn('subscription_key', $activeKeys)
|
||||||
|
->chunk(100, function ($users) {
|
||||||
|
foreach($users as $user) {
|
||||||
|
if ($user->hasRole(UserRole::SUPPORTER)) {
|
||||||
|
Log::info("Removed Supporter Role from {$user->name}");
|
||||||
|
$user->removeRole(UserRole::SUPPORTER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function markActiveUsers(array $activeKeys)
|
||||||
|
{
|
||||||
|
User::query()
|
||||||
|
->whereNotNull('subscription_key')
|
||||||
|
->whereIn('subscription_key', $activeKeys)
|
||||||
|
->chunk(100, function ($users) {
|
||||||
|
foreach($users as $user) {
|
||||||
|
if (!$user->hasRole(UserRole::SUPPORTER)) {
|
||||||
|
Log::info("Added Supporter Role for {$user->name}");
|
||||||
|
$user->addRole(UserRole::SUPPORTER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -128,7 +128,7 @@ class CacheHelper
|
|||||||
public static function getLatestComments()
|
public static function getLatestComments()
|
||||||
{
|
{
|
||||||
return Cache::remember('latest_comments', now()->addMinutes(60), function () {
|
return Cache::remember('latest_comments', now()->addMinutes(60), function () {
|
||||||
return Comment::latest()->take(10)->get();
|
return Comment::with('user')->latest()->take(10)->get();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Helpers;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Conner\Tagging\Model\Tag;
|
||||||
|
|
||||||
|
class FilterCategories
|
||||||
|
{
|
||||||
|
public static function getFilterCategories()
|
||||||
|
{
|
||||||
|
$taglist = Cache::remember(
|
||||||
|
'searchtags',
|
||||||
|
300,
|
||||||
|
fn () => Tag::where('count', '>', 0)
|
||||||
|
->orderBy('slug')
|
||||||
|
->get(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$appearances = [
|
||||||
|
'Loli',
|
||||||
|
'Shota',
|
||||||
|
'Milf',
|
||||||
|
'Futanari',
|
||||||
|
'Big Boobs',
|
||||||
|
'Small Boobs',
|
||||||
|
'Dark Skin',
|
||||||
|
'Cosplay',
|
||||||
|
'Elf',
|
||||||
|
'Maid',
|
||||||
|
'Nekomimi',
|
||||||
|
'Nurse',
|
||||||
|
'School Girl',
|
||||||
|
'Succubus',
|
||||||
|
'Teacher',
|
||||||
|
'Trap',
|
||||||
|
'Pregnant',
|
||||||
|
'Glasses',
|
||||||
|
'Swim Suit',
|
||||||
|
'Ugly Bastard',
|
||||||
|
'Monster',
|
||||||
|
];
|
||||||
|
|
||||||
|
$types = [
|
||||||
|
'3D',
|
||||||
|
'4K',
|
||||||
|
'48Fps',
|
||||||
|
'4K 48Fps',
|
||||||
|
'Censored',
|
||||||
|
'Uncensored',
|
||||||
|
'Comedy',
|
||||||
|
'Fantasy',
|
||||||
|
'Horror',
|
||||||
|
'Vanilla',
|
||||||
|
'Ntr',
|
||||||
|
'Pov',
|
||||||
|
'Filmed',
|
||||||
|
'X-Ray',
|
||||||
|
];
|
||||||
|
|
||||||
|
$actions = [
|
||||||
|
'Anal',
|
||||||
|
'Bdsm',
|
||||||
|
'Facial',
|
||||||
|
'Blow Job',
|
||||||
|
'Boob Job',
|
||||||
|
'Foot Job',
|
||||||
|
'Hand Job',
|
||||||
|
'Rimjob',
|
||||||
|
'Inflation',
|
||||||
|
'Masturbation',
|
||||||
|
'Public Sex',
|
||||||
|
'Rape',
|
||||||
|
'Reverse Rape',
|
||||||
|
'Threesome',
|
||||||
|
'Orgy',
|
||||||
|
'Gangbang',
|
||||||
|
];
|
||||||
|
|
||||||
|
$excluded = [...$appearances, ...$types, ...$actions];
|
||||||
|
|
||||||
|
$categories = [
|
||||||
|
'Genres' => $taglist
|
||||||
|
->reject(fn ($tag) => in_array($tag->name, $excluded))
|
||||||
|
->pluck('name')
|
||||||
|
->toArray(),
|
||||||
|
|
||||||
|
'Actions' => $actions,
|
||||||
|
|
||||||
|
'Appearance' => collect($appearances)
|
||||||
|
->reject(function ($tag) {
|
||||||
|
return Auth::guest() && in_array($tag, ['Loli', 'Shota']);
|
||||||
|
})
|
||||||
|
->toArray(),
|
||||||
|
|
||||||
|
'Types' => $types,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $categories;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,14 +4,16 @@ namespace App\Http\Controllers\Admin;
|
|||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Alert;
|
use App\Models\Alert;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class AlertController extends Controller
|
class AlertController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Display alert index page
|
* Display alert index page
|
||||||
*/
|
*/
|
||||||
public function index(): \Illuminate\View\View
|
public function index(): View
|
||||||
{
|
{
|
||||||
return view('admin.alert.index');
|
return view('admin.alert.index');
|
||||||
}
|
}
|
||||||
@@ -19,7 +21,7 @@ class AlertController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Create Alert.
|
* Create Alert.
|
||||||
*/
|
*/
|
||||||
public function store(Request $request): \Illuminate\Http\RedirectResponse
|
public function store(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'message' => 'required|string|max:255',
|
'message' => 'required|string|max:255',
|
||||||
@@ -39,7 +41,7 @@ class AlertController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Delete Alert.
|
* Delete Alert.
|
||||||
*/
|
*/
|
||||||
public function delete(int $alert_id): \Illuminate\Http\RedirectResponse
|
public function delete(int $alert_id): RedirectResponse
|
||||||
{
|
{
|
||||||
Alert::where('id', $alert_id)->delete();
|
Alert::where('id', $alert_id)->delete();
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,14 @@
|
|||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class CommentsController extends Controller
|
class CommentsController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Display Comments Page.
|
* Display Comments Page.
|
||||||
*/
|
*/
|
||||||
public function index(): \Illuminate\View\View
|
public function index(): View
|
||||||
{
|
{
|
||||||
return view('admin.comments.index');
|
return view('admin.comments.index');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,15 @@ namespace App\Http\Controllers\Admin;
|
|||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Contact;
|
use App\Models\Contact;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class ContactController extends Controller
|
class ContactController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Display Contact Page.
|
* Display Contact Page.
|
||||||
*/
|
*/
|
||||||
public function index(): \Illuminate\View\View
|
public function index(): View
|
||||||
{
|
{
|
||||||
$contacts = Contact::orderBy('created_at', 'DESC')->get();
|
$contacts = Contact::orderBy('created_at', 'DESC')->get();
|
||||||
|
|
||||||
@@ -22,7 +24,7 @@ class ContactController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Delete Contact.
|
* Delete Contact.
|
||||||
*/
|
*/
|
||||||
public function delete(int $contact_id): \Illuminate\Http\RedirectResponse
|
public function delete(int $contact_id): RedirectResponse
|
||||||
{
|
{
|
||||||
Contact::where('id', $contact_id)->delete();
|
Contact::where('id', $contact_id)->delete();
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Enums\UserRole;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Jobs\DiscordReleaseNotification;
|
use App\Jobs\DiscordReleaseNotification;
|
||||||
use App\Models\Episode;
|
use App\Models\Episode;
|
||||||
use App\Services\DownloadService;
|
use App\Services\DownloadService;
|
||||||
use App\Services\EpisodeService;
|
use App\Services\EpisodeService;
|
||||||
use App\Services\GalleryService;
|
use App\Services\GalleryService;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class EpisodeController extends Controller
|
class EpisodeController extends Controller
|
||||||
@@ -31,7 +33,7 @@ class EpisodeController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Add Episode to existing series
|
* Add Episode to existing series
|
||||||
*/
|
*/
|
||||||
public function store(Request $request): \Illuminate\Http\RedirectResponse
|
public function store(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$referenceEpisode = Episode::with('hentai')->where('id', $request->input('episode_id'))->firstOrFail();
|
$referenceEpisode = Episode::with('hentai')->where('id', $request->input('episode_id'))->firstOrFail();
|
||||||
$episodeNumber = $referenceEpisode->hentai->episodes()->count() + 1;
|
$episodeNumber = $referenceEpisode->hentai->episodes()->count() + 1;
|
||||||
@@ -59,9 +61,20 @@ class EpisodeController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Edit Episode
|
* Edit Episode
|
||||||
*/
|
*/
|
||||||
public function update(Request $request): \Illuminate\Http\RedirectResponse
|
public function update(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$episode = Episode::with('hentai')->where('id', $request->input('episode_id'))->firstOrFail();
|
$episode = Episode::with('hentai')->where('id', $request->input('episode_id'))->firstOrFail();
|
||||||
|
|
||||||
|
if ($request->user()->hasRole(UserRole::MODERATOR)) {
|
||||||
|
$this->episodeService->updateEpisodeModerator($request, $episode->id);
|
||||||
|
|
||||||
|
cache()->flush();
|
||||||
|
|
||||||
|
return to_route('hentai.index', [
|
||||||
|
'title' => $episode->slug,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$studio = $this->episodeService->getOrCreateStudio(json_decode($request->input('studio'))[0]->value);
|
$studio = $this->episodeService->getOrCreateStudio(json_decode($request->input('studio'))[0]->value);
|
||||||
|
|
||||||
$oldinterpolated = $episode->interpolated;
|
$oldinterpolated = $episode->interpolated;
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ use App\Models\Hentai;
|
|||||||
use App\Services\DownloadService;
|
use App\Services\DownloadService;
|
||||||
use App\Services\EpisodeService;
|
use App\Services\EpisodeService;
|
||||||
use App\Services\GalleryService;
|
use App\Services\GalleryService;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class ReleaseController extends Controller
|
class ReleaseController extends Controller
|
||||||
{
|
{
|
||||||
@@ -31,7 +33,7 @@ class ReleaseController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Display release page
|
* Display release page
|
||||||
*/
|
*/
|
||||||
public function index(): \Illuminate\View\View
|
public function index(): View
|
||||||
{
|
{
|
||||||
return view('admin.release.create');
|
return view('admin.release.create');
|
||||||
}
|
}
|
||||||
@@ -39,7 +41,7 @@ class ReleaseController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Upload New Hentai with One or Multipe Episodes
|
* Upload New Hentai with One or Multipe Episodes
|
||||||
*/
|
*/
|
||||||
public function store(Request $request): \Illuminate\Http\RedirectResponse
|
public function store(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
// Create new Hentai or find existing one
|
// Create new Hentai or find existing one
|
||||||
$slug = $this->episodeService->generateSlug($request->input('title'));
|
$slug = $this->episodeService->generateSlug($request->input('title'));
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ namespace App\Http\Controllers\Admin;
|
|||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\SiteBackground;
|
use App\Models\SiteBackground;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\File;
|
use Illuminate\Support\Facades\File;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\View\View;
|
||||||
use Intervention\Image\Encoders\WebpEncoder;
|
use Intervention\Image\Encoders\WebpEncoder;
|
||||||
use Intervention\Image\Laravel\Facades\Image;
|
use Intervention\Image\Laravel\Facades\Image;
|
||||||
|
|
||||||
@@ -16,7 +18,7 @@ class SiteBackgroundController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Display admin index page
|
* Display admin index page
|
||||||
*/
|
*/
|
||||||
public function index(): \Illuminate\View\View
|
public function index(): View
|
||||||
{
|
{
|
||||||
return view('admin.background.index', [
|
return view('admin.background.index', [
|
||||||
'images' => SiteBackground::all(),
|
'images' => SiteBackground::all(),
|
||||||
@@ -26,7 +28,7 @@ class SiteBackgroundController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Create new site backgrounds
|
* Create new site backgrounds
|
||||||
*/
|
*/
|
||||||
public function create(Request $request): \Illuminate\Http\RedirectResponse
|
public function create(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'images' => 'required',
|
'images' => 'required',
|
||||||
@@ -73,7 +75,7 @@ class SiteBackgroundController extends Controller
|
|||||||
return redirect()->back();
|
return redirect()->back();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(Request $request): \Illuminate\Http\RedirectResponse
|
public function update(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'id' => 'required|exists:site_backgrounds,id',
|
'id' => 'required|exists:site_backgrounds,id',
|
||||||
@@ -96,7 +98,7 @@ class SiteBackgroundController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Delete backround
|
* Delete backround
|
||||||
*/
|
*/
|
||||||
public function delete(Request $request): \Illuminate\Http\RedirectResponse
|
public function delete(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$id = $request->input('id');
|
$id = $request->input('id');
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
|
|||||||
use App\Models\Episode;
|
use App\Models\Episode;
|
||||||
use App\Models\EpisodeSubtitle;
|
use App\Models\EpisodeSubtitle;
|
||||||
use App\Models\Subtitle;
|
use App\Models\Subtitle;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class SubtitleController extends Controller
|
class SubtitleController extends Controller
|
||||||
@@ -13,7 +14,7 @@ class SubtitleController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Add new Subtitle.
|
* Add new Subtitle.
|
||||||
*/
|
*/
|
||||||
public function store(Request $request): \Illuminate\Http\RedirectResponse
|
public function store(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$subtitle = Subtitle::create([
|
$subtitle = Subtitle::create([
|
||||||
'name' => $request->name,
|
'name' => $request->name,
|
||||||
@@ -32,7 +33,7 @@ class SubtitleController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Update Episode Subtitles.
|
* Update Episode Subtitles.
|
||||||
*/
|
*/
|
||||||
public function update(Request $request): \Illuminate\Http\RedirectResponse
|
public function update(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$episode = Episode::where('id', $request->input('episode_id'))->firstOrFail();
|
$episode = Episode::where('id', $request->input('episode_id'))->firstOrFail();
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ use App\Enums\UserRole;
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class UserController extends Controller
|
class UserController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Display Users Page.
|
* Display Users Page.
|
||||||
*/
|
*/
|
||||||
public function index(): \Illuminate\View\View
|
public function index(): View
|
||||||
{
|
{
|
||||||
return view('admin.users.index');
|
return view('admin.users.index');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace App\Http\Controllers\Api;
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Downloads;
|
use App\Models\Downloads;
|
||||||
use App\Models\Episode;
|
use App\Models\Episode;
|
||||||
use GrantHolle\Altcha\Rules\ValidAltcha;
|
use App\Rules\ValidCaptcha;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class DownloadApiController extends Controller
|
class DownloadApiController extends Controller
|
||||||
@@ -17,7 +17,7 @@ class DownloadApiController extends Controller
|
|||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'episode_id' => ['required'],
|
'episode_id' => ['required'],
|
||||||
'captcha' => ['required', new ValidAltcha],
|
'captcha' => ['required', new ValidCaptcha],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$episode = Episode::where('id', $request->input('episode_id'))
|
$episode = Episode::where('id', $request->input('episode_id'))
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use AltchaOrg\Altcha\Algorithm\Pbkdf2;
|
||||||
|
use AltchaOrg\Altcha\Altcha;
|
||||||
|
use AltchaOrg\Altcha\CreateChallengeOptions;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
|
class CaptchaController extends Controller
|
||||||
|
{
|
||||||
|
public function create(): array
|
||||||
|
{
|
||||||
|
$pbkdf2 = new Pbkdf2;
|
||||||
|
|
||||||
|
$altcha = new Altcha(
|
||||||
|
hmacSignatureSecret: config('captcha.hmac_key'),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create challenge
|
||||||
|
$challenge = $altcha->createChallenge(new CreateChallengeOptions(
|
||||||
|
algorithm: $pbkdf2,
|
||||||
|
cost: 5000,
|
||||||
|
counter: random_int(5000, 10000),
|
||||||
|
expiresAt: time() + 600,
|
||||||
|
));
|
||||||
|
|
||||||
|
return get_object_vars($challenge);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ use Illuminate\Support\Facades\Hash;
|
|||||||
use Illuminate\Support\Facades\Password;
|
use Illuminate\Support\Facades\Password;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\Validation\Rules;
|
use Illuminate\Validation\Rules;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class NewPasswordController extends Controller
|
class NewPasswordController extends Controller
|
||||||
@@ -26,7 +27,7 @@ class NewPasswordController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Handle an incoming new password request.
|
* Handle an incoming new password request.
|
||||||
*
|
*
|
||||||
* @throws \Illuminate\Validation\ValidationException
|
* @throws ValidationException
|
||||||
*/
|
*/
|
||||||
public function store(Request $request): RedirectResponse
|
public function store(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
|
|||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Password;
|
use Illuminate\Support\Facades\Password;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class PasswordResetLinkController extends Controller
|
class PasswordResetLinkController extends Controller
|
||||||
@@ -21,7 +22,7 @@ class PasswordResetLinkController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Handle an incoming password reset link request.
|
* Handle an incoming password reset link request.
|
||||||
*
|
*
|
||||||
* @throws \Illuminate\Validation\ValidationException
|
* @throws ValidationException
|
||||||
*/
|
*/
|
||||||
public function store(Request $request): RedirectResponse
|
public function store(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,20 +4,21 @@ namespace App\Http\Controllers\Auth;
|
|||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use GrantHolle\Altcha\Rules\ValidAltcha;
|
use App\Rules\ValidCaptcha;
|
||||||
use Illuminate\Auth\Events\Registered;
|
use Illuminate\Auth\Events\Registered;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Validation\Rules;
|
use Illuminate\Validation\Rules;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
class RegisteredUserController extends Controller
|
class RegisteredUserController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Handle an incoming registration request.
|
* Handle an incoming registration request.
|
||||||
*
|
*
|
||||||
* @throws \Illuminate\Validation\ValidationException
|
* @throws ValidationException
|
||||||
*/
|
*/
|
||||||
public function store(Request $request): RedirectResponse
|
public function store(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
@@ -25,7 +26,7 @@ class RegisteredUserController extends Controller
|
|||||||
'name' => ['required', 'string', 'max:255'],
|
'name' => ['required', 'string', 'max:255'],
|
||||||
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
|
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
|
||||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||||
'altcha' => ['required', new ValidAltcha],
|
'altcha' => ['required', new ValidCaptcha],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::create([
|
$user = User::create([
|
||||||
|
|||||||
@@ -3,15 +3,17 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\Contact;
|
use App\Models\Contact;
|
||||||
use GrantHolle\Altcha\Rules\ValidAltcha;
|
use App\Rules\ValidCaptcha;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class ContactController extends Controller
|
class ContactController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Display Contact Page.
|
* Display Contact Page.
|
||||||
*/
|
*/
|
||||||
public function index(): \Illuminate\View\View
|
public function index(): View
|
||||||
{
|
{
|
||||||
return view('contact.form');
|
return view('contact.form');
|
||||||
}
|
}
|
||||||
@@ -19,14 +21,14 @@ class ContactController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Store Contact Submission.
|
* Store Contact Submission.
|
||||||
*/
|
*/
|
||||||
public function store(Request $request): \Illuminate\Http\RedirectResponse
|
public function store(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'name' => 'required|max:30',
|
'name' => 'required|max:30',
|
||||||
'email' => 'required|max:50',
|
'email' => 'required|max:50',
|
||||||
'message' => 'required|max:1000',
|
'message' => 'required|max:1000',
|
||||||
'subject' => 'required|max:50',
|
'subject' => 'required|max:50',
|
||||||
'altcha' => ['required', new ValidAltcha],
|
'altcha' => ['required', new ValidCaptcha],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$contact = new Contact;
|
$contact = new Contact;
|
||||||
|
|||||||
@@ -4,15 +4,17 @@ namespace App\Http\Controllers;
|
|||||||
|
|
||||||
use App\Helpers\CacheHelper;
|
use App\Helpers\CacheHelper;
|
||||||
use App\Models\Episode;
|
use App\Models\Episode;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class HomeController extends Controller
|
class HomeController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Display Home Page.
|
* Display Home Page.
|
||||||
*/
|
*/
|
||||||
public function index(): \Illuminate\View\View
|
public function index(): View
|
||||||
{
|
{
|
||||||
$guest = Auth::guest();
|
$guest = Auth::guest();
|
||||||
|
|
||||||
@@ -45,7 +47,7 @@ class HomeController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Display Banned Page.
|
* Display Banned Page.
|
||||||
*/
|
*/
|
||||||
public function banned(): \Illuminate\View\View
|
public function banned(): View
|
||||||
{
|
{
|
||||||
return view('auth.banned');
|
return view('auth.banned');
|
||||||
}
|
}
|
||||||
@@ -54,7 +56,7 @@ class HomeController extends Controller
|
|||||||
* Redirects to a random Hentai episode
|
* Redirects to a random Hentai episode
|
||||||
* Done due to performance reasons
|
* Done due to performance reasons
|
||||||
*/
|
*/
|
||||||
public function random(): \Illuminate\Http\RedirectResponse
|
public function random(): RedirectResponse
|
||||||
{
|
{
|
||||||
$random = Episode::inRandomOrder()
|
$random = Episode::inRandomOrder()
|
||||||
->limit(1)
|
->limit(1)
|
||||||
@@ -69,7 +71,7 @@ class HomeController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Display Search Page.
|
* Display Search Page.
|
||||||
*/
|
*/
|
||||||
public function search(): \Illuminate\View\View
|
public function search(): View
|
||||||
{
|
{
|
||||||
return view('search.index');
|
return view('search.index');
|
||||||
}
|
}
|
||||||
@@ -77,7 +79,7 @@ class HomeController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Display Download Search Page.
|
* Display Download Search Page.
|
||||||
*/
|
*/
|
||||||
public function downloadSearch(): \Illuminate\View\View
|
public function downloadSearch(): View
|
||||||
{
|
{
|
||||||
return view('search.download');
|
return view('search.download');
|
||||||
}
|
}
|
||||||
@@ -85,7 +87,7 @@ class HomeController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Redirect POST Data to GET with Query String.
|
* Redirect POST Data to GET with Query String.
|
||||||
*/
|
*/
|
||||||
public function searchRedirect(Request $request): \Illuminate\Http\RedirectResponse
|
public function searchRedirect(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
return redirect()->route('hentai.search', [
|
return redirect()->route('hentai.search', [
|
||||||
'search' => $request->input('live-search'),
|
'search' => $request->input('live-search'),
|
||||||
@@ -95,7 +97,7 @@ class HomeController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Display Stats Page.
|
* Display Stats Page.
|
||||||
*/
|
*/
|
||||||
public function stats(): \Illuminate\View\View
|
public function stats(): View
|
||||||
{
|
{
|
||||||
return view('home.stats', [
|
return view('home.stats', [
|
||||||
'viewCount' => CacheHelper::getTotalViewCount(),
|
'viewCount' => CacheHelper::getTotalViewCount(),
|
||||||
@@ -107,7 +109,7 @@ class HomeController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Manually set website language
|
* Manually set website language
|
||||||
*/
|
*/
|
||||||
public function updateLanguage(Request $request): \Illuminate\Http\RedirectResponse
|
public function updateLanguage(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
abort_unless(in_array($request->language, config('app.supported_locales'), true), 404);
|
abort_unless(in_array($request->language, config('app.supported_locales'), true), 404);
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ namespace App\Http\Controllers;
|
|||||||
use App\Http\Requests\MatrixRegisterRequest;
|
use App\Http\Requests\MatrixRegisterRequest;
|
||||||
use App\Services\MatrixRegistrationService;
|
use App\Services\MatrixRegistrationService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class MatrixController extends Controller
|
class MatrixController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Display the user page.
|
* Display the user page.
|
||||||
*/
|
*/
|
||||||
public function index(Request $request): \Illuminate\View\View
|
public function index(Request $request): View
|
||||||
{
|
{
|
||||||
$rooms = [
|
$rooms = [
|
||||||
['name' => '🏠 General', 'description' => 'Our main chat.', 'alias' => 'https://matrix.to/#/#general:hstream.moe'],
|
['name' => '🏠 General', 'description' => 'Our main chat.', 'alias' => 'https://matrix.to/#/#general:hstream.moe'],
|
||||||
|
|||||||
@@ -2,14 +2,16 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class NotificationController extends Controller
|
class NotificationController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Display the user's notification page.
|
* Display the user's notification page.
|
||||||
*/
|
*/
|
||||||
public function index(Request $request): \Illuminate\View\View
|
public function index(Request $request): View
|
||||||
{
|
{
|
||||||
return view('profile.notifications', [
|
return view('profile.notifications', [
|
||||||
'user' => $request->user(),
|
'user' => $request->user(),
|
||||||
@@ -20,7 +22,7 @@ class NotificationController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Delete Notifcation
|
* Delete Notifcation
|
||||||
*/
|
*/
|
||||||
public function delete(Request $request): \Illuminate\Http\RedirectResponse
|
public function delete(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'id' => 'required|exists:notifications,id',
|
'id' => 'required|exists:notifications,id',
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ use App\Models\Episode;
|
|||||||
use App\Models\Playlist;
|
use App\Models\Playlist;
|
||||||
use App\Models\PlaylistEpisode;
|
use App\Models\PlaylistEpisode;
|
||||||
use App\Services\PlaylistService;
|
use App\Services\PlaylistService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class PlaylistController extends Controller
|
class PlaylistController extends Controller
|
||||||
{
|
{
|
||||||
@@ -20,7 +23,7 @@ class PlaylistController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Display the public playlists page.
|
* Display the public playlists page.
|
||||||
*/
|
*/
|
||||||
public function index(): \Illuminate\View\View
|
public function index(): View
|
||||||
{
|
{
|
||||||
return view('playlist.index');
|
return view('playlist.index');
|
||||||
}
|
}
|
||||||
@@ -28,7 +31,7 @@ class PlaylistController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Display public playlist.
|
* Display public playlist.
|
||||||
*/
|
*/
|
||||||
public function show($playlist_id): \Illuminate\View\View
|
public function show($playlist_id): View
|
||||||
{
|
{
|
||||||
if (! is_numeric($playlist_id)) {
|
if (! is_numeric($playlist_id)) {
|
||||||
abort(404);
|
abort(404);
|
||||||
@@ -44,7 +47,7 @@ class PlaylistController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Display the user's playlists page.
|
* Display the user's playlists page.
|
||||||
*/
|
*/
|
||||||
public function playlists(Request $request): \Illuminate\View\View
|
public function playlists(Request $request): View
|
||||||
{
|
{
|
||||||
$title = 'Delete Playlist!';
|
$title = 'Delete Playlist!';
|
||||||
$text = 'Are you sure you want to delete?';
|
$text = 'Are you sure you want to delete?';
|
||||||
@@ -59,7 +62,7 @@ class PlaylistController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Display user's playlist.
|
* Display user's playlist.
|
||||||
*/
|
*/
|
||||||
public function showPlaylist(Request $request, $playlist_id): \Illuminate\View\View
|
public function showPlaylist(Request $request, $playlist_id): View
|
||||||
{
|
{
|
||||||
if (! is_numeric($playlist_id)) {
|
if (! is_numeric($playlist_id)) {
|
||||||
abort(404);
|
abort(404);
|
||||||
@@ -77,7 +80,7 @@ class PlaylistController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Create user playlist (Form).
|
* Create user playlist (Form).
|
||||||
*/
|
*/
|
||||||
public function createPlaylist(Request $request): \Illuminate\Http\RedirectResponse
|
public function createPlaylist(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'name' => 'required|max:30',
|
'name' => 'required|max:30',
|
||||||
@@ -95,7 +98,7 @@ class PlaylistController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Delete user playlist.
|
* Delete user playlist.
|
||||||
*/
|
*/
|
||||||
public function deletePlaylist(Request $request, $playlist_id): \Illuminate\Http\RedirectResponse
|
public function deletePlaylist(Request $request, $playlist_id): RedirectResponse
|
||||||
{
|
{
|
||||||
if (! is_numeric($playlist_id)) {
|
if (! is_numeric($playlist_id)) {
|
||||||
abort(404);
|
abort(404);
|
||||||
@@ -115,7 +118,7 @@ class PlaylistController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Delete episode from playlist.
|
* Delete episode from playlist.
|
||||||
*/
|
*/
|
||||||
public function deleteEpisodeFromPlaylist(Request $request): \Illuminate\Http\JsonResponse
|
public function deleteEpisodeFromPlaylist(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
if (! is_numeric($request->input('playlist')) || ! is_numeric($request->input('episode'))) {
|
if (! is_numeric($request->input('playlist')) || ! is_numeric($request->input('episode'))) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
@@ -143,7 +146,7 @@ class PlaylistController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Add to user playlist (API).
|
* Add to user playlist (API).
|
||||||
*/
|
*/
|
||||||
public function addPlaylistApi(Request $request): \Illuminate\Http\JsonResponse
|
public function addPlaylistApi(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
|
|
||||||
@@ -180,7 +183,7 @@ class PlaylistController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Create user playlist (API).
|
* Create user playlist (API).
|
||||||
*/
|
*/
|
||||||
public function createPlaylistApi(Request $request): \Illuminate\Http\JsonResponse
|
public function createPlaylistApi(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'name' => 'required|max:30',
|
'name' => 'required|max:30',
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use App\Models\User;
|
|||||||
use Conner\Tagging\Model\Tag;
|
use Conner\Tagging\Model\Tag;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Redirect;
|
use Illuminate\Support\Facades\Redirect;
|
||||||
@@ -94,6 +95,16 @@ class ProfileController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the user's subscription page.
|
||||||
|
*/
|
||||||
|
public function subscription(Request $request): View
|
||||||
|
{
|
||||||
|
return view('profile.subscription', [
|
||||||
|
'user' => $request->user(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update user settings.
|
* Update user settings.
|
||||||
*/
|
*/
|
||||||
@@ -138,7 +149,7 @@ class ProfileController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Delete the user's account.
|
* Delete the user's account.
|
||||||
*/
|
*/
|
||||||
public function destroy(Request $request): \Illuminate\Http\RedirectResponse
|
public function destroy(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
|
|
||||||
@@ -173,7 +184,7 @@ class ProfileController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Store custom user avatar.
|
* Store custom user avatar.
|
||||||
*/
|
*/
|
||||||
protected function storeAvatar(\Illuminate\Http\UploadedFile $file, User $user): void
|
protected function storeAvatar(UploadedFile $file, User $user): void
|
||||||
{
|
{
|
||||||
// Create Folder for Image Upload
|
// Create Folder for Image Upload
|
||||||
if (! Storage::disk('public')->exists('/images/avatars')) {
|
if (! Storage::disk('public')->exists('/images/avatars')) {
|
||||||
|
|||||||
@@ -13,13 +13,14 @@ use hisorange\BrowserDetect\Facade as Browser;
|
|||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class StreamController extends Controller
|
class StreamController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Display Stream Page.
|
* Display Stream Page.
|
||||||
*/
|
*/
|
||||||
public function index(Request $request, string $title): \Illuminate\View\View
|
public function index(Request $request, string $title): View
|
||||||
{
|
{
|
||||||
$titleParts = explode('-', $title);
|
$titleParts = explode('-', $title);
|
||||||
if (! is_numeric($titleParts[array_key_last($titleParts)])) {
|
if (! is_numeric($titleParts[array_key_last($titleParts)])) {
|
||||||
|
|||||||
+56
-29
@@ -2,7 +2,34 @@
|
|||||||
|
|
||||||
namespace App\Http;
|
namespace App\Http;
|
||||||
|
|
||||||
|
use App\Http\Middleware\Authenticate;
|
||||||
|
use App\Http\Middleware\EncryptCookies;
|
||||||
|
use App\Http\Middleware\IsAdmin;
|
||||||
|
use App\Http\Middleware\IsBanned;
|
||||||
|
use App\Http\Middleware\IsModerator;
|
||||||
|
use App\Http\Middleware\PreventRequestsDuringMaintenance;
|
||||||
|
use App\Http\Middleware\RedirectIfAuthenticated;
|
||||||
|
use App\Http\Middleware\SetLocale;
|
||||||
|
use App\Http\Middleware\TrimStrings;
|
||||||
|
use App\Http\Middleware\TrustProxies;
|
||||||
|
use App\Http\Middleware\ValidateSignature;
|
||||||
|
use App\Http\Middleware\VerifyCsrfToken;
|
||||||
|
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
|
||||||
|
use Illuminate\Auth\Middleware\Authorize;
|
||||||
|
use Illuminate\Auth\Middleware\EnsureEmailIsVerified;
|
||||||
|
use Illuminate\Auth\Middleware\RequirePassword;
|
||||||
|
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||||
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
||||||
|
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
|
||||||
|
use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests;
|
||||||
|
use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
|
||||||
|
use Illuminate\Http\Middleware\HandleCors;
|
||||||
|
use Illuminate\Http\Middleware\SetCacheHeaders;
|
||||||
|
use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||||
|
use Illuminate\Routing\Middleware\ThrottleRequests;
|
||||||
|
use Illuminate\Session\Middleware\AuthenticateSession;
|
||||||
|
use Illuminate\Session\Middleware\StartSession;
|
||||||
|
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||||
|
|
||||||
class Kernel extends HttpKernel
|
class Kernel extends HttpKernel
|
||||||
{
|
{
|
||||||
@@ -15,12 +42,12 @@ class Kernel extends HttpKernel
|
|||||||
*/
|
*/
|
||||||
protected $middleware = [
|
protected $middleware = [
|
||||||
// \App\Http\Middleware\TrustHosts::class,
|
// \App\Http\Middleware\TrustHosts::class,
|
||||||
\App\Http\Middleware\TrustProxies::class,
|
TrustProxies::class,
|
||||||
\Illuminate\Http\Middleware\HandleCors::class,
|
HandleCors::class,
|
||||||
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
|
PreventRequestsDuringMaintenance::class,
|
||||||
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
|
ValidatePostSize::class,
|
||||||
\App\Http\Middleware\TrimStrings::class,
|
TrimStrings::class,
|
||||||
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
|
ConvertEmptyStringsToNull::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,20 +57,20 @@ class Kernel extends HttpKernel
|
|||||||
*/
|
*/
|
||||||
protected $middlewareGroups = [
|
protected $middlewareGroups = [
|
||||||
'web' => [
|
'web' => [
|
||||||
\App\Http\Middleware\EncryptCookies::class,
|
EncryptCookies::class,
|
||||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
AddQueuedCookiesToResponse::class,
|
||||||
\Illuminate\Session\Middleware\StartSession::class,
|
StartSession::class,
|
||||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
ShareErrorsFromSession::class,
|
||||||
\App\Http\Middleware\VerifyCsrfToken::class,
|
VerifyCsrfToken::class,
|
||||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
SubstituteBindings::class,
|
||||||
\App\Http\Middleware\IsBanned::class,
|
IsBanned::class,
|
||||||
\App\Http\Middleware\SetLocale::class,
|
SetLocale::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
'api' => [
|
'api' => [
|
||||||
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
|
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
|
||||||
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
|
ThrottleRequests::class.':api',
|
||||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
SubstituteBindings::class,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -55,18 +82,18 @@ class Kernel extends HttpKernel
|
|||||||
* @var array<string, class-string|string>
|
* @var array<string, class-string|string>
|
||||||
*/
|
*/
|
||||||
protected $middlewareAliases = [
|
protected $middlewareAliases = [
|
||||||
'auth' => \App\Http\Middleware\Authenticate::class,
|
'auth' => Authenticate::class,
|
||||||
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
|
'auth.basic' => AuthenticateWithBasicAuth::class,
|
||||||
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
|
'auth.session' => AuthenticateSession::class,
|
||||||
'auth.admin' => \App\Http\Middleware\IsAdmin::class,
|
'auth.admin' => IsAdmin::class,
|
||||||
'auth.moderator' => \App\Http\Middleware\IsModerator::class,
|
'auth.moderator' => IsModerator::class,
|
||||||
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
|
'cache.headers' => SetCacheHeaders::class,
|
||||||
'can' => \Illuminate\Auth\Middleware\Authorize::class,
|
'can' => Authorize::class,
|
||||||
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
'guest' => RedirectIfAuthenticated::class,
|
||||||
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
|
'password.confirm' => RequirePassword::class,
|
||||||
'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,
|
'precognitive' => HandlePrecognitiveRequests::class,
|
||||||
'signed' => \App\Http\Middleware\ValidateSignature::class,
|
'signed' => ValidateSignature::class,
|
||||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
'throttle' => ThrottleRequests::class,
|
||||||
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
|
'verified' => EnsureEmailIsVerified::class,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,15 +13,17 @@ class IsModerator
|
|||||||
/**
|
/**
|
||||||
* Handle an incoming request.
|
* Handle an incoming request.
|
||||||
*
|
*
|
||||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
* @param Closure(Request): (Response) $next
|
||||||
*/
|
*/
|
||||||
public function handle(Request $request, Closure $next): Response
|
public function handle(Request $request, Closure $next): Response
|
||||||
{
|
{
|
||||||
if (Auth::check() && Auth::user()->hasRole(UserRole::MODERATOR)) {
|
if (Auth::check() && (
|
||||||
|
Auth::user()->hasRole(UserRole::MODERATOR) ||
|
||||||
|
Auth::user()->hasRole(UserRole::ADMINISTRATOR))) {
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
session()->flash('error_msg', 'This resource is restricted to Administrators!');
|
session()->flash('error_msg', 'This resource is restricted to Moderators!');
|
||||||
|
|
||||||
return redirect()->route('home.index');
|
return redirect()->route('home.index');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class RedirectIfAuthenticated
|
|||||||
/**
|
/**
|
||||||
* Handle an incoming request.
|
* Handle an incoming request.
|
||||||
*
|
*
|
||||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
* @param Closure(Request): (Response) $next
|
||||||
*/
|
*/
|
||||||
public function handle(Request $request, Closure $next, string ...$guards): Response
|
public function handle(Request $request, Closure $next, string ...$guards): Response
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class SetLocale
|
|||||||
/**
|
/**
|
||||||
* Handle an incoming request.
|
* Handle an incoming request.
|
||||||
*
|
*
|
||||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
* @param Closure(Request): (Response) $next
|
||||||
*/
|
*/
|
||||||
public function handle(Request $request, Closure $next): Response
|
public function handle(Request $request, Closure $next): Response
|
||||||
{
|
{
|
||||||
@@ -25,7 +25,9 @@ class SetLocale
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Session (guest or user override)
|
// 2. Session (guest or user override)
|
||||||
if (session()->has('locale') && in_array($request->language, config('app.supported_locales'), true)) {
|
if ($request->session()->has('locale') &&
|
||||||
|
in_array(session('locale'), config('app.supported_locales'), true)) {
|
||||||
|
|
||||||
App::setLocale(session('locale'));
|
App::setLocale(session('locale'));
|
||||||
|
|
||||||
return $next($request);
|
return $next($request);
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Http\Requests\Auth;
|
namespace App\Http\Requests\Auth;
|
||||||
|
|
||||||
use GrantHolle\Altcha\Rules\ValidAltcha;
|
use App\Rules\ValidCaptcha;
|
||||||
use Illuminate\Auth\Events\Lockout;
|
use Illuminate\Auth\Events\Lockout;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\RateLimiter;
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
@@ -23,21 +24,21 @@ class LoginRequest extends FormRequest
|
|||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
*
|
*
|
||||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
* @return array<string, ValidationRule|array<mixed>|string>
|
||||||
*/
|
*/
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'email' => ['required', 'string', 'email'],
|
'email' => ['required', 'string', 'email'],
|
||||||
'password' => ['required', 'string'],
|
'password' => ['required', 'string'],
|
||||||
'altcha' => ['required', new ValidAltcha],
|
'altcha' => ['required', new ValidCaptcha],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to authenticate the request's credentials.
|
* Attempt to authenticate the request's credentials.
|
||||||
*
|
*
|
||||||
* @throws \Illuminate\Validation\ValidationException
|
* @throws ValidationException
|
||||||
*/
|
*/
|
||||||
public function authenticate(): void
|
public function authenticate(): void
|
||||||
{
|
{
|
||||||
@@ -57,7 +58,7 @@ class LoginRequest extends FormRequest
|
|||||||
/**
|
/**
|
||||||
* Ensure the login request is not rate limited.
|
* Ensure the login request is not rate limited.
|
||||||
*
|
*
|
||||||
* @throws \Illuminate\Validation\ValidationException
|
* @throws ValidationException
|
||||||
*/
|
*/
|
||||||
public function ensureIsNotRateLimited(): void
|
public function ensureIsNotRateLimited(): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Requests;
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
class MatrixRegisterRequest extends FormRequest
|
class MatrixRegisterRequest extends FormRequest
|
||||||
@@ -20,7 +21,7 @@ class MatrixRegisterRequest extends FormRequest
|
|||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
*
|
*
|
||||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
* @return array<string, ValidationRule|array<mixed>|string>
|
||||||
*/
|
*/
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Http\Requests;
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ class ProfileUpdateRequest extends FormRequest
|
|||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
*
|
*
|
||||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
* @return array<string, ValidationRule|array<mixed>|string>
|
||||||
*/
|
*/
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Livewire;
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use App\Enums\UserRole;
|
||||||
use App\Models\Episode;
|
use App\Models\Episode;
|
||||||
|
use App\Models\ModLog;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Notifications\CommentNotification;
|
use App\Notifications\CommentNotification;
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
@@ -43,7 +45,7 @@ class Comment extends Component
|
|||||||
'replyState.body' => 'reply',
|
'replyState.body' => 'reply',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function updatedIsEditing($isEditing)
|
public function updatedIsEditing(bool $isEditing)
|
||||||
{
|
{
|
||||||
if (! $isEditing) {
|
if (! $isEditing) {
|
||||||
return;
|
return;
|
||||||
@@ -67,11 +69,45 @@ class Comment extends Component
|
|||||||
{
|
{
|
||||||
$this->authorize('destroy', $this->comment);
|
$this->authorize('destroy', $this->comment);
|
||||||
|
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
if ($user->hasRole(UserRole::ADMINISTRATOR) || $user->hasRole(UserRole::MODERATOR)) {
|
||||||
|
// Log to ModLog
|
||||||
|
ModLog::create([
|
||||||
|
'moderator' => $user->name,
|
||||||
|
'data' => "Deleted comment {$this->comment->id} written by {$this->comment->user->id} with contents: {$this->comment->body}",
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->comment->deleted_by_moderator_id = $user->id;
|
||||||
|
$this->comment->save();
|
||||||
|
$this->dispatch('refresh');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$this->comment->delete();
|
$this->comment->delete();
|
||||||
|
|
||||||
$this->dispatch('refresh');
|
$this->dispatch('refresh');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function restoreComment()
|
||||||
|
{
|
||||||
|
$this->authorize('restore', $this->comment);
|
||||||
|
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
if ($user->hasRole(UserRole::ADMINISTRATOR) || $user->hasRole(UserRole::MODERATOR)) {
|
||||||
|
// Log to ModLog
|
||||||
|
ModLog::create([
|
||||||
|
'moderator' => $user->name,
|
||||||
|
'data' => "Restored comment {$this->comment->id} written by {$this->comment->user->id} with contents: {$this->comment->body}",
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->comment->deleted_by_moderator_id = null;
|
||||||
|
$this->comment->save();
|
||||||
|
$this->dispatch('refresh');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function postReply()
|
public function postReply()
|
||||||
{
|
{
|
||||||
if (! ($this->comment->depth() < 2)) {
|
if (! ($this->comment->depth() < 2)) {
|
||||||
|
|||||||
@@ -23,11 +23,21 @@ class DownloadButton extends Component
|
|||||||
|
|
||||||
public $fileExtension = 'HEVC';
|
public $fileExtension = 'HEVC';
|
||||||
|
|
||||||
|
public $version = '';
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
if (str_contains($this->downloadUrl, 'AV1')) {
|
if (str_contains($this->downloadUrl, 'AV1')) {
|
||||||
$this->fileExtension = 'AV1';
|
$this->fileExtension = 'AV1';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (str_contains($this->downloadUrl, 'v2')) {
|
||||||
|
$this->version = 'v2';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($this->downloadUrl, 'v3')) {
|
||||||
|
$this->version = 'v3';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function clicked($downloadId)
|
public function clicked($downloadId)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Livewire;
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use App\Enums\UserRole;
|
||||||
use App\Models\Downloads;
|
use App\Models\Downloads;
|
||||||
use Livewire\Attributes\Url;
|
use Livewire\Attributes\Url;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
@@ -74,9 +75,9 @@ class DownloadsSearch extends Component
|
|||||||
$types[] = 'FHD';
|
$types[] = 'FHD';
|
||||||
} elseif ($label === 'FHD 48fps') {
|
} elseif ($label === 'FHD 48fps') {
|
||||||
$types[] = 'FHDi';
|
$types[] = 'FHDi';
|
||||||
} elseif ($label === 'UHD' && auth()->user()->hasRole(\App\Enums\UserRole::SUPPORTER)) {
|
} elseif ($label === 'UHD' && auth()->user()->hasRole(UserRole::SUPPORTER)) {
|
||||||
$types[] = 'UHD';
|
$types[] = 'UHD';
|
||||||
} elseif ($label === 'UHD 48fps' && auth()->user()->hasRole(\App\Enums\UserRole::SUPPORTER)) {
|
} elseif ($label === 'UHD 48fps' && auth()->user()->hasRole(UserRole::SUPPORTER)) {
|
||||||
$types[] = 'UHDi';
|
$types[] = 'UHDi';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,7 +100,7 @@ class DownloadsSearch extends Component
|
|||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
if (! auth()->user()->hasRole(\App\Enums\UserRole::SUPPORTER)) {
|
if (! auth()->user()->hasRole(UserRole::SUPPORTER)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use App\Enums\UserRole;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\SubscriptionService;
|
||||||
|
use Livewire\Component;
|
||||||
|
use Livewire\Attributes\Computed;
|
||||||
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
|
|
||||||
|
class UserSubscription extends Component
|
||||||
|
{
|
||||||
|
public $userId = 0;
|
||||||
|
|
||||||
|
public $subscriptionKey = '';
|
||||||
|
|
||||||
|
public $isActive = false;
|
||||||
|
|
||||||
|
protected $rules = [
|
||||||
|
'subscriptionKey' => 'required|string|size:48',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function mount(User $user)
|
||||||
|
{
|
||||||
|
$this->userId = $user ? $user->id : auth()->user()->id;
|
||||||
|
$this->subscriptionKey = $user->subscription_key ?? '';
|
||||||
|
$this->isActive = $user->hasRole(UserRole::SUPPORTER) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applyKey(SubscriptionService $subscriptionService)
|
||||||
|
{
|
||||||
|
$this->validate();
|
||||||
|
|
||||||
|
$rateLimitKey = "apply-subscription:{$this->userId}";
|
||||||
|
$rateLimitMinutes = 60 * 5; // 5 minutes
|
||||||
|
|
||||||
|
// Rate Limit to prevent users trying random keys
|
||||||
|
if (RateLimiter::tooManyAttempts($rateLimitKey, 1)) {
|
||||||
|
$seconds = RateLimiter::availableIn($rateLimitKey);
|
||||||
|
$this->addError('subscriptionKey', "Too many attempts. Try again in {$seconds} seconds.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RateLimiter::hit($rateLimitKey, $rateLimitMinutes);
|
||||||
|
|
||||||
|
// Check if token is already being used
|
||||||
|
$alreadyUsed = User::where('subscription_key', $this->subscriptionKey)
|
||||||
|
->whereNot('id', $this->userId)
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if ($alreadyUsed) {
|
||||||
|
$this->addError('subscriptionKey', 'Key already used!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = User::where('id', $this->userId)->firstOrFail();
|
||||||
|
|
||||||
|
// Verify token
|
||||||
|
$success = $subscriptionService->checkSubscriptionStatus($user, $this->subscriptionKey);
|
||||||
|
if (!$success) {
|
||||||
|
$this->addError('subscriptionKey', 'Invalid Key! If you believe this is a bug, please report this to the admin!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->subscription_key = $this->subscriptionKey;
|
||||||
|
$user->save();
|
||||||
|
$this->isActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.user-subscription');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Livewire;
|
|
||||||
|
|
||||||
use App\Models\Episode;
|
|
||||||
use Livewire\Component;
|
|
||||||
|
|
||||||
class ViewCount extends Component
|
|
||||||
{
|
|
||||||
public $episodeId = 0;
|
|
||||||
|
|
||||||
public $viewCount = 0;
|
|
||||||
|
|
||||||
public function mount(Episode $episode)
|
|
||||||
{
|
|
||||||
$this->episodeId = $episode->id;
|
|
||||||
$this->viewCount = $episode->view_count;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update()
|
|
||||||
{
|
|
||||||
$this->viewCount = Episode::where('id', $this->episodeId)->firstOrFail()->view_count;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function render()
|
|
||||||
{
|
|
||||||
return view('livewire.view-count');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -72,4 +72,12 @@ class Comment extends Model
|
|||||||
{
|
{
|
||||||
return cache()->remember('commentLikes'.$this->id, 300, fn () => $this->likes->count());
|
return cache()->remember('commentLikes'.$this->id, 300, fn () => $this->likes->count());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns wether or not comment has been removed by moderation
|
||||||
|
*/
|
||||||
|
public function isDeletedByModerator(): bool
|
||||||
|
{
|
||||||
|
return $this->deleted_by_moderator_id !== null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class ModLog extends Model
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'moderator',
|
||||||
|
'data',
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ namespace App\Models;
|
|||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
class SiteBackground extends Model
|
class SiteBackground extends Model
|
||||||
{
|
{
|
||||||
@@ -21,7 +22,7 @@ class SiteBackground extends Model
|
|||||||
/**
|
/**
|
||||||
* Returns the current IDs of active wallpaper
|
* Returns the current IDs of active wallpaper
|
||||||
*/
|
*/
|
||||||
public function getImages(): ?\Illuminate\Support\Collection
|
public function getImages(): ?Collection
|
||||||
{
|
{
|
||||||
$now = Carbon::now();
|
$now = Carbon::now();
|
||||||
|
|
||||||
|
|||||||
+11
-3
@@ -11,10 +11,12 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
|
|||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Spatie\LaravelPasskeys\Models\Concerns\HasPasskeys;
|
||||||
|
use Spatie\LaravelPasskeys\Models\Concerns\InteractsWithPasskeys;
|
||||||
|
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable implements HasPasskeys
|
||||||
{
|
{
|
||||||
use HasFactory, Notifiable;
|
use HasFactory, InteractsWithPasskeys, Notifiable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
@@ -29,6 +31,7 @@ class User extends Authenticatable
|
|||||||
// Discord
|
// Discord
|
||||||
'discord_id',
|
'discord_id',
|
||||||
'discord_avatar',
|
'discord_avatar',
|
||||||
|
'subscription_key',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,6 +42,7 @@ class User extends Authenticatable
|
|||||||
protected $hidden = [
|
protected $hidden = [
|
||||||
'password',
|
'password',
|
||||||
'remember_token',
|
'remember_token',
|
||||||
|
'subscription_key',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -153,7 +157,11 @@ class User extends Authenticatable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->roles = array_diff($this->roles, [$role->value]);
|
$this->roles = collect($this->roles)
|
||||||
|
->reject(fn ($value) => $value === $role->value)
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
$this->save();
|
$this->save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Policies;
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Enums\UserRole;
|
||||||
use App\Models\Comment;
|
use App\Models\Comment;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||||
@@ -17,6 +18,26 @@ class CommentPolicy
|
|||||||
|
|
||||||
public function destroy(User $user, Comment $comment): bool
|
public function destroy(User $user, Comment $comment): bool
|
||||||
{
|
{
|
||||||
|
if ($user->hasRole(UserRole::ADMINISTRATOR) ||
|
||||||
|
$user->hasRole(UserRole::MODERATOR)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return $user->id === $comment->user_id;
|
return $user->id === $comment->user_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function restore(User $user, Comment $comment): bool
|
||||||
|
{
|
||||||
|
// Comment not deleted
|
||||||
|
if ($comment->deleted_by_moderator_id === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->hasRole(UserRole::ADMINISTRATOR) ||
|
||||||
|
$user->hasRole(UserRole::MODERATOR)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ namespace App\Providers;
|
|||||||
|
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use SocialiteProviders\Discord\Provider;
|
||||||
|
use SocialiteProviders\Manager\SocialiteWasCalled;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
@@ -20,8 +22,8 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) {
|
Event::listen(function (SocialiteWasCalled $event) {
|
||||||
$event->extendSocialite('discord', \SocialiteProviders\Discord\Provider::class);
|
$event->extendSocialite('discord', Provider::class);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Rules;
|
||||||
|
|
||||||
|
use AltchaOrg\Altcha\Algorithm\Pbkdf2;
|
||||||
|
use AltchaOrg\Altcha\Altcha;
|
||||||
|
use AltchaOrg\Altcha\Challenge;
|
||||||
|
use AltchaOrg\Altcha\ChallengeParameters;
|
||||||
|
use AltchaOrg\Altcha\Payload;
|
||||||
|
use AltchaOrg\Altcha\Solution;
|
||||||
|
use AltchaOrg\Altcha\VerifySolutionOptions;
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
use Illuminate\Translation\PotentiallyTranslatedString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validation rule to verify captcha solution.
|
||||||
|
*/
|
||||||
|
class ValidCaptcha implements ValidationRule
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Altcha instance.
|
||||||
|
*/
|
||||||
|
protected Altcha $altcha;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pbkdf2 algorithm instance.
|
||||||
|
*/
|
||||||
|
protected Pbkdf2 $pbkdf2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class constructor.
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->pbkdf2 = new Pbkdf2;
|
||||||
|
$this->altcha = new Altcha(
|
||||||
|
hmacSignatureSecret: config('captcha.hmac_key'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse payload and return the decoded data as an array.
|
||||||
|
*/
|
||||||
|
private function parsePayload(string $value): ?array
|
||||||
|
{
|
||||||
|
$decoded = base64_decode($value, true);
|
||||||
|
if ($decoded === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = json_decode($decoded, true);
|
||||||
|
if (! is_array($payload)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify if payload has required fields.
|
||||||
|
*/
|
||||||
|
private function verifyFields(array $payload): bool
|
||||||
|
{
|
||||||
|
if (! isset($payload['challenge'], $payload['solution'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! is_array($payload['challenge']) || ! is_array($payload['solution'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Challenge object from challenge data.
|
||||||
|
*/
|
||||||
|
private function createChallenge(array $challengeData): Challenge
|
||||||
|
{
|
||||||
|
return new Challenge(
|
||||||
|
ChallengeParameters::fromArray($challengeData['parameters'] ?? []),
|
||||||
|
$challengeData['signature'] ?? null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Solution object from solution data.
|
||||||
|
*/
|
||||||
|
private function createSolution(array $solutionData): Solution
|
||||||
|
{
|
||||||
|
return new Solution(
|
||||||
|
counter: (int) ($solutionData['counter'] ?? 0),
|
||||||
|
derivedKey: (string) ($solutionData['derivedKey'] ?? ''),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the validation rule.
|
||||||
|
*
|
||||||
|
* @param Closure(string, ?string=): PotentiallyTranslatedString $fail
|
||||||
|
*/
|
||||||
|
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||||
|
{
|
||||||
|
$payload = $this->parsePayload($value);
|
||||||
|
if (! $payload) {
|
||||||
|
$fail('Invalid captcha.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $this->verifyFields($payload)) {
|
||||||
|
$fail('Invalid captcha.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$challenge = $this->createChallenge($payload['challenge']);
|
||||||
|
$solution = $this->createSolution($payload['solution']);
|
||||||
|
|
||||||
|
$result = $this->altcha->verifySolution(new VerifySolutionOptions(
|
||||||
|
algorithm: $this->pbkdf2,
|
||||||
|
payload: new Payload($challenge, $solution),
|
||||||
|
));
|
||||||
|
|
||||||
|
if (! $result->verified) {
|
||||||
|
$fail('Invalid captcha.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ namespace App\Services;
|
|||||||
use App\Models\Episode;
|
use App\Models\Episode;
|
||||||
use App\Models\Hentai;
|
use App\Models\Hentai;
|
||||||
use App\Models\Studios;
|
use App\Models\Studios;
|
||||||
|
use App\Models\ModLog;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
@@ -62,6 +63,68 @@ class EpisodeService
|
|||||||
return $episode;
|
return $episode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function applyTags(Request $request, Episode $episode): void
|
||||||
|
{
|
||||||
|
$tags = json_decode($request->input('tags'));
|
||||||
|
$newtags = [];
|
||||||
|
foreach ($tags as $t) {
|
||||||
|
$newtags[] = $t->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$newTagsTemp = $newtags;
|
||||||
|
$oldTagsTemp = $episode->tagNames();
|
||||||
|
|
||||||
|
sort($newTagsTemp);
|
||||||
|
sort($oldTagsTemp);
|
||||||
|
|
||||||
|
if ($newTagsTemp !== $oldTagsTemp) {
|
||||||
|
ModLog::create([
|
||||||
|
'moderator' => $request->user()->name,
|
||||||
|
'data' => sprintf(
|
||||||
|
'Updated Episode tags from %s to %s',
|
||||||
|
implode(', ', $oldTagsTemp),
|
||||||
|
implode(', ', $newTagsTemp),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$episode->retag($newtags);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function updateTitle(Request $request, Episode $episode): void
|
||||||
|
{
|
||||||
|
$updates = [];
|
||||||
|
|
||||||
|
if ($episode->title !== $request->input('title')) {
|
||||||
|
$updates['title'] = $request->input('title');
|
||||||
|
$updates['title_search'] = preg_replace(
|
||||||
|
'/[^A-Za-z0-9 ]/',
|
||||||
|
'',
|
||||||
|
$request->input('title')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Log to ModLog
|
||||||
|
ModLog::create([
|
||||||
|
'moderator' => $request->user()->name,
|
||||||
|
'data' => "Updating Hentai Title from {$episode->title} to {$request->input('title')}",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($episode->title_jpn !== $request->input('title_jpn')) {
|
||||||
|
$updates['title_jpn'] = $request->input('title_jpn');
|
||||||
|
|
||||||
|
// Log to ModLog
|
||||||
|
ModLog::create([
|
||||||
|
'moderator' => $request->user()->name,
|
||||||
|
'data' => "Updating Hentai Title from {$episode->title_jpn} to {$request->input('title_jpn')}",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($updates)) {
|
||||||
|
$episode->hentai->episodes()->update($updates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function updateEpisode(Request $request, Studios $studio, int $episodeId): Episode
|
public function updateEpisode(Request $request, Studios $studio, int $episodeId): Episode
|
||||||
{
|
{
|
||||||
$episode = Episode::where('id', $episodeId)->firstOrFail();
|
$episode = Episode::where('id', $episodeId)->firstOrFail();
|
||||||
@@ -75,17 +138,31 @@ class EpisodeService
|
|||||||
$episode->dmca_takedown = $request->input('dmca_takedown') == 'true';
|
$episode->dmca_takedown = $request->input('dmca_takedown') == 'true';
|
||||||
$episode->save();
|
$episode->save();
|
||||||
|
|
||||||
// Tagging
|
$this->applyTags($request, $episode);
|
||||||
$tags = json_decode($request->input('tags'));
|
$this->updateTitle($request, $episode);
|
||||||
$newtags = [];
|
|
||||||
foreach ($tags as $t) {
|
|
||||||
$newtags[] = $t->value;
|
|
||||||
}
|
|
||||||
$episode->retag($newtags);
|
|
||||||
|
|
||||||
return $episode;
|
return $episode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function updateEpisodeModerator(Request $request, int $episodeId): void
|
||||||
|
{
|
||||||
|
$episode = Episode::where('id', $episodeId)->firstOrFail();
|
||||||
|
$oldDescription = $episode->description;
|
||||||
|
$episode->description = $request->input('description');
|
||||||
|
$episode->save();
|
||||||
|
|
||||||
|
if ($episode->description !== $oldDescription) {
|
||||||
|
// Log to ModLog
|
||||||
|
ModLog::create([
|
||||||
|
'moderator' => $request->user()->name,
|
||||||
|
'data' => "Updated Episode description from {$oldDescription} to {$episode->description}",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->applyTags($request, $episode);
|
||||||
|
$this->updateTitle($request, $episode);
|
||||||
|
}
|
||||||
|
|
||||||
public function getOrCreateStudio(string $studioName): Studios
|
public function getOrCreateStudio(string $studioName): Studios
|
||||||
{
|
{
|
||||||
return Studios::firstOrCreate(
|
return Studios::firstOrCreate(
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Enums\UserRole;
|
||||||
|
use App\Models\User;
|
||||||
|
use Exception;
|
||||||
|
use Illuminate\Contracts\Encryption\DecryptException;
|
||||||
|
use Illuminate\Support\Facades\Crypt;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class SubscriptionService
|
||||||
|
{
|
||||||
|
private function generateEncryptedPayload(string $subscriptionKey): string
|
||||||
|
{
|
||||||
|
return base64_encode(Crypt::encryptString(json_encode([
|
||||||
|
'subscription_access_key' => $subscriptionKey,
|
||||||
|
'timestamp' => now()->timestamp,
|
||||||
|
'nonce' => Str::uuid()->toString(),
|
||||||
|
], JSON_THROW_ON_ERROR)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the subscription status from the subscription service.
|
||||||
|
*/
|
||||||
|
private function getSubscriptionStatus(string $subscriptionKey): array | null
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$payload = $this->generateEncryptedPayload($subscriptionKey);
|
||||||
|
|
||||||
|
$response = Http::post(config('services.subscription_service_host').'/api/membership/verify', [
|
||||||
|
'payload' => $payload,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (! $response->successful()) {
|
||||||
|
logger()->error('Subscription Service API error', [
|
||||||
|
'status' => $response->status(),
|
||||||
|
'body' => $response->body(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$encryptedResponse = $response->json('payload');
|
||||||
|
|
||||||
|
$json = json_decode(
|
||||||
|
Crypt::decryptString($encryptedResponse),
|
||||||
|
true,
|
||||||
|
flags: JSON_THROW_ON_ERROR
|
||||||
|
);
|
||||||
|
|
||||||
|
return $json;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
logger()->error('getSubscriptionStatus Exception', [
|
||||||
|
'details' => $e,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function checkSubscriptionStatus(User $user, string $subscriptionKey): bool
|
||||||
|
{
|
||||||
|
$subscriptionStatus = $this->getSubscriptionStatus($subscriptionKey);
|
||||||
|
if (!$subscriptionStatus) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($subscriptionStatus['valid'] === true &&
|
||||||
|
$subscriptionStatus['active'] === true) {
|
||||||
|
$user->addRole(UserRole::SUPPORTER);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->removeRole(UserRole::SUPPORTER);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-4
@@ -1,5 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Exceptions\Handler;
|
||||||
|
use App\Http\Kernel;
|
||||||
|
use Illuminate\Contracts\Debug\ExceptionHandler;
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Create The Application
|
| Create The Application
|
||||||
@@ -11,7 +16,7 @@
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$app = new Illuminate\Foundation\Application(
|
$app = new Application(
|
||||||
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
|
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -28,7 +33,7 @@ $app = new Illuminate\Foundation\Application(
|
|||||||
|
|
||||||
$app->singleton(
|
$app->singleton(
|
||||||
Illuminate\Contracts\Http\Kernel::class,
|
Illuminate\Contracts\Http\Kernel::class,
|
||||||
App\Http\Kernel::class
|
Kernel::class
|
||||||
);
|
);
|
||||||
|
|
||||||
$app->singleton(
|
$app->singleton(
|
||||||
@@ -37,8 +42,8 @@ $app->singleton(
|
|||||||
);
|
);
|
||||||
|
|
||||||
$app->singleton(
|
$app->singleton(
|
||||||
Illuminate\Contracts\Debug\ExceptionHandler::class,
|
ExceptionHandler::class,
|
||||||
App\Exceptions\Handler::class
|
Handler::class
|
||||||
);
|
);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
+2
-1
@@ -9,7 +9,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
"grantholle/laravel-altcha": "^2.1",
|
"altcha-org/altcha": "^2.0",
|
||||||
"guzzlehttp/guzzle": "^7.8.1",
|
"guzzlehttp/guzzle": "^7.8.1",
|
||||||
"hisorange/browser-detect": "^5.0",
|
"hisorange/browser-detect": "^5.0",
|
||||||
"http-interop/http-factory-guzzle": "^1.2",
|
"http-interop/http-factory-guzzle": "^1.2",
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
"rtconner/laravel-tagging": "^5.0",
|
"rtconner/laravel-tagging": "^5.0",
|
||||||
"socialiteproviders/discord": "^4.2",
|
"socialiteproviders/discord": "^4.2",
|
||||||
"spatie/laravel-discord-alerts": "^1.8",
|
"spatie/laravel-discord-alerts": "^1.8",
|
||||||
|
"spatie/laravel-passkeys": "^1.7",
|
||||||
"spatie/laravel-sitemap": "^7.3"
|
"spatie/laravel-sitemap": "^7.3"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
|||||||
Generated
+1807
-753
File diff suppressed because it is too large
Load Diff
+10
-5
@@ -1,7 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Providers\AppServiceProvider;
|
||||||
|
use App\Providers\AuthServiceProvider;
|
||||||
|
use App\Providers\EventServiceProvider;
|
||||||
|
use App\Providers\RouteServiceProvider;
|
||||||
use Illuminate\Support\Facades\Facade;
|
use Illuminate\Support\Facades\Facade;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Mews\Captcha\Facades\Captcha;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
@@ -175,11 +180,11 @@ return [
|
|||||||
/*
|
/*
|
||||||
* Application Service Providers...
|
* Application Service Providers...
|
||||||
*/
|
*/
|
||||||
App\Providers\AppServiceProvider::class,
|
AppServiceProvider::class,
|
||||||
App\Providers\AuthServiceProvider::class,
|
AuthServiceProvider::class,
|
||||||
// App\Providers\BroadcastServiceProvider::class,
|
// App\Providers\BroadcastServiceProvider::class,
|
||||||
App\Providers\EventServiceProvider::class,
|
EventServiceProvider::class,
|
||||||
App\Providers\RouteServiceProvider::class,
|
RouteServiceProvider::class,
|
||||||
])->toArray(),
|
])->toArray(),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -195,7 +200,7 @@ return [
|
|||||||
|
|
||||||
'aliases' => Facade::defaultAliases()->merge([
|
'aliases' => Facade::defaultAliases()->merge([
|
||||||
// 'Example' => App\Facades\Example::class,
|
// 'Example' => App\Facades\Example::class,
|
||||||
'Captcha' => Mews\Captcha\Facades\Captcha::class,
|
'Captcha' => Captcha::class,
|
||||||
])->toArray(),
|
])->toArray(),
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
+3
-1
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -62,7 +64,7 @@ return [
|
|||||||
'providers' => [
|
'providers' => [
|
||||||
'users' => [
|
'users' => [
|
||||||
'driver' => 'eloquent',
|
'driver' => 'eloquent',
|
||||||
'model' => App\Models\User::class,
|
'model' => User::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
// 'users' => [
|
// 'users' => [
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Altcha Captcha System
|
||||||
|
*/
|
||||||
|
|
||||||
|
return [
|
||||||
|
'hmac_key' => env('ALTCHA_HMAC_KEY'),
|
||||||
|
];
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use Spatie\DiscordAlerts\Jobs\SendToDiscordChannelJob;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
/*
|
/*
|
||||||
* The webhook URLs that we'll use to send a message to Discord.
|
* The webhook URLs that we'll use to send a message to Discord.
|
||||||
@@ -14,5 +16,5 @@ return [
|
|||||||
* This job will send the message to Discord. You can extend this
|
* This job will send the message to Discord. You can extend this
|
||||||
* job to set timeouts, retries, etc...
|
* job to set timeouts, retries, etc...
|
||||||
*/
|
*/
|
||||||
'job' => Spatie\DiscordAlerts\Jobs\SendToDiscordChannelJob::class,
|
'job' => SendToDiscordChannelJob::class,
|
||||||
];
|
];
|
||||||
|
|||||||
+3
-1
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use Intervention\Image\Drivers\Gd\Driver;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -16,7 +18,7 @@ return [
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'driver' => \Intervention\Image\Drivers\Gd\Driver::class,
|
'driver' => Driver::class,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Spatie\LaravelPasskeys\Actions\ConfigureCeremonyStepManagerFactoryAction;
|
||||||
|
use Spatie\LaravelPasskeys\Actions\FindPasskeyToAuthenticateAction;
|
||||||
|
use Spatie\LaravelPasskeys\Actions\GeneratePasskeyAuthenticationOptionsAction;
|
||||||
|
use Spatie\LaravelPasskeys\Actions\GeneratePasskeyRegisterOptionsAction;
|
||||||
|
use Spatie\LaravelPasskeys\Actions\StorePasskeyAction;
|
||||||
|
use Spatie\LaravelPasskeys\Models\Passkey;
|
||||||
|
|
||||||
|
return [
|
||||||
|
/*
|
||||||
|
* After a successful authentication attempt using a passkey
|
||||||
|
* we'll redirect to this URL.
|
||||||
|
*/
|
||||||
|
'redirect_to_after_login' => '/',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* These class are responsible for performing core tasks regarding passkeys.
|
||||||
|
* You can customize them by creating a class that extends the default, and
|
||||||
|
* by specifying your custom class name here.
|
||||||
|
*/
|
||||||
|
'actions' => [
|
||||||
|
'generate_passkey_register_options' => GeneratePasskeyRegisterOptionsAction::class,
|
||||||
|
'store_passkey' => StorePasskeyAction::class,
|
||||||
|
'generate_passkey_authentication_options' => GeneratePasskeyAuthenticationOptionsAction::class,
|
||||||
|
'find_passkey' => FindPasskeyToAuthenticateAction::class,
|
||||||
|
'configure_ceremony_step_manager_factory' => ConfigureCeremonyStepManagerFactoryAction::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* These properties will be used to generate the passkey.
|
||||||
|
*/
|
||||||
|
'relying_party' => [
|
||||||
|
'name' => config('app.name'),
|
||||||
|
'id' => parse_url(config('app.url'), PHP_URL_HOST),
|
||||||
|
'icon' => null,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The models used by the package.
|
||||||
|
*
|
||||||
|
* You can override this by specifying your own models
|
||||||
|
*/
|
||||||
|
'models' => [
|
||||||
|
'passkey' => Passkey::class,
|
||||||
|
'authenticatable' => env('AUTH_MODEL', User::class),
|
||||||
|
],
|
||||||
|
];
|
||||||
+6
-3
@@ -1,5 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Cookie\Middleware\EncryptCookies;
|
||||||
|
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
|
||||||
|
use Laravel\Sanctum\Http\Middleware\AuthenticateSession;
|
||||||
use Laravel\Sanctum\Sanctum;
|
use Laravel\Sanctum\Sanctum;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -60,9 +63,9 @@ return [
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
'middleware' => [
|
'middleware' => [
|
||||||
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
|
'authenticate_session' => AuthenticateSession::class,
|
||||||
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
|
'encrypt_cookies' => EncryptCookies::class,
|
||||||
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
|
'validate_csrf_token' => ValidateCsrfToken::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -54,4 +54,9 @@ return [
|
|||||||
'server' => env('MATRIX_SERVER'),
|
'server' => env('MATRIX_SERVER'),
|
||||||
'shared_secret' => env('MATRIX_SHARED_SECRET'),
|
'shared_secret' => env('MATRIX_SHARED_SECRET'),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription Service
|
||||||
|
*/
|
||||||
|
'subscription_service_host' => env('SUBSCRIPTION_SERVICE_HOST'),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
namespace Database\Factories;
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Episode;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Episode>
|
* @extends Factory<Episode>
|
||||||
*/
|
*/
|
||||||
class EpisodeFactory extends Factory
|
class EpisodeFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
namespace Database\Factories;
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Hentai;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Hentai>
|
* @extends Factory<Hentai>
|
||||||
*/
|
*/
|
||||||
class HentaiFactory extends Factory
|
class HentaiFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
namespace Database\Factories;
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Studios;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Studios>
|
* @extends Factory<Studios>
|
||||||
*/
|
*/
|
||||||
class StudiosFactory extends Factory
|
class StudiosFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
namespace Database\Factories;
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
* @extends Factory<User>
|
||||||
*/
|
*/
|
||||||
class UserFactory extends Factory
|
class UserFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ return new class extends Migration
|
|||||||
|
|
||||||
$alreadyexists = Episode::where('slug', $episode->slug)->first();
|
$alreadyexists = Episode::where('slug', $episode->slug)->first();
|
||||||
if ($alreadyexists) {
|
if ($alreadyexists) {
|
||||||
throw new \RuntimeException('Migration stopped! Slug already exists: '.$episode->slug);
|
throw new RuntimeException('Migration stopped! Slug already exists: '.$episode->slug);
|
||||||
}
|
}
|
||||||
|
|
||||||
$episode->save();
|
$episode->save();
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Spatie\LaravelPasskeys\Support\Config;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
$authenticatableClass = Config::getAuthenticatableModel();
|
||||||
|
|
||||||
|
$authenticatableTableName = (new $authenticatableClass)->getTable();
|
||||||
|
|
||||||
|
Schema::create('passkeys', function (Blueprint $table) use ($authenticatableTableName, $authenticatableClass) {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
$table
|
||||||
|
->foreignIdFor($authenticatableClass, 'authenticatable_id')
|
||||||
|
->constrained(table: $authenticatableTableName, indexName: 'passkeys_authenticatable_fk')
|
||||||
|
->cascadeOnDelete();
|
||||||
|
|
||||||
|
$table->text('name');
|
||||||
|
$table->text('credential_id');
|
||||||
|
$table->json('data');
|
||||||
|
|
||||||
|
$table->timestamp('last_used_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->string('subscription_key', 64)
|
||||||
|
->unique()
|
||||||
|
->nullable()
|
||||||
|
->after('roles');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('subscription_key');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('comments', function (Blueprint $table) {
|
||||||
|
$table->bigInteger('deleted_by_moderator_id')
|
||||||
|
->nullable()
|
||||||
|
->after('parent_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('comments', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('deleted_by_moderator_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('mod_logs', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('moderator');
|
||||||
|
$table->text('data');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('mod_logs');
|
||||||
|
}
|
||||||
|
};
|
||||||
Generated
+459
-364
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -19,8 +19,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||||
"@jellyfin/libass-wasm": "^4.1.1",
|
"@jellyfin/libass-wasm": "^4.1.1",
|
||||||
|
"@simplewebauthn/browser": "^13.3.0",
|
||||||
"@yaireo/tagify": "^4.21.2",
|
"@yaireo/tagify": "^4.21.2",
|
||||||
"altcha": "^2.3.0",
|
"altcha": "^3.0.0",
|
||||||
"chart.js": "^4.5.0",
|
"chart.js": "^4.5.0",
|
||||||
"dashjs": "^5.0.0",
|
"dashjs": "^5.0.0",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
|
|||||||
+1
-25
@@ -126,29 +126,5 @@ input:checked~.dot {
|
|||||||
|
|
||||||
/* Captcha */
|
/* Captcha */
|
||||||
:root {
|
:root {
|
||||||
--altcha-border-width: 1px;
|
color-scheme: light dark;
|
||||||
--altcha-border-radius: 0.375rem;
|
|
||||||
--altcha-color-base: #333;
|
|
||||||
--altcha-color-border: #a0a0a0;
|
|
||||||
--altcha-color-text: #fff;
|
|
||||||
--altcha-color-border-focus: currentColor;
|
|
||||||
--altcha-color-error-text: #f23939;
|
|
||||||
--altcha-color-footer-bg: #141414;
|
|
||||||
--altcha-max-width: 260px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.altcha-footer {
|
|
||||||
border-bottom-left-radius: 0.375rem;
|
|
||||||
border-bottom-right-radius: 0.375rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"] {
|
|
||||||
background-color: #ffffff;
|
|
||||||
border-color: #a0a0a0;
|
|
||||||
color: rgb(225,29,72);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"]:checked {
|
|
||||||
background-color: rgb(225,29,72);
|
|
||||||
box-shadow: 0 0 0 0px #fff, 0 0 0 calc(2px + 0px) rgba(246, 59, 118, 0.5), 0 0 #0000;
|
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
|
|
||||||
// Captcha
|
// Captcha
|
||||||
import 'altcha';
|
import 'altcha';
|
||||||
|
import "altcha/themes/cupcake.css";
|
||||||
|
|
||||||
// import Alpine from 'alpinejs';
|
// import Alpine from 'alpinejs';
|
||||||
|
|
||||||
@@ -22,3 +23,16 @@ import 'altcha';
|
|||||||
// Alpine.start();
|
// Alpine.start();
|
||||||
|
|
||||||
initTE({ Collapse, Carousel, Clipboard, Modal, Tab, Lightbox, Tooltip, Ripple });
|
initTE({ Collapse, Carousel, Clipboard, Modal, Tab, Lightbox, Tooltip, Ripple });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Passkey Support
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
browserSupportsWebAuthn,
|
||||||
|
startAuthentication,
|
||||||
|
startRegistration,
|
||||||
|
} from '@simplewebauthn/browser'
|
||||||
|
|
||||||
|
window.browserSupportsWebAuthn = browserSupportsWebAuthn;
|
||||||
|
window.startAuthentication = startAuthentication;
|
||||||
|
window.startRegistration = startRegistration;
|
||||||
+40
-44
@@ -1,56 +1,52 @@
|
|||||||
const sleep = (ms = 0) => new Promise(resolve => setTimeout(resolve, ms));
|
function initGalleryPreviews() {
|
||||||
var old_timestamp = document.getElementById('ts_reference').value;
|
const previews = document.querySelectorAll('.preview-gallery');
|
||||||
|
|
||||||
function initPreviews() {
|
previews.forEach((img) => {
|
||||||
var thumbs = document.querySelectorAll('div[data-thumbs]');
|
// Prevent double initialization
|
||||||
thumbs.forEach(function (thumb) {
|
if (img.dataset.previewInitialized) return;
|
||||||
var thumbsJSON = JSON.parse(thumb.dataset.thumbs);
|
|
||||||
var originalImage = thumb.children[0].children[1].src;
|
|
||||||
var interval;
|
|
||||||
var i = 1;
|
|
||||||
|
|
||||||
function clear() {
|
img.dataset.previewInitialized = 'true';
|
||||||
thumb.children[0].children[1].src = originalImage;
|
|
||||||
i = 1;
|
|
||||||
clearTimeout(interval);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
let images = [];
|
||||||
if (i == 0) {
|
|
||||||
clear();
|
try {
|
||||||
|
images = JSON.parse(img.dataset.gallery);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Invalid gallery JSON', e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
thumb.children[0].children[1].src = thumbsJSON[i];
|
if (images.length <= 1) return;
|
||||||
i = (i + 1) % thumbsJSON.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
function interval() {
|
const original = img.src;
|
||||||
// Start Preview
|
|
||||||
interval = setInterval(toggle, 700);
|
|
||||||
}
|
|
||||||
|
|
||||||
thumb.addEventListener('mouseenter', interval);
|
let index = 0;
|
||||||
thumb.addEventListener('mouseleave', clear);
|
let interval = null;
|
||||||
|
|
||||||
|
const startPreview = () => {
|
||||||
|
console.log("startPreview");
|
||||||
|
interval = setInterval(() => {
|
||||||
|
index = (index + 1) % images.length;
|
||||||
|
img.src = images[index];
|
||||||
|
}, 700);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopPreview = () => {
|
||||||
|
console.log("stopPreview");
|
||||||
|
clearInterval(interval);
|
||||||
|
interval = null;
|
||||||
|
|
||||||
|
index = 0;
|
||||||
|
img.src = original;
|
||||||
|
};
|
||||||
|
|
||||||
|
img.addEventListener('mouseenter', startPreview);
|
||||||
|
img.addEventListener('mouseleave', stopPreview);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function init() {
|
// Initial page load
|
||||||
for (let i = 0; i < 9; i++) {
|
document.addEventListener('DOMContentLoaded', initGalleryPreviews);
|
||||||
var new_timestamp = document.getElementById('ts_reference').value;
|
|
||||||
if (new_timestamp != old_timestamp) {
|
|
||||||
console.log('== Changed ==');
|
|
||||||
initPreviews();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
console.log('== Didnt Change ==');
|
|
||||||
await sleep(1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('contentChanged', event => {
|
// Livewire v3 navigation/update
|
||||||
console.log('== Received contentChanged Event ==');
|
document.addEventListener('contentChanged', initGalleryPreviews);
|
||||||
init();
|
|
||||||
});
|
|
||||||
|
|
||||||
initPreviews();
|
|
||||||
@@ -1,13 +1,32 @@
|
|||||||
<div data-te-modal-init class="fixed left-0 top-0 z-[1055] hidden h-full w-full overflow-y-auto overflow-x-hidden outline-none" id="modalEditEpisode" tabindex="-1" aria-labelledby="Upload" aria-modal="true" role="dialog">
|
<div
|
||||||
<div data-te-modal-dialog-ref class="pointer-events-none relative flex min-h-[calc(100%-1rem)] w-auto translate-y-[-50px] items-center opacity-0 transition-all duration-300 ease-in-out min-[576px]:mx-auto min-[576px]:mt-7 min-[576px]:min-h-[calc(100%-3.5rem)] min-[576px]:max-w-[95%] md:min-[576px]:max-w-[90%] lg:min-[576px]:max-w-[80%] xl:min-[576px]:max-w-[70%] 2xl:min-[576px]:max-w-[50%]">
|
data-te-modal-init
|
||||||
<div class="flex relative flex-col w-full text-current bg-clip-padding bg-white rounded-md border-none shadow-lg outline-none pointer-events-auto dark:bg-neutral-800">
|
id="modalEditEpisode"
|
||||||
|
tabindex="-1"
|
||||||
|
aria-modal="true"
|
||||||
|
role="dialog"
|
||||||
|
class="fixed inset-0 z-[1055] hidden overflow-y-auto bg-black/60 backdrop-blur-sm"
|
||||||
|
>
|
||||||
|
<div data-te-modal-dialog-ref class="flex min-h-screen items-center justify-center p-4">
|
||||||
|
<div class="relative w-full max-w-7xl overflow-hidden rounded-2xl border border-neutral-200 bg-white shadow-2xl dark:border-neutral-700 dark:bg-neutral-900">
|
||||||
<x-modal-header :title="__('Edit Episode')"/>
|
<x-modal-header :title="__('Edit Episode')"/>
|
||||||
|
|
||||||
<!--Modal body-->
|
<!--Modal body-->
|
||||||
<div class="relative p-4 pt-0">
|
<div class="relative p-4 pt-0">
|
||||||
<form method="POST" action="{{ route('admin.edit') }}" enctype="multipart/form-data">
|
<form method="POST" action="{{ route('admin.episode.edit') }}" enctype="multipart/form-data">
|
||||||
@csrf
|
@csrf
|
||||||
<div class="grid grid-cols-3">
|
<div class="flex flex-col gap-2 p-2">
|
||||||
|
<div>
|
||||||
|
<label class="leading-tight text-gray-800 dark:text-gray-200 w-full" for="title">Title:</label>
|
||||||
|
<x-text-input id="title" value="{{ $episode->title }}" class="block w-full" type="text" name="title" required autofocus/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="leading-tight text-gray-800 dark:text-gray-200 w-full" for="title_jpn">Title JPN:</label>
|
||||||
|
<x-text-input id="title_jpn" value="{{ $episode->title_jpn }}" class="block w-full" type="text" name="title_jpn" required />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-3 p-2">
|
||||||
<div class="col-span-2">
|
<div class="col-span-2">
|
||||||
<!-- Tags -->
|
<!-- Tags -->
|
||||||
<div class="row-span-2 p-0">
|
<div class="row-span-2 p-0">
|
||||||
@@ -16,6 +35,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if(auth()->user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
|
||||||
<div class="grid grid-rows-2">
|
<div class="grid grid-rows-2">
|
||||||
<!-- Studio -->
|
<!-- Studio -->
|
||||||
<div class="p-2 pt-0">
|
<div class="p-2 pt-0">
|
||||||
@@ -47,13 +67,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if(auth()->user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
|
||||||
<!-- Stream URL -->
|
<!-- Stream URL -->
|
||||||
<div class="p-2 pt-0">
|
<div class="p-2 pt-0">
|
||||||
<label class="w-full leading-tight text-gray-800 dark:text-gray-200" for="baseurl">Stream:</label>
|
<label class="w-full leading-tight text-gray-800 dark:text-gray-200" for="baseurl">Stream:</label>
|
||||||
<x-text-input id="baseurl" class="block w-full" type="text" name="baseurl" value="{{ $episode->url }}" required />
|
<x-text-input id="baseurl" class="block w-full" type="text" name="baseurl" value="{{ $episode->url }}" required />
|
||||||
</div>
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<input name="episode_id" id="episode_id" type="hidden" value="{{ $episode->id }}" />
|
<input name="episode_id" id="episode_id" type="hidden" value="{{ $episode->id }}" />
|
||||||
|
|
||||||
@@ -62,6 +85,7 @@
|
|||||||
<textarea rows="4" cols="50" id="description" name="description" class="block mt-1 w-full rounded-md border-gray-300 shadow-sm dark:border-gray-700 dark:bg-neutral-900 dark:text-gray-300 focus:border-rose-500 dark:focus:border-rose-600 focus:ring-rose-500 dark:focus:ring-rose-600" required>{{ $episode->description }}</textarea>
|
<textarea rows="4" cols="50" id="description" name="description" class="block mt-1 w-full rounded-md border-gray-300 shadow-sm dark:border-gray-700 dark:bg-neutral-900 dark:text-gray-300 focus:border-rose-500 dark:focus:border-rose-600 focus:ring-rose-500 dark:focus:ring-rose-600" required>{{ $episode->description }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if(auth()->user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
|
||||||
<!-- Episodes -->
|
<!-- Episodes -->
|
||||||
<div class="grid grid-cols-2">
|
<div class="grid grid-cols-2">
|
||||||
<!-- Cover -->
|
<!-- Cover -->
|
||||||
@@ -95,8 +119,10 @@
|
|||||||
<label class="w-full leading-tight text-gray-800 dark:text-gray-200" for="downloadUHDi1">Download 4k Interpolated:</label>
|
<label class="w-full leading-tight text-gray-800 dark:text-gray-200" for="downloadUHDi1">Download 4k Interpolated:</label>
|
||||||
<x-text-input id="downloadUHDi1" class="block w-full" type="text" name="downloadUHDi1" value="{{ $episode->getDownloadByType('UHDi')->url ?? '' }}" />
|
<x-text-input id="downloadUHDi1" class="block w-full" type="text" name="downloadUHDi1" value="{{ $episode->getDownloadByType('UHDi')->url ?? '' }}" />
|
||||||
</div>
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<div class="flex flex-wrap flex-shrink-0 justify-end items-center p-4 rounded-b-md">
|
<div class="sticky bottom-0 flex items-center justify-end gap-3 border-t border-neutral-200 bg-white/90 px-6 py-4 backdrop-blur dark:border-neutral-700 dark:bg-neutral-900/90">
|
||||||
|
@if(auth()->user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
|
||||||
<div class="inline-block mr-2">
|
<div class="inline-block mr-2">
|
||||||
<input class="w-4 h-4 text-rose-600 bg-gray-100 border-gray-300 rounded focus:ring-rose-500 dark:focus:ring-rose-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
<input class="w-4 h-4 text-rose-600 bg-gray-100 border-gray-300 rounded focus:ring-rose-500 dark:focus:ring-rose-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
||||||
type="checkbox" value="true" id="v2" name="v2" />
|
type="checkbox" value="true" id="v2" name="v2" />
|
||||||
@@ -111,10 +137,17 @@
|
|||||||
DMCA Takedown
|
DMCA Takedown
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="inline-block px-6 pt-2.5 pb-2 text-xs font-medium leading-normal uppercase rounded transition duration-150 ease-in-out bg-primary-100 text-primary-700 hover:bg-primary-accent-100 focus:bg-primary-accent-100 focus:outline-none focus:ring-0 active:bg-primary-accent-200" data-te-modal-dismiss data-te-ripple-init data-te-ripple-color="light">
|
@endif
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-te-modal-dismiss
|
||||||
|
class="rounded-xl border border-neutral-300 px-5 py-2.5 text-sm font-medium text-neutral-700 transition hover:bg-neutral-100 dark:border-neutral-600 dark:text-neutral-200 dark:hover:bg-neutral-800">
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" class="inline-block px-6 pt-2.5 pb-2 ml-1 text-xs font-medium leading-normal text-white uppercase bg-rose-600 rounded transition duration-150 ease-in-out hover:bg-rose-700 focus:bg-rose-600" data-te-ripple-init data-te-ripple-color="light">
|
<button
|
||||||
|
type="submit"
|
||||||
|
data-te-ripple-init
|
||||||
|
class="rounded-xl bg-rose-600 px-5 py-2.5 text-sm font-semibold text-white shadow-lg shadow-rose-600/20 transition hover:bg-rose-700">
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
@auth
|
|
||||||
@if(Auth::user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
|
|
||||||
<div class="relative p-5 bg-white dark:bg-neutral-700/40 rounded-lg overflow-hidden z-10">
|
|
||||||
<div class="float-left">
|
|
||||||
<a data-te-toggle="modal" data-te-target="#modalUploadEpisode" class="text-xl text-gray-800 dark:text-gray-200 leading-tight cursor-pointer whitespace-nowrap">
|
|
||||||
<i class="fa-solid fa-plus pr-[6px]"></i> Add Episode
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="float-right">
|
|
||||||
<a data-te-toggle="modal" data-te-target="#modalAddSubtitles" class="text-xl text-gray-800 dark:text-gray-200 leading-tight cursor-pointer whitespace-nowrap">
|
|
||||||
<i class="fa-solid fa-plus pr-[6px]"></i> Add Subtitles
|
|
||||||
</a>
|
|
||||||
<a data-te-toggle="modal" data-te-target="#modalEditEpisode" class="text-xl text-gray-800 dark:text-gray-200 leading-tight cursor-pointer whitespace-nowrap">
|
|
||||||
<i class="fa-solid fa-pen pr-[6px]"></i> Edit Episode
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
@endauth
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<x-guest-layout>
|
<x-guest-layout>
|
||||||
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-neutral-950/50 shadow-md overflow-hidden sm:rounded-lg">
|
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-neutral-800 shadow-md overflow-hidden sm:rounded-lg">
|
||||||
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||||
{{ __('This is a secure area of the application. Please confirm your password before continuing.') }}
|
{{ __('This is a secure area of the application. Please confirm your password before continuing.') }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<x-guest-layout>
|
<x-guest-layout>
|
||||||
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-neutral-950/50 shadow-md overflow-hidden sm:rounded-lg">
|
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-neutral-800 shadow-md overflow-hidden sm:rounded-lg">
|
||||||
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||||
{{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }}
|
{{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
<div class="w-full sm:max-w-md mt-6">
|
<div class="w-full sm:max-w-md mt-6">
|
||||||
<ul class="flex list-none flex-row flex-wrap border-b-0 pl-0 relative " role="tablist" data-te-nav-ref>
|
<ul class="flex list-none flex-row flex-wrap border-b-0 pl-0 relative " role="tablist" data-te-nav-ref>
|
||||||
<li role="presentation" class="flex-auto text-center">
|
<li role="presentation" class="flex-auto text-center">
|
||||||
<a href="#tabs-login" class="rounded-l-lg my-2 block border-x-0 border-b-2 border-t-0 border-transparent px-7 pb-3.5 pt-4 text-xs font-medium uppercase leading-tight text-neutral-500 hover:isolate hover:border-transparent hover:bg-neutral-50 focus:isolate focus:border-transparent data-[te-nav-active]:border-rose-600 data-[te-nav-active]:text-black dark:text-neutral-400 bg-white/50 dark:bg-neutral-950/50 backdrop-blur-sm dark:hover:bg-neutral-800 dark:data-[te-nav-active]:text-white"
|
<a href="#tabs-login" class="rounded-l-lg my-2 block border-x-0 border-b-2 border-t-0 border-transparent px-7 pb-3.5 pt-4 text-xs font-medium uppercase leading-tight text-neutral-500 hover:isolate hover:border-transparent hover:bg-neutral-50 focus:isolate focus:border-transparent data-[te-nav-active]:border-rose-600 data-[te-nav-active]:text-black dark:text-neutral-400 bg-white/50 dark:bg-neutral-800 backdrop-blur-sm dark:hover:bg-neutral-900 dark:data-[te-nav-active]:text-white"
|
||||||
data-te-toggle="pill" data-te-target="#tabs-login" data-te-nav-active role="tab" aria-controls="tabs-login" aria-selected="true">
|
data-te-toggle="pill" data-te-target="#tabs-login" data-te-nav-active role="tab" aria-controls="tabs-login" aria-selected="true">
|
||||||
{{ __('Login') }}
|
{{ __('Login') }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li role="presentation" class="flex-auto text-center">
|
<li role="presentation" class="flex-auto text-center">
|
||||||
<a href="#tabs-register" class="rounded-r-lg my-2 block border-x-0 border-b-2 border-t-0 border-transparent px-7 pb-3.5 pt-4 text-xs font-medium uppercase leading-tight text-neutral-500 hover:isolate hover:border-transparent hover:bg-neutral-50 focus:isolate focus:border-transparent data-[te-nav-active]:border-rose-600 data-[te-nav-active]:text-black dark:text-neutral-400 bg-white/50 dark:bg-neutral-950/50 backdrop-blur-sm dark:hover:bg-neutral-800 dark:data-[te-nav-active]:text-white"
|
<a href="#tabs-register" class="rounded-r-lg my-2 block border-x-0 border-b-2 border-t-0 border-transparent px-7 pb-3.5 pt-4 text-xs font-medium uppercase leading-tight text-neutral-500 hover:isolate hover:border-transparent hover:bg-neutral-50 focus:isolate focus:border-transparent data-[te-nav-active]:border-rose-600 data-[te-nav-active]:text-black dark:text-neutral-400 bg-white/50 dark:bg-neutral-800 backdrop-blur-sm dark:hover:bg-neutral-900 dark:data-[te-nav-active]:text-white"
|
||||||
data-te-toggle="pill" data-te-target="#tabs-register" role="tab" aria-controls="tabs-register" aria-selected="false">
|
data-te-toggle="pill" data-te-target="#tabs-register" role="tab" aria-controls="tabs-register" aria-selected="false">
|
||||||
{{ __('Register') }}
|
{{ __('Register') }}
|
||||||
</a>
|
</a>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
<!-- Login -->
|
<!-- Login -->
|
||||||
<div class="w-full sm:max-w-md hidden opacity-100 transition-opacity duration-150 ease-linear data-[te-tab-active]:block" id="tabs-login" role="tabpanel" aria-labelledby="tabs-login" data-te-tab-active>
|
<div class="w-full sm:max-w-md hidden opacity-100 transition-opacity duration-150 ease-linear data-[te-tab-active]:block" id="tabs-login" role="tabpanel" aria-labelledby="tabs-login" data-te-tab-active>
|
||||||
<div class="px-6 py-4 bg-white dark:bg-neutral-950/50 shadow-md overflow-hidden sm:rounded-lg">
|
<div class="px-6 py-4 bg-white dark:bg-neutral-800 shadow-md overflow-hidden sm:rounded-lg">
|
||||||
<div class="w-full text-center text-white mb-3">
|
<div class="w-full text-center text-white mb-3">
|
||||||
<a href="{{ route('discord.login') }}">
|
<a href="{{ route('discord.login') }}">
|
||||||
<div
|
<div
|
||||||
@@ -31,7 +31,18 @@
|
|||||||
<!-- Or -->
|
<!-- Or -->
|
||||||
<div class="grid grid-cols-3">
|
<div class="grid grid-cols-3">
|
||||||
<hr class="self-center border-neutral-600">
|
<hr class="self-center border-neutral-600">
|
||||||
<p>OR</p>
|
<p class="text-neutral-800 dark:text-neutral-400">OR</p>
|
||||||
|
<hr class="self-center border-neutral-600">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Passkey Login -->
|
||||||
|
<div class="w-full text-center text-white mb-3">
|
||||||
|
<x-authenticate-passkey />
|
||||||
|
|
||||||
|
<div class="grid grid-cols-3 pt-3">
|
||||||
|
<hr class="self-center border-neutral-600">
|
||||||
|
<p class="text-neutral-800 dark:text-neutral-400">OR</p>
|
||||||
<hr class="self-center border-neutral-600">
|
<hr class="self-center border-neutral-600">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -69,8 +80,8 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="block">
|
<div class="block pt-3 w-3/4 mx-auto">
|
||||||
<altcha-widget id="captcha" floating challengeurl="/altcha-challenge"></altcha-widget>
|
<altcha-widget id="captcha" theme="cupcake" challenge="/altcha-challenge"></altcha-widget>
|
||||||
<x-input-error :messages="$errors->get('altcha')" class="mt-2" />
|
<x-input-error :messages="$errors->get('altcha')" class="mt-2" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -91,7 +102,7 @@
|
|||||||
|
|
||||||
<!-- Register -->
|
<!-- Register -->
|
||||||
<div class="w-full sm:max-w-md hidden opacity-0 transition-opacity duration-150 ease-linear data-[te-tab-active]:block" id="tabs-register" role="tabpanel" aria-labelledby="tabs-register">
|
<div class="w-full sm:max-w-md hidden opacity-0 transition-opacity duration-150 ease-linear data-[te-tab-active]:block" id="tabs-register" role="tabpanel" aria-labelledby="tabs-register">
|
||||||
<div class="px-6 py-4 bg-white dark:bg-neutral-950/50 shadow-md overflow-hidden sm:rounded-lg">
|
<div class="px-6 py-4 bg-white dark:bg-neutral-800 shadow-md overflow-hidden sm:rounded-lg">
|
||||||
<form method="POST" action="{{ route('register') }}">
|
<form method="POST" action="{{ route('register') }}">
|
||||||
@csrf
|
@csrf
|
||||||
|
|
||||||
@@ -132,8 +143,8 @@
|
|||||||
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
|
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="block">
|
<div class="block pt-3 w-3/4 mx-auto">
|
||||||
<altcha-widget id="captcha" floating challengeurl="/altcha-challenge"></altcha-widget>
|
<altcha-widget id="captcha" theme="cupcake" challenge="/altcha-challenge"></altcha-widget>
|
||||||
<x-input-error :messages="$errors->get('altcha')" class="mt-2" />
|
<x-input-error :messages="$errors->get('altcha')" class="mt-2" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<x-guest-layout>
|
<x-guest-layout>
|
||||||
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-neutral-950/50 shadow-md overflow-hidden sm:rounded-lg">
|
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-neutral-800 shadow-md overflow-hidden sm:rounded-lg">
|
||||||
<form method="POST" action="{{ route('password.store') }}">
|
<form method="POST" action="{{ route('password.store') }}">
|
||||||
@csrf
|
@csrf
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<x-guest-layout>
|
<x-guest-layout>
|
||||||
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-neutral-950/50 shadow-md overflow-hidden sm:rounded-lg">
|
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-neutral-800 shadow-md overflow-hidden sm:rounded-lg">
|
||||||
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||||
{{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }}
|
{{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
@props([
|
||||||
|
'episode',
|
||||||
|
'view',
|
||||||
|
'displayjapanese' => false
|
||||||
|
])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$title = $displayjapanese
|
||||||
|
? "{$episode->title_jpn} ({$episode->title}) - {$episode->episode}"
|
||||||
|
: "{$episode->title} - {$episode->episode}";
|
||||||
|
|
||||||
|
$isLoggedIn = auth()->check();
|
||||||
|
|
||||||
|
$isWatched = $isLoggedIn
|
||||||
|
? $episode->userWatched(auth()->id())
|
||||||
|
: false;
|
||||||
|
|
||||||
|
$problematic = cache()->rememberForever(
|
||||||
|
"episodeProblematic{$episode->id}",
|
||||||
|
fn () => $episode->getProblematicTags()
|
||||||
|
);
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="group w-full p-1">
|
||||||
|
<a
|
||||||
|
href="{{ route('hentai.index', ['title' => $episode->slug]) }}"
|
||||||
|
class="block overflow-hidden rounded-2xl border border-neutral-200 bg-white transition-all duration-300 hover:-translate-y-1 hover:border-neutral-400 hover:shadow-xl dark:border-neutral-800 dark:bg-neutral-900 dark:hover:border-neutral-700"
|
||||||
|
>
|
||||||
|
<div class="relative overflow-hidden">
|
||||||
|
|
||||||
|
{{-- Thumbnail / Cover --}}
|
||||||
|
@if ($view === 'poster')
|
||||||
|
<img
|
||||||
|
src="{{ $episode->cover_url }}"
|
||||||
|
alt="{{ $episode->title }} - {{ $episode->episode }}"
|
||||||
|
loading="lazy"
|
||||||
|
width="400"
|
||||||
|
class="aspect-[11/16] w-full object-cover object-center transition-transform duration-500 group-hover:scale-[1.03]"
|
||||||
|
>
|
||||||
|
@elseif ($view === 'thumbnail')
|
||||||
|
@php
|
||||||
|
$galleryImages = $episode->gallery
|
||||||
|
->pluck('thumbnail_url')
|
||||||
|
->filter()
|
||||||
|
->values();
|
||||||
|
@endphp
|
||||||
|
<img
|
||||||
|
src="{{ $galleryImages->first() }}"
|
||||||
|
alt="{{ $episode->title }} - {{ $episode->episode }}"
|
||||||
|
loading="lazy"
|
||||||
|
width="1000"
|
||||||
|
data-gallery='@json($galleryImages)'
|
||||||
|
class="preview-gallery aspect-video w-full object-cover object-center transition-transform duration-500 group-hover:scale-[1.03]"
|
||||||
|
>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- Dark Overlay --}}
|
||||||
|
<div class="pointer-events-none absolute inset-0 bg-gradient-to-t from-black/90 via-black/20 to-transparent"></div>
|
||||||
|
|
||||||
|
{{-- Top Meta --}}
|
||||||
|
<div class="pointer-events-none absolute inset-x-0 top-0 z-20 flex items-start justify-between p-3">
|
||||||
|
|
||||||
|
{{-- Problematic Tags --}}
|
||||||
|
@if (!empty($problematic))
|
||||||
|
<div class="rounded-full bg-red-700/40 px-2 py-1 text-[11px] font-semibold uppercase tracking-wide text-white ring-1 ring-red-700/70">
|
||||||
|
<i class="fa-solid fa-triangle-exclamation mr-1"></i>
|
||||||
|
{{ $problematic }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- Resolution --}}
|
||||||
|
<div class="ml-auto rounded-full bg-black/70 px-2 py-1 text-[11px] font-semibold tracking-wide text-white ring-1 ring-white/10">
|
||||||
|
{{ $episode->getResolution() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Bottom Content --}}
|
||||||
|
<div class="pointer-events-none absolute inset-x-0 bottom-0 z-20 p-4">
|
||||||
|
|
||||||
|
{{-- Title --}}
|
||||||
|
<h3 class=" text-sm font-semibold leading-snug text-white md:text-base">
|
||||||
|
{{ $title }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{{-- Bottom Row --}}
|
||||||
|
<div class="mt-3 flex items-center justify-between gap-3">
|
||||||
|
|
||||||
|
{{-- Stats --}}
|
||||||
|
<div class="flex flex-wrap items-center gap-3 text-sm font-bold text-neutral-200">
|
||||||
|
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<i class="fa-regular fa-eye text-neutral-200 font-bold"></i>
|
||||||
|
{{ $episode->viewCountFormatted() }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<i class="fa-regular fa-heart text-neutral-200 font-bold"></i>
|
||||||
|
{{ $episode->likeCount() }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<i class="fa-regular fa-comment text-neutral-200 font-bold"></i>
|
||||||
|
{{ $episode->commentCount() }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Watched Status (logged in users only) --}}
|
||||||
|
@auth
|
||||||
|
@if ($isWatched)
|
||||||
|
<div class="shrink-0 rounded-full bg-emerald-800/40 px-2.5 py-1 text-xs font-semibold text-emerald-300 ring-1 ring-emerald-500/30">
|
||||||
|
@if ($view === 'thumbnail')
|
||||||
|
<i class="fa-solid fa-eye mr-1"></i> Watched
|
||||||
|
@else
|
||||||
|
<i class="fa-solid fa-eye"></i>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="shrink-0 rounded-full bg-rose-800/40 px-2.5 py-1 text-xs font-semibold text-rose-300 ring-1 ring-rose-500/30">
|
||||||
|
<i class="fa-solid fa-eye-slash"></i>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@endauth
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
@props(['episode'])
|
|
||||||
|
|
||||||
<div class="relative p-1 mb-8 w-full transition duration-300 ease-in-out md:p-2 md:hover:-translate-y-1 md:hover:scale-110">
|
|
||||||
<a class="hover:text-blue-600" href="{{ route('hentai.index', ['title' => $episode->slug]) }}">
|
|
||||||
<img alt="{{ $episode->title }} - {{ $episode->episode }}" loading="lazy" width="1000"
|
|
||||||
class="block object-cover object-center relative z-20 rounded-lg aspect-video"
|
|
||||||
src="{{ $episode->gallery->first()->thumbnail_url }}"></img>
|
|
||||||
|
|
||||||
@guest
|
|
||||||
<p
|
|
||||||
class="absolute right-1 md:right-2 top-1 md:top-2 bg-rose-700/70 !text-white rounded-bl-lg rounded-tr-lg p-1 pr-2 pl-2 font-semibold text-sm z-30">
|
|
||||||
{{ $episode->getResolution() }}</p>
|
|
||||||
<p
|
|
||||||
class="absolute left-1 md:left-4 bottom-1 md:bottom-2 bg-rose-700/70 !text-white rounded-bl-lg rounded-tr-lg p-1 pr-2 pl-2 font-semibold text-sm z-30">
|
|
||||||
<i class="fa-regular fa-eye"></i> {{ $episode->viewCountFormatted() }} <i class="fa-regular fa-heart"></i>
|
|
||||||
{{ $episode->likeCount() }} <i class="fa-regular fa-comment"></i>
|
|
||||||
{{ $episode->commentCount() }}
|
|
||||||
</p>
|
|
||||||
@endguest
|
|
||||||
|
|
||||||
@php $problematic = cache()->rememberForever('episodeProblematic'.$episode->id, fn () => $episode->getProblematicTags()); @endphp
|
|
||||||
@if (!empty($problematic))
|
|
||||||
<p
|
|
||||||
class="absolute left-4 top-2 bg-red-700/70 !text-white rounded-br-lg rounded-tl-lg p-1 pr-2 pl-2 font-semibold text-sm z-30">
|
|
||||||
<i class="fa-solid fa-triangle-exclamation"></i> {{ $problematic }}
|
|
||||||
</p>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
@auth
|
|
||||||
@if ($episode->userWatched(auth()->user()->id))
|
|
||||||
<p
|
|
||||||
class="absolute right-1 md:right-2 top-1 md:top-2 bg-green-600/80 !text-white rounded-bl-lg rounded-tr-lg p-1 pr-2 pl-2 font-semibold text-sm z-30">
|
|
||||||
{{ $episode->getResolution() }}</p>
|
|
||||||
<p
|
|
||||||
class="absolute left-1 md:left-2 bottom-1 md:bottom-2 bg-green-600/80 !text-white rounded-bl-lg rounded-tr-lg p-1 pr-2 pl-2 font-semibold text-sm z-30">
|
|
||||||
<i class="fa-regular fa-eye"></i> {{ $episode->viewCountFormatted() }} <i
|
|
||||||
class="fa-regular fa-heart"></i> {{ $episode->likeCount() }} <i class="fa-regular fa-comment"></i>
|
|
||||||
{{ $episode->commentCount() }}
|
|
||||||
</p>
|
|
||||||
@else
|
|
||||||
<p
|
|
||||||
class="absolute right-1 md:right-2 top-1 md:top-2 bg-rose-700/70 !text-white rounded-bl-lg rounded-tr-lg p-1 pr-2 pl-2 font-semibold text-sm z-30">
|
|
||||||
{{ $episode->getResolution() }}</p>
|
|
||||||
<p
|
|
||||||
class="absolute left-1 md:left-2 bottom-1 md:bottom-2 bg-rose-700/70 !text-white rounded-bl-lg rounded-tr-lg p-1 pr-2 pl-2 font-semibold text-sm z-30">
|
|
||||||
<i class="fa-regular fa-eye"></i> {{ $episode->viewCountFormatted() }} <i
|
|
||||||
class="fa-regular fa-heart"></i> {{ $episode->likeCount() }} <i class="fa-regular fa-comment"></i>
|
|
||||||
{{ $episode->commentCount() }}
|
|
||||||
</p>
|
|
||||||
@endif
|
|
||||||
@endauth
|
|
||||||
|
|
||||||
<div class="absolute w-[95%] grid grid-cols-1 text-center">
|
|
||||||
<p class="text-sm text-center text-black dark:text-white">{{ $episode->title }} -
|
|
||||||
{{ $episode->episode }}</p>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
@@ -1,13 +1,19 @@
|
|||||||
@props(['title'])
|
@props(['title'])
|
||||||
<div class="flex flex-shrink-0 items-center justify-between rounded-t-md p-4 bg-rose-600">
|
<div class="sticky top-0 z-10 flex items-center justify-between border-b border-neutral-200 bg-white/90 px-6 py-4 backdrop-blur dark:border-neutral-700 dark:bg-neutral-900/90">
|
||||||
<!--Modal title-->
|
<div>
|
||||||
<h5 class="text-xl font-medium leading-normal text-white">
|
<h2
|
||||||
|
id="modalGenresLabel"
|
||||||
|
class="text-xl font-semibold text-neutral-900 dark:text-white"
|
||||||
|
>
|
||||||
{{ $title }}
|
{{ $title }}
|
||||||
</h5>
|
</h2>
|
||||||
<!--Close button-->
|
</div>
|
||||||
<button type="button" class="box-content text-white rounded-none border-none hover:no-underline hover:opacity-75 focus:opacity-100 focus:shadow-none focus:outline-none" data-te-modal-dismiss aria-label="Close">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-6 w-6">
|
<button
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
type="button"
|
||||||
</svg>
|
data-te-modal-dismiss
|
||||||
|
class="rounded-lg p-2 text-neutral-500 transition hover:bg-neutral-100 hover:text-black dark:hover:bg-neutral-800 dark:hover:text-white"
|
||||||
|
>
|
||||||
|
✕
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
<x-input-error class="mt-2" :messages="$errors->get('message')" />
|
<x-input-error class="mt-2" :messages="$errors->get('message')" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<altcha-widget id="captcha" floating challengeurl="/altcha-challenge"></altcha-widget>
|
<altcha-widget id="captcha" theme="cupcake" challenge="/altcha-challenge"></altcha-widget>
|
||||||
|
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<x-primary-button>{{ __('Submit') }}</x-primary-button>
|
<x-primary-button>{{ __('Submit') }}</x-primary-button>
|
||||||
|
|||||||
@@ -1,61 +1,83 @@
|
|||||||
<p class="leading-normal font-bold text-lg text-neutral-800 dark:text-white">
|
<p class="text-lg font-bold leading-normal text-neutral-800 dark:text-white">
|
||||||
{{ __('home.categories') }}
|
{{ __('home.categories') }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@php
|
@php
|
||||||
$categories = [
|
$categories = collect([
|
||||||
'Uncensored' => 'uncensored',
|
['name' => 'Uncensored', 'slug' => 'uncensored'],
|
||||||
'Milf' => 'milf',
|
['name' => 'Milf', 'slug' => 'milf'],
|
||||||
'Maid' => 'maid',
|
['name' => 'Maid', 'slug' => 'maid'],
|
||||||
'School Girl' => 'school-girl',
|
['name' => 'School Girl', 'slug' => 'school-girl'],
|
||||||
'Succubus' => 'succubus',
|
['name' => 'Succubus', 'slug' => 'succubus'],
|
||||||
'Tentacle' => 'tentacle',
|
['name' => 'Tentacle', 'slug' => 'tentacle'],
|
||||||
'Big Boobs' => 'big-boobs',
|
['name' => 'Big Boobs', 'slug' => 'big-boobs'],
|
||||||
'BDSM' => 'bdsm',
|
['name' => 'BDSM', 'slug' => 'bdsm'],
|
||||||
'Elf' => 'elf',
|
['name' => 'Elf', 'slug' => 'elf'],
|
||||||
'4k 48fps' => '4k-48fps',
|
['name' => '4K 48FPS', 'slug' => '4k-48fps'],
|
||||||
];
|
]);
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-5 lg:grid-cols-5 xl:grid-cols-5 2xl:grid-cols-5 gap-2">
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-5">
|
||||||
@foreach ($categories as $name => $slug)
|
@foreach ($categories as $category)
|
||||||
@php
|
@php
|
||||||
$cacheKey = 'category_' . $slug;
|
$episodes = cache()->remember(
|
||||||
|
"category_{$category['slug']}",
|
||||||
$collection = \cache()->remember(
|
now()->addMinutes(15),
|
||||||
$cacheKey,
|
fn () => \App\Models\Episode::query()
|
||||||
900,
|
->withAllTags([$category['slug']])
|
||||||
fn() => \App\Models\Episode::withAllTags([$slug])
|
|
||||||
->inRandomOrder()
|
->inRandomOrder()
|
||||||
->limit(3)
|
->limit(3)
|
||||||
->get(),
|
->get()
|
||||||
);
|
);
|
||||||
|
|
||||||
$count = $collection->count();
|
[$left, $center, $right] = [
|
||||||
|
$episodes->get(0),
|
||||||
|
$episodes->get(1),
|
||||||
|
$episodes->get(2),
|
||||||
|
];
|
||||||
@endphp
|
@endphp
|
||||||
<a href="{{ route('hentai.search', ['order' => 'recently-uploaded', 'tags[0]' => $slug]) }}"
|
|
||||||
class="relative mx-auto w-96 sm:w-full h-56 bg-white dark:bg-neutral-800 text-black dark:text-white rounded-lg overflow-hidden shadow-lg mt-4 transition ease-in-out hover:-translate-y-1 hover:scale-110 duration-300">
|
<a
|
||||||
<h2 class="text-lg font-semibold text-center pt-2">{{ $name }}</h2>
|
href="{{ route('hentai.search', [
|
||||||
<div class="relative w-full h-full flex justify-center">
|
'order' => 'recently-uploaded',
|
||||||
<!-- Left Image -->
|
'tags[0]' => $category['slug'],
|
||||||
@if ($count > 0)
|
]) }}"
|
||||||
<img src="{{ $collection->first()->cover_url }}"
|
class="group relative overflow-hidden rounded-2xl border dark:border-neutral-800 border-neutral-300 dark:bg-neutral-900 dark:hover:border-neutral-700 hover:border-neutral-400 hover:shadow-2xl hover:shadow-black/30 shadow-md transition-all duration-300 hover:-translate-y-1"
|
||||||
class="absolute w-32 h-44 rounded-lg object-cover shadow-md left-4 top-4 rotate-[-15deg] z-0">
|
>
|
||||||
|
<div class="p-4">
|
||||||
|
<h2 class="text-center text-lg font-semibold text-neutral-900 dark:text-white">
|
||||||
|
{{ $category['name'] }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="relative mt-4 flex h-52 items-center justify-center">
|
||||||
|
@if ($left)
|
||||||
|
<img
|
||||||
|
src="{{ $left->cover_url }}"
|
||||||
|
alt="{{ $category['name'] }}"
|
||||||
|
loading="lazy"
|
||||||
|
class="absolute left-2 top-4 h-44 w-32 rotate-[-12deg] rounded-xl object-cover shadow-lg transition-transform duration-300 group-hover:rotate-[-16deg]"
|
||||||
|
>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<!-- Center Image -->
|
@if ($center)
|
||||||
@if ($count > 1)
|
<img
|
||||||
<img src="{{ $collection->skip(1)->first()->cover_url }}"
|
src="{{ $center->cover_url }}"
|
||||||
class="absolute w-32 h-44 rounded-lg object-cover shadow-lg top-4 z-10">
|
alt="{{ $category['name'] }}"
|
||||||
|
loading="lazy"
|
||||||
|
class="absolute top-2 z-10 h-44 w-32 rounded-xl object-cover shadow-2xl transition-transform duration-300 group-hover:scale-105"
|
||||||
|
>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<!-- Right Image -->
|
@if ($right)
|
||||||
@if ($count > 2)
|
<img
|
||||||
<img src="{{ $collection->skip(2)->first()->cover_url }}"
|
src="{{ $right->cover_url }}"
|
||||||
class="absolute w-32 h-44 rounded-lg object-cover shadow-lg right-4 top-14 z-20 rotate-[15deg]">
|
alt="{{ $category['name'] }}"
|
||||||
|
loading="lazy"
|
||||||
|
class="absolute right-2 top-8 z-20 h-44 w-32 rotate-[12deg] rounded-xl object-cover shadow-lg transition-transform duration-300 group-hover:rotate-[16deg]"
|
||||||
|
>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
@@ -1,68 +1,80 @@
|
|||||||
<p class="text-lg font-bold leading-normal text-neutral-800 dark:text-white">
|
<p class="mb-6 text-2xl font-bold tracking-tight text-neutral-900 dark:text-white">
|
||||||
{{ __('home.latest-comments') }}
|
{{ __('home.latest-comments') }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="grid gap-2 grid-cols-1 xl:grid-cols-2">
|
<div class="grid grid-cols-1 gap-6 xl:grid-cols-2">
|
||||||
@foreach ($latestComments as $comment)
|
@foreach ($latestComments as $comment)
|
||||||
@if ($comment->commentable_type == \App\Models\Episode::class)
|
|
||||||
@php $episode = cache()->rememberForever('commentEpisode'.$comment->commentable_id, fn () => App\Models\Episode::with('gallery')->where('id', $comment->commentable_id)->first()); @endphp
|
@php
|
||||||
<div id="comments" class="flex p-4 bg-white rounded-lg dark:bg-neutral-950">
|
$isEpisode = $comment->commentable_type == \App\Models\Episode::class;
|
||||||
|
|
||||||
|
if ($isEpisode) {
|
||||||
|
$item = cache()->rememberForever(
|
||||||
|
'commentEpisode' . $comment->commentable_id,
|
||||||
|
fn () => \App\Models\Episode::with('gallery')
|
||||||
|
->find($comment->commentable_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
$url = route('hentai.index', ['title' => $item->slug]);
|
||||||
|
$title = $item->title . ' - ' . $item->episode;
|
||||||
|
$thumbnail = $item->gallery->first()?->thumbnail_url ?? $item->cover_url;
|
||||||
|
$cover = $item->cover_url;
|
||||||
|
$resolution = $item->getResolution();
|
||||||
|
} else {
|
||||||
|
$item = cache()->rememberForever(
|
||||||
|
'commentHentai' . $comment->commentable_id,
|
||||||
|
fn () => \App\Models\Hentai::with('gallery', 'episodes')
|
||||||
|
->find($comment->commentable_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
$episode = $item->episodes->first();
|
||||||
|
|
||||||
|
$url = route('hentai.index', ['title' => $item->slug]);
|
||||||
|
$title = $episode?->title;
|
||||||
|
$thumbnail = $item->gallery->first()?->thumbnail_url ?? $episode?->cover_url;
|
||||||
|
$cover = $episode?->cover_url;
|
||||||
|
$resolution = $episode?->getResolution();
|
||||||
|
}
|
||||||
|
@endphp
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="w-[15vw] mr-5 p-1 md:p-2 mb-4 relative transition ease-in-out hover:-translate-y-1 hover:scale-110 duration-300">
|
class="group overflow-hidden rounded-2xl border dark:border-neutral-800 border-neutral-300 dark:bg-neutral-900 dark:hover:border-neutral-700 hover:border-neutral-400 hover:shadow-2xl hover:shadow-black/30 shadow-md transition-all duration-300 hover:-translate-y-1">
|
||||||
<a class="hidden 2xl:block"
|
|
||||||
href="{{ route('hentai.index', ['title' => $episode->slug]) }}">
|
<div class="flex flex-col md:flex-row">
|
||||||
<img alt="{{ $episode->title }} - {{ $episode->episode }}" loading="lazy" width="1000"
|
|
||||||
class="block object-cover object-center relative z-20 rounded-lg aspect-video"
|
{{-- Thumbnail --}}
|
||||||
src="{{ $episode->gallery->first()->thumbnail_url }}"></img>
|
<a href="{{ $url }}"
|
||||||
<p
|
class="relative w-full md:w-72 shrink-0 overflow-hidden">
|
||||||
class="absolute right-2 top-2 bg-rose-700/70 !text-white rounded-bl-lg rounded-tr-lg p-1 pr-2 pl-2 font-semibold text-sm z-30">
|
|
||||||
{{ $episode->getResolution() }}</p>
|
{{-- Desktop Thumbnail --}}
|
||||||
<div class="absolute w-[95%] grid grid-cols-1 text-center">
|
<img
|
||||||
<p class="text-sm text-center text-black dark:text-white truncate">{{ $episode->title }} -
|
src="{{ $thumbnail }}"
|
||||||
{{ $episode->episode }}</p>
|
alt="{{ $title }}"
|
||||||
|
loading="lazy"
|
||||||
|
class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105 aspect-video"
|
||||||
|
>
|
||||||
|
|
||||||
|
{{-- Resolution Badge --}}
|
||||||
|
<div class="absolute right-2 top-2 rounded-lg bg-black/70 px-2 py-1 text-[11px] font-semibold tracking-wide text-white ring-1 ring-white/10">
|
||||||
|
{{ $resolution }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Gradient Overlay --}}
|
||||||
|
<div
|
||||||
|
class="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/80 via-black/30 to-transparent p-4">
|
||||||
|
<p class="line-clamp-1 text-sm font-medium text-white">
|
||||||
|
{{ $title }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a class="block 2xl:hidden"
|
{{-- Comment Content --}}
|
||||||
href="{{ route('hentai.index', ['title' => $episode->slug]) }}">
|
<div class="flex-1 p-4 md:p-6 bg-neutral-50 dark:bg-neutral-900">
|
||||||
<img alt="{{ $episode->title }} - {{ $episode->episode }}" loading="lazy" width="1000"
|
|
||||||
class="block object-cover object-center relative z-20 rounded-lg"
|
|
||||||
src="{{ $episode->cover_url }}"></img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="w-[60vw] pt-4 bg-neutral-100 dark:bg-neutral-800 rounded-lg pl-4">
|
|
||||||
@include('partials.comment', ['comment' => $comment])
|
@include('partials.comment', ['comment' => $comment])
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
@elseif($comment->commentable_type == \App\Models\Hentai::class)
|
|
||||||
@php $hentai = cache()->rememberForever('commentHentai'.$comment->commentable_id, fn () => App\Models\Hentai::with('gallery', 'episodes')->where('id', $comment->commentable_id)->first()); @endphp
|
|
||||||
<div id="comments" class="flex p-4 bg-white rounded-lg dark:bg-neutral-950">
|
|
||||||
<div
|
|
||||||
class="w-[15vw] mr-5 p-1 md:p-2 mb-8 relative transition ease-in-out hover:-translate-y-1 hover:scale-110 duration-300">
|
|
||||||
<a class="hidden 2xl:block" href="{{ route('hentai.index', ['title' => $hentai->slug]) }}">
|
|
||||||
<img alt="{{ $hentai->episodes->first()->title }}" loading="lazy" width="1000"
|
|
||||||
class="block object-cover object-center relative z-20 rounded-lg aspect-video"
|
|
||||||
src="{{ $hentai->gallery->first()->thumbnail_url }}"></img>
|
|
||||||
<p
|
|
||||||
class="absolute right-2 top-2 bg-rose-700/70 !text-white rounded-bl-lg rounded-tr-lg p-1 pr-2 pl-2 font-semibold text-sm z-30">
|
|
||||||
{{ $hentai->episodes->first()->getResolution() }}</p>
|
|
||||||
<div class="absolute w-[95%] grid grid-cols-1 text-center">
|
|
||||||
<p class="text-sm text-center text-black dark:text-white truncate">
|
|
||||||
{{ $hentai->episodes->first()->title }}</p>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a class="block 2xl:hidden"
|
|
||||||
href="{{ route('hentai.index', ['title' => $hentai->slug]) }}">
|
|
||||||
<img alt="{{ $hentai->episodes->first()->title }}" loading="lazy" width="1000"
|
|
||||||
class="block object-cover object-center relative z-20 rounded-lg"
|
|
||||||
src="{{ $hentai->episodes->first()->cover_url }}"></img>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="w-[60vw] pt-4 bg-neutral-100 dark:bg-neutral-800 rounded-lg pl-4">
|
|
||||||
@include('partials.comment', ['comment' => $comment])
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
@php
|
@php
|
||||||
$random = \cache()->remember('random_home', 300, function () {
|
$random = \cache()->remember('random_home', 300, function () {
|
||||||
return \App\Models\Episode::inRandomOrder()->limit(8)->get(); ;
|
return \App\Models\Episode::inRandomOrder()->limit(16)->get(); ;
|
||||||
});
|
});
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
@include('home.partials.tab.template', ['episodes' => $random, 'showThumbnails' => false])
|
@include('home.partials.tab.template', ['episodes' => $random, 'isThumbnail' => false])
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,46 @@
|
|||||||
@if ($showThumbnails)
|
@props(['isThumbnail'])
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-2">
|
|
||||||
@include('partials.episode-thumbnail', ['limit' => 15])
|
@php
|
||||||
</div>
|
// Render enough items for largest possible layout
|
||||||
|
$limit = 16;
|
||||||
|
|
||||||
|
$view = $isThumbnail ? 'thumbnail' : 'poster';
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
@if ($isThumbnail)
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-4 3xl:grid-cols-5
|
||||||
|
[&>.episode-item]:hidden
|
||||||
|
[&>.episode-item:nth-child(-n+8)]:block
|
||||||
|
md:[&>.episode-item:nth-child(-n+8)]:block
|
||||||
|
lg:[&>.episode-item:nth-child(-n+9)]:block
|
||||||
|
xl:[&>.episode-item:nth-child(-n+9)]:block
|
||||||
|
2xl:[&>.episode-item:nth-child(-n+12)]:block
|
||||||
|
3xl:[&>.episode-item:nth-child(-n+15)]:block"
|
||||||
|
>
|
||||||
@else
|
@else
|
||||||
<div class="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-7 2xl:grid-cols-8 gap-2">
|
<div
|
||||||
@include('partials.episode-cover', ['limit' => 16])
|
class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-4 2xl:grid-cols-5 3xl:grid-cols-8
|
||||||
</div>
|
[&>.episode-item]:hidden
|
||||||
|
[&>.episode-item:nth-child(-n+12)]:block
|
||||||
|
md:[&>.episode-item:nth-child(-n+12)]:block
|
||||||
|
xl:[&>.episode-item:nth-child(-n+12)]:block
|
||||||
|
2xl:[&>.episode-item:nth-child(-n+15)]:block
|
||||||
|
3xl:[&>.episode-item:nth-child(-n+16)]:block"
|
||||||
|
>
|
||||||
@endif
|
@endif
|
||||||
|
@foreach ($episodes->take($limit) as $ep)
|
||||||
|
@php
|
||||||
|
$episode = isset($popularView)
|
||||||
|
? $ep->episode
|
||||||
|
: $ep;
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="episode-item p-1">
|
||||||
|
<x-episode-cover
|
||||||
|
:episode="$episode"
|
||||||
|
:view="$view"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<ul class="-mb-6 flex list-none flex-row flex-wrap border-b-0 pl-0 relative z-10" role="tablist" data-te-nav-ref>
|
<ul class="-mb-6 flex list-none flex-row flex-wrap border-b-0 pl-0 relative z-10" role="tablist" data-te-nav-ref>
|
||||||
<li role="presentation" class="flex-auto text-center">
|
<li role="presentation" class="flex-auto text-center">
|
||||||
<a href="#tabs-most-views"
|
<a href="#tabs-most-views"
|
||||||
class="rounded-l-lg my-2 block border-x-0 border-b-2 border-t-0 border-transparent px-7 pb-3.5 pt-4 text-xs font-medium uppercase leading-tight text-neutral-500 hover:isolate hover:border-transparent hover:bg-neutral-50 focus:isolate focus:border-transparent data-[te-nav-active]:border-rose-600 data-[te-nav-active]:text-black dark:text-neutral-400 bg-white dark:bg-neutral-950 dark:hover:bg-neutral-800 dark:data-[te-nav-active]:text-white"
|
class="rounded-l-xl my-2 block border-x-0 border-b-2 border-t-0 border-transparent px-7 pb-3.5 pt-4 text-xs font-medium uppercase leading-tight text-neutral-500 hover:isolate hover:border-transparent hover:bg-neutral-50 focus:isolate focus:border-transparent data-[te-nav-active]:border-rose-600 data-[te-nav-active]:text-black dark:text-neutral-400 bg-white dark:bg-neutral-900 dark:hover:bg-neutral-800 dark:data-[te-nav-active]:text-white"
|
||||||
data-te-toggle="pill" data-te-target="#tabs-most-views" data-te-nav-active role="tab"
|
data-te-toggle="pill" data-te-target="#tabs-most-views" data-te-nav-active role="tab"
|
||||||
aria-controls="tabs-most-views" aria-selected="true">
|
aria-controls="tabs-most-views" aria-selected="true">
|
||||||
{{ __('home.most-views') }}
|
{{ __('home.most-views') }}
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li role="presentation" class="flex-auto text-center">
|
<li role="presentation" class="flex-auto text-center">
|
||||||
<a href="#tabs-most-likes"
|
<a href="#tabs-most-likes"
|
||||||
class="my-2 block border-x-0 border-b-2 border-t-0 border-transparent px-7 pb-3.5 pt-4 text-xs font-medium uppercase leading-tight text-neutral-500 hover:isolate hover:border-transparent hover:bg-neutral-50 focus:isolate focus:border-transparent data-[te-nav-active]:border-rose-600 data-[te-nav-active]:text-black dark:text-neutral-400 bg-white dark:bg-neutral-950 dark:hover:bg-neutral-800 dark:data-[te-nav-active]:text-white"
|
class="my-2 block border-x-0 border-b-2 border-t-0 border-transparent px-7 pb-3.5 pt-4 text-xs font-medium uppercase leading-tight text-neutral-500 hover:isolate hover:border-transparent hover:bg-neutral-50 focus:isolate focus:border-transparent data-[te-nav-active]:border-rose-600 data-[te-nav-active]:text-black dark:text-neutral-400 bg-white dark:bg-neutral-900 dark:hover:bg-neutral-800 dark:data-[te-nav-active]:text-white"
|
||||||
data-te-toggle="pill" data-te-target="#tabs-most-likes" role="tab" aria-controls="tabs-most-likes"
|
data-te-toggle="pill" data-te-target="#tabs-most-likes" role="tab" aria-controls="tabs-most-likes"
|
||||||
aria-selected="false">
|
aria-selected="false">
|
||||||
{{ __('home.most-likes') }}
|
{{ __('home.most-likes') }}
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li role="presentation" class="flex-auto text-center">
|
<li role="presentation" class="flex-auto text-center">
|
||||||
<a href="#tabs-popular-weekly"
|
<a href="#tabs-popular-weekly"
|
||||||
class="my-2 block border-x-0 border-b-2 border-t-0 border-transparent px-7 pb-3.5 pt-4 text-xs font-medium uppercase leading-tight text-neutral-500 hover:isolate hover:border-transparent hover:bg-neutral-50 focus:isolate focus:border-transparent data-[te-nav-active]:border-rose-600 data-[te-nav-active]:text-black dark:text-neutral-400 bg-white dark:bg-neutral-950 dark:hover:bg-neutral-800 dark:data-[te-nav-active]:text-white"
|
class="my-2 block border-x-0 border-b-2 border-t-0 border-transparent px-7 pb-3.5 pt-4 text-xs font-medium uppercase leading-tight text-neutral-500 hover:isolate hover:border-transparent hover:bg-neutral-50 focus:isolate focus:border-transparent data-[te-nav-active]:border-rose-600 data-[te-nav-active]:text-black dark:text-neutral-400 bg-white dark:bg-neutral-900 dark:hover:bg-neutral-800 dark:data-[te-nav-active]:text-white"
|
||||||
data-te-toggle="pill" data-te-target="#tabs-popular-weekly" role="tab"
|
data-te-toggle="pill" data-te-target="#tabs-popular-weekly" role="tab"
|
||||||
aria-controls="tabs-popular-weekly" aria-selected="false">
|
aria-controls="tabs-popular-weekly" aria-selected="false">
|
||||||
{{ __('home.popular-weekly') }}
|
{{ __('home.popular-weekly') }}
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li role="presentation" class="flex-auto text-center">
|
<li role="presentation" class="flex-auto text-center">
|
||||||
<a href="#tabs-popular-monthly"
|
<a href="#tabs-popular-monthly"
|
||||||
class="rounded-r-lg my-2 block border-x-0 border-b-2 border-t-0 border-transparent px-7 pb-3.5 pt-4 text-xs font-medium uppercase leading-tight text-neutral-500 hover:isolate hover:border-transparent hover:bg-neutral-50 focus:isolate focus:border-transparent data-[te-nav-active]:border-rose-600 data-[te-nav-active]:text-black dark:text-neutral-400 bg-white dark:bg-neutral-950 dark:hover:bg-neutral-800 dark:data-[te-nav-active]:text-white"
|
class="rounded-r-xl my-2 block border-x-0 border-b-2 border-t-0 border-transparent px-7 pb-3.5 pt-4 text-xs font-medium uppercase leading-tight text-neutral-500 hover:isolate hover:border-transparent hover:bg-neutral-50 focus:isolate focus:border-transparent data-[te-nav-active]:border-rose-600 data-[te-nav-active]:text-black dark:text-neutral-400 bg-white dark:bg-neutral-900 dark:hover:bg-neutral-800 dark:data-[te-nav-active]:text-white"
|
||||||
data-te-toggle="pill" data-te-target="#tabs-popular-monthly" role="tab"
|
data-te-toggle="pill" data-te-target="#tabs-popular-monthly" role="tab"
|
||||||
aria-controls="tabs-popular-monthly" aria-selected="false">
|
aria-controls="tabs-popular-monthly" aria-selected="false">
|
||||||
{{ __('home.popular-monthly') }}
|
{{ __('home.popular-monthly') }}
|
||||||
@@ -34,11 +34,11 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@php $showThumbnails = true; @endphp
|
@php $isThumbnail = true; @endphp
|
||||||
|
|
||||||
@auth
|
@auth
|
||||||
@if (!Auth::user()->home_middle_design)
|
@if (!Auth::user()->home_middle_design)
|
||||||
@php $showThumbnails = false; @endphp
|
@php $isThumbnail = false; @endphp
|
||||||
@endif
|
@endif
|
||||||
@endauth
|
@endauth
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
id="tabs-most-views" role="tabpanel" aria-labelledby="tabs-most-views-tab" data-te-tab-active>
|
id="tabs-most-views" role="tabpanel" aria-labelledby="tabs-most-views-tab" data-te-tab-active>
|
||||||
@include('home.partials.tab.template', [
|
@include('home.partials.tab.template', [
|
||||||
'episodes' => $popularAllTime,
|
'episodes' => $popularAllTime,
|
||||||
'showThumbnails' => $showThumbnails,
|
'isThumbnail' => $isThumbnail,
|
||||||
])
|
])
|
||||||
<div class="grid text-center pt-5 ">
|
<div class="grid text-center pt-5 ">
|
||||||
<a href="{{ route('hentai.search', ['order' => 'view-count']) }}"
|
<a href="{{ route('hentai.search', ['order' => 'view-count']) }}"
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
id="tabs-most-likes" role="tabpanel" aria-labelledby="tabs-most-likes-tab">
|
id="tabs-most-likes" role="tabpanel" aria-labelledby="tabs-most-likes-tab">
|
||||||
@include('home.partials.tab.template', [
|
@include('home.partials.tab.template', [
|
||||||
'episodes' => $mostLikes,
|
'episodes' => $mostLikes,
|
||||||
'showThumbnails' => $showThumbnails,
|
'isThumbnail' => $isThumbnail,
|
||||||
])
|
])
|
||||||
<div class="grid text-center pt-5 ">
|
<div class="grid text-center pt-5 ">
|
||||||
<a href="{{ route('hentai.search', ['order' => 'view-count']) }}"
|
<a href="{{ route('hentai.search', ['order' => 'view-count']) }}"
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
id="tabs-popular-weekly" role="tabpanel" aria-labelledby="tabs-popular-weekly-tab">
|
id="tabs-popular-weekly" role="tabpanel" aria-labelledby="tabs-popular-weekly-tab">
|
||||||
@include('home.partials.tab.template', [
|
@include('home.partials.tab.template', [
|
||||||
'episodes' => $popularWeekly,
|
'episodes' => $popularWeekly,
|
||||||
'showThumbnails' => $showThumbnails,
|
'isThumbnail' => $isThumbnail,
|
||||||
'popularView' => true,
|
'popularView' => true,
|
||||||
])
|
])
|
||||||
<div class="grid text-center pt-5 ">
|
<div class="grid text-center pt-5 ">
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
id="tabs-popular-monthly" role="tabpanel" aria-labelledby="tabs-popular-monthly-tab">
|
id="tabs-popular-monthly" role="tabpanel" aria-labelledby="tabs-popular-monthly-tab">
|
||||||
@include('home.partials.tab.template', [
|
@include('home.partials.tab.template', [
|
||||||
'episodes' => $popularMonthly,
|
'episodes' => $popularMonthly,
|
||||||
'showThumbnails' => $showThumbnails,
|
'isThumbnail' => $isThumbnail,
|
||||||
'popularView' => true,
|
'popularView' => true,
|
||||||
])
|
])
|
||||||
<div class="grid text-center pt-5 ">
|
<div class="grid text-center pt-5 ">
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
<!--Tabs navigation-->
|
<!--Tabs navigation-->
|
||||||
<ul class="-mb-6 flex list-none flex-row flex-wrap border-b-0 pl-0 relative z-10" role="tablist" data-te-nav-ref>
|
<ul class="-mb-6 flex list-none flex-row flex-wrap border-b-0 pl-0 relative z-10" role="tablist" data-te-nav-ref>
|
||||||
<li role="presentation" class="flex-auto text-center">
|
<li role="presentation" class="flex-auto text-center">
|
||||||
<a href="#tabs-recently-uploaded" class="rounded-l-lg my-2 block border-x-0 border-b-2 border-t-0 border-transparent px-7 pb-3.5 pt-4 text-xs font-medium uppercase leading-tight text-neutral-500 hover:isolate hover:border-transparent hover:bg-neutral-50 focus:isolate focus:border-transparent data-[te-nav-active]:border-rose-600 data-[te-nav-active]:text-black dark:text-neutral-400 bg-white/50 dark:bg-neutral-950/50 backdrop-blur-sm dark:hover:bg-neutral-800 dark:data-[te-nav-active]:text-white"
|
<a href="#tabs-recently-uploaded" class="rounded-l-xl my-2 block border-x-0 border-b-2 border-t-0 border-transparent px-7 pb-3.5 pt-4 text-xs font-medium uppercase leading-tight text-neutral-500 hover:isolate hover:border-transparent hover:bg-neutral-50 focus:isolate focus:border-transparent data-[te-nav-active]:border-rose-600 data-[te-nav-active]:text-black dark:text-neutral-400 bg-white/50 dark:bg-neutral-900/50 backdrop-blur-sm dark:hover:dark:hover:bg-neutral-900 dark:data-[te-nav-active]:text-white"
|
||||||
data-te-toggle="pill" data-te-target="#tabs-recently-uploaded" data-te-nav-active role="tab" aria-controls="tabs-recently-uploaded" aria-selected="true">
|
data-te-toggle="pill" data-te-target="#tabs-recently-uploaded" data-te-nav-active role="tab" aria-controls="tabs-recently-uploaded" aria-selected="true">
|
||||||
{{ __('home.recently-uploaded') }} ({{ Carbon\Carbon::parse($recentlyUploaded[0]->created_at)->diffForHumans([ 'parts' => 2 ]) }})
|
{{ __('home.recently-uploaded') }} ({{ Carbon\Carbon::parse($recentlyUploaded[0]->created_at)->diffForHumans([ 'parts' => 2 ]) }})
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li role="presentation" class="flex-auto text-center">
|
<li role="presentation" class="flex-auto text-center">
|
||||||
<a href="#tabs-recently-released" class="my-2 block border-x-0 border-b-2 border-t-0 border-transparent px-7 pb-3.5 pt-4 text-xs font-medium uppercase leading-tight text-neutral-500 hover:isolate hover:border-transparent hover:bg-neutral-50 focus:isolate focus:border-transparent data-[te-nav-active]:border-rose-600 data-[te-nav-active]:text-black dark:text-neutral-400 bg-white/50 dark:bg-neutral-950/50 backdrop-blur-sm dark:hover:bg-neutral-800 dark:data-[te-nav-active]:text-white"
|
<a href="#tabs-recently-released" class="my-2 block border-x-0 border-b-2 border-t-0 border-transparent px-7 pb-3.5 pt-4 text-xs font-medium uppercase leading-tight text-neutral-500 hover:isolate hover:border-transparent hover:bg-neutral-50 focus:isolate focus:border-transparent data-[te-nav-active]:border-rose-600 data-[te-nav-active]:text-black dark:text-neutral-400 bg-white/50 dark:bg-neutral-900/50 backdrop-blur-sm dark:hover:bg-neutral-900 dark:data-[te-nav-active]:text-white"
|
||||||
data-te-toggle="pill" data-te-target="#tabs-recently-released" role="tab" aria-controls="tabs-recently-released" aria-selected="false">
|
data-te-toggle="pill" data-te-target="#tabs-recently-released" role="tab" aria-controls="tabs-recently-released" aria-selected="false">
|
||||||
{{ __('home.recently-released') }}
|
{{ __('home.recently-released') }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li role="presentation" class="flex-auto text-center">
|
<li role="presentation" class="flex-auto text-center">
|
||||||
<a href="#tabs-trending" class="rounded-r-lg my-2 block border-x-0 border-b-2 border-t-0 border-transparent px-7 pb-3.5 pt-4 text-xs font-medium uppercase leading-tight text-neutral-500 hover:isolate hover:border-transparent hover:bg-neutral-50 focus:isolate focus:border-transparent data-[te-nav-active]:border-rose-600 data-[te-nav-active]:text-black dark:text-neutral-400 bg-white/50 dark:bg-neutral-950/50 backdrop-blur-sm dark:hover:bg-neutral-800 dark:data-[te-nav-active]:text-white"
|
<a href="#tabs-trending" class="rounded-r-xl my-2 block border-x-0 border-b-2 border-t-0 border-transparent px-7 pb-3.5 pt-4 text-xs font-medium uppercase leading-tight text-neutral-500 hover:isolate hover:border-transparent hover:bg-neutral-50 focus:isolate focus:border-transparent data-[te-nav-active]:border-rose-600 data-[te-nav-active]:text-black dark:text-neutral-400 bg-white/50 dark:bg-neutral-900/50 backdrop-blur-sm dark:hover:bg-neutral-900 dark:data-[te-nav-active]:text-white"
|
||||||
data-te-toggle="pill" data-te-target="#tabs-trending" role="tab" aria-controls="tabs-trending" aria-selected="false">
|
data-te-toggle="pill" data-te-target="#tabs-trending" role="tab" aria-controls="tabs-trending" aria-selected="false">
|
||||||
{{ __('home.trending') }}
|
{{ __('home.trending') }}
|
||||||
</a>
|
</a>
|
||||||
@@ -21,18 +21,18 @@
|
|||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@php $showThumbnails = false; @endphp
|
@php $isThumbnail = false; @endphp
|
||||||
|
|
||||||
@auth
|
@auth
|
||||||
@if(Auth::user()->home_top_design)
|
@if(Auth::user()->home_top_design)
|
||||||
@php $showThumbnails = true; @endphp
|
@php $isThumbnail = true; @endphp
|
||||||
@endif
|
@endif
|
||||||
@endauth
|
@endauth
|
||||||
|
|
||||||
<!--Tabs content-->
|
<!--Tabs content-->
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<div class="hidden opacity-100 transition-opacity duration-150 ease-linear data-[te-tab-active]:block" id="tabs-recently-uploaded" role="tabpanel" aria-labelledby="tabs-recently-uploaded-tab" data-te-tab-active>
|
<div class="hidden opacity-100 transition-opacity duration-150 ease-linear data-[te-tab-active]:block" id="tabs-recently-uploaded" role="tabpanel" aria-labelledby="tabs-recently-uploaded-tab" data-te-tab-active>
|
||||||
@include('home.partials.tab.template', ['episodes' => $recentlyUploaded, 'showThumbnails' => $showThumbnails])
|
@include('home.partials.tab.template', ['episodes' => $recentlyUploaded, 'isThumbnail' => $isThumbnail])
|
||||||
<div class="grid text-center pt-5 ">
|
<div class="grid text-center pt-5 ">
|
||||||
<a href="{{ route('hentai.search', ['order' => 'recently-uploaded']) }}"
|
<a href="{{ route('hentai.search', ['order' => 'recently-uploaded']) }}"
|
||||||
class="rounded bg-rose-600 p-1 mr-2 text-xs font-medium uppercase leading-normal text-white transition duration-150 ease-in-out hover:bg-rose-700 focus:bg-rose-600">
|
class="rounded bg-rose-600 p-1 mr-2 text-xs font-medium uppercase leading-normal text-white transition duration-150 ease-in-out hover:bg-rose-700 focus:bg-rose-600">
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden opacity-0 transition-opacity duration-150 ease-linear data-[te-tab-active]:block" id="tabs-recently-released" role="tabpanel"aria-labelledby="tabs-recently-released-tab">
|
<div class="hidden opacity-0 transition-opacity duration-150 ease-linear data-[te-tab-active]:block" id="tabs-recently-released" role="tabpanel"aria-labelledby="tabs-recently-released-tab">
|
||||||
@include('home.partials.tab.template', ['episodes' => $recentlyReleased, 'showThumbnails' => $showThumbnails])
|
@include('home.partials.tab.template', ['episodes' => $recentlyReleased, 'isThumbnail' => $isThumbnail])
|
||||||
<div class="grid text-center pt-5 ">
|
<div class="grid text-center pt-5 ">
|
||||||
<a href="{{ route('hentai.search', ['order' => 'recently-released']) }}"
|
<a href="{{ route('hentai.search', ['order' => 'recently-released']) }}"
|
||||||
class="rounded bg-rose-600 p-1 mr-2 text-xs font-medium uppercase leading-normal text-white transition duration-150 ease-in-out hover:bg-rose-700 focus:bg-rose-600">
|
class="rounded bg-rose-600 p-1 mr-2 text-xs font-medium uppercase leading-normal text-white transition duration-150 ease-in-out hover:bg-rose-700 focus:bg-rose-600">
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden opacity-0 transition-opacity duration-150 ease-linear data-[te-tab-active]:block" id="tabs-trending" role="tabpanel"aria-labelledby="tabs-trending-tab">
|
<div class="hidden opacity-0 transition-opacity duration-150 ease-linear data-[te-tab-active]:block" id="tabs-trending" role="tabpanel"aria-labelledby="tabs-trending-tab">
|
||||||
@include('home.partials.tab.template', ['episodes' => $popularDaily, 'showThumbnails' => $showThumbnails, 'popularView' => true])
|
@include('home.partials.tab.template', ['episodes' => $popularDaily, 'isThumbnail' => $isThumbnail, 'popularView' => true])
|
||||||
<div class="grid text-center pt-5 ">
|
<div class="grid text-center pt-5 ">
|
||||||
<a href="{{ route('hentai.search', ['order' => 'recently-released']) }}"
|
<a href="{{ route('hentai.search', ['order' => 'recently-released']) }}"
|
||||||
class="rounded invisible bg-rose-600 p-1 mr-2 text-xs font-medium uppercase leading-normal text-white transition duration-150 ease-in-out hover:bg-rose-700 focus:bg-rose-600">
|
class="rounded invisible bg-rose-600 p-1 mr-2 text-xs font-medium uppercase leading-normal text-white transition duration-150 ease-in-out hover:bg-rose-700 focus:bg-rose-600">
|
||||||
|
|||||||
@@ -1,47 +1,78 @@
|
|||||||
<x-app-layout>
|
<x-app-layout>
|
||||||
<div class="container my-24 mx-auto md:px-6 z-10 relative">
|
<div class="container mx-auto px-4 py-12 md:py-24">
|
||||||
<section class="mb-32 text-center">
|
<section class="text-center mb-16">
|
||||||
<div class="flex justify-center pb-10">
|
<!-- Logo -->
|
||||||
<img src="/images/cropped-HS-1-270x270.webp" class="max-w-[150px]" alt="hstream.moe Logo" />
|
<div class="flex justify-center mb-8">
|
||||||
|
<img
|
||||||
|
src="/images/cropped-HS-1-270x270.webp"
|
||||||
|
alt="hstream.moe Logo"
|
||||||
|
class="max-w-[150px] w-full h-auto rounded-lg"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid md:grid-cols-2 lg:grid-cols-4 lg:gap-x-12">
|
|
||||||
<div class="mb-12 md:mb-0">
|
<!-- Stats Grid -->
|
||||||
<div class="mb-6 inline-block rounded-md bg-white dark:bg-neutral-950 p-4 text-sky-500">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 md:gap-8">
|
||||||
<i class="fa-solid fa-eye text-3xl"> {{ number_format($viewCount) }}</i>
|
<!-- View Count Card -->
|
||||||
|
<div class="bg-sky-300/50 dark:bg-sky-950/50 rounded-xl p-6 shadow-sm hover:shadow-md transition-shadow duration-300">
|
||||||
|
<div class="flex justify-center mb-4">
|
||||||
|
<i class="fa-solid fa-eye text-4xl text-sky-600 dark:text-sky-400 p-3"></i>
|
||||||
</div>
|
</div>
|
||||||
<h5 class="text-lg font-medium dark:text-neutral-300">
|
<div class="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||||
|
{{ number_format($viewCount) }}
|
||||||
|
</div>
|
||||||
|
<h5 class="text-lg font-medium text-gray-700 dark:text-neutral-300">
|
||||||
total views
|
total views
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-12 md:mb-0">
|
|
||||||
<div class="b-6 inline-block rounded-md bg-white dark:bg-neutral-950 p-4 text-sky-500">
|
<!-- Episode Count Card -->
|
||||||
<i class="fa-solid fa-video text-3xl"> {{ $episodeCount }}</i>
|
<div class="bg-sky-300/50 dark:bg-sky-950/50 rounded-xl p-6 shadow-sm hover:shadow-md transition-shadow duration-300">
|
||||||
|
<div class="flex justify-center mb-4">
|
||||||
|
<i class="fa-solid fa-video text-4xl text-sky-600 dark:text-sky-400 p-3"></i>
|
||||||
</div>
|
</div>
|
||||||
<h5 class="text-lg font-medium dark:text-neutral-300">
|
<div class="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||||
|
{{ $episodeCount }}
|
||||||
|
</div>
|
||||||
|
<h5 class="text-lg font-medium text-gray-700 dark:text-neutral-300">
|
||||||
episodes on this site
|
episodes on this site
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-12 md:mb-0">
|
|
||||||
<div class="mb-6 inline-block rounded-md bg-white dark:bg-neutral-950 p-4 text-rose-600">
|
<!-- Hentai Count Card -->
|
||||||
<i class="fa-solid fa-list text-3xl"> {{ $hentaiCount }}</i>
|
<div class="bg-rose-300/50 dark:bg-rose-950/50 rounded-xl p-6 shadow-sm hover:shadow-md transition-shadow duration-300">
|
||||||
|
<div class="flex justify-center mb-4">
|
||||||
|
<i class="fa-solid fa-list text-4xl text-rose-600 dark:text-rose-400 p-3"></i>
|
||||||
</div>
|
</div>
|
||||||
<h5 class="text-lg font-medium dark:text-neutral-300">
|
<div class="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||||
|
{{ $hentaiCount }}
|
||||||
|
</div>
|
||||||
|
<h5 class="text-lg font-medium text-gray-700 dark:text-neutral-300">
|
||||||
hentais on this site
|
hentais on this site
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-12 md:mb-0">
|
|
||||||
<div class="mb-6 inline-block rounded-md bg-white dark:bg-neutral-950 p-4 text-rose-600">
|
<!-- Watch Time Card -->
|
||||||
<i class="fa-solid fa-clock text-3xl"> {{ number_format($viewCount * 6) }}</i>
|
<div class="bg-rose-300/50 dark:bg-rose-950/50 rounded-xl p-6 shadow-sm hover:shadow-md transition-shadow duration-300">
|
||||||
|
<div class="flex justify-center mb-4">
|
||||||
|
<i class="fa-solid fa-clock text-4xl text-rose-600 dark:text-rose-400 p-3"></i>
|
||||||
</div>
|
</div>
|
||||||
<h5 class="text-lg font-medium dark:text-neutral-300">
|
<div class="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||||
|
{{ number_format($viewCount * 6) }}
|
||||||
|
</div>
|
||||||
|
<h5 class="text-lg font-medium text-gray-700 dark:text-neutral-300">
|
||||||
estimated minutes of watch time
|
estimated minutes of watch time
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center dark:bg-neutral-950 bg-gray-50 rounded-xl md:m-11 hidden md:block">
|
|
||||||
<canvas id="monthlyChart"></canvas>
|
<!-- Chart Container -->
|
||||||
|
<div class="mt-12 mx-auto max-w-4xl">
|
||||||
|
<div class="bg-gray-50 dark:bg-neutral-950 rounded-xl p-4 md:p-6 shadow-inner hidden sm:block">
|
||||||
|
<canvas id="monthlyChart" class="w-full h-64 md:h-80"></canvas>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@vite(['resources/js/stats.js'])
|
@vite(['resources/js/stats.js'])
|
||||||
</x-app-layout>
|
</x-app-layout>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
@include('partials.head')
|
@include('partials.head')
|
||||||
|
|
||||||
<body class="font-sans antialiased">
|
<body class="font-sans antialiased">
|
||||||
<div class="flex flex-col min-h-screen bg-gray-100 dark:bg-neutral-900">
|
<div class="flex flex-col min-h-screen bg-gray-100 dark:bg-neutral-950">
|
||||||
@include('layouts.navigation')
|
@include('layouts.navigation')
|
||||||
|
|
||||||
<!-- Page Heading -->
|
<!-- Page Heading -->
|
||||||
|
|||||||
@@ -1,77 +1,99 @@
|
|||||||
<footer class="bg-white z-10 rounded-lg shadow dark:bg-neutral-950 m-4 mb-0 mt-auto">
|
<footer class="bg-white z-10 rounded-xl shadow-lg dark:bg-neutral-950 m-4 mb-0 mt-auto">
|
||||||
<div class="w-full xl:max-w-[95%] 2xl:max-w-[84%] mx-auto p-4">
|
<div class="w-full max-w-7xl mx-auto px-4 py-6">
|
||||||
<div class="sm:flex sm:items-center sm:justify-between">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 items-center">
|
||||||
<a href="https://hstream.moe/" class="flex items-center mb-4 sm:mb-0">
|
<!-- Logo -->
|
||||||
<img src="/images/cropped-HS-1-192x192.webp" class="h-8 mr-3" alt="hstream.moe Logo" />
|
<div class="flex items-center justify-center md:justify-start">
|
||||||
<span class="self-center text-2xl font-semibold whitespace-nowrap dark:text-white">hstream.moe</span>
|
<a href="https://hstream.moe/" class="flex items-center">
|
||||||
|
<img src="/images/cropped-HS-1-192x192.webp"
|
||||||
|
class="h-10 w-10 mr-3 rounded-lg object-cover"
|
||||||
|
alt="hstream.moe Logo" />
|
||||||
|
<span class="text-xl font-bold whitespace-nowrap text-gray-600 dark:text-white">hstream.moe</span>
|
||||||
</a>
|
</a>
|
||||||
<ul class="flex flex-wrap items-center mb-6 text-sm font-medium text-gray-500 sm:mb-0 dark:text-gray-400">
|
</div>
|
||||||
<li>
|
|
||||||
<a href="{{ route('contact.index') }}" class="mr-4 hover:underline md:mr-6 "><i
|
|
||||||
class="fa-solid fa-message"></i> Contact</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="{{ config('discord.invite_link') }}" class="mr-4 hover:underline md:mr-6 "><i
|
|
||||||
class="fa-brands fa-discord"></i> Discord</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="{{ route('home.stats') }}" class="mr-4 hover:underline md:mr-6 "><i
|
|
||||||
class="fa-solid fa-chart-simple"></i> Stats</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<!-- Links to friendly sites -->
|
|
||||||
<ul class="flex flex-wrap items-center mb-6 text-sm font-medium text-gray-500 sm:mb-0 dark:text-gray-400">
|
|
||||||
<li>
|
|
||||||
<a target="_blank" href="https://everythingmoe.com/"
|
|
||||||
class="mr-4 hover:underline md:mr-6">everythingmoe.com</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a target="_blank" href="https://theindex.moe/"
|
|
||||||
class="mr-4 hover:underline md:mr-6">theindex.moe</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a target="_blank" href="https://www.squid-board.org/"
|
|
||||||
class="mr-4 hover:underline md:mr-6">squidboard.org</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a target="_blank" href="https://hentaizilla.com/"
|
|
||||||
class="hover:underline md:mr-6">hentaizilla.com</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a target="_blank" href="https://hentaipulse.com/"
|
|
||||||
class="hover:underline md:mr-6">hentaipulse.com</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a target="_blank" href="https://hentaisites.com/"
|
|
||||||
class="hover:underline md:mr-6">hentaisites.com</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a target="_blank" href="https://zhentube.com/"
|
|
||||||
class="hover:underline md:mr-6">zhentube.com</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<ul class="flex flex-wrap items-center mb-6 text-sm font-medium text-gray-500 sm:mb-0 dark:text-gray-400">
|
|
||||||
<li>
|
|
||||||
<a class="hover:underline md:mr-6 cursor-pointer" data-te-toggle="modal"
|
|
||||||
data-te-target="#modalLanguage">Language</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
@if (!Session::has('alert.config'))
|
<!-- Main Navigation -->
|
||||||
<script src="{{ asset('vendor/sweetalert/sweetalert.all.js') }}"></script>
|
<div class="flex justify-center">
|
||||||
@endif
|
<ul class="flex flex-wrap items-center gap-4 text-sm font-medium text-gray-600 dark:text-gray-300">
|
||||||
@if (config('sweetalert.theme') != 'default')
|
<li>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/@sweetalert2/theme-{{ config('sweetalert.theme') }}"
|
<a href="{{ route('contact.index') }}"
|
||||||
rel="stylesheet">
|
class="flex items-center gap-1 hover:text-blue-600 transition-colors">
|
||||||
@endif
|
<i class="fa-solid fa-message"></i> Contact
|
||||||
@include('sweetalert::alert')
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ config('discord.invite_link') }}"
|
||||||
|
class="flex items-center gap-1 hover:text-blue-600 transition-colors">
|
||||||
|
<i class="fa-brands fa-discord"></i> Discord
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ route('home.stats') }}"
|
||||||
|
class="flex items-center gap-1 hover:text-blue-600 transition-colors">
|
||||||
|
<i class="fa-solid fa-chart-simple"></i> Stats
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Language Selector -->
|
||||||
|
<div class="flex justify-center md:justify-end">
|
||||||
|
<button data-te-toggle="modal"
|
||||||
|
data-te-target="#modalLanguage"
|
||||||
|
class="flex items-center gap-1 text-sm font-medium text-gray-600 hover:text-blue-600 transition-colors dark:text-gray-300">
|
||||||
|
<i class="fa-solid fa-globe"></i> Language
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Friendly Sites -->
|
||||||
|
<div class="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<h3 class="text-sm font-semibold text-gray-500 dark:text-gray-400 mb-3 text-center">Friendly Sites</h3>
|
||||||
|
<div class="flex flex-wrap justify-center gap-3">
|
||||||
|
@foreach([
|
||||||
|
'everythingmoe.com' => 'https://everythingmoe.com/',
|
||||||
|
'theindex.moe' => 'https://theindex.moe/',
|
||||||
|
'squidboard.org' => 'https://www.squid-board.org/',
|
||||||
|
'hentaizilla.com' => 'https://hentaizilla.com/',
|
||||||
|
'hentaipulse.com' => 'https://hentaipulse.com/',
|
||||||
|
'hentaisites.com' => 'https://hentaisites.com/',
|
||||||
|
'zhentube.com' => 'https://zhentube.com/'
|
||||||
|
] as $name => $url)
|
||||||
|
<a href="{{ $url }}"
|
||||||
|
target="_blank"
|
||||||
|
class="text-sm text-gray-600 hover:text-blue-600 transition-colors dark:text-gray-300">
|
||||||
|
{{ $name }}
|
||||||
|
</a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
@include('modals.language-selector')
|
|
||||||
|
|
||||||
<div class="m-2 w-full mx-auto">
|
<!-- Footer Info -->
|
||||||
<div class="text-sm text-gray-500 text-center">
|
<div class="m-4 w-full max-w-7xl mx-auto">
|
||||||
<p>Render time: {{ number_format(microtime(true) - (defined('LARAVEL_START') ? LARAVEL_START : request()->server('REQUEST_TIME_FLOAT')), 3) }} seconds | Memory usage: {{ number_format(memory_get_peak_usage(true) / 1048576, 2) }} MB | Git: <a href="https://gitea.hstream.moe/w33b/hstream/commits/branch/main" target="_blank">{{ \App\Helpers\GitHelper::shortCommit() }}</a></p>
|
<div class="text-xs text-gray-500 text-center dark:text-gray-400">
|
||||||
|
<p>Render time: {{ number_format(microtime(true) - (defined('LARAVEL_START') ? LARAVEL_START : request()->server('REQUEST_TIME_FLOAT')), 3) }} seconds |
|
||||||
|
Memory usage: {{ number_format(memory_get_peak_usage(true) / 1048576, 2) }} MB |
|
||||||
|
Git: <a href="https://gitea.hstream.moe/w33b/hstream/commits/branch/main"
|
||||||
|
target="_blank"
|
||||||
|
class="hover:text-blue-600 transition-colors">
|
||||||
|
{{ \App\Helpers\GitHelper::shortCommit() }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- SweetAlert Scripts -->
|
||||||
|
@if (!Session::has('alert.config'))
|
||||||
|
<script src="{{ asset('vendor/sweetalert/sweetalert.all.js') }}"></script>
|
||||||
|
@endif
|
||||||
|
@if (config('sweetalert.theme') != 'default')
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/@sweetalert2/theme-{{ config('sweetalert.theme') }}" rel="stylesheet">
|
||||||
|
@endif
|
||||||
|
@include('sweetalert::alert')
|
||||||
|
|
||||||
|
<!-- Modals -->
|
||||||
|
@include('modals.language-selector')
|
||||||
|
|
||||||
|
<!-- Thumbnail hover -->
|
||||||
|
@vite(['resources/js/preview.js'])
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
@include('partials.head')
|
@include('partials.head')
|
||||||
|
|
||||||
<body class="font-sans antialiased">
|
<body class="font-sans antialiased">
|
||||||
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100 dark:bg-neutral-900">
|
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100 dark:bg-neutral-950">
|
||||||
<div>
|
<div>
|
||||||
<a href="/">
|
<a href="/">
|
||||||
<x-application-logo class="w-24 h-24 fill-current text-gray-500" />
|
<x-application-logo class="w-24 h-24 fill-current text-gray-500" />
|
||||||
|
|||||||
@@ -1,121 +1,253 @@
|
|||||||
<div>
|
<div id="comment-{{ $comment->id }}">
|
||||||
<div class="flex" id="comment-{{ $comment->id }}">
|
|
||||||
<div class="flex-shrink-0 mr-4">
|
<div class="group flex gap-4">
|
||||||
<img class="h-10 w-10 rounded-full" src="{{ $comment->user->getAvatar() }}" alt="{{ $comment->user->name }}">
|
|
||||||
|
{{-- Avatar --}}
|
||||||
|
<div class="shrink-0">
|
||||||
|
@if($comment->isDeletedByModerator())
|
||||||
|
<img
|
||||||
|
class="h-10 w-10 rounded-full object-cover opacity-60"
|
||||||
|
src="{{ asset('images/default-avatar.webp') }}"
|
||||||
|
alt="Deleted comment"
|
||||||
|
>
|
||||||
|
@else
|
||||||
|
<img
|
||||||
|
class="h-10 w-10 rounded-full object-cover ring-2 ring-white dark:ring-neutral-800"
|
||||||
|
src="{{ $comment->user->getAvatar() }}"
|
||||||
|
alt="{{ $comment->user->name }}"
|
||||||
|
>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow">
|
|
||||||
<div class="flex gap-2">
|
{{-- Content --}}
|
||||||
<p class="font-medium text-gray-900 dark:text-gray-100">{{ $comment->user->name }}</p>
|
<div class="min-w-0 flex-1">
|
||||||
|
|
||||||
|
<div class="rounded-2xl border border-neutral-200 bg-white px-5 py-4 shadow-sm transition group-hover:border-neutral-300 dark:border-neutral-800 dark:bg-neutral-800 dark:group-hover:border-neutral-700">
|
||||||
|
|
||||||
|
{{-- Header --}}
|
||||||
|
<div class="mb-3 flex flex-wrap items-center gap-2">
|
||||||
|
|
||||||
|
@if($comment->isDeletedByModerator())
|
||||||
|
|
||||||
|
<span class="font-semibold text-neutral-900 dark:text-neutral-100">
|
||||||
|
@if (Auth::check() && (Auth::user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR) || Auth::user()->hasRole(\App\Enums\UserRole::MODERATOR)))
|
||||||
|
Deleted ({{ $comment->user->name }})
|
||||||
|
@else
|
||||||
|
Deleted
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
|
|
||||||
|
@else
|
||||||
|
|
||||||
|
<span class="font-semibold text-neutral-900 dark:text-neutral-100">
|
||||||
|
{{ $comment->user->name }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- Badges --}}
|
||||||
@if($comment->user->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
|
@if($comment->user->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
|
||||||
<a data-te-toggle="tooltip" title="Admin"><i class="fa-solid fa-crown text-yellow-600"></i></a>
|
<span class="inline-flex items-center rounded-full bg-yellow-100 px-2 py-0.5 text-xs font-medium text-yellow-700 dark:bg-yellow-500/10 dark:text-yellow-400">
|
||||||
|
<i class="fa-solid fa-crown mr-1"></i>
|
||||||
|
Admin
|
||||||
|
</span>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
@if($comment->user->hasRole(\App\Enums\UserRole::MODERATOR))
|
||||||
|
<span class="inline-flex items-center rounded-full bg-rose-100 px-2 py-0.5 text-xs font-medium text-rose-700 dark:bg-rose-500/10 dark:text-rose-400">
|
||||||
|
Moderator
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
|
||||||
@if($comment->user->hasRole(\App\Enums\UserRole::SUPPORTER))
|
@if($comment->user->hasRole(\App\Enums\UserRole::SUPPORTER))
|
||||||
<a data-te-toggle="tooltip" title="Badge of appreciation for the horny people supporting us! :3"><i class="fa-solid fa-hand-holding-heart text-rose-600"></i></a>
|
<span class="inline-flex items-center rounded-full bg-pink-100 px-2 py-0.5 text-xs font-medium text-pink-700 dark:bg-pink-500/10 dark:text-pink-400">
|
||||||
|
<i class="fa-solid fa-heart mr-1"></i>
|
||||||
|
Supporter
|
||||||
|
</span>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 flex-grow w-full">
|
|
||||||
|
{{-- Body --}}
|
||||||
|
<div class="prose prose-sm max-w-none dark:prose-invert">
|
||||||
|
|
||||||
|
@if($comment->isDeletedByModerator())
|
||||||
|
|
||||||
|
<p class="italic text-neutral-500 dark:text-neutral-400">
|
||||||
|
Deleted by moderation.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@if (Auth::check() && (Auth::user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR) || Auth::user()->hasRole(\App\Enums\UserRole::MODERATOR)))
|
||||||
|
<div class="mt-3 rounded-xl bg-neutral-100 p-3 text-sm dark:bg-neutral-800">
|
||||||
|
{!! $comment->presenter()->markdownBody() !!}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@else
|
||||||
|
|
||||||
@if ($isEditing)
|
@if ($isEditing)
|
||||||
<form wire:submit.prevent="editComment">
|
|
||||||
<div>
|
<form wire:submit.prevent="editComment" class="space-y-4">
|
||||||
<label for="comment" class="sr-only">Comment body</label>
|
|
||||||
<textarea id="comment" name="comment" rows="3"
|
<textarea
|
||||||
class="bg-white dark:bg-neutral-700 shadow-sm block w-full focus:ring-rose-500 focus:border-rose-500 border-gray-300 dark:border-gray-400/40 text-gray-900 dark:text-gray-200 placeholder:text-gray-400 rounded-md
|
rows="4"
|
||||||
@error('editState.body') border-red-500 @enderror"
|
wire:model.defer="editState.body"
|
||||||
placeholder="Write something" wire:model.defer="editState.body"></textarea>
|
class="w-full rounded-2xl border border-neutral-300 bg-white px-4 py-3 text-sm text-neutral-900 shadow-sm transition focus:border-rose-500 focus:outline-none focus:ring-4 focus:ring-rose-500/10 dark:border-neutral-700 dark:bg-neutral-950 dark:text-neutral-100 @error('editState.body') border-red-500 @enderror"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
@error('editState.body')
|
@error('editState.body')
|
||||||
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
|
<p class="text-sm text-red-500">
|
||||||
|
{{ $message }}
|
||||||
|
</p>
|
||||||
@enderror
|
@enderror
|
||||||
</div>
|
|
||||||
<div class="mt-3 flex items-center justify-between">
|
<div class="flex justify-end">
|
||||||
<button type="submit"
|
<button
|
||||||
class="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md shadow-sm text-white bg-rose-600 hover:bg-rose-700 focus:outline-none focus:ring-2 focus:ring-rose-500">
|
type="submit"
|
||||||
Edit
|
class="rounded-xl bg-rose-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-rose-700"
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@else
|
@else
|
||||||
<div class="text-gray-700 dark:text-gray-200">{!! $comment->presenter()->markdownBody() !!}</div>
|
<div class="text-gray-700 dark:text-gray-200">
|
||||||
@endif
|
{!! $comment->presenter()->markdownBody() !!}
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 space-x-2 flex flex-row">
|
@endif
|
||||||
<span class="text-gray-500 dark:text-gray-300">
|
|
||||||
|
@endif
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Footer --}}
|
||||||
|
<div class="mt-4 flex flex-wrap items-center gap-4 text-sm">
|
||||||
|
|
||||||
|
<span class="text-neutral-500 dark:text-neutral-400">
|
||||||
{{ $comment->presenter()->relativeCreatedAt() }}
|
{{ $comment->presenter()->relativeCreatedAt() }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
{{-- Like --}}
|
||||||
@guest
|
@guest
|
||||||
<span data-te-toggle="tooltip" title="Please login to like the episode" class="text-gray-800 cursor-pointer dark:text-gray-200">
|
<span class="flex items-center gap-1 text-neutral-500 dark:text-neutral-400">
|
||||||
<i class="fa-regular fa-heart"></i> {{ $comment->likeCount() }}
|
<i class="fa-regular fa-heart"></i>
|
||||||
|
{{ $comment->likeCount() }}
|
||||||
</span>
|
</span>
|
||||||
@endguest
|
@endguest
|
||||||
|
|
||||||
@auth
|
@auth
|
||||||
<!-- Like Button -->
|
<button
|
||||||
<button class="text-gray-800 dark:text-gray-200 leading-tight cursor-pointer whitespace-nowrap" wire:click="like">
|
wire:click="like"
|
||||||
|
class="flex items-center gap-1 text-neutral-500 transition hover:text-rose-600 dark:text-neutral-400 dark:hover:text-rose-400"
|
||||||
|
>
|
||||||
@if ($liked)
|
@if ($liked)
|
||||||
<i class="fa-solid fa-heart text-rose-600"></i> {{ $likeCount }}
|
<i class="fa-solid fa-heart text-rose-600"></i>
|
||||||
@else
|
@else
|
||||||
<i class="fa-solid fa-heart"></i> {{ $likeCount }}
|
<i class="fa-regular fa-heart"></i>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
{{ $likeCount }}
|
||||||
</button>
|
</button>
|
||||||
@endauth
|
@endauth
|
||||||
|
|
||||||
|
{{-- Actions --}}
|
||||||
@auth
|
@auth
|
||||||
|
|
||||||
@if ($comment->depth() < 2)
|
@if ($comment->depth() < 2)
|
||||||
<button wire:click="$toggle('isReplying')" type="button" class="text-gray-900 dark:text-gray-100 font-medium">
|
<button
|
||||||
|
wire:click="$toggle('isReplying')"
|
||||||
|
class="font-medium text-neutral-600 transition hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100"
|
||||||
|
>
|
||||||
Reply
|
Reply
|
||||||
</button>
|
</button>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@can ('update', $comment)
|
@can ('update', $comment)
|
||||||
<button wire:click="$toggle('isEditing')" type="button" class="text-gray-900 dark:text-gray-100 font-medium">
|
<button
|
||||||
|
wire:click="$toggle('isEditing')"
|
||||||
|
class="font-medium text-neutral-600 transition hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100"
|
||||||
|
>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
@endcan
|
@endcan
|
||||||
|
|
||||||
@can ('destroy', $comment)
|
@can ('destroy', $comment)
|
||||||
<button x-data="{
|
<button
|
||||||
|
x-data="{
|
||||||
confirmCommentDeletion () {
|
confirmCommentDeletion () {
|
||||||
if (window.confirm('Are you sure you want to delete this comment?')) {
|
if (window.confirm('Delete this comment?')) {
|
||||||
@this.call('deleteComment');
|
@this.call('deleteComment');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
@click="confirmCommentDeletion"
|
@click="confirmCommentDeletion"
|
||||||
type="button"
|
class="font-medium text-red-500 transition hover:text-red-600"
|
||||||
class="text-gray-900 dark:text-gray-100 font-medium"
|
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
@endcan
|
@endcan
|
||||||
|
|
||||||
|
@can ('restore', $comment)
|
||||||
|
<button
|
||||||
|
wire:click="restoreComment"
|
||||||
|
class="font-medium text-emerald-600 transition hover:text-emerald-700"
|
||||||
|
>
|
||||||
|
Restore
|
||||||
|
</button>
|
||||||
|
@endcan
|
||||||
|
|
||||||
@endauth
|
@endauth
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ml-14 mt-6">
|
|
||||||
@if ($isReplying)
|
|
||||||
<form wire:submit.prevent="postReply" class="my-4">
|
|
||||||
<div>
|
|
||||||
<label for="comment" class="sr-only">Reply body</label>
|
|
||||||
<textarea id="comment" name="comment" rows="3"
|
|
||||||
class="bg-white dark:bg-neutral-700 shadow-sm block w-full focus:ring-rose-500 focus:border-rose-500 border-gray-300 dark:border-gray-400/40 text-gray-900 dark:text-gray-200 placeholder:text-gray-400 rounded-md
|
|
||||||
@error('replyState.body') border-red-500 @enderror"
|
|
||||||
placeholder="Write something" wire:model.defer="replyState.body"></textarea>
|
|
||||||
@error('replyState.body')
|
|
||||||
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
|
|
||||||
@enderror
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3 flex items-center justify-between">
|
|
||||||
<button type="submit"
|
{{-- Reply Form --}}
|
||||||
class="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md shadow-sm text-white bg-rose-600 hover:bg-rose-700 focus:outline-none focus:ring-2 focus:ring-rose-500">
|
@if ($isReplying)
|
||||||
Comment
|
|
||||||
|
<div class="mt-4 ml-2">
|
||||||
|
<form wire:submit.prevent="postReply" class="space-y-4">
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
rows="3"
|
||||||
|
wire:model.defer="replyState.body"
|
||||||
|
placeholder="Write a reply..."
|
||||||
|
class="w-full rounded-2xl border border-neutral-300 bg-white px-4 py-3 text-sm text-neutral-900 shadow-sm transition focus:border-rose-500 focus:outline-none focus:ring-4 focus:ring-rose-500/10 dark:border-neutral-700 dark:bg-neutral-950 dark:text-neutral-100 @error('replyState.body') border-red-500 @enderror"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
|
@error('replyState.body')
|
||||||
|
<p class="text-sm text-red-500">
|
||||||
|
{{ $message }}
|
||||||
|
</p>
|
||||||
|
@enderror
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="rounded-xl bg-rose-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-rose-700"
|
||||||
|
>
|
||||||
|
Reply
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
{{-- Replies --}}
|
||||||
|
@if ($comment->children->count())
|
||||||
|
<div class="mt-2 space-y-2 border-l-2 border-neutral-200 pl-6 dark:border-neutral-700">
|
||||||
@foreach ($comment->children as $child)
|
@foreach ($comment->children as $child)
|
||||||
<livewire:comment :comment="$child" :key="$child->id"/>
|
<livewire:comment :comment="$child" :key="$child->id"/>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -1,60 +1,93 @@
|
|||||||
<section>
|
<section>
|
||||||
<div class="bg-white dark:bg-neutral-800 shadow sm:rounded-lg sm:overflow-hidden">
|
<div id="comments" class="overflow-hidden rounded-2xl border border-neutral-200 bg-white shadow-sm dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
<div class="divide-y divide-gray-200 dark:divide-gray-400/40">
|
|
||||||
<div class="px-4 py-5 sm:px-6">
|
{{-- Header --}}
|
||||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-200">Comments</h2>
|
<div class="border-b border-neutral-200 px-6 py-5 dark:border-neutral-800">
|
||||||
|
<h2 class="text-xl font-semibold tracking-tight text-neutral-900 dark:text-neutral-100">
|
||||||
|
Comments
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<!-- Comment Input -->
|
{{-- Comment Form --}}
|
||||||
<div class="bg-gray-50 dark:bg-neutral-800 px-4 py-6 sm:px-6">
|
<div class="border-b border-neutral-200 bg-neutral-50/80 px-6 py-6 dark:border-neutral-800 dark:bg-neutral-950/40">
|
||||||
@auth
|
@auth
|
||||||
<div class="flex">
|
<div class="flex gap-4">
|
||||||
<div class="flex-shrink-0 mr-4">
|
<img
|
||||||
<img class="h-10 w-10 rounded-full" src="{{ auth()->user()->getAvatar() }}" alt="{{ auth()->user()->name }}">
|
class="h-11 w-11 rounded-full object-cover ring-2 ring-white dark:ring-neutral-800"
|
||||||
</div>
|
src="{{ auth()->user()->getAvatar() }}"
|
||||||
<div class="min-w-0 flex-1">
|
alt="{{ auth()->user()->name }}"
|
||||||
<form wire:submit.prevent="postComment">
|
>
|
||||||
|
|
||||||
|
<div class="flex-1">
|
||||||
|
<form wire:submit.prevent="postComment" class="space-y-4">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="comment" class="sr-only">Comment body</label>
|
<label for="comment" class="sr-only">
|
||||||
<textarea id="comment" name="comment" rows="3"
|
Comment body
|
||||||
class="peer block min-h-[auto] w-full border-1 bg-transparent px-3 py-[0.32rem] leading-[1.6] outline-none transition-all duration-200 ease-linear dark:placeholder:text-neutral-200 border-gray-300 dark:border-neutral-950 dark:bg-neutral-900 dark:text-gray-300 focus:border-rose-500 dark:focus:border-rose-600 focus:ring-rose-500 dark:focus:ring-rose-600 rounded-md shadow-sm
|
</label>
|
||||||
@error('newCommentState.body') border-red-500 @enderror"
|
|
||||||
placeholder="Write something" wire:model.defer="newCommentState.body"></textarea>
|
<textarea
|
||||||
|
id="comment"
|
||||||
|
rows="4"
|
||||||
|
wire:model.defer="newCommentState.body"
|
||||||
|
placeholder="Write a comment..."
|
||||||
|
class="w-full rounded-2xl border border-neutral-300 bg-white px-4 py-3 text-sm text-neutral-900 placeholder:text-neutral-400 shadow-sm transition focus:border-rose-500 focus:outline-none focus:ring-4 focus:ring-rose-500/10 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100 dark:placeholder:text-neutral-500 dark:focus:border-rose-500 @error('newCommentState.body') border-red-500 @enderror"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
@error('newCommentState.body')
|
@error('newCommentState.body')
|
||||||
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
|
<p class="mt-2 text-sm text-red-500">
|
||||||
|
{{ $message }}
|
||||||
|
</p>
|
||||||
@enderror
|
@enderror
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3 flex items-center justify-between">
|
|
||||||
<button type="submit"
|
<div class="flex justify-end">
|
||||||
class="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md shadow-sm text-white bg-rose-600 hover:bg-rose-700 focus:outline-none focus:ring-2 focus:ring-rose-500">
|
<button
|
||||||
Comment
|
type="submit"
|
||||||
|
class="inline-flex items-center rounded-xl bg-rose-600 px-5 py-2.5 text-sm font-medium text-white transition hover:bg-rose-700 focus:outline-none focus:ring-4 focus:ring-rose-500/30"
|
||||||
|
>
|
||||||
|
Post Comment
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endauth
|
@endauth
|
||||||
|
|
||||||
@guest
|
@guest
|
||||||
<p class="text-gray-900 dark:text-gray-200">Log in to comment.</p>
|
<div class="rounded-xl border border-dashed border-neutral-300 p-6 text-center dark:border-neutral-700">
|
||||||
|
<p class="text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
|
Log in to join the discussion.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
@endguest
|
@endguest
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Comments -->
|
{{-- Comments --}}
|
||||||
<div class="px-4 py-6 sm:px-6">
|
<div class="px-6 py-6">
|
||||||
<div class="space-y-8">
|
|
||||||
@if ($comments->isNotEmpty())
|
@if ($comments->isNotEmpty())
|
||||||
|
|
||||||
|
<div class="space-y-6">
|
||||||
@foreach($comments as $comment)
|
@foreach($comments as $comment)
|
||||||
<livewire:comment :comment="$comment" :key="$comment->id"/>
|
<livewire:comment :comment="$comment" :key="$comment->id"/>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8">
|
||||||
{{ $comments->links('pagination::tailwind') }}
|
{{ $comments->links('pagination::tailwind') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
@else
|
@else
|
||||||
<p class="text-gray-900 dark:text-gray-200">No comments yet.</p>
|
|
||||||
|
<div class="rounded-2xl border border-dashed border-neutral-300 py-12 text-center dark:border-neutral-700">
|
||||||
|
<p class="text-neutral-500 dark:text-neutral-400">
|
||||||
|
No comments yet.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
@@ -8,10 +8,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col text-center w-full">
|
<div class="flex flex-col text-center w-full">
|
||||||
@if($fillNumbers)
|
@if($fillNumbers)
|
||||||
|
@if($version)
|
||||||
|
<p class="text-lg">Episode {{ str_pad($episodeNumber, 2, '0', STR_PAD_LEFT) }} ({{ $version }})</p>
|
||||||
|
@else
|
||||||
<p class="text-lg">Episode {{ str_pad($episodeNumber, 2, '0', STR_PAD_LEFT) }}</p>
|
<p class="text-lg">Episode {{ str_pad($episodeNumber, 2, '0', STR_PAD_LEFT) }}</p>
|
||||||
|
@endif
|
||||||
|
@else
|
||||||
|
@if($version)
|
||||||
|
<p class="text-lg">Episode {{ $episodeNumber }} ({{ $version }})</p>
|
||||||
@else
|
@else
|
||||||
<p class="text-lg">Episode {{ $episodeNumber }}</p>
|
<p class="text-lg">Episode {{ $episodeNumber }}</p>
|
||||||
@endif
|
@endif
|
||||||
|
@endif
|
||||||
<p class="text-xs">{{ $fileExtension }} MKV {{ $fileSize ?? '' }}</p>
|
<p class="text-xs">{{ $fileExtension }} MKV {{ $fileSize ?? '' }}</p>
|
||||||
<p class="text-xs" id="count-{{ $downloadId }}">Downloaded {{ $downloadCount }} times</p>
|
<p class="text-xs" id="count-{{ $downloadId }}">Downloaded {{ $downloadCount }} times</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
<div>
|
<div>
|
||||||
@if (Auth::check())
|
<button class="inline-flex font-bold items-center gap-2 rounded-xl bg-gray-100 px-4 py-2 text-gray-700 dark:bg-white/5 dark:text-gray-200" wire:click="like" wire:poll.90000ms="update">
|
||||||
<div class="text-xl text-gray-800 dark:text-gray-200 leading-tight cursor-pointer whitespace-nowrap" wire:click="like" wire:poll.90000ms="update">
|
|
||||||
@else
|
|
||||||
<div data-te-toggle="tooltip" title="Please login to like the episode" class="text-xl text-gray-800 dark:text-gray-200 leading-tight cursor-pointer whitespace-nowrap" wire:poll.60000ms="update">
|
|
||||||
@endif
|
|
||||||
@if ($liked)
|
@if ($liked)
|
||||||
<i class="fa-solid fa-heart pr-[4px] text-rose-600"></i> {{ $likeCount }}
|
<i class="fa-solid fa-heart pr-[4px] text-rose-600"></i> {{ $likeCount }}
|
||||||
@else
|
@else
|
||||||
<i class="fa-regular fa-heart pr-[4px]"></i> {{ $likeCount }}
|
<i class="fa-regular fa-heart pr-[4px]"></i> {{ $likeCount }}
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div class="py-24">
|
<div class="py-10">
|
||||||
<div class="mx-auto sm:px-6 lg:px-8 space-y-6 max-w-[100%] xl:max-w-[95%] 2xl:max-w-[90%]">
|
<div class="mx-auto sm:px-6 lg:px-8 space-y-6 max-w-[100%] xl:max-w-[95%] 2xl:max-w-[90%]">
|
||||||
@include('livewire.partials.search-filter')
|
@include('livewire.partials.search-filter')
|
||||||
</div>
|
</div>
|
||||||
@@ -24,5 +24,4 @@
|
|||||||
</div>
|
</div>
|
||||||
{{ $episodes->appends(['tags' => $selectedtags])->links('pagination::tailwind') }}
|
{{ $episodes->appends(['tags' => $selectedtags])->links('pagination::tailwind') }}
|
||||||
</div>
|
</div>
|
||||||
@vite(['resources/js/preview.js'])
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,90 +1,141 @@
|
|||||||
<!-- Search Filter -->
|
<!-- Search Filters -->
|
||||||
<div>
|
<div>
|
||||||
<div class="p-4 sm:p-8 bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow sm:rounded-lg">
|
<div class="rounded-2xl border border-neutral-200/70 bg-white/80 p-4 shadow-sm backdrop-blur-xl dark:border-neutral-800 dark:bg-neutral-950/70 space-y-4">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
|
||||||
|
|
||||||
<!-- Title -->
|
<!-- Filters Grid -->
|
||||||
<div>
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-5">
|
||||||
<label for="live-search" class="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white">Search</label>
|
|
||||||
<div class="relative right-2 left-0 sm:left-2 transition-all">
|
<!-- Search -->
|
||||||
<div class="absolute inset-y-0 left-2 flex items-center pl-3 pointer-events-none">
|
<div class="xl:col-span-2">
|
||||||
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
<label for="live-search" class="sr-only">
|
||||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" />
|
{{ __('search.search-hentai') }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="relative">
|
||||||
|
<!-- Search Icon -->
|
||||||
|
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-4">
|
||||||
|
<svg class="h-5 w-5 text-neutral-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input wire:model.live.debounce.600ms="search" type="search" id="live-search" class="block w-full p-4 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-rose-800 focus:border-rose-900 dark:bg-neutral-900 dark:border-neutral-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-rose-800 dark:focus:border-rose-900" placeholder="{{ __('search.search-hentai') }}" required>
|
<!-- Input -->
|
||||||
|
<input
|
||||||
|
wire:model.live.debounce.500ms="search"
|
||||||
|
type="search"
|
||||||
|
id="live-search"
|
||||||
|
placeholder="{{ __('search.search-hentai') }}"
|
||||||
|
class="w-full rounded-xl border border-neutral-300 bg-white py-5 pl-12 pr-12 text-sm text-neutral-900 shadow-sm transition focus:border-rose-500 focus:outline-none focus:ring-4 focus:ring-rose-500/20 dark:border-neutral-700 dark:bg-neutral-900 dark:text-white dark:placeholder-neutral-500"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="absolute right-0 top-[11px]" wire:loading>
|
<!-- Loading -->
|
||||||
<svg aria-hidden="true" class="inline w-8 h-8 mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-pink-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<div wire:loading.class="opacity-100" class="opacity-0">
|
||||||
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor" />
|
<div class="absolute inset-y-0 right-3 flex items-center">
|
||||||
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill" />
|
<svg class="h-5 w-5 animate-spin text-rose-500" viewBox="0 0 24 24" fill="none">
|
||||||
|
<circle class="opacity-20" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-90" fill="currentColor"
|
||||||
|
d="M22 12a10 10 0 0 1-10 10V18a6 6 0 0 0 6-6h4Z">
|
||||||
|
</path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Genres -->
|
<!-- Genres -->
|
||||||
<div>
|
<button
|
||||||
<div class="relative right-2 left-0 sm:left-2 transition-all">
|
type="button"
|
||||||
<div class="absolute inset-y-0 left-2 flex items-center pl-3 pointer-events-none">
|
data-te-toggle="modal"
|
||||||
<i class="fa-solid fa-sliders text-gray-500 dark:text-gray-400"></i>
|
data-te-target="#modalGenres"
|
||||||
</div>
|
class="group flex items-center gap-3 rounded-xl border border-neutral-300 bg-white px-4 py-3 text-left shadow-sm transition hover:border-rose-400 hover:bg-rose-50 dark:border-neutral-700 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
||||||
<p data-te-toggle="modal" data-te-target="#modalGenres" data-te-ripple-init data-te-ripple-color="light" id="genres-filter" class="block cursor-pointer w-full p-4 pl-10 text-sm text-gray-500 dark:text-gray-400 border border-gray-300 rounded-lg bg-gray-50 focus:ring-rose-800 focus:border-rose-900 dark:bg-neutral-900 dark:border-neutral-600 dark:placeholder-gray-400 dark:focus:ring-rose-800 dark:focus:border-rose-900">
|
>
|
||||||
|
<i class="fa-solid fa-sliders text-neutral-400 group-hover:text-rose-500"></i>
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-xs font-medium uppercase tracking-wide text-neutral-400">
|
||||||
|
Genres
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="text-sm text-neutral-700 dark:text-neutral-200">
|
||||||
@if($tagcount === 0)
|
@if($tagcount === 0)
|
||||||
Select Genres
|
Select Genres
|
||||||
@elseif($tagcount === 1)
|
@elseif($tagcount === 1)
|
||||||
Selected {{$tagcount }} Genre
|
1 Genre Selected
|
||||||
@elseif($tagcount > 1)
|
@else
|
||||||
Selected {{$tagcount }} Genres
|
{{ $tagcount }} Genres Selected
|
||||||
@endif
|
@endif
|
||||||
</p>
|
</span>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- Genres Blacklist -->
|
<!-- Blacklist -->
|
||||||
<div>
|
<button
|
||||||
<div class="relative right-2 left-0 sm:left-2 transition-all">
|
type="button"
|
||||||
<div class="absolute inset-y-0 left-2 flex items-center pl-3 pointer-events-none">
|
data-te-toggle="modal"
|
||||||
<i class="fa-solid fa-shield text-gray-500 dark:text-gray-400"></i>
|
data-te-target="#modalBlacklist"
|
||||||
</div>
|
class="group flex items-center gap-3 rounded-xl border border-neutral-300 bg-white px-4 py-3 text-left shadow-sm transition hover:border-rose-400 hover:bg-rose-50 dark:border-neutral-700 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
||||||
<p data-te-toggle="modal" data-te-target="#modalBlacklist" data-te-ripple-init data-te-ripple-color="light" id="blacklist-filter" class="block cursor-pointer w-full p-4 pl-10 text-sm text-gray-500 dark:text-gray-400 border border-gray-300 rounded-lg bg-gray-50 focus:ring-rose-800 focus:border-rose-900 dark:bg-neutral-900 dark:border-neutral-600 dark:placeholder-gray-400 dark:focus:ring-rose-800 dark:focus:border-rose-900">
|
>
|
||||||
|
<i class="fa-solid fa-shield text-neutral-400 group-hover:text-rose-500"></i>
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-xs font-medium uppercase tracking-wide text-neutral-400">
|
||||||
|
Blacklist
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="text-sm text-neutral-700 dark:text-neutral-200">
|
||||||
@if($blacklistcount === 0)
|
@if($blacklistcount === 0)
|
||||||
Select Blacklist
|
Select Blacklist
|
||||||
@elseif($blacklistcount === 1)
|
@elseif($blacklistcount === 1)
|
||||||
Selected {{ $blacklistcount }} Blacklist Item
|
1 Item Selected
|
||||||
@elseif($blacklistcount > 1)
|
@else
|
||||||
Selected {{ $blacklistcount }} Blacklist Items
|
{{ $blacklistcount }} Items Selected
|
||||||
@endif
|
@endif
|
||||||
</p>
|
</span>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- Studios -->
|
<!-- Studios -->
|
||||||
<div>
|
<button
|
||||||
<div class="relative right-2 left-0 sm:left-2 transition-all">
|
type="button"
|
||||||
<div class="absolute inset-y-0 left-2 flex items-center pl-3 pointer-events-none">
|
data-te-toggle="modal"
|
||||||
<i class="fa-solid fa-microphone-lines text-gray-500 dark:text-gray-400"></i>
|
data-te-target="#modalStudios"
|
||||||
</div>
|
class="group flex items-center gap-3 rounded-xl border border-neutral-300 bg-white px-4 py-3 text-left shadow-sm transition hover:border-rose-400 hover:bg-rose-50 dark:border-neutral-700 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
||||||
<p data-te-toggle="modal" data-te-target="#modalStudios" data-te-ripple-init data-te-ripple-color="light" id="studios-filter" class="block cursor-pointer w-full p-4 pl-10 text-sm text-gray-500 dark:text-gray-400 border border-gray-300 rounded-lg bg-gray-50 focus:ring-rose-800 focus:border-rose-900 dark:bg-neutral-900 dark:border-neutral-600 dark:placeholder-gray-400 dark:focus:ring-rose-800 dark:focus:border-rose-900">
|
>
|
||||||
|
<i class="fa-solid fa-microphone-lines text-neutral-400 group-hover:text-rose-500"></i>
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-xs font-medium uppercase tracking-wide text-neutral-400">
|
||||||
|
Studios
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="text-sm text-neutral-700 dark:text-neutral-200">
|
||||||
@if($studiocount === 0)
|
@if($studiocount === 0)
|
||||||
Select Studios
|
Select Studios
|
||||||
@elseif($studiocount === 1)
|
@elseif($studiocount === 1)
|
||||||
Selected {{ $studiocount }} Studio
|
1 Studio Selected
|
||||||
@elseif($studiocount > 1)
|
@else
|
||||||
Selected {{ $studiocount }} Studios
|
{{ $studiocount }} Studios Selected
|
||||||
@endif
|
@endif
|
||||||
</p>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Ordering -->
|
<!-- Bottom Controls -->
|
||||||
<div class="grid grid-cols-2">
|
<div class="mt-4 flex flex-col gap-4 border-t border-neutral-200 pt-4 dark:border-neutral-800 lg:flex-row lg:items-center lg:justify-between">
|
||||||
<div class="relative right-2 left-0 sm:left-2 transition-all">
|
|
||||||
<div class="absolute inset-y-0 left-2 flex items-center pl-3 pointer-events-none">
|
<!-- Selects -->
|
||||||
<i class="fa-solid fa-sort text-gray-500 dark:text-gray-400"></i>
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
</div>
|
|
||||||
<select wire:model.live="order" class="block w-full p-4 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-rose-800 focus:border-rose-900 dark:bg-neutral-900 dark:border-neutral-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-rose-800 dark:focus:border-rose-900">
|
<!-- Order -->
|
||||||
|
<div class="relative">
|
||||||
|
<i class="fa-solid fa-sort pointer-events-none absolute left-4 top-1/2 -translate-y-1/2 text-neutral-400"></i>
|
||||||
|
|
||||||
|
<select
|
||||||
|
wire:model.live="order"
|
||||||
|
class="w-full appearance-none rounded-xl border border-neutral-300 bg-white py-3 pl-11 pr-10 text-sm text-neutral-900 shadow-sm transition focus:border-rose-500 focus:outline-none focus:ring-4 focus:ring-rose-500/20 dark:border-neutral-700 dark:bg-neutral-900 dark:text-white"
|
||||||
|
>
|
||||||
<option value="az">A-Z</option>
|
<option value="az">A-Z</option>
|
||||||
<option value="za">Z-A</option>
|
<option value="za">Z-A</option>
|
||||||
<option value="recently-uploaded">{{ __('home.recently-uploaded') }}</option>
|
<option value="recently-uploaded">{{ __('home.recently-uploaded') }}</option>
|
||||||
@@ -95,28 +146,40 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative right-2 left-0 ml-2 sm:left-2 transition-all">
|
<!-- View -->
|
||||||
<div class="absolute inset-y-0 left-2 flex items-center pl-3 pointer-events-none">
|
<div class="relative">
|
||||||
<i class="fa-solid fa-list text-gray-500 dark:text-gray-400"></i>
|
<i class="fa-solid fa-list pointer-events-none absolute left-4 top-1/2 -translate-y-1/2 text-neutral-400"></i>
|
||||||
</div>
|
|
||||||
<select wire:model.live="view" class="block w-full p-4 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-rose-800 focus:border-rose-900 dark:bg-neutral-900 dark:border-neutral-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-rose-800 dark:focus:border-rose-900">
|
<select
|
||||||
|
wire:model.live="view"
|
||||||
|
class="w-full appearance-none rounded-xl border border-neutral-300 bg-white py-3 pl-11 pr-10 text-sm text-neutral-900 shadow-sm transition focus:border-rose-500 focus:outline-none focus:ring-4 focus:ring-rose-500/20 dark:border-neutral-700 dark:bg-neutral-900 dark:text-white"
|
||||||
|
>
|
||||||
<option value="thumbnail">Thumbnail</option>
|
<option value="thumbnail">Thumbnail</option>
|
||||||
<option value="poster">Poster</option>
|
<option value="poster">Poster</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<!-- Auth Options -->
|
||||||
@auth
|
@auth
|
||||||
<div class="float-right pt-1">
|
<label
|
||||||
<input class="w-4 h-4 text-rose-600 bg-gray-100 border-gray-300 rounded focus:ring-rose-500 dark:focus:ring-rose-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
for="checkBoxHideWatched"
|
||||||
type="checkbox" wire:model.live="hideWatched" value="true" id="checkBoxHideWatched" />
|
class="flex cursor-pointer items-center gap-3 text-sm text-neutral-700 dark:text-neutral-300"
|
||||||
<label class="inline-block hover:cursor-pointer dark:text-white" for="checkBoxHideWatched">
|
>
|
||||||
Hide watched
|
<input
|
||||||
|
id="checkBoxHideWatched"
|
||||||
|
type="checkbox"
|
||||||
|
wire:model.live="hideWatched"
|
||||||
|
class="h-4 w-4 rounded border-neutral-300 text-rose-600 focus:ring-rose-500 dark:border-neutral-700 dark:bg-neutral-800"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span>Hide watched</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
|
||||||
@endauth
|
@endauth
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modals -->
|
||||||
@include('modals.filter-genres')
|
@include('modals.filter-genres')
|
||||||
@include('modals.filter-studios')
|
@include('modals.filter-studios')
|
||||||
@include('modals.filter-blacklist')
|
@include('modals.filter-blacklist')
|
||||||
|
|||||||
@@ -1,86 +1,3 @@
|
|||||||
<div wire:key="episode-{{ $episode->id }}">
|
<div wire:key="episode-{{ $episode->id }}">
|
||||||
@if ($searchIsJpn)
|
<x-episode-cover :episode="$episode" :view="$view" :displayjapanese="$searchIsJpn" />
|
||||||
<div class="relative p-1 mb-14 w-full transition duration-300 ease-in-out md:p-2 md:hover:-translate-y-1 md:hover:scale-110"
|
|
||||||
data-thumbs="{{ optional($episode->gallery)->pluck('thumbnail_url') }}">
|
|
||||||
@else
|
|
||||||
<div class="relative p-1 mb-8 w-full transition duration-300 ease-in-out md:p-2 md:hover:-translate-y-1 md:hover:scale-110"
|
|
||||||
data-thumbs="{{ optional($episode->gallery)->pluck('thumbnail_url') }}">
|
|
||||||
@endif
|
|
||||||
<a class="hover:text-blue-600" href="{{ route('hentai.index', ['title' => $episode->slug]) }}">
|
|
||||||
<div class="absolute w-[95%] top-[38%] text-center z-10">
|
|
||||||
<svg aria-hidden="true"
|
|
||||||
class="inline mr-2 w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-pink-600"
|
|
||||||
viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path
|
|
||||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
|
||||||
fill="currentColor" />
|
|
||||||
<path
|
|
||||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
|
||||||
fill="currentFill" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@switch(true)
|
|
||||||
@case($view === 'thumbnail')
|
|
||||||
<img alt="{{ $episode->title }} - {{ $episode->episode }}" loading="lazy" width="500"
|
|
||||||
class="block object-cover object-center relative z-20 rounded-lg aspect-video"
|
|
||||||
src="{{ optional($episode->gallery->first())->thumbnail_url }}">
|
|
||||||
@if ($episode->hasAutoTrans())
|
|
||||||
<p
|
|
||||||
class="absolute right-1 md:right-2 bottom-1 md:bottom-2 bg-blue-600/80 !text-white rounded-tl-lg rounded-br-lg p-1 pr-2 pl-2 font-semibold text-sm z-30">
|
|
||||||
<i class="fa-regular fa-closed-captioning"></i> Multi-Subs
|
|
||||||
</p>
|
|
||||||
@endif
|
|
||||||
@break
|
|
||||||
|
|
||||||
@case($view === 'poster')
|
|
||||||
<img alt="{{ $episode->title }} - {{ $episode->episode }}" loading="lazy" width="400"
|
|
||||||
class="block relative rounded-lg object-cover object-center aspect-[11/16] z-20"
|
|
||||||
src="{{ $episode->cover_url }}">
|
|
||||||
@break
|
|
||||||
|
|
||||||
@endswitch
|
|
||||||
|
|
||||||
@php $problematic = cache()->rememberForever('episodeProblematic'.$episode->id, fn () => $episode->getProblematicTags()); @endphp
|
|
||||||
@if (!empty($problematic))
|
|
||||||
<p
|
|
||||||
class="absolute left-1 md:left-2 top-1 md:top-2 bg-red-700/70 !text-white rounded-br-lg rounded-tl-lg p-1 pr-2 pl-2 font-semibold text-sm z-30">
|
|
||||||
<i class="fa-solid fa-triangle-exclamation"></i> {{ $problematic }}
|
|
||||||
</p>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
@if (auth()->check() && $episode->userWatched(auth()->user()->id))
|
|
||||||
<p
|
|
||||||
class="absolute right-1 md:right-2 top-1 md:top-2 bg-green-600/80 !text-white rounded-bl-lg rounded-tr-lg p-1 pr-2 pl-2 font-semibold text-sm z-30">
|
|
||||||
{{ $episode->getResolution() }}</p>
|
|
||||||
<p
|
|
||||||
class="absolute left-1 md:left-2 bottom-1 md:bottom-2 bg-green-600/80 !text-white rounded-bl-lg rounded-tr-lg p-1 pr-2 pl-2 font-semibold text-sm z-30">
|
|
||||||
<i class="fa-regular fa-eye"></i> {{ $episode->viewCountFormatted() }} <i
|
|
||||||
class="fa-regular fa-heart"></i>
|
|
||||||
{{ $episode->likeCount() }} <i class="fa-regular fa-comment"></i> {{ $episode->commentCount() }}
|
|
||||||
</p>
|
|
||||||
@else
|
|
||||||
<p
|
|
||||||
class="absolute right-1 md:right-2 top-1 md:top-2 bg-rose-700/70 !text-white rounded-bl-lg rounded-tr-lg p-1 pr-2 pl-2 font-semibold text-sm z-30">
|
|
||||||
{{ $episode->getResolution() }}</p>
|
|
||||||
<p
|
|
||||||
class="absolute left-1 md:left-2 bottom-1 md:bottom-2 bg-rose-700/70 !text-white rounded-bl-lg rounded-tr-lg p-1 pr-2 pl-2 font-semibold text-sm z-30">
|
|
||||||
<i class="fa-regular fa-eye"></i>
|
|
||||||
{{ $episode->viewCountFormatted() }}
|
|
||||||
<i class="fa-regular fa-heart"></i> {{ $episode->likeCount() }} <i class="fa-regular fa-comment"></i>
|
|
||||||
{{ $episode->commentCount() }}
|
|
||||||
</p>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
<div class="absolute w-[95%] grid grid-cols-1 text-center">
|
|
||||||
@if ($searchIsJpn)
|
|
||||||
<p class="text-sm text-center text-black dark:text-white">{{ $episode->title }}
|
|
||||||
({{ $episode->title_jpn }}) - {{ $episode->episode }}</p>
|
|
||||||
@else
|
|
||||||
<p class="text-sm text-center text-black dark:text-white">{{ $episode->title }} -
|
|
||||||
{{ $episode->episode }}</p>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,5 +24,4 @@
|
|||||||
</div>
|
</div>
|
||||||
{{ $episodes->appends(['tags' => $selectedtags])->links('pagination::tailwind') }}
|
{{ $episodes->appends(['tags' => $selectedtags])->links('pagination::tailwind') }}
|
||||||
</div>
|
</div>
|
||||||
@vite(['resources/js/preview.js'])
|
|
||||||
</div
|
</div
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3">
|
||||||
|
<!-- Subscription Card -->
|
||||||
|
<section class="lg:col-span-3 rounded-2xl border border-white/10 shadow-black/20 overflow-hidden p-4 sm:p-8 bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow sm:rounded-lg">
|
||||||
|
<div class="p-6 border-b border-white/10">
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Subscription Status</h3>
|
||||||
|
<p class="p-2 text-sm dark:text-gray-200 text-gray-800">
|
||||||
|
Your current membership status for unlimited 4k Downloads.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="inline-flex items-center gap-2 rounded-full px-3 py-1 text-sm font-medium border
|
||||||
|
{{ $isActive ? 'bg-green-500/10 text-green-300 border-green-500/20' : 'bg-red-500/10 text-red-300 border-red-500/20' }}">
|
||||||
|
<span class="h-2 w-2 rounded-full {{ $isActive ? 'bg-green-400' : 'bg-red-400' }}"></span>
|
||||||
|
{{ $isActive ? 'Active' : 'Inactive' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Subscription Access Key -->
|
||||||
|
<div class="lg:col-span-3 rounded-2xl border border-blue-400/20 bg-blue-500/[0.06] shadow-2xl shadow-blue-950/20 p-6 mt-4">
|
||||||
|
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-6">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Subscription Access Key</h3>
|
||||||
|
<p class="p-2 text-sm dark:text-gray-200 text-gray-800">
|
||||||
|
Paste your subscription key to apply the membership status.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full lg:w-auto">
|
||||||
|
<div class="flex flex-col sm:flex-row gap-3">
|
||||||
|
<input
|
||||||
|
id="subscriptionKey"
|
||||||
|
type="text"
|
||||||
|
value="{{ $subscriptionKey }}"
|
||||||
|
wire:model="subscriptionKey"
|
||||||
|
class="w-full sm:w-[420px] rounded-xl border border-white/10 dark:bg-gray-950/80 px-4 py-3 font-mono text-sm text-blue-400 dark:text-blue-200 outline-none focus:border-blue-400/50"
|
||||||
|
>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
wire:click="applyKey"
|
||||||
|
class="rounded-xl bg-rose-500 px-5 py-3 text-sm font-semibold text-white hover:bg-rose-400 transition shadow-lg shadow-rose-500/20"
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@error('subscriptionKey') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user