Compare commits

...

43 Commits

Author SHA1 Message Date
5310908b0c Update login button on mobile 2026-01-11 00:21:05 +01:00
4b05b3db6d Fix cache not flushed after comment delete by admin 2026-01-10 23:24:31 +01:00
df47a926e4 Fix comment mass delete 2026-01-10 23:21:29 +01:00
1e9e95f35f Fix admin comment moderation 2026-01-10 22:35:35 +01:00
2aa76baafd Fix account deletion anonymizing comments 2026-01-10 22:35:21 +01:00
aa50bb1f72 Merge pull request 'Replace Comment System' (#4) from comment-system into main
Reviewed-on: #4
2026-01-10 21:16:55 +00:00
dfedf4058e Fix rate limit and make it more strict (1 message in 5 minutes) 2026-01-10 22:15:50 +01:00
268e3eb4c2 Add Notification for comments 2026-01-10 22:00:09 +01:00
ab61574956 Fix comment depth chain check 2026-01-10 21:59:53 +01:00
81038b6c26 Add id to comment, so it can autoscroll to that notification 2026-01-10 21:59:08 +01:00
e949ba955a Add rate limiter to comment system 2026-01-10 21:04:26 +01:00
819e2fde27 Misc changes 2026-01-10 20:33:35 +01:00
3259e2197b Update design comments home page 2026-01-10 19:45:19 +01:00
b133db0573 Add likes to comments 2026-01-10 19:41:23 +01:00
41c34e6d89 Fix style 2026-01-10 19:15:32 +01:00
db6da608aa Add comments to home page 2026-01-10 19:11:28 +01:00
13b70fdf23 Misc changes 2026-01-10 18:55:53 +01:00
cfd6af59fb Add Profile Comment Search (Livewire) 2026-01-10 18:55:47 +01:00
7810cd53fb Add comments to Hentai 2026-01-10 18:54:48 +01:00
871028930b Migrate existing comments 2026-01-10 16:41:06 +01:00
6ce0255764 Remove ring offset 2026-01-10 15:45:52 +01:00
e136e8e1b6 Refresh on delete 2026-01-10 15:45:41 +01:00
a3b66b483b Add admin and donator badge 2026-01-10 15:34:05 +01:00
4c2a6024d7 Add dark mode 2026-01-10 15:27:37 +01:00
5f575024e2 Add Livewire comment system 2026-01-10 15:02:14 +01:00
67f5d0db8b Remove laravelista/comments 2026-01-10 14:06:00 +01:00
571bf4584c Remove view_count from meilisearch 2026-01-10 12:27:54 +01:00
d7dc96e11c Don't trigger update on view_count increase 2026-01-10 12:27:24 +01:00
58426b6e4e Add studio filter on download page closes #1 2026-01-09 22:51:15 +01:00
53b600daea Fix certain livewire components not working 2026-01-09 22:32:16 +01:00
224cdbcdc5 Save mute state of player - fixes #2 2026-01-09 22:28:53 +01:00
972d3d0aa4 Add zhentube.com to footer 2026-01-09 22:20:02 +01:00
8f7f012c14 Merge pull request 'Replace Auth System' (#3) from auth-redo into main
Reviewed-on: #3
2026-01-09 15:11:36 +00:00
c0b068de58 Misc changes 2026-01-09 13:01:53 +01:00
51c67bb797 Improve Migrations & Fix Discord Avatars 2026-01-09 10:45:41 +01:00
3d78f9e524 Optionally update discord avatar on login 2026-01-08 22:17:00 +01:00
2d28a37463 Don't set password on new account with oauth 2026-01-08 20:03:24 +01:00
ac853920ee Fix delete account function & delete modal 2026-01-08 19:28:44 +01:00
fb3722036a Add ability to set custom avatar 2026-01-08 18:47:31 +01:00
ab4e7c7999 Update Mail Design (Password Reset) 2026-01-08 17:21:03 +01:00
8f99718058 Allow changing email, username and password 2026-01-08 16:14:35 +01:00
2029af334c Fix Signup redirect 2026-01-08 16:13:54 +01:00
b1c48830c4 Remove nickname 2026-01-08 16:13:43 +01:00
85 changed files with 1941 additions and 1051 deletions

View File

@@ -30,9 +30,9 @@ class AutoStats extends Command
*/ */
public function handle() public function handle()
{ {
PopularDaily::where('created_at', '<=', Carbon::now()->subMinutes(1440))->forceDelete(); PopularDaily::where('created_at', '<=', Carbon::now()->subMinutes(1440))->delete();
PopularWeekly::where('created_at', '<=', Carbon::now()->subMinutes(10080))->forceDelete(); PopularWeekly::where('created_at', '<=', Carbon::now()->subMinutes(10080))->delete();
PopularMonthly::where('created_at', '<=', Carbon::now()->subMinutes(43200))->forceDelete(); PopularMonthly::where('created_at', '<=', Carbon::now()->subMinutes(43200))->delete();
$this->comment('Automated Purge Stats Complete'); $this->comment('Automated Purge Stats Complete');
} }

View File

@@ -35,6 +35,6 @@ class ResetUserDownloads extends Command
// Clear old downloads which have expired // Clear old downloads which have expired
UserDownload::where('created_at', '<=', Carbon::now()->subHour(6)) UserDownload::where('created_at', '<=', Carbon::now()->subHour(6))
->forceDelete(); ->delete();
} }
} }

View File

@@ -2,6 +2,7 @@
namespace App\Helpers; namespace App\Helpers;
use App\Models\Comment;
use App\Models\Episode; use App\Models\Episode;
use App\Models\Hentai; use App\Models\Hentai;
use App\Models\PopularMonthly; use App\Models\PopularMonthly;
@@ -126,7 +127,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 DB::table('comments')->latest()->take(10)->get(); return Comment::latest()->take(10)->get();
}); });
} }
} }

View File

@@ -41,7 +41,7 @@ class AlertController extends Controller
*/ */
public function delete(int $alert_id): \Illuminate\Http\RedirectResponse public function delete(int $alert_id): \Illuminate\Http\RedirectResponse
{ {
Alert::where('id', $alert_id)->forceDelete(); Alert::where('id', $alert_id)->delete();
cache()->forget('alerts'); cache()->forget('alerts');

View File

@@ -105,7 +105,7 @@ class SiteBackgroundController extends Controller
DB::beginTransaction(); DB::beginTransaction();
$bg = SiteBackground::where('id', $id)->firstOrFail(); $bg = SiteBackground::where('id', $id)->firstOrFail();
$bg->forceDelete(); $bg->delete();
$resolutions = [1440, 1080, 720, 640]; $resolutions = [1440, 1080, 720, 640];
try { try {

View File

@@ -38,7 +38,7 @@ class SubtitleController extends Controller
// Clear everything // Clear everything
foreach($episode->subtitles as $sub) { foreach($episode->subtitles as $sub) {
$sub->forceDelete(); $sub->delete();
} }
if (! $request->input('subtitles')) { if (! $request->input('subtitles')) {

View File

@@ -31,14 +31,13 @@ class DiscordAuthController extends Controller
$user = User::where('discord_id', $discordUser->id)->first(); $user = User::where('discord_id', $discordUser->id)->first();
if (! $user) { if (!$user) {
// link by email if it already exists // link by email if it already exists
$user = User::where('email', $discordUser->email)->first(); $user = User::where('email', $discordUser->email)->first();
if ($user) { if ($user) {
$user->update([ $user->update([
'discord_id' => $discordUser->id, 'discord_id' => $discordUser->id,
'discord_name' => $discordUser->nickname ?? $discordUser->name,
'discord_avatar' => $discordUser->avatar, 'discord_avatar' => $discordUser->avatar,
]); ]);
} else { } else {
@@ -47,13 +46,13 @@ class DiscordAuthController extends Controller
'name' => $discordUser->name, 'name' => $discordUser->name,
'email' => $discordUser->email, 'email' => $discordUser->email,
'discord_id' => $discordUser->id, 'discord_id' => $discordUser->id,
'discord_name' => $discordUser->nickname ?? $discordUser->name,
'discord_avatar' => $discordUser->avatar, 'discord_avatar' => $discordUser->avatar,
'password' => bcrypt(Str::random(40)), 'password' => null,
]); ]);
} }
} }
$this->checkDiscordAvatar($discordUser, $user);
$this->checkDiscordRoles($user); $this->checkDiscordRoles($user);
Auth::login($user, true); Auth::login($user, true);
@@ -61,6 +60,16 @@ class DiscordAuthController extends Controller
return redirect()->route('home.index'); return redirect()->route('home.index');
} }
/**
* Check if discord avatar changed
*/
private function checkDiscordAvatar(\Laravel\Socialite\Contracts\User $socialiteUser, User $user): void
{
if ($socialiteUser->avatar != $user->discord_avatar) {
$user->update(['discord_avatar' => $socialiteUser->avatar]);
}
}
/** /**
* Check Discord Roles if user is Patreon member * Check Discord Roles if user is Patreon member
*/ */

View File

@@ -15,6 +15,20 @@ class PasswordController extends Controller
*/ */
public function update(Request $request): RedirectResponse public function update(Request $request): RedirectResponse
{ {
// If user logged in with Discord and has not yet a password, allow to set password
if ($request->user()->discord_id && is_null($request->user()->password))
{
$validated = $request->validateWithBag('updatePassword', [
'password' => ['required', Password::defaults(), 'confirmed'],
]);
$request->user()->update([
'password' => Hash::make($validated['password']),
]);
return back()->with('status', 'password-updated');
}
$validated = $request->validateWithBag('updatePassword', [ $validated = $request->validateWithBag('updatePassword', [
'current_password' => ['required', 'current_password'], 'current_password' => ['required', 'current_password'],
'password' => ['required', Password::defaults(), 'confirmed'], 'password' => ['required', Password::defaults(), 'confirmed'],

View File

@@ -36,6 +36,6 @@ class RegisteredUserController extends Controller
Auth::login($user); Auth::login($user);
return redirect(route('dashboard', absolute: false)); return redirect(route('home.index', absolute: false));
} }
} }

View File

@@ -32,7 +32,7 @@ class NotificationController extends Controller
->where('id', $request->input('id')) ->where('id', $request->input('id'))
->firstOrFail(); ->firstOrFail();
$notification->forceDelete(); $notification->delete();
return redirect()->back(); return redirect()->back();
} }

View File

@@ -105,13 +105,11 @@ class PlaylistController extends Controller
$user = $request->user(); $user = $request->user();
$playlist = Playlist::where('user_id', $user->id)->where('id', $playlist_id)->firstOrFail(); $playlist = Playlist::where('user_id', $user->id)
->where('id', $playlist_id)
->firstOrFail();
// Delete Playlist Episodes $playlist->delete();
PlaylistEpisode::where('playlist_id', $playlist->id)->forceDelete();
// Delete Playlist
$playlist->forceDelete();
return to_route('profile.playlists'); return to_route('profile.playlists');
} }
@@ -128,8 +126,14 @@ class PlaylistController extends Controller
], 404); ], 404);
} }
$playlist = Playlist::where('user_id', $request->user()->id)->where('id', (int) $request->input('playlist'))->firstOrFail(); $playlist = Playlist::where('user_id', $request->user()->id)
PlaylistEpisode::where('playlist_id', $playlist->id)->where('episode_id', (int) $request->input('episode'))->forceDelete(); ->where('id', (int) $request->input('playlist'))
->firstOrFail();
PlaylistEpisode::where('playlist_id', $playlist->id)
->where('episode_id', (int) $request->input('episode'))
->delete();
$this->playlistService->reorderPositions($playlist); $this->playlistService->reorderPositions($playlist);
return response()->json([ return response()->json([

View File

@@ -3,8 +3,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Episode; use App\Models\Episode;
use App\Models\Playlist; use App\Models\User;
use App\Models\PlaylistEpisode;
use App\Http\Requests\ProfileUpdateRequest; use App\Http\Requests\ProfileUpdateRequest;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -12,8 +11,11 @@ use Illuminate\Support\Str;
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;
use Illuminate\Support\Facades\Storage;
use Illuminate\View\View; use Illuminate\View\View;
use Intervention\Image\Laravel\Facades\Image;
use Conner\Tagging\Model\Tag; use Conner\Tagging\Model\Tag;
class ProfileController extends Controller class ProfileController extends Controller
@@ -46,13 +48,20 @@ class ProfileController extends Controller
*/ */
public function update(ProfileUpdateRequest $request): RedirectResponse public function update(ProfileUpdateRequest $request): RedirectResponse
{ {
$request->user()->fill($request->validated()); $user = $request->user();
// Fill everything except the image
$user->fill($request->safe()->except('image'));
if ($request->user()->isDirty('email')) { if ($request->user()->isDirty('email')) {
$request->user()->email_verified_at = null; $request->user()->email_verified_at = null;
} }
$request->user()->save(); if ($request->hasFile('image')) {
$this->storeAvatar($request->file('image'), $user);
}
$user->save();
return Redirect::route('profile.settings')->with('status', 'profile-updated'); return Redirect::route('profile.settings')->with('status', 'profile-updated');
} }
@@ -73,7 +82,7 @@ class ProfileController extends Controller
public function comments(Request $request): View public function comments(Request $request): View
{ {
return view('profile.comments', [ return view('profile.comments', [
'user' => $request->user(), 'user' => $request->user(),
]); ]);
} }
@@ -134,19 +143,24 @@ class ProfileController extends Controller
{ {
$user = $request->user(); $user = $request->user();
// Delete Playlist // Verify password if user has password
$playlists = Playlist::where('user_id', $user->id)->get(); if (!is_null($user->password)) {
foreach($playlists as $playlist) { $request->validateWithBag('userDeletion', [
PlaylistEpisode::where('playlist_id', $playlist->id)->forceDelete(); 'password' => ['required', 'current_password'],
$playlist->forceDelete(); ]);
} }
// Update comments to deleted user // Update comments to deleted user
DB::table('comments')->where('commenter_id', '=', $user->id)->update(['commenter_id' => 1]); DB::table('comments')->where('user_id', '=', $user->id)->update(['user_id' => 1]);
// Delete Profile Picture
if ($user->avatar) {
Storage::disk('public')->delete($user->avatar);
}
Auth::logout(); Auth::logout();
$user->forceDelete(); $user->delete();
$request->session()->invalidate(); $request->session()->invalidate();
@@ -156,4 +170,31 @@ class ProfileController extends Controller
return Redirect::to('/'); return Redirect::to('/');
} }
/**
* Store custom user avatar.
*/
protected function storeAvatar(\Illuminate\Http\UploadedFile $file, User $user): void
{
// Create Folder for Image Upload
if (! Storage::disk('public')->exists("/images/avatars")) {
Storage::disk('public')->makeDirectory("/images/avatars");
}
// Delete old avatar if it exists
if ($user->avatar) {
Storage::disk('public')->delete($user->avatar);
}
$filename = "images/avatars/{$user->id}.webp";
$image = Image::read($file->getRealPath())
->cover(128, 128)
->toWebp(quality: 85);
Storage::disk('public')->put($filename, $image);
$user->avatar = $filename;
}
} }

View File

@@ -17,6 +17,12 @@ class ProfileUpdateRequest extends FormRequest
{ {
return [ return [
'name' => ['required', 'string', 'max:255'], 'name' => ['required', 'string', 'max:255'],
'image' => [
'nullable',
'image',
'mimes:jpg,png,jpeg,webp,gif',
'max:8192'
],
'email' => [ 'email' => [
'required', 'required',
'string', 'string',

View File

@@ -2,6 +2,7 @@
namespace App\Livewire; namespace App\Livewire;
use App\Models\Comment;
use Livewire\Component; use Livewire\Component;
use Livewire\WithPagination; use Livewire\WithPagination;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@@ -24,13 +25,19 @@ class AdminCommentSearch extends Component
$this->resetPage(); $this->resetPage();
} }
public function deleteComment($commentId)
{
$comment = Comment::where('id', (int) $commentId)->firstOrFail();
$comment->delete();
cache()->flush();
}
public function render() public function render()
{ {
$comments = DB::table('comments') $comments = Comment::when($this->search !== '', fn ($query) => $query->where('body', 'LIKE', "%$this->search%"))
->join('users', 'comments.commenter_id', '=', 'users.id') ->when($this->userSearch !== '', fn ($query) => $query->whereHas('user', fn ($query) => $query->where('name', 'LIKE', "%{$this->userSearch}%")))
->select('comments.*', 'users.name') ->orderBy('created_at', 'DESC')
->when($this->search !== '', fn ($query) => $query->where('comment', 'LIKE', "%$this->search%"))
->when($this->userSearch !== '', fn ($query) => $query->where('name', 'LIKE', "%$this->userSearch%"))
->paginate(12); ->paginate(12);
return view('livewire.admin-comment-search', [ return view('livewire.admin-comment-search', [

View File

@@ -2,14 +2,13 @@
namespace App\Livewire; namespace App\Livewire;
use App\Models\Comment;
use App\Models\User; use App\Models\User;
use Livewire\Component; use Livewire\Component;
use Livewire\WithPagination; use Livewire\WithPagination;
use Livewire\Attributes\Url; use Livewire\Attributes\Url;
use Illuminate\Support\Facades\DB;
class AdminUserSearch extends Component class AdminUserSearch extends Component
{ {
use WithPagination; use WithPagination;
@@ -31,8 +30,7 @@ class AdminUserSearch extends Component
$user = User::where('id', $userID) $user = User::where('id', $userID)
->firstOrFail(); ->firstOrFail();
DB::table('comments') Comment::where('user_id', $user->id)
->where('commenter_id', '=', $user->id)
->delete(); ->delete();
cache()->flush(); cache()->flush();
@@ -43,10 +41,7 @@ class AdminUserSearch extends Component
$users = User::when($this->filtered !== [], fn ($query) => $query->where('id', '>=', 10000)) $users = User::when($this->filtered !== [], fn ($query) => $query->where('id', '>=', 10000))
->when($this->patreon !== [], fn ($query) => $query->where('is_patreon', 1)) ->when($this->patreon !== [], fn ($query) => $query->where('is_patreon', 1))
->when($this->banned !== [], fn ($query) => $query->where('is_banned', 1)) ->when($this->banned !== [], fn ($query) => $query->where('is_banned', 1))
->when($this->search !== '', fn ($query) => $query->where(function($query) { ->when($this->search !== '', fn ($query) => $query->where('name', 'like', '%'.$this->search.'%'))
$query->where('name', 'like', '%'.$this->search.'%')
->orWhere('discord_name', 'like', '%'.$this->search.'%');
}))
->paginate(20); ->paginate(20);
return view('livewire.admin-user-search', [ return view('livewire.admin-user-search', [

166
app/Livewire/Comment.php Normal file
View File

@@ -0,0 +1,166 @@
<?php
namespace App\Livewire;
use App\Models\User;
use App\Models\Episode;
use App\Notifications\CommentNotification;
use Livewire\Component;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Maize\Markable\Models\Like;
class Comment extends Component
{
use AuthorizesRequests;
public $comment;
public $isReplying = false;
public $likeCount = 0;
public $liked = false;
public $replyState = [
'body' => ''
];
public $isEditing = false;
public $editState = [
'body' => ''
];
protected $listeners = [
'refresh' => '$refresh'
];
protected $validationAttributes = [
'replyState.body' => 'reply'
];
public function updatedIsEditing($isEditing)
{
if (! $isEditing) {
return;
}
$this->editState = [
'body' => $this->comment->body
];
}
public function editComment()
{
$this->authorize('update', $this->comment);
$this->comment->update($this->editState);
$this->isEditing = false;
}
public function deleteComment()
{
$this->authorize('destroy', $this->comment);
$this->comment->delete();
$this->dispatch('refresh');
}
public function postReply()
{
if (!($this->comment->depth() < 2)) {
$this->addError('replyState.body', "Too many sub comments.");
return;
}
$user = auth()->user();
$rateLimitKey = "send-comment:{$user->id}";
$rateLimitMinutes = 60 * 5; // 5 minutes
if (RateLimiter::tooManyAttempts($rateLimitKey, 1)) {
$seconds = RateLimiter::availableIn($rateLimitKey);
$this->addError('replyState.body', "Too many comments. Try again in {$seconds} seconds.");
return;
}
RateLimiter::hit($rateLimitKey, $rateLimitMinutes);
$this->validate([
'replyState.body' => 'required'
]);
$reply = $this->comment->children()->make($this->replyState);
$reply->user()->associate($user);
$reply->commentable()->associate($this->comment->commentable);
$reply->save();
// Notify if Episode and if not the same user
if ($reply->commentable_type == Episode::class && $user->id !== $reply->parent->user->id) {
$episode = Episode::where('id', $reply->commentable_id)
->firstOrFail();
$url = route('hentai.index', ['title' => $episode->slug]);
$reply->parent->user->notify(
new CommentNotification(
"{$user->name} replied to your comment.",
Str::limit($reply->body, 50),
"{$url}#comment-{$reply->id}"
)
);
}
$this->replyState = [
'body' => ''
];
$this->isReplying = false;
$this->dispatch('refresh')->self();
}
public function like()
{
if (! Auth::check()) {
return;
}
Like::toggle($this->comment, User::where('id', Auth::user()->id)->firstOrFail());
Cache::forget('commentLikes'.$this->comment->id);
if ($this->liked) {
$this->liked = false;
$this->likeCount--;
return;
}
$this->liked = true;
$this->likeCount++;
}
public function mount()
{
if (Auth::check()) {
$this->likeCount = $this->comment->likeCount();
$this->liked = Like::has($this->comment, User::where('id', Auth::user()->id)->firstOrFail());
}
}
public function render()
{
return view('livewire.comment');
}
}

71
app/Livewire/Comments.php Normal file
View File

@@ -0,0 +1,71 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use Livewire\WithPagination;
use Illuminate\Support\Facades\RateLimiter;
class Comments extends Component
{
use WithPagination;
public $model;
public $newCommentState = [
'body' => ''
];
protected $validationAttributes = [
'newCommentState.body' => 'comment'
];
protected $listeners = [
'refresh' => '$refresh'
];
public function postComment()
{
$this->validate([
'newCommentState.body' => 'required'
]);
$user = auth()->user();
$rateLimitKey = "send-comment:{$user->id}";
$rateLimitMinutes = 60 * 5; // 5 minutes
if (RateLimiter::tooManyAttempts($rateLimitKey, 1)) {
$seconds = RateLimiter::availableIn($rateLimitKey);
$this->addError('newCommentState.body', "Too many comments. Try again in {$seconds} seconds.");
return;
}
RateLimiter::hit($rateLimitKey, $rateLimitMinutes);
$comment = $this->model->comments()->make($this->newCommentState);
$comment->user()->associate($user);
$comment->save();
$this->newCommentState = [
'body' => ''
];
$this->resetPage();
}
public function render()
{
$comments = $this->model
->comments()
->with('user', 'children.user', 'children.children')
->parent()
->latest()
->paginate(50);
return view('livewire.comments', [
'comments' => $comments
]);
}
}

View File

@@ -10,7 +10,6 @@ use Livewire\Component;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
class DownloadsFree extends Component class DownloadsFree extends Component
{ {
@@ -51,7 +50,7 @@ class DownloadsFree extends Component
// Check timestamp // Check timestamp
if (Carbon::parse($alreadyDownloaded->created_at)->addHours(6) <= Carbon::now()) { if (Carbon::parse($alreadyDownloaded->created_at)->addHours(6) <= Carbon::now()) {
// Already expired // Already expired
$alreadyDownloaded->forceDelete(); $alreadyDownloaded->delete();
return; return;
} }

View File

@@ -23,6 +23,10 @@ class DownloadsSearch extends Component
public $isOpen = false; public $isOpen = false;
#[Url(history: true)]
public $studios = [];
public $studiosCopy = [];
// To toggle individual option selection // To toggle individual option selection
public function toggleOption($option) public function toggleOption($option)
{ {
@@ -46,6 +50,17 @@ class DownloadsSearch extends Component
$this->resetPage(); $this->resetPage();
} }
public function applyFilters(): void
{
$this->studiosCopy = $this->studios;
$this->resetPage();
}
public function revertFilters(): void
{
$this->studios = $this->studiosCopy;
}
// Map the selected options to database types // Map the selected options to database types
private function getSelectedTypes() private function getSelectedTypes()
{ {
@@ -129,6 +144,7 @@ class DownloadsSearch extends Component
$downloads = Downloads::when($this->fileSearch != '', fn ($query) => $query->where('url', 'like', '%'.$this->fileSearch.'%')) $downloads = Downloads::when($this->fileSearch != '', fn ($query) => $query->where('url', 'like', '%'.$this->fileSearch.'%'))
->whereIn('type', $this->getSelectedTypes()) ->whereIn('type', $this->getSelectedTypes())
->when($this->studios !== [], fn ($q) => $q->whereHas('episode', fn ($query) => $query->whereHas('studio', function ($query) { $query->whereIn('slug', $this->studios); })))
->whereNotNull('size') ->whereNotNull('size')
->orderBy($orderby, $orderdirection) ->orderBy($orderby, $orderdirection)
->paginate(20); ->paginate(20);
@@ -136,6 +152,7 @@ class DownloadsSearch extends Component
return view('livewire.downloads-search', [ return view('livewire.downloads-search', [
'downloads' => $downloads, 'downloads' => $downloads,
'query' => $this->fileSearch, 'query' => $this->fileSearch,
'studiocount' => is_array($this->studios) ? count($this->studios) : 0,
]); ]);
} }
} }

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Livewire;
use App\Models\Comment;
use Livewire\Component;
use Livewire\WithPagination;
class UserComments extends Component
{
use WithPagination;
public $model;
public $commentSearch;
public $order = 'created_at_desc';
public function render()
{
$orderby = 'created_at';
$orderdirection = 'desc';
switch ($this->order) {
case 'created_at_desc':
$orderby = 'created_at';
$orderdirection = 'desc';
break;
case 'created_at_asc':
$orderby = 'created_at';
$orderdirection = 'asc';
break;
default:
$orderby = 'created_at';
$orderdirection = 'desc';
}
$comments = Comment::where('user_id', $this->model->id)
->when($this->commentSearch != '', fn ($query) => $query->where('body', 'like', '%'.$this->commentSearch.'%'))
->orderBy($orderby, $orderdirection)
->paginate(10);
return view('livewire.user-comments', [
'comments' => $comments
]);
}
}

76
app/Models/Comment.php Normal file
View File

@@ -0,0 +1,76 @@
<?php
namespace App\Models;
use App\Models\Presenters\CommentPresenter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Maize\Markable\Markable;
use Maize\Markable\Models\Like;
class Comment extends Model
{
use HasFactory, SoftDeletes, Markable;
protected static $marks = [
Like::class
];
/**
* The attributes that are mass assignable.
*
* @var string[]
*/
protected $fillable = [
'body'
];
public function presenter()
{
return new CommentPresenter($this);
}
public function user()
{
return $this->belongsTo(User::class);
}
public function scopeParent(Builder $builder)
{
$builder->whereNull('parent_id');
}
public function children()
{
return $this->hasMany(Comment::class, 'parent_id')->oldest();
}
public function commentable()
{
return $this->morphTo();
}
public function parent()
{
return $this->hasOne(Comment::class, 'id', 'parent_id');
}
// Recursevly calculates how deep the nesting is
public function depth(): int
{
return $this->parent
? $this->parent->depth() + 1
: 0;
}
/**
* Get cached like count
*/
public function likeCount(): int
{
return cache()->remember('commentLikes' . $this->id, 300, fn() => $this->likes->count());
}
}

View File

@@ -8,7 +8,6 @@ use App\Models\PopularWeekly;
use App\Models\PopularDaily; use App\Models\PopularDaily;
use Conner\Tagging\Taggable; use Conner\Tagging\Taggable;
use Laravelista\Comments\Commentable;
use Laravel\Scout\Searchable; use Laravel\Scout\Searchable;
use Maize\Markable\Markable; use Maize\Markable\Markable;
use Maize\Markable\Models\Like; use Maize\Markable\Models\Like;
@@ -18,6 +17,7 @@ use Spatie\Sitemap\Tags\Url;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -25,7 +25,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
class Episode extends Model implements Sitemapable class Episode extends Model implements Sitemapable
{ {
use Commentable, Markable, Taggable; use Markable, Taggable;
use HasFactory; use HasFactory;
use Searchable; use Searchable;
@@ -54,7 +54,6 @@ class Episode extends Model implements Sitemapable
'title_jpn' => $this->title_jpn, 'title_jpn' => $this->title_jpn,
'slug' => $this->slug, 'slug' => $this->slug,
'description' => $this->description, 'description' => $this->description,
'view_count' => $this->view_count,
'tags' => $this->tagNames(), 'tags' => $this->tagNames(),
'release_date' => $this->release_date, 'release_date' => $this->release_date,
'created_at' => $this->created_at, 'created_at' => $this->created_at,
@@ -104,10 +103,11 @@ class Episode extends Model implements Sitemapable
/** /**
* Increment View Count. * Increment View Count.
*/ */
public function incrementViewCount(): bool public function incrementViewCount(): void
{ {
$this->view_count++; DB::table('episodes')
return $this->save(); ->where('id', $this->id)
->update(['view_count' => $this->view_count + 1]);
} }
/** /**
@@ -160,6 +160,11 @@ class Episode extends Model implements Sitemapable
return cache()->remember('episodeComments' . $this->id, 300, fn() => $this->comments->count()); return cache()->remember('episodeComments' . $this->id, 300, fn() => $this->comments->count());
} }
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
public function getProblematicTags(): string public function getProblematicTags(): string
{ {
$problematicTags = ['Gore', 'Scat', 'Horror']; $problematicTags = ['Gore', 'Scat', 'Horror'];

View File

@@ -10,11 +10,10 @@ use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Conner\Tagging\Taggable; use Conner\Tagging\Taggable;
use Laravelista\Comments\Commentable;
class Hentai extends Model implements Sitemapable class Hentai extends Model implements Sitemapable
{ {
use Commentable, Taggable; use Taggable;
use HasFactory; use HasFactory;
/** /**
@@ -37,6 +36,11 @@ class Hentai extends Model implements Sitemapable
return $this->episodes->first()->title; return $this->episodes->first()->title;
} }
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
/** /**
* Has a Gallery. * Has a Gallery.
*/ */

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Models\Presenters;
use App\Models\Comment;
use Illuminate\Support\Str;
class CommentPresenter
{
public $comment;
public function __construct(Comment $comment)
{
$this->comment = $comment;
}
public function markdownBody()
{
return Str::of($this->comment->body)->markdown([
'html_input' => 'strip',
]);
}
public function relativeCreatedAt()
{
return $this->comment->created_at->diffForHumans();
}
}

View File

@@ -7,14 +7,13 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Storage;
use Laravelista\Comments\Commenter;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
class User extends Authenticatable class User extends Authenticatable
{ {
use HasFactory, Notifiable, Commenter; use HasFactory, Notifiable;
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.
@@ -29,7 +28,6 @@ class User extends Authenticatable
'is_banned', 'is_banned',
// Discord // Discord
'discord_id', 'discord_id',
'discord_name',
'discord_avatar', 'discord_avatar',
]; ];
@@ -60,26 +58,9 @@ class User extends Authenticatable
'tag_blacklist' => 'array', 'tag_blacklist' => 'array',
// Discord // Discord
'discord_id' => 'integer', 'discord_id' => 'integer',
'discord_name' => 'string',
'discord_avatar' => 'string', 'discord_avatar' => 'string',
]; ];
/**
* Get the user name
*/
public function getUserName(): string
{
if (!$this->discord_name) {
return $this->name;
}
if ($this->discord_name == $this->name)
{
return $this->name;
}
return "{$this->name} ({$this->discord_name})";
}
/** /**
* Has Many Playlists. * Has Many Playlists.
@@ -108,8 +89,34 @@ class User extends Authenticatable
/** /**
* Has Many Comments. * Has Many Comments.
*/ */
public function comments()
{
return $this->hasMany(Comment::class, 'user_id');
}
/**
* Get Comment Count.
*/
public function commentCount(): int public function commentCount(): int
{ {
return DB::table('comments')->where('commenter_id', $this->id)->count(); return cache()->remember('userComments' . $this->id, 300, fn() => $this->comments->count());
}
/**
* Returns the user avatar image url.
*/
public function getAvatar(): string
{
if ($this->discord_id && $this->discord_avatar && !$this->avatar)
{
return "https://external-content.duckduckgo.com/iu/?u={$this->discord_avatar}";
}
if ($this->avatar)
{
return Storage::url($this->avatar);
}
return asset('images/default-avatar.webp');
} }
} }

View File

@@ -1,56 +0,0 @@
<?php
namespace Laravelista\Comments;
use Laravelista\Comments\Comment;
class CommentPolicy
{
/**
* Can user create the comment
*
* @param $user
* @return bool
*/
public function create($user) : bool
{
return true;
}
/**
* Can user delete the comment
*
* @param $user
* @param Comment $comment
* @return bool
*/
public function delete($user, Comment $comment) : bool
{
return ($user->getKey() == $comment->commenter_id) || $user->is_admin;
}
/**
* Can user update the comment
*
* @param $user
* @param Comment $comment
* @return bool
*/
public function update($user, Comment $comment) : bool
{
return $user->getKey() == $comment->commenter_id;
}
/**
* Can user reply to the comment
*
* @param $user
* @param Comment $comment
* @return bool
*/
public function reply($user, Comment $comment) : bool
{
return $user->getKey() != $comment->commenter_id;
}
}

View File

@@ -1,139 +0,0 @@
<?php
namespace Laravelista\Comments;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Validator;
use App\Notifications\CommentNotification;
use App\Models\User;
use App\Models\Episode;
class CommentService
{
/**
* Handles creating a new comment for given model.
* @return mixed the configured comment-model
*/
public function store(Request $request)
{
// If guest commenting is turned off, authorize this action.
if (Config::get('comments.guest_commenting') == false) {
Gate::authorize('create-comment', Comment::class);
}
// Define guest rules if user is not logged in.
if (!Auth::check()) {
$guest_rules = [
'guest_name' => 'required|string|max:255',
'guest_email' => 'required|string|email|max:255',
];
}
// Merge guest rules, if any, with normal validation rules.
Validator::make($request->all(), array_merge($guest_rules ?? [], [
'commentable_type' => 'required|string',
'commentable_id' => 'required|string|min:1',
'message' => 'required|string'
]))->validate();
$model = $request->commentable_type::findOrFail($request->commentable_id);
$commentClass = Config::get('comments.model');
$comment = new $commentClass;
if (!Auth::check()) {
$comment->guest_name = $request->guest_name;
$comment->guest_email = $request->guest_email;
} else {
$comment->commenter()->associate(Auth::user());
}
$comment->commentable()->associate($model);
$comment->comment = $request->message;
$comment->approved = !Config::get('comments.approval_required');
$comment->save();
return $comment;
}
/**
* Handles updating the message of the comment.
* @return mixed the configured comment-model
*/
public function update(Request $request, Comment $comment)
{
Gate::authorize('edit-comment', $comment);
Validator::make($request->all(), [
'message' => 'required|string'
])->validate();
$comment->update([
'comment' => $request->message
]);
return $comment;
}
/**
* Handles deleting a comment.
* @return mixed the configured comment-model
*/
public function destroy(Comment $comment): void
{
Gate::authorize('delete-comment', $comment);
if (Config::get('comments.soft_deletes') == true) {
$comment->delete();
} else {
$comment->forceDelete();
}
}
/**
* Handles creating a reply "comment" to a comment.
* @return mixed the configured comment-model
*/
public function reply(Request $request, Comment $comment)
{
Gate::authorize('reply-to-comment', $comment);
Validator::make($request->all(), [
'message' => 'required|string'
])->validate();
$commentClass = Config::get('comments.model');
$reply = new $commentClass;
$reply->commenter()->associate(Auth::user());
$reply->commentable()->associate($comment->commentable);
$reply->parent()->associate($comment);
$reply->comment = $request->message;
$reply->approved = !Config::get('comments.approval_required');
$reply->save();
// Notify
if ($comment->commentable_type == 'App\Models\Episode') {
$episode = Episode::where('id', $comment->commentable_id)->firstOrFail();
$url = '/hentai/' . $episode->slug . '#comment-' . $reply->id;
$user = Auth::user();
$username = $user->discord_name ?? $user->name;
$parentCommentUser = User::where('id', $comment->commenter_id)->firstOrFail();
$parentCommentUser->notify(
new CommentNotification(
"{$username} replied to your comment.",
Str::limit($reply->comment, 50),
$url
)
);
}
return $reply;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Policies;
use App\Models\User;
use App\Models\Comment;
use Illuminate\Auth\Access\HandlesAuthorization;
class CommentPolicy
{
use HandlesAuthorization;
public function update(User $user, Comment $comment): bool
{
return $user->id === $comment->user_id;
}
public function destroy(User $user, Comment $comment): bool
{
return $user->id === $comment->user_id;
}
}

View File

@@ -74,7 +74,7 @@ class GalleryService
foreach ($oldGallery as $oldImage) { foreach ($oldGallery as $oldImage) {
Storage::disk('public')->delete($oldImage->image_url); Storage::disk('public')->delete($oldImage->image_url);
Storage::disk('public')->delete($oldImage->thumbnail_url); Storage::disk('public')->delete($oldImage->thumbnail_url);
$oldImage->forceDelete(); $oldImage->delete();
} }
} }
} }

View File

@@ -19,7 +19,6 @@
"laravel/scout": "^10.20", "laravel/scout": "^10.20",
"laravel/socialite": "^5.24", "laravel/socialite": "^5.24",
"laravel/tinker": "^2.10", "laravel/tinker": "^2.10",
"laravelista/comments": "dev-l11-compatibility",
"livewire/livewire": "^3.6.4", "livewire/livewire": "^3.6.4",
"maize-tech/laravel-markable": "^2.3.0", "maize-tech/laravel-markable": "^2.3.0",
"meilisearch/meilisearch-php": "^1.16", "meilisearch/meilisearch-php": "^1.16",
@@ -50,19 +49,13 @@
} }
], ],
"autoload": { "autoload": {
"exclude-from-classmap": [ "exclude-from-classmap": [],
"vendor/laravelista/comments/src/CommentPolicy.php",
"vendor/laravelista/comments/src/CommentService.php"
],
"psr-4": { "psr-4": {
"App\\": "app/", "App\\": "app/",
"Database\\Factories\\": "database/factories/", "Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/" "Database\\Seeders\\": "database/seeders/"
}, },
"files": [ "files": []
"app/Override/Comments/CommentPolicy.php",
"app/Override/Comments/CommentService.php"
]
}, },
"autoload-dev": { "autoload-dev": {
"psr-4": { "psr-4": {

196
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "749225dc4ea2aca06f1639bef889cc59", "content-hash": "cf750c98603544d91cf1bdb428866a8f",
"packages": [ "packages": [
{ {
"name": "brick/math", "name": "brick/math",
@@ -581,56 +581,6 @@
], ],
"time": "2025-03-06T22:45:56+00:00" "time": "2025-03-06T22:45:56+00:00"
}, },
{
"name": "erusev/parsedown",
"version": "1.7.4",
"source": {
"type": "git",
"url": "https://github.com/erusev/parsedown.git",
"reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/erusev/parsedown/zipball/cb17b6477dfff935958ba01325f2e8a2bfa6dab3",
"reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": ">=5.3.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35"
},
"type": "library",
"autoload": {
"psr-0": {
"Parsedown": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Emanuil Rusev",
"email": "hello@erusev.com",
"homepage": "http://erusev.com"
}
],
"description": "Parser for Markdown.",
"homepage": "http://parsedown.org",
"keywords": [
"markdown",
"parser"
],
"support": {
"issues": "https://github.com/erusev/parsedown/issues",
"source": "https://github.com/erusev/parsedown/tree/1.7.x"
},
"time": "2019-12-30T22:54:17+00:00"
},
{ {
"name": "firebase/php-jwt", "name": "firebase/php-jwt",
"version": "v7.0.2", "version": "v7.0.2",
@@ -2266,70 +2216,6 @@
}, },
"time": "2025-01-27T14:24:01+00:00" "time": "2025-01-27T14:24:01+00:00"
}, },
{
"name": "laravelista/comments",
"version": "dev-l11-compatibility",
"source": {
"type": "git",
"url": "https://github.com/renatokira/comments.git",
"reference": "490764a774d520a4d9e43395b472d0f9bf802ef6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/renatokira/comments/zipball/490764a774d520a4d9e43395b472d0f9bf802ef6",
"reference": "490764a774d520a4d9e43395b472d0f9bf802ef6",
"shasum": ""
},
"require": {
"erusev/parsedown": "^1.7",
"illuminate/database": "^9.0|^10.0|^11.0",
"illuminate/http": "^9.0|^10.0|^11.0",
"illuminate/pagination": "^9.0|^10.0|^11.0",
"illuminate/queue": "^9.0|^10.0|^11.0",
"illuminate/routing": "^9.0|^10.0|^11.0",
"illuminate/support": "^9.0|^10.0|^11.0",
"php": "^8.0",
"spatie/laravel-honeypot": "^4.5"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravelista\\Comments\\ServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravelista\\Comments\\": "src/"
}
},
"license": [
"MIT"
],
"authors": [
{
"name": "Mario Bašić",
"email": "mario@laravelista.hr",
"homepage": "https://laravelista.hr"
}
],
"description": "Comments for Laravel.",
"keywords": [
"comments",
"laravel"
],
"support": {
"source": "https://github.com/renatokira/comments/tree/l11-compatibility"
},
"funding": [
{
"type": "patreon",
"url": "https://www.patreon.com/laravelista"
}
],
"time": "2024-03-16T14:14:11+00:00"
},
{ {
"name": "league/commonmark", "name": "league/commonmark",
"version": "2.7.1", "version": "2.7.1",
@@ -5817,82 +5703,6 @@
], ],
"time": "2025-05-20T08:42:52+00:00" "time": "2025-05-20T08:42:52+00:00"
}, },
{
"name": "spatie/laravel-honeypot",
"version": "4.6.1",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-honeypot.git",
"reference": "38d164f14939e943b92771859fc206c74cba8397"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-honeypot/zipball/38d164f14939e943b92771859fc206c74cba8397",
"reference": "38d164f14939e943b92771859fc206c74cba8397",
"shasum": ""
},
"require": {
"illuminate/contracts": "^8.0|^9.0|^10.0|^11.0|^12.0",
"illuminate/encryption": "^8.0|^9.0|^10.0|^11.0|^12.0",
"illuminate/http": "^8.0|^9.0|^10.0|^11.0|^12.0",
"illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0",
"illuminate/validation": "^8.0|^9.0|^10.0|^11.0|^12.0",
"nesbot/carbon": "^2.0|^3.0",
"php": "^8.0",
"spatie/laravel-package-tools": "^1.9",
"symfony/http-foundation": "^5.1.2|^6.0|^7.0"
},
"require-dev": {
"livewire/livewire": "^2.10|^3.0",
"orchestra/testbench": "^6.23|^7.0|^8.0|^9.0|^10.0",
"pestphp/pest-plugin-livewire": "^1.0|^2.1|^3.0",
"phpunit/phpunit": "^9.6|^10.5|^11.5",
"spatie/pest-plugin-snapshots": "^1.1|^2.1",
"spatie/phpunit-snapshot-assertions": "^4.2|^5.1",
"spatie/test-time": "^1.2.1"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Spatie\\Honeypot\\HoneypotServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Spatie\\Honeypot\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
}
],
"description": "Preventing spam submitted through forms",
"homepage": "https://github.com/spatie/laravel-honeypot",
"keywords": [
"laravel-honeypot",
"spatie"
],
"support": {
"source": "https://github.com/spatie/laravel-honeypot/tree/4.6.1"
},
"funding": [
{
"url": "https://spatie.be/open-source/support-us",
"type": "custom"
}
],
"time": "2025-05-05T13:50:37+00:00"
},
{ {
"name": "spatie/laravel-package-tools", "name": "spatie/laravel-package-tools",
"version": "1.92.7", "version": "1.92.7",
@@ -11900,9 +11710,7 @@
], ],
"aliases": [], "aliases": [],
"minimum-stability": "stable", "minimum-stability": "stable",
"stability-flags": { "stability-flags": {},
"laravelista/comments": 20
},
"prefer-stable": true, "prefer-stable": true,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {

View File

@@ -153,8 +153,7 @@ return [
], ],
'sortableAttributes' => [ 'sortableAttributes' => [
'created_at', 'created_at',
'release_date', 'release_date',
'view_count',
'title' 'title'
], ],
], ],

View File

@@ -15,7 +15,7 @@ return new class extends Migration
public function up(): void public function up(): void
{ {
# Delete entries with "#" as URL # Delete entries with "#" as URL
Downloads::where('url', '#')->forceDelete(); Downloads::where('url', '#')->delete();
# Remove duplicate entries # Remove duplicate entries
$duplicates = DB::table('downloads') $duplicates = DB::table('downloads')

View File

@@ -1,154 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Collection;
use App\Models\User;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// 1. Create new column discord_id
Schema::table('users', function (Blueprint $table) {
$table->unsignedBigInteger('discord_id')->nullable()->after('id');
});
// 2. Migrate Discord Users IDs
DB::table('users')
->where('id', '>', 10000)
->update(['discord_id' => DB::raw('id')]);
// 3. Temporary new auto increment column
Schema::table('users', function (Blueprint $table) {
$table->unsignedBigInteger('new_id')->first();
});
// 3.5 Manually count (cursed)
$counter = 1;
foreach(User::orderBy('id')->get() as $user) {
$user->new_id = $counter;
$user->save();
$counter++;
}
// 4. Drop foreign keys
$this->dropForeignKeys();
// 5. Fix ID's in other tables
$this->updateUserIDsInOtherTables();
// 6. Remove old ID
Schema::table('users', function (Blueprint $table) {
$table->bigInteger('id')->unsigned()->change();
$table->dropPrimary('id');
$table->dropColumn('id');
});
// 7. Rename new_id to id
Schema::table('users', function (Blueprint $table) {
$table->renameColumn('new_id', 'id');
$table->unsignedBigInteger('id')->autoIncrement()->primary()->change();
});
// 8. Recreate foreign key constraints
$this->addForeignKeys();
}
/**
* Drop Foreign Keys referencing the user id
*/
private function dropForeignKeys(): void
{
Schema::table('markable_likes', function (Blueprint $table) {
$table->dropForeign(['user_id']);
});
Schema::table('watched', function (Blueprint $table) {
$table->dropForeign(['user_id']);
});
Schema::table('discord_access_tokens', function (Blueprint $table) {
$table->dropForeign(['user_id']);
});
// Our Schema does include a foreign key, for whatever reason it doesn't exist in the first palce
// Schema::table('user_downloads', function (Blueprint $table) {
// $table->dropForeign(['user_id']);
// });
}
/**
* Tables to fix the IDs:
* - comments ['commenter_id']
* - discord_access_tokens ['user_id']
* - markable_likes ['user_id']
* - notifications ['notifiable_id']
* - playlists ['user_id']
* - user_downloads ['user_id']
* - watched ['user_id']
*/
private function updateUserIDsInOtherTables(): void
{
DB::table('users')->orderBy('id')->chunk(100, function (Collection $users) {
foreach ($users as $user) {
DB::table('comments')
->where('commenter_id', $user->id)
->update(['commenter_id' => $user->new_id]);
DB::table('discord_access_tokens')
->where('user_id', $user->id)
->update(['user_id' => $user->new_id]);
DB::table('markable_likes')
->where('user_id', $user->id)
->update(['user_id' => $user->new_id]);
DB::table('notifications')
->where('notifiable_id', $user->id)
->update(['notifiable_id' => $user->new_id]);
DB::table('playlists')
->where('user_id', $user->id)
->update(['user_id' => $user->new_id]);
DB::table('user_downloads')
->where('user_id', $user->id)
->update(['user_id' => $user->new_id]);
DB::table('watched')
->where('user_id', $user->id)
->update(['user_id' => $user->new_id]);
}
});
}
/**
* Re-Add Foreign Keys to tables which we dropped previously
*/
private function addForeignKeys(): void
{
Schema::table('markable_likes', function (Blueprint $table) {
$table->unsignedBigInteger('user_id')->references('id')->on('users')->onDelete('cascade')->change();
});
Schema::table('watched', function (Blueprint $table) {
$table->unsignedBigInteger('user_id')->references('id')->on('users')->onDelete('cascade')->change();
});
Schema::table('discord_access_tokens', function (Blueprint $table) {
$table->unsignedBigInteger('user_id')->references('id')->on('users')->onDelete('cascade')->change();
});
Schema::table('user_downloads', function (Blueprint $table) {
$table->unsignedBigInteger('user_id')->references('id')->on('users')->onDelete('cascade')->change();
});
}
};

View File

@@ -1,5 +1,6 @@
<?php <?php
use App\Models\User;
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
@@ -26,14 +27,16 @@ return new class extends Migration
$table->dropColumn('public_flags'); $table->dropColumn('public_flags');
$table->dropColumn('verified'); $table->dropColumn('verified');
$table->dropColumn('mfa_enabled'); $table->dropColumn('mfa_enabled');
$table->dropColumn('global_name');
$table->dropColumn('locale');
}); });
// Change & Add Columns // Change & Add Columns
Schema::table('users', function (Blueprint $table) { Schema::table('users', function (Blueprint $table) {
// Rename // Rename
$table->renameColumn('username', 'name'); $table->renameColumn('username', 'name');
$table->renameColumn('global_name', 'discord_name');
$table->renameColumn('avatar', 'discord_avatar'); $table->renameColumn('avatar', 'discord_avatar');
$table->string('avatar')->nullable()->after('email');
// Re-Add Email verification // Re-Add Email verification
$table->timestamp('email_verified_at')->nullable()->after('email'); $table->timestamp('email_verified_at')->nullable()->after('email');
@@ -42,5 +45,21 @@ return new class extends Migration
$table->string('password')->nullable()->after('email_verified_at'); $table->string('password')->nullable()->after('email_verified_at');
$table->rememberToken()->after('password'); $table->rememberToken()->after('password');
}); });
/**
* --------------------------------------------------------------------
* Fix Discord Profile Pictures
* --------------------------------------------------------------------
* The oauth package by socialite now returns a full url of the avatar.
* Meaning all the old entries have to be fixed.
*/
foreach (User::whereNotNull('discord_avatar')->get() as $user)
{
$isGif = preg_match('/a_.+/m', $user->discord_avatar) === 1;
$extension = $isGif ? 'gif' : 'webp';
$user->discord_avatar = sprintf('https://cdn.discordapp.com/avatars/%s/%s.%s', $user->id, $user->discord_avatar, $extension);
$user->save();
}
} }
}; };

View File

@@ -0,0 +1,207 @@
<?php
use App\Models\Playlist;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// 1. Create new column discord_id
Schema::table('users', function (Blueprint $table) {
$table->unsignedBigInteger('discord_id')->nullable()->after('id');
});
// 2. Migrate Discord Users IDs
DB::table('users')
->where('id', '>', 10000)
->update(['discord_id' => DB::raw('id')]);
// 3. Temporary new auto increment column
Schema::table('users', function (Blueprint $table) {
$table->unsignedBigInteger('new_id')->first();
});
// 3.5 Count
DB::statement('
UPDATE users u
JOIN (
SELECT id, ROW_NUMBER() OVER (ORDER BY id) AS rn
FROM users
) t ON u.id = t.id
SET u.new_id = t.rn
');
// 4. Drop foreign keys
$this->dropForeignKeys();
// 5. Fix ID's in other tables
$this->updateUserIDsInOtherTables();
// 6. Remove old ID
Schema::table('users', function (Blueprint $table) {
$table->bigInteger('id')->unsigned()->change();
$table->dropPrimary('id');
$table->dropColumn('id');
});
// 7. Rename new_id to id
Schema::table('users', function (Blueprint $table) {
$table->renameColumn('new_id', 'id');
});
// 8. Change new ID to auto increment and set as primary key
Schema::table('users', function (Blueprint $table) {
$table->unsignedBigInteger('id')->autoIncrement()->primary()->change();
});
// 9. Remove data that would conflict with constraints
$this->deleteUnreferencedData();
// 9. Recreate foreign key constraints
$this->addForeignKeys();
}
/**
* Drop Foreign Keys referencing the user id
*/
private function dropForeignKeys(): void
{
Schema::table('markable_likes', function (Blueprint $table) {
$table->dropForeign(['user_id']);
});
Schema::table('watched', function (Blueprint $table) {
$table->dropForeign(['user_id']);
});
// Our Schema does include a foreign key, for whatever reason it doesn't exist in the first palce
// Schema::table('user_downloads', function (Blueprint $table) {
// $table->dropForeign(['user_id']);
// });
}
/**
* Tables to fix the IDs:
* - comments ['commenter_id']
* - markable_likes ['user_id']
* - notifications ['notifiable_id']
* - playlists ['user_id']
* - user_downloads ['user_id']
* - watched ['user_id']
*/
private function updateUserIDsInOtherTables(): void
{
DB::statement('
UPDATE comments c
JOIN users u ON c.commenter_id = u.id
SET c.commenter_id = u.new_id
');
DB::statement('
UPDATE watched w
JOIN users u ON w.user_id = u.id
SET w.user_id = u.new_id
');
DB::statement('
UPDATE markable_likes ml
JOIN users u ON ml.user_id = u.id
SET ml.user_id = u.new_id
');
DB::statement('
UPDATE notifications n
JOIN users u ON n.notifiable_id = u.id
SET n.notifiable_id = u.new_id
');
DB::statement('
UPDATE playlists p
JOIN users u ON p.user_id = u.id
SET p.user_id = u.new_id
');
DB::statement('
UPDATE user_downloads ud
JOIN users u ON ud.user_id = u.id
SET ud.user_id = u.new_id
');
}
/**
* Due to incorrect handling of user deletes,
* we have unreferenced data
*/
private function deleteUnreferencedData(): void
{
// User Downloads Table
DB::table('user_downloads')
->where('user_id', '>', 1_000_000)
->delete();
// User Playlists Table
$playlists = Playlist::where('user_id', '>', 1_000_000)
->get();
foreach($playlists as $playlist) {
DB::table('playlist_episodes')
->where('playlist_id', '=', $playlist->id)
->delete();
$playlist->delete();
}
}
/**
* Re-Add Foreign Keys to tables which we dropped previously
*/
private function addForeignKeys(): void
{
Schema::table('markable_likes', function (Blueprint $table) {
// Ensure the column is unsigned
$table->bigInteger('user_id')->unsigned()->change();
// Add the foreign key constraint
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
Schema::table('watched', function (Blueprint $table) {
// Ensure the column is unsigned
$table->bigInteger('user_id')->unsigned()->change();
// Add the foreign key constraint
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
Schema::table('user_downloads', function (Blueprint $table) {
// Ensure the column is unsigned
$table->bigInteger('user_id')->unsigned()->change();
// Add the foreign key constraint
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
Schema::table('playlist_episodes', function (Blueprint $table) {
// Ensure the column is unsigned
$table->bigInteger('playlist_id')->unsigned()->change();
// Add the foreign key constraint
$table->foreign('playlist_id')->references('id')->on('playlists')->onDelete('cascade');
});
Schema::table('playlists', function (Blueprint $table) {
// Ensure the column is unsigned
$table->bigInteger('user_id')->unsigned()->change();
// Add the foreign key constraint
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
}
};

View File

@@ -0,0 +1,75 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Drop Foreign Keys and Index
Schema::table('comments', function (Blueprint $table) {
$table->dropForeign(['child_id']);
$table->dropIndex(['commenter_id', 'commenter_type']);
});
// Rename and Drop columns
Schema::table('comments', function (Blueprint $table) {
$table->renameColumn('commenter_id', 'user_id');
$table->dropColumn('commenter_type');
$table->dropColumn('guest_name');
$table->dropColumn('guest_email');
$table->renameColumn('child_id', 'parent_id');
$table->renameColumn('comment', 'body');
$table->dropColumn('approved');
});
// Add Foreign Keys
Schema::table('comments', function (Blueprint $table) {
// Ensure the column is unsigned
$table->bigInteger('user_id')->unsigned()->change();
// Add the foreign key constraint
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('parent_id')->references('id')->on('comments')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
// Drop Foreign Keys
Schema::table('comments', function (Blueprint $table) {
$table->dropForeign(['parent_id']);
$table->dropForeign(['user_id']);
});
// Rename and Re-Add Columns
Schema::table('comments', function (Blueprint $table) {
$table->renameColumn('user_id', 'commenter_id');
$table->string('commenter_type')->nullable()->after('commenter_id');
$table->string('guest_name')->nullable()->after('commenter_type');
$table->string('guest_email')->nullable()->after('guest_name');
$table->renameColumn('parent_id', 'child_id');
$table->renameColumn('body', 'comment');
$table->boolean('approved')->default(true)->after('comment');
});
DB::table('comments')->update([
'commenter_type' => 'App\Models\User',
]);
// Re-Add foreign key constraint and index
Schema::table('comments', function (Blueprint $table) {
$table->foreign('child_id')->references('id')->on('comments')->onDelete('cascade');
$table->index(["commenter_id", "commenter_type"]);
});
}
};

View File

@@ -12,10 +12,10 @@ import {
initTE, initTE,
} from "tw-elements"; } from "tw-elements";
import Alpine from 'alpinejs'; // import Alpine from 'alpinejs';
window.Alpine = Alpine; // window.Alpine = Alpine;
Alpine.start(); // Alpine.start();
initTE({ Collapse, Carousel, Clipboard, Modal, Tab, Lightbox, Tooltip, Ripple }); initTE({ Collapse, Carousel, Clipboard, Modal, Tab, Lightbox, Tooltip, Ripple });

View File

@@ -28,6 +28,7 @@ var av1Supported = (!!document.createElement('video').canPlayType('video/webm; c
var dashSupported = dashjs.supportsMediaSource(); var dashSupported = dashjs.supportsMediaSource();
var apiResponse = {}; var apiResponse = {};
var volume = 0.5; var volume = 0.5;
var muted = false;
var captions = true; var captions = true;
var lastTime = 0.0; var lastTime = 0.0;
var streamServer = ''; var streamServer = '';
@@ -65,10 +66,16 @@ if (localStorage.hstreamCaptions) {
console.log('Loaded Captions Status from Local Storage: ' + captions); console.log('Loaded Captions Status from Local Storage: ' + captions);
} }
// Load Muted from LocalStorage
if (localStorage.hstreamCaptions) {
muted = (localStorage.getItem('hstreamMuted') == 'true');
console.log('Loaded Muted Status from Local Storage: ' + muted);
}
// Asia Server Fallback // Asia Server Fallback
if (localStorage.hstreamServerFallback) { if (localStorage.hstreamServerFallback) {
serverFallback = (localStorage.getItem('hstreamServerFallback') == 'true'); serverFallback = (localStorage.getItem('hstreamServerFallback') == 'true');
console.log('Loaded Captions Status from Local Storage: ' + captions); console.log('Loaded Server Fallback Status from Local Storage: ' + serverFallback);
} }
// Alert User when AV1 is not supported // Alert User when AV1 is not supported
@@ -224,6 +231,7 @@ function initPlayer() {
}; };
player.volume = volume; player.volume = volume;
player.muted = muted;
//player.captions.languages = ['en']; //player.captions.languages = ['en'];
player.captions.language = 'en'; player.captions.language = 'en';
player.captions.active = captions; player.captions.active = captions;
@@ -306,6 +314,8 @@ function initPlayer() {
player.on('volumechange', () => { player.on('volumechange', () => {
console.log('Saving Audio Volume to Local Storage: ' + player.volume); console.log('Saving Audio Volume to Local Storage: ' + player.volume);
localStorage.setItem('hstreamVolume', player.volume.toString()) localStorage.setItem('hstreamVolume', player.volume.toString())
console.log('Saving Audio Muted to Local Storage: ' + player.muted.toString());
localStorage.setItem('hstreamMuted', player.muted.toString())
}); });
player.on('ended', () => { player.on('ended', () => {

View File

@@ -46,7 +46,7 @@ $maxWidth = [
x-on:keydown.tab.prevent="$event.shiftKey || nextFocusable().focus()" x-on:keydown.tab.prevent="$event.shiftKey || nextFocusable().focus()"
x-on:keydown.shift.tab.prevent="prevFocusable().focus()" x-on:keydown.shift.tab.prevent="prevFocusable().focus()"
x-show="show" x-show="show"
class="fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-50" class="fixed inset-0 overflow-y-auto px-4 py-12 sm:px-0 z-50"
style="display: {{ $show ? 'block' : 'none' }};" style="display: {{ $show ? 'block' : 'none' }};"
> >
<div <div
@@ -60,12 +60,12 @@ $maxWidth = [
x-transition:leave-start="opacity-100" x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0" x-transition:leave-end="opacity-0"
> >
<div class="absolute inset-0 bg-gray-500 dark:bg-gray-900 opacity-75"></div> <div class="absolute inset-0 bg-neutral-500 dark:bg-neutral-900 opacity-75"></div>
</div> </div>
<div <div
x-show="show" x-show="show"
class="mb-6 bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full {{ $maxWidth }} sm:mx-auto" class="mb-6 bg-white dark:bg-neutral-800 rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full {{ $maxWidth }} sm:mx-auto"
x-transition:enter="ease-out duration-300" x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100" x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"

View File

@@ -1,3 +1,3 @@
<button {{ $attributes->merge(['type' => 'button', 'class' => 'inline-flex items-center px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-500 rounded-md font-semibold text-xs text-gray-700 dark:text-gray-300 uppercase tracking-widest shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:opacity-25 transition ease-in-out duration-150']) }}> <button {{ $attributes->merge(['type' => 'button', 'class' => 'inline-flex items-center px-4 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-500 rounded-md font-semibold text-xs text-neutral-700 dark:text-neutral-300 uppercase tracking-widest shadow-sm hover:bg-neutral-50 dark:hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-rose-500 focus:ring-offset-2 dark:focus:ring-offset-neutral-800 disabled:opacity-25 transition ease-in-out duration-150']) }}>
{{ $slot }} {{ $slot }}
</button> </button>

View File

@@ -2,14 +2,14 @@
{{ __('home.latest-comments') }} {{ __('home.latest-comments') }}
</p> </p>
<div class="grid grid-cols-1 gap-2 md:grid-cols-2"> <div class="grid gap-2 grid-cols-1 xl:grid-cols-2">
@foreach ($latestComments as $comment) @foreach ($latestComments as $comment)
@if ($comment->commentable_type == 'App\Models\Episode') @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 $episode = cache()->rememberForever('commentEpisode'.$comment->commentable_id, fn () => App\Models\Episode::with('gallery')->where('id', $comment->commentable_id)->first()); @endphp
<div id="comments" class="flex p-4 bg-white rounded-lg dark:bg-neutral-950"> <div id="comments" class="flex p-4 bg-white rounded-lg dark:bg-neutral-950">
<div <div
class="w-[20vw] mr-5 p-1 md:p-2 mb-8 relative transition ease-in-out hover:-translate-y-1 hover:scale-110 duration-300"> 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">
<a class="hidden hover:text-blue-600 xl:block" <a class="hidden 2xl:block"
href="{{ route('hentai.index', ['title' => $episode->slug]) }}"> href="{{ route('hentai.index', ['title' => $episode->slug]) }}">
<img alt="{{ $episode->title }} - {{ $episode->episode }}" loading="lazy" width="1000" <img alt="{{ $episode->title }} - {{ $episode->episode }}" loading="lazy" width="1000"
class="block object-cover object-center relative z-20 rounded-lg aspect-video" class="block object-cover object-center relative z-20 rounded-lg aspect-video"
@@ -18,28 +18,28 @@
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"> 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> {{ $episode->getResolution() }}</p>
<div class="absolute w-[95%] grid grid-cols-1 text-center"> <div class="absolute w-[95%] grid grid-cols-1 text-center">
<p class="text-sm text-center text-black dark:text-white">{{ $episode->title }} - <p class="text-sm text-center text-black dark:text-white truncate">{{ $episode->title }} -
{{ $episode->episode }}</p> {{ $episode->episode }}</p>
</div> </div>
</a> </a>
<a class="block hover:text-blue-600 xl:hidden" <a class="block 2xl:hidden"
href="{{ route('hentai.index', ['title' => $episode->slug]) }}"> href="{{ route('hentai.index', ['title' => $episode->slug]) }}">
<img alt="{{ $episode->title }} - {{ $episode->episode }}" loading="lazy" width="1000" <img alt="{{ $episode->title }} - {{ $episode->episode }}" loading="lazy" width="1000"
class="block object-cover object-center relative z-20 rounded-lg" class="block object-cover object-center relative z-20 rounded-lg"
src="{{ $episode->cover_url }}"></img> src="{{ $episode->cover_url }}"></img>
</a> </a>
</div> </div>
<div class="w-[60vw]"> <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> </div>
@elseif($comment->commentable_type == 'App\Models\Hentai') @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 @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 id="comments" class="flex p-4 bg-white rounded-lg dark:bg-neutral-950">
<div <div
class="w-[20vw] mr-5 p-1 md:p-2 mb-8 relative transition ease-in-out hover:-translate-y-1 hover:scale-110 duration-300"> 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="hover:text-blue-600" href="{{ route('hentai.index', ['title' => $hentai->slug]) }}"> <a class="hidden 2xl:block" href="{{ route('hentai.index', ['title' => $hentai->slug]) }}">
<img alt="{{ $hentai->episodes->first()->title }}" loading="lazy" width="1000" <img alt="{{ $hentai->episodes->first()->title }}" loading="lazy" width="1000"
class="block object-cover object-center relative z-20 rounded-lg aspect-video" class="block object-cover object-center relative z-20 rounded-lg aspect-video"
src="{{ $hentai->gallery->first()->thumbnail_url }}"></img> src="{{ $hentai->gallery->first()->thumbnail_url }}"></img>
@@ -47,12 +47,19 @@
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"> 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> {{ $hentai->episodes->first()->getResolution() }}</p>
<div class="absolute w-[95%] grid grid-cols-1 text-center"> <div class="absolute w-[95%] grid grid-cols-1 text-center">
<p class="text-sm text-center text-black dark:text-white"> <p class="text-sm text-center text-black dark:text-white truncate">
{{ $hentai->episodes->first()->title }}</p> {{ $hentai->episodes->first()->title }}</p>
</div> </div>
</a> </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>
<div class="w-[60vw]"> <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> </div>

View File

@@ -45,6 +45,10 @@
<a target="_blank" href="https://hentaisites.com/" <a target="_blank" href="https://hentaisites.com/"
class="hover:underline md:mr-6">hentaisites.com</a> class="hover:underline md:mr-6">hentaisites.com</a>
</li> </li>
<li>
<a target="_blank" href="https://zhentube.com/"
class="hover:underline md:mr-6">zhentube.com</a>
</li>
</ul> </ul>
<ul class="flex flex-wrap items-center mb-6 text-sm font-medium text-gray-500 sm:mb-0 dark:text-gray-400"> <ul class="flex flex-wrap items-center mb-6 text-sm font-medium text-gray-500 sm:mb-0 dark:text-gray-400">
<li> <li>

View File

@@ -94,11 +94,9 @@
<button <button
class="inline-flex items-center px-3 py-2 border text-sm leading-4 font-medium rounded-md text-gray-500 border-neutral-300/50 dark:text-gray-400 bg-white/20 dark:bg-neutral-950/20 dark:border-neutral-800/50 hover:text-gray-700 dark:hover:text-gray-300 focus:outline-none transition ease-in-out duration-150"> class="inline-flex items-center px-3 py-2 border text-sm leading-4 font-medium rounded-md text-gray-500 border-neutral-300/50 dark:text-gray-400 bg-white/20 dark:bg-neutral-950/20 dark:border-neutral-800/50 hover:text-gray-700 dark:hover:text-gray-300 focus:outline-none transition ease-in-out duration-150">
@auth @auth
@if (Auth::user()->discord_avatar) <img class="h-8 w-8 rounded-full object-cover mr-2"
<img class="h-8 w-8 rounded-full object-cover mr-2" src="{{ Auth::user()->getAvatar() }}"
src="https://external-content.duckduckgo.com/iu/?u=https://cdn.discordapp.com/avatars/{{ Auth::user()->discord_id }}/{{ Auth::user()->discord_avatar }}.webp" alt="{{ Auth::user()->name }}" />
alt="{{ Auth::user()->getUserName() }}" />
@endif
@else @else
<img class="h-8 w-8 rounded-full object-cover mr-2" src="/images/default-avatar.webp" <img class="h-8 w-8 rounded-full object-cover mr-2" src="/images/default-avatar.webp"
alt="Guest" /> alt="Guest" />
@@ -106,7 +104,7 @@
@auth @auth
<div style="display: flex; flex-direction: row; align-items: flex-start;"> <div style="display: flex; flex-direction: row; align-items: flex-start;">
{{ Auth::user()->getUserName() }} {{ Auth::user()->name }}
@if ($notAvailable) @if ($notAvailable)
<i class="fa-solid fa-bell text-rose-600"></i> <i class="fa-solid fa-bell text-rose-600"></i>
@endif @endif
@@ -231,15 +229,11 @@
<div class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-600 dark:bg-neutral-900/30"> <div class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-600 dark:bg-neutral-900/30">
<div class="flex justify-center"> <div class="flex justify-center">
@if (Auth::user()->discord_avatar) <img class="h-8 w-8 rounded-full object-cover mr-2"
<img class="h-8 w-8 rounded-full object-cover mr-2" src="{{ Auth::user()->getAvatar() }}"
src="https://external-content.duckduckgo.com/iu/?u=https://cdn.discordapp.com/avatars/{{ Auth::user()->discord_id }}/{{ Auth::user()->discord_avatar }}.webp" alt="{{ Auth::user()->name }}" />
alt="{{ Auth::user()->getUserName() }}" /> <span class="font-medium text-base text-gray-800 dark:text-neutral-200">
@else {{ Auth::user()->name }}
<img class="h-8 w-8 rounded-full object-cover mr-2" src="/images/default-avatar.webp"
alt="Guest" />
@endif
<span class="font-medium text-base text-gray-800 dark:text-neutral-200">{{ Auth::user()->name }}
</span> </span>
</div> </div>
@@ -302,8 +296,8 @@
<div class="pb-1 text-center w-full"> <div class="pb-1 text-center w-full">
<x-responsive-nav-link :href="route('login')"> <x-responsive-nav-link :href="route('login')">
<div <div
class="relative bg-blue-700 hover:bg-blue-600 text-white font-bold px-4 h-10 rounded text-center p-[10px]"> class="relative bg-rose-700 hover:bg-rose-600 text-white font-bold px-4 h-10 rounded text-center p-[10px]">
<i class="fa-brands fa-discord"></i> {{ __('nav.login') }} <i class="fa-solid fa-arrow-right-to-bracket"></i> {{ __('nav.login') }}
</div> </div>
</x-responsive-nav-link> </x-responsive-nav-link>
</div> </div>

View File

@@ -25,6 +25,8 @@
placeholder="Search..." placeholder="Search..."
> >
</th> </th>
<th scope="col" class="px-6 py-3">
</th>
<th scope="col" class="px-6 py-3"> <th scope="col" class="px-6 py-3">
Actions Actions
</th> </th>
@@ -34,17 +36,18 @@
@foreach($comments as $comment) @foreach($comments as $comment)
<tr wire:key="comment-{{ $comment->id }}" class="bg-white border-t dark:bg-neutral-800 dark:border-pink-700"> <tr wire:key="comment-{{ $comment->id }}" class="bg-white border-t dark:bg-neutral-800 dark:border-pink-700">
<td class="px-6 py-4"> <td class="px-6 py-4">
{{ $comment->name }} {{ $comment->user->name }}
</td> </td>
<th scope="row" class="px-6 py-4 font-medium text-gray-900 dark:text-white max-w-lg"> <th scope="row" class="px-6 py-4 font-medium text-gray-900 dark:text-white max-w-lg">
{{ $comment->comment }} {{ $comment->body }}
</th>
<th scope="row" class="px-6 py-4 font-medium text-gray-900 dark:text-white max-w-lg">
{{ $comment->created_at }}
</th> </th>
<td class="px-6 py-4"> <td class="px-6 py-4">
<a href="{{ route('comments.destroy', $comment->id) }}" onclick="event.preventDefault();document.getElementById('comment-delete-form-{{ $comment->id }}').submit();" class="inline-flex items-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 active:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150 mt-2">@lang('comments::comments.delete')</a> <button wire:click="deleteComment({{$comment->id}})" type="button" class="inline-flex items-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 active:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150 mt-2">
<form id="comment-delete-form-{{ $comment->id }}" action="{{ route('comments.destroy', $comment->id) }}" method="POST" style="display: none;"> Delete
@method('DELETE') </button>
@csrf
</form>
</td> </td>
</tr> </tr>
@endforeach @endforeach

View File

@@ -60,7 +60,7 @@
{{ $user->id }} {{ $user->id }}
</th> </th>
<td class="px-6 py-4"> <td class="px-6 py-4">
{{ $user->discord_name ?? $user->name }} {{ $user->name }}
</td> </td>
<td class="px-6 py-4"> <td class="px-6 py-4">
{{ $user->is_patreon ? 'Yes' : 'No' }} {{ $user->is_patreon ? 'Yes' : 'No' }}

View File

@@ -0,0 +1,121 @@
<div>
<div class="flex" id="comment-{{ $comment->id }}">
<div class="flex-shrink-0 mr-4">
<img class="h-10 w-10 rounded-full" src="{{ $comment->user->getAvatar() }}" alt="{{ $comment->user->name }}">
</div>
<div class="flex-grow">
<div class="flex gap-2">
<p class="font-medium text-gray-900 dark:text-gray-100">{{ $comment->user->name }}</p>
@if($comment->user->is_admin)
<a data-te-toggle="tooltip" title="Admin"><i class="fa-solid fa-crown text-yellow-600"></i></a>
@endif
@if($comment->user->is_patreon)
<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>
@endif
</div>
<div class="mt-1 flex-grow w-full">
@if ($isEditing)
<form wire:submit.prevent="editComment">
<div>
<label for="comment" class="sr-only">Comment 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('editState.body') border-red-500 @enderror"
placeholder="Write something" wire:model.defer="editState.body"></textarea>
@error('editState.body')
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
<div class="mt-3 flex items-center justify-between">
<button type="submit"
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">
Edit
</button>
</div>
</form>
@else
<div class="text-gray-700 dark:text-gray-200">{!! $comment->presenter()->markdownBody() !!}</div>
@endif
</div>
<div class="mt-2 space-x-2 flex flex-row">
<span class="text-gray-500 dark:text-gray-300">
{{ $comment->presenter()->relativeCreatedAt() }}
</span>
@guest
<span data-te-toggle="tooltip" title="Please login to like the episode" class="text-gray-800 cursor-pointer dark:text-gray-200">
<i class="fa-regular fa-heart"></i> {{ $comment->likeCount() }}
</span>
@endguest
@auth
<!-- Like Button -->
<button class="text-gray-800 dark:text-gray-200 leading-tight cursor-pointer whitespace-nowrap" wire:click="like">
@if ($liked)
<i class="fa-solid fa-heart text-rose-600"></i> {{ $likeCount }}
@else
<i class="fa-solid fa-heart"></i> {{ $likeCount }}
@endif
</button>
@endauth
@auth
@if ($comment->depth() < 2)
<button wire:click="$toggle('isReplying')" type="button" class="text-gray-900 dark:text-gray-100 font-medium">
Reply
</button>
@endif
@can ('update', $comment)
<button wire:click="$toggle('isEditing')" type="button" class="text-gray-900 dark:text-gray-100 font-medium">
Edit
</button>
@endcan
@can ('destroy', $comment)
<button x-data="{
confirmCommentDeletion () {
if (window.confirm('Are you sure you want to delete this comment?')) {
@this.call('deleteComment');
}
}
}"
@click="confirmCommentDeletion"
type="button"
class="text-gray-900 dark:text-gray-100 font-medium"
>
Delete
</button>
@endcan
@endauth
</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 class="mt-3 flex items-center justify-between">
<button type="submit"
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">
Comment
</button>
</div>
</form>
@endif
@foreach ($comment->children as $child)
<livewire:comment :comment="$child" :key="$child->id"/>
@endforeach
</div>
</div>

View File

@@ -0,0 +1,60 @@
<section>
<div class="bg-white dark:bg-neutral-800 shadow sm:rounded-lg sm:overflow-hidden">
<div class="divide-y divide-gray-200 dark:divide-gray-400/40">
<div class="px-4 py-5 sm:px-6">
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-200">Comments</h2>
</div>
<div>
<!-- Comment Input -->
<div class="bg-gray-50 dark:bg-neutral-800 px-4 py-6 sm:px-6">
@auth
<div class="flex">
<div class="flex-shrink-0 mr-4">
<img class="h-10 w-10 rounded-full" src="{{ auth()->user()->getAvatar() }}" alt="{{ auth()->user()->name }}">
</div>
<div class="min-w-0 flex-1">
<form wire:submit.prevent="postComment">
<div>
<label for="comment" class="sr-only">Comment body</label>
<textarea id="comment" name="comment" rows="3"
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
@error('newCommentState.body') border-red-500 @enderror"
placeholder="Write something" wire:model.defer="newCommentState.body"></textarea>
@error('newCommentState.body')
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
<div class="mt-3 flex items-center justify-between">
<button type="submit"
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">
Comment
</button>
</div>
</form>
</div>
</div>
@endauth
@guest
<p class="text-gray-900 dark:text-gray-200">Log in to comment.</p>
@endguest
</div>
<!-- Comments -->
<div class="px-4 py-6 sm:px-6">
<div class="space-y-8">
@if ($comments->isNotEmpty())
@foreach($comments as $comment)
<livewire:comment :comment="$comment" :key="$comment->id"/>
@endforeach
{{ $comments->links('pagination::tailwind') }}
@else
<p class="text-gray-900 dark:text-gray-200">No comments yet.</p>
@endif
</div>
</div>
</div>
</div>
</div>
</section>

View File

@@ -2,7 +2,7 @@
<div class="mx-auto sm:px-6 lg:px-8 space-y-6 max-w-[100%] lg:max-w-[90%] xl:max-w-[80%] 2xl:max-w-[60%] relative z-10"> <div class="mx-auto sm:px-6 lg:px-8 space-y-6 max-w-[100%] lg:max-w-[90%] xl:max-w-[80%] 2xl:max-w-[60%] relative z-10">
<!-- Search Filter --> <!-- Search Filter -->
<div class="p-4 sm:p-8 bg-white/30 dark:bg-neutral-950/40 shadow sm:rounded-lg backdrop-blur relative z-100"> <div class="p-4 sm:p-8 bg-white/30 dark:bg-neutral-950/40 shadow sm:rounded-lg backdrop-blur relative z-100">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 "> <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 ">
<!-- Title --> <!-- Title -->
<div> <div>
@@ -73,6 +73,23 @@
@endif @endif
</div> </div>
<!-- Studios -->
<div>
<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">
<i class="fa-solid fa-microphone-lines text-gray-500 dark:text-gray-400"></i>
</div>
<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">
@if($studiocount === 0)
Select Studios
@elseif($studiocount === 1)
Selected {{ $studiocount }} Studio
@elseif($studiocount > 1)
Selected {{ $studiocount }} Studios
@endif
</p>
</div>
</div>
<!-- Ordering --> <!-- Ordering -->
<div> <div>
@@ -247,4 +264,5 @@
</div> </div>
{{ $downloads->links('pagination::tailwind') }} {{ $downloads->links('pagination::tailwind') }}
</div> </div>
@include('modals.filter-studios')
</div> </div>

View File

@@ -5,12 +5,8 @@
<!-- Header --> <!-- Header -->
<div class="flex text-sm font-light bg-neutral-950/50 backdrop-blur-lg rounded-lg p-10 gap-2"> <div class="flex text-sm font-light bg-neutral-950/50 backdrop-blur-lg rounded-lg p-10 gap-2">
<div> <div>
@if ($playlist->user->discord_avatar) <img class="relative w-24 h-24 flex-none rounded-full shadow-lg"
<img class="relative w-24 h-24 flex-none rounded-full shadow-lg" src="{{ $playlist->user->getAvatar() }}">
src="https://external-content.duckduckgo.com/iu/?u=https://cdn.discordapp.com/avatars/{{ $playlist->user->discord_id }}/{{ $playlist->user->discord_avatar }}.webp">
@else
<img class="relative w-24 h-24 flex-none rounded-full shadow-lg" src="/images/default-avatar.webp">
@endif
</div> </div>
<div class="flex flex-col justify-center flex-1 pl-4"> <div class="flex flex-col justify-center flex-1 pl-4">
<h1 class="font-bold text-3xl">{{ $playlist->name }}</h1> <h1 class="font-bold text-3xl">{{ $playlist->name }}</h1>

View File

@@ -0,0 +1,130 @@
<div class="py-3 relative">
<div class="mx-auto sm:px-6 lg:px-8 space-y-6 max-w-[100%] lg:max-w-[90%] xl:max-w-[80%] 2xl:max-w-[90%] relative z-10">
<!-- Search Filter -->
<div class="p-4 sm:p-8 bg-white/30 dark:bg-neutral-950/40 shadow sm:rounded-lg backdrop-blur relative z-100">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 ">
<!-- Title -->
<div>
<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">
<div class="absolute inset-y-0 left-2 flex items-center pl-3 pointer-events-none">
<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">
<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>
</div>
<input wire:model.live.debounce.600ms="commentSearch" 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 comment..." required>
<div class="absolute right-0 top-[11px]" wire: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">
<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>
</div>
</div>
<!-- Ordering -->
<div>
<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">
<i class="fa-solid fa-sort text-gray-500 dark:text-gray-400"></i>
</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">
<option value="created_at_desc">Created DESC</option>
<option value="created_at_asc">Created ASC</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div
class="relative pt-5 mx-auto sm:px-6 lg:px-8 space-y-6 text-gray-900 dark:text-white max-w-[100%] lg:max-w-[90%] xl:max-w-[80%] 2xl:max-w-[90%] 2xl:w-[50vw]">
<div class="flex flex-col">
<div class="overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 sm:px-6 lg:px-8">
<div class="overflow-hidden">
<!-- Desktop -->
<div class="w-full text-left text-sm font-light">
<!-- Header -->
<div
class="flex bg-white/30 dark:bg-neutral-950/40 backdrop-blur font-medium dark:border-neutral-500 border-b rounded-tl-lg rounded-tr-lg">
<div class="flex-1 px-6 py-4 text-center">Comment</div>
</div>
<!-- Rows -->
@foreach ($comments as $comment)
<div wire:key="comment-{{ $comment->id }}"
class="flex flex-col sm:flex-row items-center border-b bg-white dark:bg-neutral-950 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-neutral-800">
<!-- Image -->
<div class="flex w-fit sm:w-56">
@if($comment->commentable_type == \App\Models\Episode::class)
@php $episode = \App\Models\Episode::find($comment->commentable_id); @endphp
<div class="relative p-1 w-full transition duration-300 ease-in-out md:p-2 md:hover:-translate-y-1 md:hover:scale-110">
<a 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 }}" />
<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>
</a>
</div>
@elseif($comment->commentable_type == \App\Models\Hentai::class)
@php
$hentai = \App\Models\Hentai::find($comment->commentable_id);
$episode = $hentai->episodes->first();
@endphp
<div class="relative p-1 w-full transition duration-300 ease-in-out md:p-2 md:hover:-translate-y-1 md:hover:scale-110">
<a href="{{ route('hentai.index', ['title' => $hentai->slug]) }}">
<img alt="{{ $episode->title }}" loading="lazy" width="1000"
class="block object-cover object-center relative z-20 rounded-lg aspect-video"
src="{{ $episode->gallery->first()->thumbnail_url }}" />
</a>
</div>
@endif
</div>
<!-- Body -->
<div class="flex text-lg flex-1 items-center space-x-2 px-3 py-2 bg-neutral-200 dark:bg-neutral-900 h-[115px] rounded-lg sm:mr-2">
{!! $comment->presenter()->markdownBody() !!}
</div>
<div class="space-x-2 sm:mr-2 w-24">
<span class="text-gray-500 dark:text-gray-300 font-medium">
{{ $comment->presenter()->relativeCreatedAt() }}
</span>
</div>
</div>
@endforeach
</div>
</div>
</div>
</div>
</div>
{{ $comments->links('pagination::tailwind') }}
</div>
</div>

View File

@@ -1,26 +1,26 @@
@inject('markdown', 'Parsedown') <div>
@php <div class="flex">
// TODO: There should be a better place for this. <div class="flex-shrink-0 mr-4">
$markdown->setSafeMode(true); <img class="h-10 w-10 rounded-full" src="{{ $comment->user->getAvatar() }}" alt="{{ $comment->user->name }}">
@endphp </div>
<div id="comment-{{ $comment->id }}" class="flex rounded-lg bg-white p-1 mb-2 shadow-[0_2px_15px_-3px_rgba(0,0,0,0.07),0_10px_20px_-2px_rgba(0,0,0,0.04)] dark:bg-neutral-900"> <div class="flex-grow">
@php $user = cache()->rememberForever('commentUser'.$comment->commenter_id, fn () => \App\Models\User::where('id', $comment->commenter_id)->first()); @endphp <div class="flex gap-2">
<div> <p class="font-medium text-gray-900 dark:text-gray-100">{{ $comment->user->name }}</p>
@if($user->discord_avatar) @if($comment->user->is_admin)
<img class="w-16 h-16 rounded-full m-2" src="https://external-content.duckduckgo.com/iu/?u=https://cdn.discordapp.com/avatars/{{ $user->discord_id }}/{{ $user->discord_avatar }}.webp" alt="{{ $user->discord_name ?? $user->name }} Avatar"> <a data-te-toggle="tooltip" title="Admin"><i class="fa-solid fa-crown text-yellow-600"></i></a>
@else @endif
<img class="w-16 h-16 rounded-full m-2" src="/images/default-avatar.webp" alt="{{ $user->discord_name ?? $user->name }} Avatar"> @if($comment->user->is_patreon)
@endif <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>
</div> @endif
<div class="text-gray-800 dark:text-gray-200"> </div>
<div> <div class="mt-1 flex-grow w-full">
@if($user->is_patreon) <div class="text-gray-700 dark:text-gray-200">{!! $comment->presenter()->markdownBody() !!}</div>
<h5 class="text-gray-800 dark:text-gray-400">{{ $user->discord_name ?? $user->name }} <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 animate-pulse"></i></a> <small class="text-muted">- {{ \Carbon\Carbon::parse($comment->created_at)->diffForHumans() }}</small></h5> </div>
@else <div class="mt-2 space-x-2">
<h5 class="text-gray-800 dark:text-gray-400">{{ $user->discord_name ?? $user->name }} <small class="text-muted">- {{ \Carbon\Carbon::parse($comment->created_at)->diffForHumans() }}</small></h5> <span class="text-gray-500 dark:text-gray-300 font-medium">
@endif {{ $comment->presenter()->relativeCreatedAt() }}
</span>
</div>
</div> </div>
<div style="white-space: pre-wrap;">{!! $markdown->line($comment->comment) !!}</div>
<br />
</div> </div>
</div> </div>

View File

@@ -12,63 +12,10 @@
class="relative max-w-[120rem] mx-auto sm:px-6 lg:px-8 space-y-6 pt-20 mt-[65px] flex flex-row justify-center xl:justify-normal"> class="relative max-w-[120rem] mx-auto sm:px-6 lg:px-8 space-y-6 pt-20 mt-[65px] flex flex-row justify-center xl:justify-normal">
<div class="flex flex-col xl:flex-row"> <div class="flex flex-col xl:flex-row">
@include('profile.partials.sidebar') @include('profile.partials.sidebar')
<div class="pb-2 space-y-6 max-w-7xl px-0 md:px-6 lg:px-8 pt-8 xl:pt-0"> <div class="pb-2 space-y-6 max-w-7xl px-0 md:px-6 lg:px-8 pt-8 xl:pt-0 flex flex-row">
<livewire:user-comments :model="$user"/>
@php
$episode_ids = array_unique(
DB::table('comments')
->where('commenter_id', $user->id)
->get()
->pluck('commentable_id')
->toArray(),
);
@endphp
@foreach ($episode_ids as $episode_id)
@php $episode = App\Models\Episode::where('id', $episode_id)->first(); @endphp
<div
class="flex flex-col 2xl:flex-row p-5 bg-white/40 dark:bg-neutral-950/40 backdrop-blur rounded-lg items-center">
<div
class="w-[20vw] 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 hover:text-blue-600 md:block"
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>
<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">
{{ $episode->getResolution() }}</p>
<p
class="absolute left-2 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>
<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>
<a class="block hover:text-blue-600 md:hidden"
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"
src="{{ $episode->cover_url }}"></img>
<div class="relative 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>
<div class="md:w-[60vw]">
@comments(['model' => $episode])
</div>
</div>
@endforeach
</div> </div>
</div> </div>
</div> </div>

View File

@@ -14,42 +14,4 @@
x-on:click.prevent="$dispatch('open-modal', 'confirm-user-deletion')" x-on:click.prevent="$dispatch('open-modal', 'confirm-user-deletion')"
>{{ __('Delete Account') }}</x-danger-button> >{{ __('Delete Account') }}</x-danger-button>
<x-modal name="confirm-user-deletion" :show="$errors->userDeletion->isNotEmpty()" focusable>
<form method="POST" action="{{ route('profile.delete') }}" class="p-6">
@csrf
@method('delete')
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __('Are you sure you want to delete your account?') }}
</h2>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }}
</p>
{{-- <div class="mt-6">
<x-input-label for="password" value="{{ __('Password') }}" class="sr-only" />
<x-text-input
id="password"
name="password"
type="password"
class="mt-1 block w-3/4"
placeholder="{{ __('Password') }}"
/>
<x-input-error :messages="$errors->userDeletion->get('password')" class="mt-2" />
</div> --}}
<div class="mt-6 flex justify-end">
<x-secondary-button x-on:click="$dispatch('close')">
{{ __('Cancel') }}
</x-secondary-button>
<x-danger-button class="ms-3">
{{ __('Delete Account') }}
</x-danger-button>
</div>
</form>
</x-modal>
</section> </section>

View File

@@ -0,0 +1,46 @@
<x-modal name="confirm-user-deletion" :show="$errors->userDeletion->isNotEmpty()" focusable>
<form method="POST" action="{{ route('profile.delete') }}" class="p-6">
@csrf
@method('delete')
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __('Are you sure you want to delete your account?') }}
</h2>
@if (is_null($user->password))
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{{ __('Once your account is deleted, all of its resources and data will be permanently deleted.') }}
</p>
@else
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }}
</p>
@endif
@if (!is_null($user->password))
<div class="mt-6">
<x-input-label for="password" value="{{ __('Password') }}" class="sr-only" />
<x-text-input
id="password"
name="password"
type="password"
class="mt-1 block w-3/4"
placeholder="{{ __('Password') }}"
/>
<x-input-error :messages="$errors->userDeletion->get('password')" class="mt-2" />
</div>
@endif
<div class="mt-6 flex justify-end">
<x-secondary-button x-on:click="$dispatch('close')">
{{ __('Cancel') }}
</x-secondary-button>
<x-danger-button class="ms-3">
{{ __('Delete Account') }}
</x-danger-button>
</div>
</form>
</x-modal>

View File

@@ -7,17 +7,33 @@
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400"> <p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{{ __('Ensure your account is using a long, random password to stay secure.') }} {{ __('Ensure your account is using a long, random password to stay secure.') }}
</p> </p>
@if ($user->discord_id && is_null($user->password))
<div class="p-2 rounded-lg bg-rose-600/80 mt-4">
<p class="p-2 text-sm dark:text-gray-200 text-white">
{{ __('You currently don\'t have a password set, as you use Discord authentication. You can set a password to be able to login with email & password.') }}
</p>
</div>
@elseif ($user->discord_id && !is_null($user->password))
<div class="p-2 rounded-lg bg-green-600/80 mt-4">
<p class="p-2 text-sm dark:text-gray-200 text-white">
{{ __('Both Discord and email login are enabled.') }}
</p>
</div>
@endif
</header> </header>
<form method="post" action="{{ route('password.update') }}" class="mt-6 space-y-6"> <form method="post" action="{{ route('password.update') }}" class="mt-6 space-y-6">
@csrf @csrf
@method('put') @method('put')
@if (!(is_null($user->password) && $user->discord_id))
<div> <div>
<x-input-label for="update_password_current_password" :value="__('Current Password')" /> <x-input-label for="update_password_current_password" :value="__('Current Password')" />
<x-text-input id="update_password_current_password" name="current_password" type="password" class="mt-1 block w-full" autocomplete="current-password" /> <x-text-input id="update_password_current_password" name="current_password" type="password" class="mt-1 block w-full" autocomplete="current-password" />
<x-input-error :messages="$errors->updatePassword->get('current_password')" class="mt-2" /> <x-input-error :messages="$errors->updatePassword->get('current_password')" class="mt-2" />
</div> </div>
@endif
<div> <div>
<x-input-label for="update_password_password" :value="__('New Password')" /> <x-input-label for="update_password_password" :value="__('New Password')" />

View File

@@ -4,36 +4,64 @@
{{ __('Profile Information') }} {{ __('Profile Information') }}
</h2> </h2>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400"> @if ($user->discord_id)
{{ __("Update your account's profile information and email address.") }} <div class="p-2 rounded-lg bg-rose-600/80 mt-4">
</p> <p class="p-2 text-sm dark:text-gray-200 text-white">
{{ __('Changing your name or email will not affect Discord authentication, as your Discord ID has been stored.') }}
</p>
</div>
@else
<div class="p-2 rounded-lg bg-green-600/80 mt-4">
<p class="p-2 text-sm dark:text-gray-200 text-white">
{{ __('If you want to use Discord authentication, ensure the email addresses match for initial login. After login with Discord, email can be changed.') }}
</p>
</div>
@endif
</header> </header>
<form id="send-verification" method="post" action="{{ route('verification.send') }}"> <form id="send-verification" method="post" action="{{ route('verification.send') }}">
@csrf @csrf
</form> </form>
<form method="post" action="{{ route('profile.update') }}" class="mt-6 space-y-6"> <form method="post" action="{{ route('profile.update') }}" class="mt-6 space-y-6" enctype="multipart/form-data">
@csrf @csrf
@method('patch') @method('patch')
<div>
<x-input-label for="image" :value="__('Avatar')" />
<div class="mt-2 flex items-center gap-4">
<img
src="{{ $user->getAvatar() }}"
alt="{{ $user->name }}"
class="h-16 w-16 rounded-full object-cover"
>
<input
id="image"
name="image"
type="file"
accept="image/*"
class="block w-full text-sm text-gray-900 dark:text-gray-100
file:mr-4 file:rounded-md file:border-0
file:bg-rose-600 file:px-4 file:py-2
file:text-sm file:font-semibold file:text-white
hover:file:bg-rose-500"
/>
</div>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
JPG, PNG, WebP or GIF. Max 8MB. Will be cropped to 128×128.
</p>
<x-input-error class="mt-2" :messages="$errors->get('image')" />
</div>
<div> <div>
<x-input-label for="name" :value="__('Name')" /> <x-input-label for="name" :value="__('Name')" />
<x-text-input id="name" name="name" type="text" class="mt-1 block w-full" :value="old('name', $user->name)" required autofocus autocomplete="name" disabled /> <x-text-input id="name" name="name" type="text" class="mt-1 block w-full" :value="old('name', $user->name)" required autofocus autocomplete="name" />
<x-input-error class="mt-2" :messages="$errors->get('name')" /> <x-input-error class="mt-2" :messages="$errors->get('name')" />
</div> </div>
@if (Auth::user()->discord_name)
<div>
<x-input-label for="discord_name" :value="__('Discord Name')" />
<x-text-input id="discord_name" name="discord_name" type="text" class="mt-1 block w-full" :value="old('discord_name', $user->discord_name)" required autocomplete="discord_name" disabled />
<x-input-error class="mt-2" :messages="$errors->get('discord_name')" />
</div>
@endif
<div> <div>
<x-input-label for="email" :value="__('Email')" /> <x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" name="email" type="email" class="mt-1 block w-full" :value="old('email', $user->email)" required autocomplete="email" disabled /> <x-text-input id="email" name="email" type="email" class="mt-1 block w-full" :value="old('email', $user->email)" required autocomplete="email" />
<x-input-error class="mt-2" :messages="$errors->get('email')" /> <x-input-error class="mt-2" :messages="$errors->get('email')" />
@if ($user instanceof \Illuminate\Contracts\Auth\MustVerifyEmail && ! $user->hasVerifiedEmail()) @if ($user instanceof \Illuminate\Contracts\Auth\MustVerifyEmail && ! $user->hasVerifiedEmail())
@@ -55,7 +83,7 @@
@endif @endif
</div> </div>
{{-- <div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<x-primary-button>{{ __('Save') }}</x-primary-button> <x-primary-button>{{ __('Save') }}</x-primary-button>
@if (session('status') === 'profile-updated') @if (session('status') === 'profile-updated')
@@ -67,6 +95,6 @@
class="text-sm text-gray-600 dark:text-gray-400" class="text-sm text-gray-600 dark:text-gray-400"
>{{ __('Saved.') }}</p> >{{ __('Saved.') }}</p>
@endif @endif
</div> --}} </div>
</form> </form>
</section> </section>

View File

@@ -8,13 +8,16 @@
<!-- Page Content --> <!-- Page Content -->
<main> <main>
@include('user.partials.background') @include('user.partials.background')
<div class="relative max-w-[120rem] mx-auto sm:px-6 lg:px-8 space-y-6 pt-20 mt-[65px] flex flex-row"> <div class="relative max-w-[120rem] mx-auto sm:px-6 lg:px-8 space-y-6 pt-20 mt-[65px] mb-14 flex flex-row">
<div class="flex flex-col md:flex-row"> <div class="flex flex-col md:flex-row">
@include('profile.partials.sidebar') @include('profile.partials.sidebar')
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8 mt-8 md:mt-0 space-y-6">
<div class="p-4 sm:p-8 bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow sm:rounded-lg"> <div class="p-4 sm:p-8 bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow sm:rounded-lg">
@include('profile.partials.update-profile-information-form') @include('profile.partials.update-profile-information-form')
</div> </div>
<div class="p-4 sm:p-8 bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow sm:rounded-lg">
@include('profile.partials.update-password-form')
</div>
<div class="p-4 sm:p-8 bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow sm:rounded-lg"> <div class="p-4 sm:p-8 bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow sm:rounded-lg">
@include('profile.partials.update-blacklist-form') @include('profile.partials.update-blacklist-form')
</div> </div>
@@ -24,6 +27,7 @@
<div class="p-4 sm:p-8 bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow sm:rounded-lg"> <div class="p-4 sm:p-8 bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow sm:rounded-lg">
@include('profile.partials.delete-user-form') @include('profile.partials.delete-user-form')
</div> </div>
@include('profile.partials.delete-user-modal')
</div> </div>
@vite(['resources/js/user-blacklist.js']) @vite(['resources/js/user-blacklist.js'])
</div> </div>

View File

@@ -16,7 +16,7 @@
@include('series.partials.episodes') @include('series.partials.episodes')
@include('series.partials.comments') <livewire:comments :model="$hentai"/>
</div> </div>
@include('series.partials.popular') @include('series.partials.popular')

View File

@@ -1,8 +0,0 @@
<div class="bg-transparent rounded-lg overflow-hidden bg-white dark:bg-neutral-800 p-5">
<div id="comments" class="grid grid-cols-1 bg-transparent rounded-lg">
<p class="leading-normal font-bold text-lg text-rose-600">
{{ __('home.latest-comments') }}
</p>
@comments(['model' => $hentai])
</div>
</div>

View File

@@ -26,7 +26,7 @@
<!-- Infos --> <!-- Infos -->
@include('stream.partials.info') @include('stream.partials.info')
<!-- Comments --> <!-- Comments -->
@include('stream.partials.comments') <livewire:comments :model="$episode"/>
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
@if(! $isMobile) @if(! $isMobile)

View File

@@ -1,8 +0,0 @@
<div class="bg-transparent rounded-lg overflow-hidden bg-white dark:bg-neutral-700/40 p-5">
<div id="comments" class="grid grid-cols-1 bg-transparent rounded-lg">
<p class="leading-normal font-bold text-lg text-rose-600">
{{ __('home.latest-comments') }}
</p>
@comments(['model' => $episode])
</div>
</div>

View File

@@ -22,7 +22,7 @@
} }
@endphp @endphp
<p class="text-neutral-800 dark:text-neutral-300"> <p class="text-neutral-800 dark:text-neutral-300">
{{ $playlist->user->discord_name ?? $playlist->user->name }} {{ $currentIndex + 1 }}/{{ $episodeCount }} Episodes {{ $playlist->user->name }} {{ $currentIndex + 1 }}/{{ $episodeCount }} Episodes
</p> </p>
</div> </div>

View File

@@ -1,14 +1,9 @@
<div <div
class="overflow-hidden relative max-w-sm min-w-80 mx-auto bg-white/40 shadow-lg ring-1 ring-black/5 rounded-xl flex items-center gap-6 dark:bg-neutral-950/40 backdrop-blur dark:highlight-white/5"> class="overflow-hidden relative max-w-sm min-w-80 mx-auto bg-white/40 shadow-lg ring-1 ring-black/5 rounded-xl flex items-center gap-6 dark:bg-neutral-950/40 backdrop-blur dark:highlight-white/5">
@if($user->discord_avatar) <img class="absolute -left-6 w-24 h-24 rounded-full shadow-lg" src="{{ $user->getAvatar() }}">
<img class="absolute -left-6 w-24 h-24 rounded-full shadow-lg"
src="https://external-content.duckduckgo.com/iu/?u=https://cdn.discordapp.com/avatars/{{ $user->discord_id }}/{{ $user->discord_avatar }}.webp">
@else
<img class="absolute -left-6 w-24 h-24 rounded-full shadow-lg" src="/images/default-avatar.webp">
@endif
<div class="flex flex-col py-5 pl-24"> <div class="flex flex-col py-5 pl-24">
<strong class="text-slate-900 text-xl font-bold dark:text-slate-200"> <strong class="text-slate-900 text-xl font-bold dark:text-slate-200">
{{ $user->discord_name ?? $user->name }} {{ $user->name }}
@if ($user->is_patreon) @if ($user->is_patreon)
<a data-te-toggle="tooltip" title="Badge of appreciation for the horny people supporting us! :3"><i <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 animate-pulse"></i></a> class="fa-solid fa-hand-holding-heart text-rose-600 animate-pulse"></i></a>

View File

@@ -1,91 +0,0 @@
@inject('markdown', 'Parsedown')
@php
// TODO: There should be a better place for this.
$markdown->setSafeMode(true);
@endphp
<div id="comment-{{ $comment->getKey() }}" class="flex rounded-lg p-1 mb-2 ">
<div class="contents">
@if($comment->commenter->discord_avatar)
<img class="w-12 h-12 rounded-lg m-2" src="https://external-content.duckduckgo.com/iu/?u=https://cdn.discordapp.com/avatars/{{ $comment->commenter->discord_id }}/{{ $comment->commenter->discord_avatar }}.webp" alt="{{ $comment->commenter->discord_name ?? $comment->commenter->name }} Avatar">
@else
<img class="w-12 h-12 rounded-lg m-2" src="/images/default-avatar.webp" alt="{{ $comment->commenter->discord_name ?? $comment->commenter->name }} Avatar">
@endif
</div>
<div class="text-gray-800 dark:text-gray-200">
<div>
@if($comment->commenter->is_patreon)
<h5 class="text-gray-800 dark:text-gray-400">{{ $comment->commenter->discord_name ?? $comment->commenter->name }} <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 animate-pulse"></i></a> <small class="text-muted">- {{ $comment->created_at->diffForHumans() }}</small></h5>
@else
<h5 class="text-gray-800 dark:text-gray-400">{{ $comment->commenter->discord_name ?? $comment->commenter->name }} <small class="text-muted">- {{ $comment->created_at->diffForHumans() }}</small></h5>
@endif
</div>
<div style="white-space: pre-wrap;">{!! $markdown->line($comment->comment) !!}</div>
@if (! Illuminate\Support\Facades\Route::is('profile.comments'))
<div>
@can('reply-to-comment', $comment)
<button data-te-toggle="modal" data-te-target="#reply-modal-{{ $comment->getKey() }}" class="inline-flex items-center px-4 py-2 mt-2 dark:focus:ring-offset-gray-800 bg-rose-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-rose-700 focus:bg-rose-700 active:bg-rose-900 focus:outline-none focus:ring-2 focus:ring-rose-500 focus:ring-offset-2 transition ease-in-out duration-150">@lang('comments::comments.reply')</button>
@endcan
@can('edit-comment', $comment)
<button data-te-toggle="modal" data-te-target="#comment-modal-{{ $comment->getKey() }}" class="inline-flex items-center px-4 py-2 mt-2 dark:focus:ring-offset-gray-800 bg-rose-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-rose-700 focus:bg-rose-700 active:bg-rose-900 focus:outline-none focus:ring-2 focus:ring-rose-500 focus:ring-offset-2 transition ease-in-out duration-150">@lang('comments::comments.edit')</button>
@endcan
@can('delete-comment', $comment)
<a href="{{ route('comments.destroy', $comment->getKey()) }}" onclick="event.preventDefault();document.getElementById('comment-delete-form-{{ $comment->getKey() }}').submit();" class="inline-flex items-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 active:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150 mt-2">@lang('comments::comments.delete')</a>
<form id="comment-delete-form-{{ $comment->getKey() }}" action="{{ route('comments.destroy', $comment->getKey()) }}" method="POST" style="display: none;">
@method('DELETE')
@csrf
</form>
@endcan
</div>
@endif
@can('edit-comment', $comment)
@include('modals.comment-edit')
@endcan
@can('reply-to-comment', $comment)
@include('modals.comment-reply')
@endcan
<br />{{-- Margin bottom --}}
<?php
if (!isset($indentationLevel)) {
$indentationLevel = 1;
} else {
$indentationLevel++;
}
?>
{{-- Recursion for children --}}
@if($grouped_comments->has($comment->getKey()) && $indentationLevel <= $maxIndentationLevel)
{{-- TODO: Don't repeat code. Extract to a new file and include it. --}}
@foreach($grouped_comments[$comment->getKey()] as $child)
<div class="flex">
<div class="h-[100px] bg-rose-600 w-[4px] rounded-lg"></div>
@include('comments::_comment', [
'comment' => $child,
'grouped_comments' => $grouped_comments
])
</div>
@endforeach
@endif
</div>
</div>
{{-- Recursion for children --}}
@if($grouped_comments->has($comment->getKey()) && $indentationLevel > $maxIndentationLevel)
{{-- TODO: Don't repeat code. Extract to a new file and include it. --}}
@foreach($grouped_comments[$comment->getKey()] as $child)
@include('comments::_comment', [
'comment' => $child,
'grouped_comments' => $grouped_comments
])
@endforeach
@endif

View File

@@ -1,30 +0,0 @@
<div class="pt-5">
@if($errors->has('commentable_type'))
<div class="alert alert-danger" role="alert">
{{ $errors->first('commentable_type') }}
</div>
@endif
@if($errors->has('commentable_id'))
<div class="alert alert-danger" role="alert">
{{ $errors->first('commentable_id') }}
</div>
@endif
<form class="block rounded-lg p-6 dark:bg-neutral-700/40" method="POST" action="{{ route('comments.store') }}">
@csrf
@honeypot
<input type="hidden" name="commentable_type" value="\{{ get_class($model) }}" />
<input type="hidden" name="commentable_id" value="{{ $model->getKey() }}" />
<div class="pb-5">
<label class="mb-2 text-xl font-medium leading-tight text-gray-800 dark:text-gray-200 w-full" for="message">@lang('comments::comments.enter_your_message_here')</label>
<textarea 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 @if($errors->has('message')) is-invalid @endif" name="message" rows="3"></textarea>
</div>
<x-primary-button>
@lang('comments::comments.submit')
</x-primary-button>
</form>
</div>
<br />

View File

@@ -1,80 +0,0 @@
@php
if (isset($approved) and $approved == true) {
$comments = $model->approvedComments;
} else {
$comments = $model->comments;
}
@endphp
@if($comments->count() < 1)
<div class="mb-4 rounded-lg text-black px-6 py-5 text-base dark:text-neutral-50 bg-white dark:bg-neutral-700/40">@lang('comments::comments.there_are_no_comments')</div>
@endif
<div>
@php
$comments = $comments->sortByDesc('created_at');
if (isset($perPage)) {
$page = request()->query('page', 1) - 1;
$parentComments = $comments->where('child_id', '');
$slicedParentComments = $parentComments->slice($page * $perPage, $perPage);
$m = Config::get('comments.model'); // This has to be done like this, otherwise it will complain.
$modelKeyName = (new $m)->getKeyName(); // This defaults to 'id' if not changed.
$slicedParentCommentsIds = $slicedParentComments->pluck($modelKeyName)->toArray();
// Remove parent Comments from comments.
$comments = $comments->where('child_id', '!=', '');
$grouped_comments = new \Illuminate\Pagination\LengthAwarePaginator(
$slicedParentComments->merge($comments)->groupBy('child_id'),
$parentComments->count(),
$perPage
);
$grouped_comments->withPath(request()->url());
} else {
$grouped_comments = $comments->groupBy('child_id');
}
@endphp
@foreach($grouped_comments as $comment_id => $comments)
{{-- Process parent nodes --}}
@if($comment_id == '')
@foreach($comments as $comment)
@include('comments::_comment', [
'comment' => $comment,
'grouped_comments' => $grouped_comments,
'maxIndentationLevel' => $maxIndentationLevel ?? 3
])
@endforeach
@endif
@endforeach
</div>
@isset ($perPage)
{{ $grouped_comments->links() }}
@endisset
@if ((! Illuminate\Support\Facades\Route::is('profile.comments')) && (! Illuminate\Support\Facades\Route::is('home.index')))
@auth
@include('comments::_form')
@elseif(Config::get('comments.guest_commenting') == true)
@include('comments::_form', [
'guest_commenting' => true
])
@else
<div class="block rounded-lg p-6 shadow-[0_2px_15px_-3px_rgba(0,0,0,0.07),0_10px_20px_-2px_rgba(0,0,0,0.04)] bg-white dark:bg-neutral-700/40">
<div class="card-body">
<h5 class="mb-2 text-xl font-medium leading-tight text-gray-800 dark:text-gray-200 w-full">@lang('comments::comments.authentication_required')</h5>
<p class="mb-2 leading-tight text-gray-800 dark:text-gray-200 w-full">@lang('comments::comments.you_must_login_to_post_a_comment')</p>
<br>
<a href="{{ route('login') }}" class="relative bg-blue-700 hover:bg-blue-600 text-white font-bold px-6 h-10 rounded pt-2 pb-2" style="width: 12px">
<i class="fa-brands fa-discord"></i> Login
</a>
</div>
</div>
@endauth
@endif

View File

@@ -0,0 +1,24 @@
@props([
'url',
'color' => 'primary',
'align' => 'center',
])
<table class="action" align="{{ $align }}" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="{{ $align }}">
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="{{ $align }}">
<table border="0" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td>
<a href="{{ $url }}" class="button button-{{ $color }}" target="_blank" rel="noopener">{!! $slot !!}</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>

View File

@@ -0,0 +1,11 @@
<tr>
<td>
<table class="footer" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="content-cell" align="center">
{{ Illuminate\Mail\Markdown::parse($slot) }}
</td>
</tr>
</table>
</td>
</tr>

View File

@@ -0,0 +1,12 @@
@props(['url'])
<tr>
<td class="header">
<a href="{{ $url }}" style="display: inline-block;">
@if (trim($slot) === 'Laravel')
<img src="https://laravel.com/img/notification-logo.png" class="logo" alt="Laravel Logo">
@else
{!! $slot !!}
@endif
</a>
</td>
</tr>

View File

@@ -0,0 +1,58 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>{{ config('app.name') }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="color-scheme" content="light">
<meta name="supported-color-schemes" content="light">
<style>
@media only screen and (max-width: 600px) {
.inner-body {
width: 100% !important;
}
.footer {
width: 100% !important;
}
}
@media only screen and (max-width: 500px) {
.button {
width: 100% !important;
}
}
</style>
{!! $head ?? '' !!}
</head>
<body>
<table class="wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<table class="content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
{!! $header ?? '' !!}
<!-- Email Body -->
<tr>
<td class="body" width="100%" cellpadding="0" cellspacing="0" style="border: hidden !important;">
<table class="inner-body" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
<!-- Body content -->
<tr>
<td class="content-cell">
{!! Illuminate\Mail\Markdown::parse($slot) !!}
{!! $subcopy ?? '' !!}
</td>
</tr>
</table>
</td>
</tr>
{!! $footer ?? '' !!}
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,27 @@
<x-mail::layout>
{{-- Header --}}
<x-slot:header>
<x-mail::header :url="config('app.url')">
<img src="{{ url('/images/hs_banner.png') }}" class="logo" alt="{{ config('app.name') }} Banner">
</x-mail::header>
</x-slot:header>
{{-- Body --}}
{!! $slot !!}
{{-- Subcopy --}}
@isset($subcopy)
<x-slot:subcopy>
<x-mail::subcopy>
{!! $subcopy !!}
</x-mail::subcopy>
</x-slot:subcopy>
@endisset
{{-- Footer --}}
<x-slot:footer>
<x-mail::footer>
© {{ date('Y') }} {{ config('app.name') }}. {{ __('All rights not reserved.') }}
</x-mail::footer>
</x-slot:footer>
</x-mail::layout>

View File

@@ -0,0 +1,14 @@
<table class="panel" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="panel-content">
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="panel-item">
{{ Illuminate\Mail\Markdown::parse($slot) }}
</td>
</tr>
</table>
</td>
</tr>
</table>

View File

@@ -0,0 +1,7 @@
<table class="subcopy" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td>
{{ Illuminate\Mail\Markdown::parse($slot) }}
</td>
</tr>
</table>

View File

@@ -0,0 +1,3 @@
<div class="table">
{{ Illuminate\Mail\Markdown::parse($slot) }}
</div>

View File

@@ -0,0 +1,295 @@
/* Base */
body,
body *:not(html):not(style):not(br):not(tr):not(code) {
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
position: relative;
}
body {
-webkit-text-size-adjust: none;
background-color: #000;
color: #fff;
height: 100%;
line-height: 1.4;
margin: 0;
padding: 0;
width: 100% !important;
}
p,
ul,
ol,
blockquote {
line-height: 1.4;
text-align: left;
}
a {
color: #3869d4;
}
a img {
border: none;
}
/* Typography */
h1 {
color: #fff;
font-size: 18px;
font-weight: bold;
margin-top: 0;
text-align: left;
}
h2 {
font-size: 16px;
font-weight: bold;
margin-top: 0;
text-align: left;
}
h3 {
font-size: 14px;
font-weight: bold;
margin-top: 0;
text-align: left;
}
p {
font-size: 16px;
line-height: 1.5em;
margin-top: 0;
text-align: left;
}
p.sub {
font-size: 12px;
}
img {
max-width: 100%;
}
/* Layout */
.wrapper {
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
background-color: #000;
margin: 0;
padding: 0;
width: 100%;
}
.content {
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
margin: 0;
padding: 0;
width: 100%;
}
/* Header */
.header {
padding: 25px 0;
text-align: center;
}
.header a {
color: #fff;
font-size: 19px;
font-weight: bold;
text-decoration: none;
}
/* Logo */
.logo {
height: 75px;
max-height: 75px;
width: 267px;
}
/* Body */
.body {
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
background-color: #000;
border-bottom: 1px solid #edf2f7;
border-top: 1px solid #edf2f7;
margin: 0;
padding: 0;
width: 100%;
}
.inner-body {
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 570px;
background-color: #212121;
border-color: #e8e5ef;
border-radius: 12px;
border-width: 1px;
box-shadow: 0 2px 0 rgba(0, 0, 150, 0.025), 2px 4px 0 rgba(0, 0, 150, 0.015);
margin: 0 auto;
padding: 0;
width: 570px;
}
.inner-body a {
word-break: break-all;
}
/* Subcopy */
.subcopy {
border-top: 1px solid #e8e5ef;
margin-top: 25px;
padding-top: 25px;
}
.subcopy p {
font-size: 14px;
}
/* Footer */
.footer {
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 570px;
margin: 0 auto;
padding: 0;
text-align: center;
width: 570px;
}
.footer p {
color: #b0adc5;
font-size: 12px;
text-align: center;
}
.footer a {
color: #b0adc5;
text-decoration: underline;
}
/* Tables */
.table table {
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
margin: 30px auto;
width: 100%;
}
.table th {
border-bottom: 1px solid #edeff2;
margin: 0;
padding-bottom: 8px;
}
.table td {
color: #74787e;
font-size: 15px;
line-height: 18px;
margin: 0;
padding: 10px 0;
}
.content-cell {
max-width: 100vw;
padding: 32px;
}
/* Buttons */
.action {
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
margin: 30px auto;
padding: 0;
text-align: center;
width: 100%;
float: unset;
}
.button {
-webkit-text-size-adjust: none;
border-radius: 12px;
color: #fff;
display: inline-block;
overflow: hidden;
text-decoration: none;
}
.button-blue,
.button-primary {
background-color: #be123c;
border-bottom: 8px solid #be123c;
border-left: 18px solid #be123c;
border-right: 18px solid #be123c;
border-top: 8px solid #be123c;
}
.button-green,
.button-success {
background-color: #48bb78;
border-bottom: 8px solid #48bb78;
border-left: 18px solid #48bb78;
border-right: 18px solid #48bb78;
border-top: 8px solid #48bb78;
}
.button-red,
.button-error {
background-color: #e53e3e;
border-bottom: 8px solid #e53e3e;
border-left: 18px solid #e53e3e;
border-right: 18px solid #e53e3e;
border-top: 8px solid #e53e3e;
}
/* Panels */
.panel {
border-left: #2d3748 solid 4px;
margin: 21px 0;
}
.panel-content {
background-color: #edf2f7;
color: #718096;
padding: 16px;
}
.panel-content p {
color: #718096;
}
.panel-item {
padding: 0;
}
.panel-item p:last-of-type {
margin-bottom: 0;
padding-bottom: 0;
}
/* Utilities */
.break-all {
word-break: break-all;
}

View File

@@ -0,0 +1 @@
{{ $slot }}: {{ $url }}

View File

@@ -0,0 +1 @@
{{ $slot }}

View File

@@ -0,0 +1 @@
{{ $slot }}: {{ $url }}

View File

@@ -0,0 +1,9 @@
{!! strip_tags($header ?? '') !!}
{!! strip_tags($slot) !!}
@isset($subcopy)
{!! strip_tags($subcopy) !!}
@endisset
{!! strip_tags($footer ?? '') !!}

View File

@@ -0,0 +1,27 @@
<x-mail::layout>
{{-- Header --}}
<x-slot:header>
<x-mail::header :url="config('app.url')">
{{ config('app.name') }}
</x-mail::header>
</x-slot:header>
{{-- Body --}}
{{ $slot }}
{{-- Subcopy --}}
@isset($subcopy)
<x-slot:subcopy>
<x-mail::subcopy>
{{ $subcopy }}
</x-mail::subcopy>
</x-slot:subcopy>
@endisset
{{-- Footer --}}
<x-slot:footer>
<x-mail::footer>
© {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.')
</x-mail::footer>
</x-slot:footer>
</x-mail::layout>

View File

@@ -0,0 +1 @@
{{ $slot }}

View File

@@ -0,0 +1 @@
{{ $slot }}

View File

@@ -0,0 +1 @@
{{ $slot }}

View File

@@ -66,7 +66,7 @@ Route::middleware('auth')->group(function () {
// User Profile Actions // User Profile Actions
Route::get('/user/settings', [ProfileController::class, 'settings'])->name('profile.settings'); Route::get('/user/settings', [ProfileController::class, 'settings'])->name('profile.settings');
Route::patch('/user/settings', [ProfileController::class, 'update'])->name('profile.update'); Route::patch('/user/settings', [ProfileController::class, 'update'])->name('profile.update');
Route::post('/user/delete', [ProfileController::class, 'destroy'])->name('profile.delete'); Route::delete('/user/delete', [ProfileController::class, 'destroy'])->name('profile.delete');
Route::post('/user/settings', [ProfileController::class, 'saveSettings'])->name('profile.settings.save'); Route::post('/user/settings', [ProfileController::class, 'saveSettings'])->name('profile.settings.save');
Route::get('/user/blacklist', [UserApiController::class, 'getBlacklist'])->name('profile.blacklist'); Route::get('/user/blacklist', [UserApiController::class, 'getBlacklist'])->name('profile.blacklist');
Route::post('/user/blacklist', [ProfileController::class, 'saveBlacklist'])->name('profile.blacklist.save'); Route::post('/user/blacklist', [ProfileController::class, 'saveBlacklist'])->name('profile.blacklist.save');