Compare commits
57 Commits
e972f8db41
...
laravel-12
| Author | SHA1 | Date | |
|---|---|---|---|
|
af739e3c88
|
|||
| 273ed65a8d | |||
| ccfd5b996b | |||
| e5ef197ed6 | |||
| c0be2e294a | |||
| 823a284fbc | |||
| 67e601d0c4 | |||
| 7e4ebd91ad | |||
| 4dc5dee2b9 | |||
| 5310908b0c | |||
| 4b05b3db6d | |||
| df47a926e4 | |||
| 1e9e95f35f | |||
| 2aa76baafd | |||
| aa50bb1f72 | |||
| dfedf4058e | |||
| 268e3eb4c2 | |||
| ab61574956 | |||
| 81038b6c26 | |||
| e949ba955a | |||
| 819e2fde27 | |||
| 3259e2197b | |||
| b133db0573 | |||
| 41c34e6d89 | |||
| db6da608aa | |||
| 13b70fdf23 | |||
| cfd6af59fb | |||
| 7810cd53fb | |||
| 871028930b | |||
| 6ce0255764 | |||
| e136e8e1b6 | |||
| a3b66b483b | |||
| 4c2a6024d7 | |||
| 5f575024e2 | |||
| 67f5d0db8b | |||
| 571bf4584c | |||
| d7dc96e11c | |||
| 58426b6e4e | |||
| 53b600daea | |||
| 224cdbcdc5 | |||
| 972d3d0aa4 | |||
| 8f7f012c14 | |||
| c0b068de58 | |||
| 51c67bb797 | |||
| 3d78f9e524 | |||
| 2d28a37463 | |||
| ac853920ee | |||
| fb3722036a | |||
| ab4e7c7999 | |||
| 8f99718058 | |||
| 2029af334c | |||
| b1c48830c4 | |||
| e100f3bf23 | |||
| c13d443696 | |||
| 8e7a56f559 | |||
| 30777a6968 | |||
| 256af435ad |
@@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
app/Enums/UserRole.php
Normal file
11
app/Enums/UserRole.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum UserRole: string
|
||||||
|
{
|
||||||
|
case ADMINISTRATOR = 'admin';
|
||||||
|
case MODERATOR = 'moderator';
|
||||||
|
case SUPPORTER = 'supporter';
|
||||||
|
case BANNED = 'banned';
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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')) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Enums\UserRole;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -31,11 +32,11 @@ class UserController extends Controller
|
|||||||
|
|
||||||
switch ($validated['action']) {
|
switch ($validated['action']) {
|
||||||
case 'ban':
|
case 'ban':
|
||||||
$user->update(['is_banned' => 1]);
|
$user->addRole(UserRole::BANNED);
|
||||||
alert()->success('Banned', 'User has been banned.');
|
alert()->success('Banned', 'User has been banned.');
|
||||||
break;
|
break;
|
||||||
case 'unban':
|
case 'unban':
|
||||||
$user->update(['is_banned' => 0]);
|
$user->removeRole(UserRole::BANNED);
|
||||||
alert()->success('Unbanned', 'User has been unbanned.');
|
alert()->success('Unbanned', 'User has been unbanned.');
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ use App\Models\Episode;
|
|||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
|
use GrantHolle\Altcha\Rules\ValidAltcha;
|
||||||
|
|
||||||
class DownloadApiController extends Controller
|
class DownloadApiController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
@@ -16,11 +18,12 @@ class DownloadApiController extends Controller
|
|||||||
public function getDownload(Request $request)
|
public function getDownload(Request $request)
|
||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'episode_id' => 'required',
|
'episode_id' => ['required'],
|
||||||
'captcha' => 'required|captcha'
|
'captcha' => ['required', new ValidAltcha],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$episode = Episode::where('id', $request->input('episode_id'))->firstOrFail();
|
$episode = Episode::where('id', $request->input('episode_id'))
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
// Increase download count, as we assume the user
|
// Increase download count, as we assume the user
|
||||||
// downloads after submitting the captcha
|
// downloads after submitting the captcha
|
||||||
|
|||||||
47
app/Http/Controllers/Auth/AuthenticatedSessionController.php
Normal file
47
app/Http/Controllers/Auth/AuthenticatedSessionController.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Auth\LoginRequest;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class AuthenticatedSessionController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the login view.
|
||||||
|
*/
|
||||||
|
public function create(): View
|
||||||
|
{
|
||||||
|
return view('auth.login');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming authentication request.
|
||||||
|
*/
|
||||||
|
public function store(LoginRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->authenticate();
|
||||||
|
|
||||||
|
$request->session()->regenerate();
|
||||||
|
|
||||||
|
return redirect()->intended(route('home.index', absolute: false));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy an authenticated session.
|
||||||
|
*/
|
||||||
|
public function destroy(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
Auth::guard('web')->logout();
|
||||||
|
|
||||||
|
$request->session()->invalidate();
|
||||||
|
|
||||||
|
$request->session()->regenerateToken();
|
||||||
|
|
||||||
|
return redirect('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
40
app/Http/Controllers/Auth/ConfirmablePasswordController.php
Normal file
40
app/Http/Controllers/Auth/ConfirmablePasswordController.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class ConfirmablePasswordController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Show the confirm password view.
|
||||||
|
*/
|
||||||
|
public function show(): View
|
||||||
|
{
|
||||||
|
return view('auth.confirm-password');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm the user's password.
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
if (! Auth::guard('web')->validate([
|
||||||
|
'email' => $request->user()->email,
|
||||||
|
'password' => $request->password,
|
||||||
|
])) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'password' => __('auth.password'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->session()->put('auth.password_confirmed_at', time());
|
||||||
|
|
||||||
|
return redirect()->intended(route('home.index', absolute: false));
|
||||||
|
}
|
||||||
|
}
|
||||||
122
app/Http/Controllers/Auth/DiscordAuthController.php
Normal file
122
app/Http/Controllers/Auth/DiscordAuthController.php
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Enums\UserRole;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Laravel\Socialite\Facades\Socialite;
|
||||||
|
|
||||||
|
class DiscordAuthController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Redirect to Discord
|
||||||
|
*/
|
||||||
|
public function redirect(): RedirectResponse
|
||||||
|
{
|
||||||
|
return Socialite::driver('discord')->redirect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback received from Discord
|
||||||
|
*/
|
||||||
|
public function callback(): RedirectResponse
|
||||||
|
{
|
||||||
|
$discordUser = Socialite::driver('discord')->user();
|
||||||
|
|
||||||
|
$user = User::where('discord_id', $discordUser->id)->first();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
// link by email if it already exists
|
||||||
|
$user = User::where('email', $discordUser->email)->first();
|
||||||
|
|
||||||
|
if ($user) {
|
||||||
|
$user->update([
|
||||||
|
'discord_id' => $discordUser->id,
|
||||||
|
'discord_avatar' => $discordUser->avatar,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
// Create new user
|
||||||
|
$user = User::create([
|
||||||
|
'name' => $discordUser->name,
|
||||||
|
'email' => $discordUser->email,
|
||||||
|
'discord_id' => $discordUser->id,
|
||||||
|
'discord_avatar' => $discordUser->avatar,
|
||||||
|
'password' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->checkDiscordAvatar($discordUser, $user);
|
||||||
|
$this->checkDiscordRoles($user);
|
||||||
|
|
||||||
|
Auth::login($user, true);
|
||||||
|
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
private function checkDiscordRoles(User $user): void
|
||||||
|
{
|
||||||
|
// Should not ever happen
|
||||||
|
if (!$user->discord_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$guildId = config('discord.guild_id');
|
||||||
|
|
||||||
|
$response = Http::withToken(config('discord.discord_bot_token'), 'Bot')
|
||||||
|
->timeout(5)
|
||||||
|
->get("https://discord.com/api/v10/guilds/{$guildId}/members/{$user->discord_id}");
|
||||||
|
|
||||||
|
// User is not in the guild
|
||||||
|
if ($response->status() === 404) {
|
||||||
|
$user->removeRole(UserRole::SUPPORTER);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Something else failed
|
||||||
|
if ($response->failed()) {
|
||||||
|
Log::warning('Discord role check failed', [
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'discord_id' => $user->discord_id,
|
||||||
|
'status' => $response->status(),
|
||||||
|
'body' => $response->body(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$discordRoles = $response->json('roles', []);
|
||||||
|
$patreonRoles = config('discord.patreon_roles', []);
|
||||||
|
|
||||||
|
// If intersect of array is empty, then the user doesn't have the role
|
||||||
|
$hasSupporterRole = !empty(array_intersect($discordRoles, $patreonRoles));
|
||||||
|
|
||||||
|
if (!$hasSupporterRole) {
|
||||||
|
// Remove role if not found
|
||||||
|
$user->removeRole(UserRole::SUPPORTER);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->addRole(UserRole::SUPPORTER);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class EmailVerificationNotificationController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Send a new email verification notification.
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
if ($request->user()->hasVerifiedEmail()) {
|
||||||
|
return redirect()->intended(route('home.index', absolute: false));
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->user()->sendEmailVerificationNotification();
|
||||||
|
|
||||||
|
return back()->with('status', 'verification-link-sent');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class EmailVerificationPromptController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the email verification prompt.
|
||||||
|
*/
|
||||||
|
public function __invoke(Request $request): RedirectResponse|View
|
||||||
|
{
|
||||||
|
return $request->user()->hasVerifiedEmail()
|
||||||
|
? redirect()->intended(route('home.index', absolute: false))
|
||||||
|
: view('auth.verify-email');
|
||||||
|
}
|
||||||
|
}
|
||||||
62
app/Http/Controllers/Auth/NewPasswordController.php
Normal file
62
app/Http/Controllers/Auth/NewPasswordController.php
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Auth\Events\PasswordReset;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Password;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\Rules;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class NewPasswordController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the password reset view.
|
||||||
|
*/
|
||||||
|
public function create(Request $request): View
|
||||||
|
{
|
||||||
|
return view('auth.reset-password', ['request' => $request]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming new password request.
|
||||||
|
*
|
||||||
|
* @throws \Illuminate\Validation\ValidationException
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'token' => ['required'],
|
||||||
|
'email' => ['required', 'email'],
|
||||||
|
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Here we will attempt to reset the user's password. If it is successful we
|
||||||
|
// will update the password on an actual user model and persist it to the
|
||||||
|
// database. Otherwise we will parse the error and return the response.
|
||||||
|
$status = Password::reset(
|
||||||
|
$request->only('email', 'password', 'password_confirmation', 'token'),
|
||||||
|
function (User $user) use ($request) {
|
||||||
|
$user->forceFill([
|
||||||
|
'password' => Hash::make($request->password),
|
||||||
|
'remember_token' => Str::random(60),
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
event(new PasswordReset($user));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// If the password was successfully reset, we will redirect the user back to
|
||||||
|
// the application's home authenticated view. If there is an error we can
|
||||||
|
// redirect them back to where they came from with their error message.
|
||||||
|
return $status == Password::PASSWORD_RESET
|
||||||
|
? redirect()->route('login')->with('status', __($status))
|
||||||
|
: back()->withInput($request->only('email'))
|
||||||
|
->withErrors(['email' => __($status)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
43
app/Http/Controllers/Auth/PasswordController.php
Normal file
43
app/Http/Controllers/Auth/PasswordController.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Validation\Rules\Password;
|
||||||
|
|
||||||
|
class PasswordController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Update the user's password.
|
||||||
|
*/
|
||||||
|
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', [
|
||||||
|
'current_password' => ['required', 'current_password'],
|
||||||
|
'password' => ['required', Password::defaults(), 'confirmed'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$request->user()->update([
|
||||||
|
'password' => Hash::make($validated['password']),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('status', 'password-updated');
|
||||||
|
}
|
||||||
|
}
|
||||||
44
app/Http/Controllers/Auth/PasswordResetLinkController.php
Normal file
44
app/Http/Controllers/Auth/PasswordResetLinkController.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Password;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class PasswordResetLinkController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the password reset link request view.
|
||||||
|
*/
|
||||||
|
public function create(): View
|
||||||
|
{
|
||||||
|
return view('auth.forgot-password');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming password reset link request.
|
||||||
|
*
|
||||||
|
* @throws \Illuminate\Validation\ValidationException
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'email' => ['required', 'email'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// We will send the password reset link to this user. Once we have attempted
|
||||||
|
// to send the link, we will examine the response then see the message we
|
||||||
|
// need to show to the user. Finally, we'll send out a proper response.
|
||||||
|
$status = Password::sendResetLink(
|
||||||
|
$request->only('email')
|
||||||
|
);
|
||||||
|
|
||||||
|
return $status == Password::RESET_LINK_SENT
|
||||||
|
? back()->with('status', __($status))
|
||||||
|
: back()->withInput($request->only('email'))
|
||||||
|
->withErrors(['email' => __($status)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
app/Http/Controllers/Auth/RegisteredUserController.php
Normal file
44
app/Http/Controllers/Auth/RegisteredUserController.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Auth\Events\Registered;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Validation\Rules;
|
||||||
|
|
||||||
|
use GrantHolle\Altcha\Rules\ValidAltcha;
|
||||||
|
|
||||||
|
class RegisteredUserController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming registration request.
|
||||||
|
*
|
||||||
|
* @throws \Illuminate\Validation\ValidationException
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
|
||||||
|
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||||
|
'altcha' => ['required', new ValidAltcha],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = User::create([
|
||||||
|
'name' => $request->name,
|
||||||
|
'email' => $request->email,
|
||||||
|
'password' => Hash::make($request->password),
|
||||||
|
]);
|
||||||
|
|
||||||
|
event(new Registered($user));
|
||||||
|
|
||||||
|
Auth::login($user);
|
||||||
|
|
||||||
|
return redirect(route('home.index', absolute: false));
|
||||||
|
}
|
||||||
|
}
|
||||||
27
app/Http/Controllers/Auth/VerifyEmailController.php
Normal file
27
app/Http/Controllers/Auth/VerifyEmailController.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Auth\Events\Verified;
|
||||||
|
use Illuminate\Foundation\Auth\EmailVerificationRequest;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
|
||||||
|
class VerifyEmailController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Mark the authenticated user's email address as verified.
|
||||||
|
*/
|
||||||
|
public function __invoke(EmailVerificationRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
if ($request->user()->hasVerifiedEmail()) {
|
||||||
|
return redirect()->intended(route('home.index', absolute: false).'?verified=1');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->user()->markEmailAsVerified()) {
|
||||||
|
event(new Verified($request->user()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->intended(route('home.index', absolute: false).'?verified=1');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ namespace App\Http\Controllers;
|
|||||||
use App\Models\Contact;
|
use App\Models\Contact;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
use GrantHolle\Altcha\Rules\ValidAltcha;
|
||||||
|
|
||||||
class ContactController extends Controller
|
class ContactController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
@@ -25,7 +27,7 @@ class ContactController extends Controller
|
|||||||
'email' => 'required|max:50',
|
'email' => 'required|max:50',
|
||||||
'message' => 'required|max:1000',
|
'message' => 'required|max:1000',
|
||||||
'subject' => 'required|max:50',
|
'subject' => 'required|max:50',
|
||||||
'captcha' => 'required|captcha',
|
'altcha' => ['required', new ValidAltcha],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$contact = new Contact();
|
$contact = new Contact();
|
||||||
@@ -37,9 +39,4 @@ class ContactController extends Controller
|
|||||||
|
|
||||||
return back()->with('status', 'contact-submitted');
|
return back()->with('status', 'contact-submitted');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function reloadCaptcha(): \Illuminate\Http\JsonResponse
|
|
||||||
{
|
|
||||||
return response()->json(['captcha'=> captcha_img()]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,11 +111,13 @@ class HomeController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function updateLanguage(Request $request): \Illuminate\Http\RedirectResponse
|
public function updateLanguage(Request $request): \Illuminate\Http\RedirectResponse
|
||||||
{
|
{
|
||||||
if(! in_array($request->language, config('lang-detector.languages'))) {
|
abort_unless(in_array($request->language, config('app.supported_locales'), true), 404);
|
||||||
return redirect()->back();
|
|
||||||
}
|
|
||||||
|
|
||||||
Cookie::queue(Cookie::forever('locale', $request->language));
|
session(['locale' => $request->language]);
|
||||||
|
|
||||||
|
if (Auth::check()) {
|
||||||
|
Auth::user()->update(['locale' => $request->language]);
|
||||||
|
}
|
||||||
|
|
||||||
return redirect()->back();
|
return redirect()->back();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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([
|
||||||
|
|||||||
@@ -3,12 +3,18 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\Episode;
|
use App\Models\Episode;
|
||||||
|
use App\Models\User;
|
||||||
use App\Http\Requests\ProfileUpdateRequest;
|
use App\Http\Requests\ProfileUpdateRequest;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
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 Intervention\Image\Laravel\Facades\Image;
|
||||||
|
|
||||||
use Conner\Tagging\Model\Tag;
|
use Conner\Tagging\Model\Tag;
|
||||||
|
|
||||||
@@ -17,7 +23,7 @@ class ProfileController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Display the user page.
|
* Display the user page.
|
||||||
*/
|
*/
|
||||||
public function index(Request $request): \Illuminate\View\View
|
public function index(Request $request): View
|
||||||
{
|
{
|
||||||
return view('profile.index', [
|
return view('profile.index', [
|
||||||
'user' => $request->user(),
|
'user' => $request->user(),
|
||||||
@@ -27,7 +33,7 @@ class ProfileController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Display the user's settings form.
|
* Display the user's settings form.
|
||||||
*/
|
*/
|
||||||
public function settings(Request $request): \Illuminate\View\View
|
public function settings(Request $request): View
|
||||||
{
|
{
|
||||||
$example = Episode::where('title', 'Succubus Yondara Gibo ga Kita!?')->first();
|
$example = Episode::where('title', 'Succubus Yondara Gibo ga Kita!?')->first();
|
||||||
|
|
||||||
@@ -37,10 +43,33 @@ class ProfileController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the user's profile information.
|
||||||
|
*/
|
||||||
|
public function update(ProfileUpdateRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
// Fill everything except the image
|
||||||
|
$user->fill($request->safe()->except('image'));
|
||||||
|
|
||||||
|
if ($request->user()->isDirty('email')) {
|
||||||
|
$request->user()->email_verified_at = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->hasFile('image')) {
|
||||||
|
$this->storeAvatar($request->file('image'), $user);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
return Redirect::route('profile.settings')->with('status', 'profile-updated');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the user's watched page.
|
* Display the user's watched page.
|
||||||
*/
|
*/
|
||||||
public function watched(Request $request): \Illuminate\View\View
|
public function watched(Request $request): View
|
||||||
{
|
{
|
||||||
return view('profile.watched', [
|
return view('profile.watched', [
|
||||||
'user' => $request->user(),
|
'user' => $request->user(),
|
||||||
@@ -50,17 +79,17 @@ class ProfileController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Display the user's comments page.
|
* Display the user's comments page.
|
||||||
*/
|
*/
|
||||||
public function comments(Request $request): \Illuminate\View\View
|
public function comments(Request $request): View
|
||||||
{
|
{
|
||||||
return view('profile.comments', [
|
return view('profile.comments', [
|
||||||
'user' => $request->user(),
|
'user' => $request->user(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the user's likes page.
|
* Display the user's likes page.
|
||||||
*/
|
*/
|
||||||
public function likes(Request $request): \Illuminate\View\View
|
public function likes(Request $request): View
|
||||||
{
|
{
|
||||||
return view('profile.likes', [
|
return view('profile.likes', [
|
||||||
'user' => $request->user(),
|
'user' => $request->user(),
|
||||||
@@ -70,7 +99,7 @@ class ProfileController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Update user settings.
|
* Update user settings.
|
||||||
*/
|
*/
|
||||||
public function saveSettings(Request $request): \Illuminate\Http\RedirectResponse
|
public function saveSettings(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$user->search_design = $request->input('searchDesign') == 'thumbnail';
|
$user->search_design = $request->input('searchDesign') == 'thumbnail';
|
||||||
@@ -84,7 +113,7 @@ class ProfileController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Update user tag blacklist.
|
* Update user tag blacklist.
|
||||||
*/
|
*/
|
||||||
public function saveBlacklist(Request $request): \Illuminate\Http\RedirectResponse
|
public function saveBlacklist(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$tags = json_decode($request->input('tags'));
|
$tags = json_decode($request->input('tags'));
|
||||||
@@ -112,19 +141,60 @@ class ProfileController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function destroy(Request $request): \Illuminate\Http\RedirectResponse
|
public function destroy(Request $request): \Illuminate\Http\RedirectResponse
|
||||||
{
|
{
|
||||||
$request->validateWithBag('userDeletion', [
|
|
||||||
'password' => ['required', 'current_password'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
|
|
||||||
|
// Verify password if user has password
|
||||||
|
if (!is_null($user->password)) {
|
||||||
|
$request->validateWithBag('userDeletion', [
|
||||||
|
'password' => ['required', 'current_password'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update comments to deleted user
|
||||||
|
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->delete();
|
$user->delete();
|
||||||
|
|
||||||
$request->session()->invalidate();
|
$request->session()->invalidate();
|
||||||
|
|
||||||
$request->session()->regenerateToken();
|
$request->session()->regenerateToken();
|
||||||
|
|
||||||
|
cache()->flush();
|
||||||
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Models\Playlist;
|
|
||||||
use App\Models\PlaylistEpisode;
|
|
||||||
use App\Models\Watched;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
|
|
||||||
class UserController extends Controller
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Display User Page.
|
|
||||||
*/
|
|
||||||
public function index(string $username): \Illuminate\View\View
|
|
||||||
{
|
|
||||||
$user = User::where('name', $username)
|
|
||||||
->select('id', 'name', 'discord_name', 'avatar', 'created_at', 'is_patreon')
|
|
||||||
->firstOrFail();
|
|
||||||
|
|
||||||
return view('user.index', [
|
|
||||||
'user' => $user,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete User.
|
|
||||||
*/
|
|
||||||
public function delete(Request $request): \Illuminate\Http\RedirectResponse
|
|
||||||
{
|
|
||||||
$user = User::where('id', $request->user()->id)->firstOrFail();
|
|
||||||
|
|
||||||
// Delete Playlist
|
|
||||||
$playlists = Playlist::where('user_id', $user->id)->get();
|
|
||||||
foreach($playlists as $playlist) {
|
|
||||||
PlaylistEpisode::where('playlist_id', $playlist->id)->forceDelete();
|
|
||||||
$playlist->forceDelete();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update comments to deleted user
|
|
||||||
DB::table('comments')->where('commenter_id', '=', $user->id)->update(['commenter_id' => 1]);
|
|
||||||
|
|
||||||
$user->forceDelete();
|
|
||||||
|
|
||||||
Auth::guard('web')->logout();
|
|
||||||
|
|
||||||
$request->session()->invalidate();
|
|
||||||
|
|
||||||
$request->session()->regenerateToken();
|
|
||||||
|
|
||||||
cache()->flush();
|
|
||||||
|
|
||||||
return redirect('/');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -37,6 +37,7 @@ class Kernel extends HttpKernel
|
|||||||
\App\Http\Middleware\VerifyCsrfToken::class,
|
\App\Http\Middleware\VerifyCsrfToken::class,
|
||||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||||
\App\Http\Middleware\IsBanned::class,
|
\App\Http\Middleware\IsBanned::class,
|
||||||
|
\App\Http\Middleware\SetLocale::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
'api' => [
|
'api' => [
|
||||||
@@ -58,6 +59,7 @@ class Kernel extends HttpKernel
|
|||||||
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
|
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
|
||||||
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
|
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
|
||||||
'auth.admin' => \App\Http\Middleware\IsAdmin::class,
|
'auth.admin' => \App\Http\Middleware\IsAdmin::class,
|
||||||
|
'auth.moderator' => \App\Http\Middleware\IsModerator::class,
|
||||||
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
|
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
|
||||||
'can' => \Illuminate\Auth\Middleware\Authorize::class,
|
'can' => \Illuminate\Auth\Middleware\Authorize::class,
|
||||||
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
||||||
|
|||||||
@@ -1,28 +1,14 @@
|
|||||||
<?php namespace app\Http\Middleware;
|
<?php namespace app\Http\Middleware;
|
||||||
|
|
||||||
|
use App\Enums\UserRole;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Contracts\Auth\Guard;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
class IsAdmin {
|
class IsAdmin {
|
||||||
|
|
||||||
/**
|
|
||||||
* The Guard implementation.
|
|
||||||
*
|
|
||||||
* @var Guard
|
|
||||||
*/
|
|
||||||
protected $auth;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new filter instance.
|
|
||||||
*
|
|
||||||
* @param Guard $auth
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct(Guard $auth)
|
|
||||||
{
|
|
||||||
$this->auth = $auth;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle an incoming request.
|
* Handle an incoming request.
|
||||||
*
|
*
|
||||||
@@ -30,15 +16,14 @@ class IsAdmin {
|
|||||||
* @param \Closure $next
|
* @param \Closure $next
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public function handle($request, Closure $next)
|
public function handle(Request $request, Closure $next): Response
|
||||||
{
|
{
|
||||||
if( ! $this->auth->user()->is_admin)
|
if(Auth::check() && Auth::user()->hasRole(UserRole::ADMINISTRATOR))
|
||||||
{
|
{
|
||||||
session()->flash('error_msg','This resource is restricted to Administrators!');
|
return $next($request);
|
||||||
return redirect()->route('home.index');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $next($request);
|
session()->flash('error_msg','This resource is restricted to Administrators!');
|
||||||
|
return redirect()->route('home.index');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
<?php namespace app\Http\Middleware;
|
<?php namespace app\Http\Middleware;
|
||||||
|
|
||||||
|
use App\Enums\UserRole;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Contracts\Auth\Guard;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
class IsBanned {
|
class IsBanned {
|
||||||
|
|
||||||
@@ -13,9 +16,9 @@ class IsBanned {
|
|||||||
* @param \Closure $next
|
* @param \Closure $next
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public function handle($request, Closure $next)
|
public function handle(Request $request, Closure $next): Response
|
||||||
{
|
{
|
||||||
if(auth()->check() && auth()->user()->is_banned == 1)
|
if(Auth::check() && Auth::user()->hasRole(UserRole::BANNED))
|
||||||
{
|
{
|
||||||
Auth::logout();
|
Auth::logout();
|
||||||
$request->session()->invalidate();
|
$request->session()->invalidate();
|
||||||
|
|||||||
29
app/Http/Middleware/IsModerator.php
Normal file
29
app/Http/Middleware/IsModerator.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use App\Enums\UserRole;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class IsModerator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
if (Auth::check() && Auth::user()->hasRole(UserRole::MODERATOR))
|
||||||
|
{
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
session()->flash('error_msg','This resource is restricted to Administrators!');
|
||||||
|
return redirect()->route('home.index');
|
||||||
|
}
|
||||||
|
}
|
||||||
41
app/Http/Middleware/SetLocale.php
Normal file
41
app/Http/Middleware/SetLocale.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\App;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class SetLocale
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
// 1. Logged-in user preference
|
||||||
|
if (Auth::check() && Auth::user()->locale) {
|
||||||
|
App::setLocale(Auth::user()->locale);
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Session (guest or user override)
|
||||||
|
if (session()->has('locale') && in_array($request->language, config('app.supported_locales'), true)) {
|
||||||
|
App::setLocale(session('locale'));
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Browser language
|
||||||
|
$locale = $request->getPreferredLanguage(config('app.supported_locales'));
|
||||||
|
|
||||||
|
if ($locale) {
|
||||||
|
App::setLocale($locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ use Illuminate\Support\Facades\RateLimiter;
|
|||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
use GrantHolle\Altcha\Rules\ValidAltcha;
|
||||||
|
|
||||||
class LoginRequest extends FormRequest
|
class LoginRequest extends FormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
@@ -22,13 +24,14 @@ class LoginRequest extends FormRequest
|
|||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
*
|
*
|
||||||
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
*/
|
*/
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'email' => ['required', 'string', 'email'],
|
'email' => ['required', 'string', 'email'],
|
||||||
'password' => ['required', 'string'],
|
'password' => ['required', 'string'],
|
||||||
|
'altcha' => ['required', new ValidAltcha],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +83,6 @@ class LoginRequest extends FormRequest
|
|||||||
*/
|
*/
|
||||||
public function throttleKey(): string
|
public function throttleKey(): string
|
||||||
{
|
{
|
||||||
return Str::transliterate(Str::lower($this->input('email')).'|'.$this->ip());
|
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,13 +11,26 @@ class ProfileUpdateRequest extends FormRequest
|
|||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
*
|
*
|
||||||
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
*/
|
*/
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'name' => ['string', 'max:255'],
|
'name' => ['required', 'string', 'max:255'],
|
||||||
'email' => ['email', 'max:255', Rule::unique(User::class)->ignore($this->user()->id)],
|
'image' => [
|
||||||
|
'nullable',
|
||||||
|
'image',
|
||||||
|
'mimes:jpg,png,jpeg,webp,gif',
|
||||||
|
'max:8192'
|
||||||
|
],
|
||||||
|
'email' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'lowercase',
|
||||||
|
'email',
|
||||||
|
'max:255',
|
||||||
|
Rule::unique(User::class)->ignore($this->user()->id),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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', [
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
|
|
||||||
namespace App\Livewire;
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use App\Enums\UserRole;
|
||||||
|
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;
|
||||||
@@ -18,7 +18,7 @@ class AdminUserSearch extends Component
|
|||||||
public $search = '';
|
public $search = '';
|
||||||
|
|
||||||
#[Url(history: true)]
|
#[Url(history: true)]
|
||||||
public $filtered = ['true'];
|
public $discordId = '';
|
||||||
|
|
||||||
#[Url(history: true)]
|
#[Url(history: true)]
|
||||||
public $patreon = [];
|
public $patreon = [];
|
||||||
@@ -31,8 +31,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();
|
||||||
@@ -40,13 +39,10 @@ class AdminUserSearch extends Component
|
|||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
{
|
{
|
||||||
$users = User::when($this->filtered !== [], fn ($query) => $query->where('id', '>=', 10000))
|
$users = User::when($this->patreon !== [], fn ($query) => $query->whereJsonContains('roles', UserRole::SUPPORTER->value))
|
||||||
->when($this->patreon !== [], fn ($query) => $query->where('is_patreon', 1))
|
->when($this->banned !== [], fn ($query) => $query->whereJsonContains('roles', UserRole::BANNED->value))
|
||||||
->when($this->banned !== [], fn ($query) => $query->where('is_banned', 1))
|
->when($this->search !== '', fn ($query) => $query->where('name', 'like', '%'.$this->search.'%'))
|
||||||
->when($this->search !== '', fn ($query) => $query->where(function($query) {
|
->when($this->discordId !== '', fn ($query) => $query->where('discord_id', '=', $this->discordId))
|
||||||
$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
166
app/Livewire/Comment.php
Normal 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
71
app/Livewire/Comments.php
Normal 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
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
{
|
{
|
||||||
@@ -58,9 +73,9 @@ class DownloadsSearch extends Component
|
|||||||
$types[] = 'FHD';
|
$types[] = 'FHD';
|
||||||
} elseif ($label === 'FHD 48fps') {
|
} elseif ($label === 'FHD 48fps') {
|
||||||
$types[] = 'FHDi';
|
$types[] = 'FHDi';
|
||||||
} elseif ($label === 'UHD' && auth()->user()->is_patreon) {
|
} elseif ($label === 'UHD' && auth()->user()->hasRole(\App\Enums\UserRole::SUPPORTER)) {
|
||||||
$types[] = 'UHD';
|
$types[] = 'UHD';
|
||||||
} elseif ($label === 'UHD 48fps' && auth()->user()->is_patreon) {
|
} elseif ($label === 'UHD 48fps' && auth()->user()->hasRole(\App\Enums\UserRole::SUPPORTER)) {
|
||||||
$types[] = 'UHDi';
|
$types[] = 'UHDi';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,7 +98,7 @@ class DownloadsSearch extends Component
|
|||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
if (!auth()->user()->is_patreon) {
|
if (!auth()->user()->hasRole(\App\Enums\UserRole::SUPPORTER)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
49
app/Livewire/UserComments.php
Normal file
49
app/Livewire/UserComments.php
Normal 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
76
app/Models/Comment.php
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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'];
|
||||||
|
|||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
28
app/Models/Presenters/CommentPresenter.php
Normal file
28
app/Models/Presenters/CommentPresenter.php
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,18 +2,20 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
//use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
|
|
||||||
|
use App\Enums\UserRole;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
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.
|
||||||
@@ -25,10 +27,8 @@ class User extends Authenticatable
|
|||||||
'email',
|
'email',
|
||||||
'password',
|
'password',
|
||||||
'locale',
|
'locale',
|
||||||
'is_banned',
|
|
||||||
// Discord
|
// Discord
|
||||||
'discord_id',
|
'discord_id',
|
||||||
'discord_name',
|
|
||||||
'discord_avatar',
|
'discord_avatar',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -55,14 +55,14 @@ class User extends Authenticatable
|
|||||||
'name' => 'string',
|
'name' => 'string',
|
||||||
'email' => 'string',
|
'email' => 'string',
|
||||||
'locale' => 'string',
|
'locale' => 'string',
|
||||||
'roles' => 'json',
|
'roles' => 'array',
|
||||||
'tag_blacklist' => 'array',
|
'tag_blacklist' => 'array',
|
||||||
// Discord
|
// Discord
|
||||||
'discord_id' => 'integer',
|
'discord_id' => 'integer',
|
||||||
'discord_name' => 'string',
|
|
||||||
'discord_avatar' => 'string',
|
'discord_avatar' => 'string',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Has Many Playlists.
|
* Has Many Playlists.
|
||||||
*/
|
*/
|
||||||
@@ -90,8 +90,74 @@ 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user has a specific role
|
||||||
|
*/
|
||||||
|
public function hasRole(UserRole $role): bool
|
||||||
|
{
|
||||||
|
return in_array($role->value, $this->roles ?? [], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add Role to User
|
||||||
|
*/
|
||||||
|
public function addRole(UserRole $role): void
|
||||||
|
{
|
||||||
|
if ($this->hasRole($role)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all current roles
|
||||||
|
$roles = $this->roles ?? [];
|
||||||
|
|
||||||
|
// Add new role
|
||||||
|
$roles[] = $role->value;
|
||||||
|
|
||||||
|
$this->roles = $roles;
|
||||||
|
$this->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove Role from User
|
||||||
|
*/
|
||||||
|
public function removeRole(UserRole $role): void
|
||||||
|
{
|
||||||
|
if (!$this->hasRole($role)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->roles = array_diff($this->roles, [$role->value]);
|
||||||
|
$this->save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
22
app/Policies/CommentPolicy.php
Normal file
22
app/Policies/CommentPolicy.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
@@ -19,6 +20,8 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
//
|
Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) {
|
||||||
|
$event->extendSocialite('discord', \SocialiteProviders\Discord\Provider::class);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ class EpisodeService
|
|||||||
Request $request,
|
Request $request,
|
||||||
Hentai $hentai,
|
Hentai $hentai,
|
||||||
int $episodeNumber,
|
int $episodeNumber,
|
||||||
Studios $studio = null,
|
?Studios $studio = null,
|
||||||
Episode $referenceEpisode = null
|
?Episode $referenceEpisode = null
|
||||||
): Episode
|
): Episode
|
||||||
{
|
{
|
||||||
$episode = new Episode();
|
$episode = new Episode();
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "laravel/laravel",
|
"name": "w33b/hstream",
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"description": "The skeleton application for the Laravel framework.",
|
"description": "The website of hstream.moe",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"laravel",
|
"laravel",
|
||||||
"framework"
|
"framework"
|
||||||
@@ -9,57 +9,46 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
|
"grantholle/laravel-altcha": "^2.1",
|
||||||
"guzzlehttp/guzzle": "^7.8.1",
|
"guzzlehttp/guzzle": "^7.8.1",
|
||||||
"hisorange/browser-detect": "^5.0",
|
"hisorange/browser-detect": "^5.0",
|
||||||
"http-interop/http-factory-guzzle": "^1.2",
|
"http-interop/http-factory-guzzle": "^1.2",
|
||||||
"intervention/image": "^3.9",
|
"intervention/image": "^3.11",
|
||||||
"intervention/image-laravel": "^1.3",
|
"intervention/image-laravel": "^1.5",
|
||||||
"laravel/framework": "^11.0",
|
"laravel/framework": "^12.0",
|
||||||
"laravel/sanctum": "^4.0",
|
"laravel/sanctum": "^4.2",
|
||||||
"laravel/scout": "^10.20",
|
"laravel/scout": "^10.20",
|
||||||
|
"laravel/socialite": "^5.24",
|
||||||
"laravel/tinker": "^2.10",
|
"laravel/tinker": "^2.10",
|
||||||
"laravelista/comments": "dev-l11-compatibility",
|
"livewire/livewire": "^3.7.0",
|
||||||
"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",
|
||||||
"mews/captcha": "3.4.4",
|
|
||||||
"predis/predis": "^2.2",
|
"predis/predis": "^2.2",
|
||||||
"realrashid/sweet-alert": "^7.2",
|
"realrashid/sweet-alert": "^7.2",
|
||||||
"rtconner/laravel-tagging": "^4.1",
|
"rtconner/laravel-tagging": "^5.0",
|
||||||
"spatie/laravel-discord-alerts": "^1.5",
|
"socialiteproviders/discord": "^4.2",
|
||||||
"spatie/laravel-sitemap": "^7.3",
|
"spatie/laravel-discord-alerts": "^1.8",
|
||||||
"vluzrmos/language-detector": "^2.3"
|
"spatie/laravel-sitemap": "^7.3"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"barryvdh/laravel-debugbar": "^3.14.7",
|
"barryvdh/laravel-debugbar": "^3.16",
|
||||||
"fakerphp/faker": "^1.24.0",
|
"fakerphp/faker": "^1.24.0",
|
||||||
|
"laravel/breeze": "^2.3",
|
||||||
"laravel/pint": "^1.18",
|
"laravel/pint": "^1.18",
|
||||||
"laravel/sail": "^1.38",
|
|
||||||
"mockery/mockery": "^1.4.4",
|
"mockery/mockery": "^1.4.4",
|
||||||
"nunomaduro/collision": "^8.1",
|
"nunomaduro/collision": "^8.1",
|
||||||
"phpunit/phpunit": "^11.4",
|
"phpunit/phpunit": "^11.4",
|
||||||
"spatie/laravel-ignition": "^2.0"
|
"spatie/laravel-ignition": "^2.0"
|
||||||
},
|
},
|
||||||
"repositories": [
|
"repositories": [],
|
||||||
{
|
|
||||||
"type": "vcs",
|
|
||||||
"url": "https://github.com/renatokira/comments.git"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"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": {
|
||||||
|
|||||||
2812
composer.lock
generated
2812
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -111,6 +111,18 @@ return [
|
|||||||
|
|
||||||
'faker_locale' => 'en_US',
|
'faker_locale' => 'en_US',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Supported Locales
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This is used to display the supported locales by this app, it also is
|
||||||
|
| used to verify session data and requests in the SetLocale Middleware
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'supported_locales' => ['en', 'de', 'fr'],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Encryption Key
|
| Encryption Key
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
return [
|
|
||||||
'characters' => ['2', '3', '4', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'M', 'N', 'P', 'Q', 'R', 'T', 'U', 'X', 'Y', 'Z'],
|
|
||||||
'default' => [
|
|
||||||
'length' => 5,
|
|
||||||
'width' => 120,
|
|
||||||
'height' => 36,
|
|
||||||
'quality' => 90,
|
|
||||||
'math' => false,
|
|
||||||
'expire' => 60,
|
|
||||||
'encrypt' => false,
|
|
||||||
],
|
|
||||||
'math' => [
|
|
||||||
'length' => 9,
|
|
||||||
'width' => 120,
|
|
||||||
'height' => 36,
|
|
||||||
'quality' => 90,
|
|
||||||
'math' => true,
|
|
||||||
],
|
|
||||||
|
|
||||||
'flat' => [
|
|
||||||
'length' => 6,
|
|
||||||
'width' => 160,
|
|
||||||
'height' => 46,
|
|
||||||
'quality' => 90,
|
|
||||||
'lines' => 6,
|
|
||||||
'bgImage' => false,
|
|
||||||
'bgColor' => '#ecf2f4',
|
|
||||||
'fontColors' => ['#2c3e50', '#c0392b', '#16a085', '#c0392b', '#8e44ad', '#303f9f', '#f57c00', '#795548'],
|
|
||||||
'contrast' => -5,
|
|
||||||
],
|
|
||||||
'mini' => [
|
|
||||||
'length' => 3,
|
|
||||||
'width' => 60,
|
|
||||||
'height' => 32,
|
|
||||||
],
|
|
||||||
'inverse' => [
|
|
||||||
'length' => 5,
|
|
||||||
'width' => 120,
|
|
||||||
'height' => 36,
|
|
||||||
'quality' => 90,
|
|
||||||
'sensitive' => true,
|
|
||||||
'angle' => 12,
|
|
||||||
'sharpen' => 10,
|
|
||||||
'blur' => 2,
|
|
||||||
'invert' => true,
|
|
||||||
'contrast' => -5,
|
|
||||||
]
|
|
||||||
];
|
|
||||||
@@ -1,10 +1,19 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'invite_link' => 'https://discord.gg/yAqgVKNgG5',
|
'invite_link' => 'https://discord.gg/yAqgVKNgG5',
|
||||||
|
|
||||||
'guild_id' => 802233383710228550,
|
'guild_id' => 802233383710228550,
|
||||||
|
|
||||||
'patreon_roles' => [841798154999169054, 803329707650187364, 803327903659196416, 803325441942356059, 803322725576736858, 802270568912519198, 802234830384267315],
|
'patreon_roles' => [
|
||||||
|
'841798154999169054', // ????
|
||||||
|
'803329707650187364', // Tier-5
|
||||||
|
'803327903659196416', // ????
|
||||||
|
'803325441942356059', // Tier-3
|
||||||
|
'803322725576736858', // Tier-2
|
||||||
|
'802270568912519198', // Tier-1
|
||||||
|
'802234830384267315' // admin
|
||||||
|
],
|
||||||
|
|
||||||
|
'discord_bot_token' => env('DISCORD_BOT_TOKEN'),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
return [
|
|
||||||
/*
|
|
||||||
* Indicates whenever should autodetect and apply the language of the request.
|
|
||||||
*/
|
|
||||||
'autodetect' => env('LANG_DETECTOR_AUTODETECT', true),
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Default driver to use to detect the request language.
|
|
||||||
*
|
|
||||||
* Available: browser, subdomain, uri.
|
|
||||||
*/
|
|
||||||
'driver' => env('LANG_DETECTOR_DRIVER', 'browser'),
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Used on subdomain and uri drivers. That indicates which segment should be used
|
|
||||||
* to verify the language.
|
|
||||||
*/
|
|
||||||
'segment' => env('LANG_DETECTOR_SEGMENT', 0),
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Languages available on the application.
|
|
||||||
*
|
|
||||||
* You could use parse_langs_to_array to use the string syntax
|
|
||||||
* or just use the array of languages with its aliases.
|
|
||||||
*/
|
|
||||||
'languages' => parse_langs_to_array(
|
|
||||||
env('LANG_DETECTOR_LANGUAGES', ['en', 'de', 'fr'])
|
|
||||||
),
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Indicates if should store detected locale on cookies
|
|
||||||
*/
|
|
||||||
'cookie' => (bool) env('LANG_DETECTOR_COOKIE', true),
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Indicates if should encrypt cookie
|
|
||||||
*/
|
|
||||||
'cookie_encrypt' => (bool) env('LANG_DETECTOR_COOKIE_ENCRYPT', false),
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Cookie name
|
|
||||||
*/
|
|
||||||
'cookie_name' => env('LANG_DETECTOR_COOKIE', 'locale'),
|
|
||||||
];
|
|
||||||
@@ -153,8 +153,7 @@ return [
|
|||||||
],
|
],
|
||||||
'sortableAttributes' => [
|
'sortableAttributes' => [
|
||||||
'created_at',
|
'created_at',
|
||||||
'release_date',
|
'release_date',
|
||||||
'view_count',
|
|
||||||
'title'
|
'title'
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -31,4 +31,21 @@ return [
|
|||||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Socialite Providers
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
'discord' => [
|
||||||
|
'client_id' => env('DISCORD_CLIENT_ID'),
|
||||||
|
'client_secret' => env('DISCORD_CLIENT_SECRET'),
|
||||||
|
'redirect' => '/auth/discord/callback',
|
||||||
|
|
||||||
|
// optional
|
||||||
|
'allow_gif_avatars' => (bool) env('DISCORD_AVATAR_GIF', true),
|
||||||
|
'avatar_default_extension' => env('DISCORD_EXTENSION_DEFAULT', 'webp'), // only pick from jpg, png, webp
|
||||||
|
],
|
||||||
|
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
207
database/migrations/2026_01_08_213625_fix_database_structure.php
Normal file
207
database/migrations/2026_01_08_213625_fix_database_structure.php
Normal 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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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"]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->string('locale', 10)
|
||||||
|
->nullable()
|
||||||
|
->after('discord_avatar');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('locale');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?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
|
||||||
|
{
|
||||||
|
// Migrate supporters
|
||||||
|
DB::table('users')->where('is_patreon', 1)->update([
|
||||||
|
'roles' => DB::raw("JSON_ARRAY('supporter')")
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Migrate banned
|
||||||
|
DB::table('users')->where('is_banned', 1)->update([
|
||||||
|
'roles' => DB::raw("JSON_ARRAY('banned')")
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Migrate admins
|
||||||
|
DB::table('users')->where('is_admin', 1)->update([
|
||||||
|
'roles' => DB::raw("JSON_ARRAY('admin')")
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Drop columns
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('is_admin');
|
||||||
|
$table->dropColumn('is_patreon');
|
||||||
|
$table->dropColumn('is_banned');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
DB::table('users')->update(['roles' => null]);
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->boolean('is_admin')->default(0);
|
||||||
|
$table->boolean('is_patreon')->default(0);
|
||||||
|
$table->boolean('is_banned')->default(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
services:
|
|
||||||
laravel.test:
|
|
||||||
build:
|
|
||||||
context: './vendor/laravel/sail/runtimes/8.3'
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
args:
|
|
||||||
WWWGROUP: '${WWWGROUP}'
|
|
||||||
MYSQL_CLIENT: mariadb-client
|
|
||||||
image: 'sail-8.3/app'
|
|
||||||
extra_hosts:
|
|
||||||
- 'host.docker.internal:host-gateway'
|
|
||||||
ports:
|
|
||||||
- '${APP_PORT:-80}:80'
|
|
||||||
- '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
|
|
||||||
environment:
|
|
||||||
WWWUSER: '${WWWUSER}'
|
|
||||||
LARAVEL_SAIL: 1
|
|
||||||
XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
|
|
||||||
XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
|
|
||||||
IGNITION_LOCAL_SITES_PATH: '${PWD}'
|
|
||||||
volumes:
|
|
||||||
- '.:/var/www/html'
|
|
||||||
networks:
|
|
||||||
- sail
|
|
||||||
depends_on:
|
|
||||||
- mariadb
|
|
||||||
- redis
|
|
||||||
mariadb:
|
|
||||||
image: 'mariadb:11'
|
|
||||||
ports:
|
|
||||||
- '${FORWARD_DB_PORT:-3306}:3306'
|
|
||||||
environment:
|
|
||||||
MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
|
|
||||||
MYSQL_ROOT_HOST: '%'
|
|
||||||
MYSQL_DATABASE: '${DB_DATABASE}'
|
|
||||||
MYSQL_USER: '${DB_USERNAME}'
|
|
||||||
MYSQL_PASSWORD: '${DB_PASSWORD}'
|
|
||||||
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
|
|
||||||
volumes:
|
|
||||||
- 'sail-mariadb:/var/lib/mysql'
|
|
||||||
- './vendor/laravel/sail/database/mariadb/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
|
|
||||||
networks:
|
|
||||||
- sail
|
|
||||||
healthcheck:
|
|
||||||
test:
|
|
||||||
- CMD
|
|
||||||
- healthcheck.sh
|
|
||||||
- '--connect'
|
|
||||||
- '--innodb_initialized'
|
|
||||||
retries: 3
|
|
||||||
timeout: 5s
|
|
||||||
redis:
|
|
||||||
image: 'redis:alpine'
|
|
||||||
ports:
|
|
||||||
- '${FORWARD_REDIS_PORT:-6379}:6379'
|
|
||||||
volumes:
|
|
||||||
- 'sail-redis:/data'
|
|
||||||
networks:
|
|
||||||
- sail
|
|
||||||
healthcheck:
|
|
||||||
test:
|
|
||||||
- CMD
|
|
||||||
- redis-cli
|
|
||||||
- ping
|
|
||||||
retries: 3
|
|
||||||
timeout: 5s
|
|
||||||
networks:
|
|
||||||
sail:
|
|
||||||
driver: bridge
|
|
||||||
volumes:
|
|
||||||
sail-mariadb:
|
|
||||||
driver: local
|
|
||||||
sail-redis:
|
|
||||||
driver: local
|
|
||||||
1347
package-lock.json
generated
1347
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -6,12 +6,13 @@
|
|||||||
"build": "vite build"
|
"build": "vite build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/forms": "^0.5.7",
|
"@tailwindcss/forms": "^0.5.2",
|
||||||
"autoprefixer": "^10.4.18",
|
"alpinejs": "^3.4.2",
|
||||||
|
"autoprefixer": "^10.4.2",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
"laravel-vite-plugin": "^2.0.0",
|
"laravel-vite-plugin": "^2.0.0",
|
||||||
"postcss": "^8.4.35",
|
"postcss": "^8.4.31",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.1.0",
|
||||||
"vite": "^7.1.6",
|
"vite": "^7.1.6",
|
||||||
"vite-plugin-static-copy": "^3.0.1"
|
"vite-plugin-static-copy": "^3.0.1"
|
||||||
},
|
},
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||||
"@jellyfin/libass-wasm": "^4.1.1",
|
"@jellyfin/libass-wasm": "^4.1.1",
|
||||||
"@yaireo/tagify": "^4.21.2",
|
"@yaireo/tagify": "^4.21.2",
|
||||||
|
"altcha": "^2.3.0",
|
||||||
"chart.js": "^4.5.0",
|
"chart.js": "^4.5.0",
|
||||||
"dashjs": "^5.0.0",
|
"dashjs": "^5.0.0",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
|
|||||||
@@ -122,4 +122,33 @@ input:checked~.dot {
|
|||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(https://fonts.bunny.net/figtree/files/figtree-latin-ext-600-normal.woff2) format('woff2'), url(https://fonts.bunny.net/figtree/files/figtree-latin-ext-600-normal.woff) format('woff');
|
src: url(https://fonts.bunny.net/figtree/files/figtree-latin-ext-600-normal.woff2) format('woff2'), url(https://fonts.bunny.net/figtree/files/figtree-latin-ext-600-normal.woff) format('woff');
|
||||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Captcha */
|
||||||
|
:root {
|
||||||
|
--altcha-border-width: 1px;
|
||||||
|
--altcha-border-radius: 0.375rem;
|
||||||
|
--altcha-color-base: #333;
|
||||||
|
--altcha-color-border: #a0a0a0;
|
||||||
|
--altcha-color-text: #fff;
|
||||||
|
--altcha-color-border-focus: currentColor;
|
||||||
|
--altcha-color-error-text: #f23939;
|
||||||
|
--altcha-color-footer-bg: #141414;
|
||||||
|
--altcha-max-width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.altcha-footer {
|
||||||
|
border-bottom-left-radius: 0.375rem;
|
||||||
|
border-bottom-right-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-color: #a0a0a0;
|
||||||
|
color: rgb(225,29,72);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"]:checked {
|
||||||
|
background-color: rgb(225,29,72);
|
||||||
|
box-shadow: 0 0 0 0px #fff, 0 0 0 calc(2px + 0px) rgba(246, 59, 118, 0.5), 0 0 #0000;
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
import './bootstrap';
|
import './bootstrap';
|
||||||
|
import 'hammerjs';
|
||||||
// import { Alpine } from '../../vendor/livewire/livewire/dist/livewire.esm';
|
|
||||||
// Alpine.start();
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Collapse,
|
Collapse,
|
||||||
Carousel,
|
Carousel,
|
||||||
@@ -15,6 +12,13 @@ import {
|
|||||||
initTE,
|
initTE,
|
||||||
} from "tw-elements";
|
} from "tw-elements";
|
||||||
|
|
||||||
initTE({ Collapse, Carousel, Clipboard, Modal, Tab, Lightbox, Tooltip, Ripple });
|
// Captcha
|
||||||
|
import 'altcha';
|
||||||
|
|
||||||
import 'hammerjs';
|
// import Alpine from 'alpinejs';
|
||||||
|
|
||||||
|
// window.Alpine = Alpine;
|
||||||
|
|
||||||
|
// Alpine.start();
|
||||||
|
|
||||||
|
initTE({ Collapse, Carousel, Clipboard, Modal, Tab, Lightbox, Tooltip, Ripple });
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<div class="mb-4 rounded-lg bg-success-400 px-6 py-5 text-base text-success-800 mt-5" role="alert">
|
<div class="mb-4 rounded-lg bg-success-400 px-6 py-5 text-base text-success-800 mt-5" role="alert">
|
||||||
{{ $alert->text }}
|
{{ $alert->text }}
|
||||||
@auth
|
@auth
|
||||||
@if(Auth::user()->is_admin)
|
@if(Auth::user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
|
||||||
<form method="POST" action="{{ route('admin.alert.delete', $alert->id) }}" class="float-right hover:text-success-900">
|
<form method="POST" action="{{ route('admin.alert.delete', $alert->id) }}" class="float-right hover:text-success-900">
|
||||||
@csrf
|
@csrf
|
||||||
@method('delete')
|
@method('delete')
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<div class="mb-4 rounded-lg bg-danger-400 px-6 py-5 text-base text-danger-800 mt-5" role="alert">
|
<div class="mb-4 rounded-lg bg-danger-400 px-6 py-5 text-base text-danger-800 mt-5" role="alert">
|
||||||
{{ $alert->text }}
|
{{ $alert->text }}
|
||||||
@auth
|
@auth
|
||||||
@if(Auth::user()->is_admin)
|
@if(Auth::user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
|
||||||
<form method="POST" action="{{ route('admin.alert.delete', $alert->id) }}" class="float-right hover:text-danger-900">
|
<form method="POST" action="{{ route('admin.alert.delete', $alert->id) }}" class="float-right hover:text-danger-900">
|
||||||
@csrf
|
@csrf
|
||||||
@method('delete')
|
@method('delete')
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@auth
|
@auth
|
||||||
@if(Auth::user()->is_admin)
|
@if(Auth::user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
|
||||||
<div class="relative p-5 bg-white dark:bg-neutral-700/40 rounded-lg overflow-hidden z-10">
|
<div class="relative p-5 bg-white dark:bg-neutral-700/40 rounded-lg overflow-hidden z-10">
|
||||||
<div class="float-left">
|
<div class="float-left">
|
||||||
<a data-te-toggle="modal" data-te-target="#modalUploadEpisode" class="text-xl text-gray-800 dark:text-gray-200 leading-tight cursor-pointer whitespace-nowrap">
|
<a data-te-toggle="modal" data-te-target="#modalUploadEpisode" class="text-xl text-gray-800 dark:text-gray-200 leading-tight cursor-pointer whitespace-nowrap">
|
||||||
|
|||||||
29
resources/views/auth/confirm-password.blade.php
Normal file
29
resources/views/auth/confirm-password.blade.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<x-guest-layout>
|
||||||
|
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-neutral-950/50 shadow-md overflow-hidden sm:rounded-lg">
|
||||||
|
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{{ __('This is a secure area of the application. Please confirm your password before continuing.') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('password.confirm') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<!-- Password -->
|
||||||
|
<div>
|
||||||
|
<x-input-label for="password" :value="__('Password')" />
|
||||||
|
|
||||||
|
<x-text-input id="password" class="block mt-1 w-full"
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
required autocomplete="current-password" />
|
||||||
|
|
||||||
|
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end mt-4">
|
||||||
|
<x-primary-button>
|
||||||
|
{{ __('Confirm') }}
|
||||||
|
</x-primary-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</x-guest-layout>
|
||||||
27
resources/views/auth/forgot-password.blade.php
Normal file
27
resources/views/auth/forgot-password.blade.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<x-guest-layout>
|
||||||
|
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-neutral-950/50 shadow-md overflow-hidden sm:rounded-lg">
|
||||||
|
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Session Status -->
|
||||||
|
<x-auth-session-status class="mb-4" :status="session('status')" />
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('password.email') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<!-- Email Address -->
|
||||||
|
<div>
|
||||||
|
<x-input-label for="email" :value="__('Email')" />
|
||||||
|
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus />
|
||||||
|
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-end mt-4">
|
||||||
|
<x-primary-button>
|
||||||
|
{{ __('Send Password Reset Link') }}
|
||||||
|
</x-primary-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</x-guest-layout>
|
||||||
148
resources/views/auth/login.blade.php
Normal file
148
resources/views/auth/login.blade.php
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
<x-guest-layout>
|
||||||
|
<!-- Tabs -->
|
||||||
|
<div class="w-full sm:max-w-md mt-6">
|
||||||
|
<ul class="flex list-none flex-row flex-wrap border-b-0 pl-0 relative " role="tablist" data-te-nav-ref>
|
||||||
|
<li role="presentation" class="flex-auto text-center">
|
||||||
|
<a href="#tabs-login" class="rounded-l-lg my-2 block border-x-0 border-b-2 border-t-0 border-transparent px-7 pb-3.5 pt-4 text-xs font-medium uppercase leading-tight text-neutral-500 hover:isolate hover:border-transparent hover:bg-neutral-50 focus:isolate focus:border-transparent data-[te-nav-active]:border-rose-600 data-[te-nav-active]:text-black dark:text-neutral-400 bg-white/50 dark:bg-neutral-950/50 backdrop-blur-sm dark:hover:bg-neutral-800 dark:data-[te-nav-active]:text-white"
|
||||||
|
data-te-toggle="pill" data-te-target="#tabs-login" data-te-nav-active role="tab" aria-controls="tabs-login" aria-selected="true">
|
||||||
|
{{ __('Login') }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li role="presentation" class="flex-auto text-center">
|
||||||
|
<a href="#tabs-register" class="rounded-r-lg my-2 block border-x-0 border-b-2 border-t-0 border-transparent px-7 pb-3.5 pt-4 text-xs font-medium uppercase leading-tight text-neutral-500 hover:isolate hover:border-transparent hover:bg-neutral-50 focus:isolate focus:border-transparent data-[te-nav-active]:border-rose-600 data-[te-nav-active]:text-black dark:text-neutral-400 bg-white/50 dark:bg-neutral-950/50 backdrop-blur-sm dark:hover:bg-neutral-800 dark:data-[te-nav-active]:text-white"
|
||||||
|
data-te-toggle="pill" data-te-target="#tabs-register" role="tab" aria-controls="tabs-register" aria-selected="false">
|
||||||
|
{{ __('Register') }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Login -->
|
||||||
|
<div class="w-full sm:max-w-md hidden opacity-100 transition-opacity duration-150 ease-linear data-[te-tab-active]:block" id="tabs-login" role="tabpanel" aria-labelledby="tabs-login" data-te-tab-active>
|
||||||
|
<div class="px-6 py-4 bg-white dark:bg-neutral-950/50 shadow-md overflow-hidden sm:rounded-lg">
|
||||||
|
<div class="w-full text-center text-white mb-3">
|
||||||
|
<a href="{{ route('discord.login') }}">
|
||||||
|
<div
|
||||||
|
class="relative bg-blue-700 hover:bg-blue-600 text-white font-bold px-4 h-10 rounded text-center p-[10px] mb-4">
|
||||||
|
<i class="fa-brands fa-discord"></i> {{ __('Use Discord Account') }}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Or -->
|
||||||
|
<div class="grid grid-cols-3">
|
||||||
|
<hr class="self-center border-neutral-600">
|
||||||
|
<p>OR</p>
|
||||||
|
<hr class="self-center border-neutral-600">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Session Status -->
|
||||||
|
<x-auth-session-status class="mb-4" :status="session('status')" />
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('login') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<!-- Email Address -->
|
||||||
|
<div>
|
||||||
|
<x-input-label for="email" :value="__('Email')" />
|
||||||
|
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus autocomplete="username" />
|
||||||
|
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Password -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<x-input-label for="password" :value="__('Password')" />
|
||||||
|
|
||||||
|
<x-text-input id="password" class="block mt-1 w-full"
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
required autocomplete="current-password" />
|
||||||
|
|
||||||
|
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Remember Me -->
|
||||||
|
<div class="block mt-4">
|
||||||
|
<label for="remember_me" class="inline-flex items-center">
|
||||||
|
<input id="remember_me" type="checkbox" class="rounded dark:bg-neutral-900 border-neutral-300 dark:border-neutral-700 text-rose-600 shadow-sm focus:ring-rose-500 dark:focus:ring-rose-600 dark:focus:ring-offset-neutral-800" name="remember">
|
||||||
|
<span class="ms-2 text-sm text-neutral-600 dark:text-neutral-400">{{ __('Remember me') }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<altcha-widget id="captcha" floating challengeurl="/altcha-challenge"></altcha-widget>
|
||||||
|
<x-input-error :messages="$errors->get('altcha')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-end mt-4">
|
||||||
|
@if (Route::has('password.request'))
|
||||||
|
<a class="underline text-sm text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-neutral-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-rose-500 dark:focus:ring-offset-neutral-800" href="{{ route('password.request') }}">
|
||||||
|
{{ __('Forgot your password?') }}
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<x-primary-button class="ms-3">
|
||||||
|
{{ __('Log in') }}
|
||||||
|
</x-primary-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Register -->
|
||||||
|
<div class="w-full sm:max-w-md hidden opacity-0 transition-opacity duration-150 ease-linear data-[te-tab-active]:block" id="tabs-register" role="tabpanel" aria-labelledby="tabs-register">
|
||||||
|
<div class="px-6 py-4 bg-white dark:bg-neutral-950/50 shadow-md overflow-hidden sm:rounded-lg">
|
||||||
|
<form method="POST" action="{{ route('register') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<!-- Name -->
|
||||||
|
<div>
|
||||||
|
<x-input-label for="name" :value="__('Name')" />
|
||||||
|
<x-text-input id="name" class="block mt-1 w-full" type="text" name="name" :value="old('name')" required autofocus autocomplete="name" />
|
||||||
|
<x-input-error :messages="$errors->get('name')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email Address -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<x-input-label for="email" :value="__('Email')" />
|
||||||
|
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autocomplete="username" />
|
||||||
|
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Password -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<x-input-label for="password" :value="__('Password')" />
|
||||||
|
|
||||||
|
<x-text-input id="password" class="block mt-1 w-full"
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
required autocomplete="new-password" />
|
||||||
|
|
||||||
|
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Confirm Password -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
|
||||||
|
|
||||||
|
<x-text-input id="password_confirmation" class="block mt-1 w-full"
|
||||||
|
type="password"
|
||||||
|
name="password_confirmation" required autocomplete="new-password" />
|
||||||
|
|
||||||
|
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<altcha-widget id="captcha" floating challengeurl="/altcha-challenge"></altcha-widget>
|
||||||
|
<x-input-error :messages="$errors->get('altcha')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-end mt-4">
|
||||||
|
<x-primary-button class="ms-4">
|
||||||
|
{{ __('Register') }}
|
||||||
|
</x-primary-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-guest-layout>
|
||||||
41
resources/views/auth/reset-password.blade.php
Normal file
41
resources/views/auth/reset-password.blade.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<x-guest-layout>
|
||||||
|
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-neutral-950/50 shadow-md overflow-hidden sm:rounded-lg">
|
||||||
|
<form method="POST" action="{{ route('password.store') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<!-- Password Reset Token -->
|
||||||
|
<input type="hidden" name="token" value="{{ $request->route('token') }}">
|
||||||
|
|
||||||
|
<!-- Email Address -->
|
||||||
|
<div>
|
||||||
|
<x-input-label for="email" :value="__('Email')" />
|
||||||
|
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email', $request->email)" required autofocus autocomplete="username" />
|
||||||
|
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Password -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<x-input-label for="password" :value="__('Password')" />
|
||||||
|
<x-text-input id="password" class="block mt-1 w-full" type="password" name="password" required autocomplete="new-password" />
|
||||||
|
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Confirm Password -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
|
||||||
|
|
||||||
|
<x-text-input id="password_confirmation" class="block mt-1 w-full"
|
||||||
|
type="password"
|
||||||
|
name="password_confirmation" required autocomplete="new-password" />
|
||||||
|
|
||||||
|
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-end mt-4">
|
||||||
|
<x-primary-button>
|
||||||
|
{{ __('Reset Password') }}
|
||||||
|
</x-primary-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</x-guest-layout>
|
||||||
33
resources/views/auth/verify-email.blade.php
Normal file
33
resources/views/auth/verify-email.blade.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<x-guest-layout>
|
||||||
|
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-neutral-950/50 shadow-md overflow-hidden sm:rounded-lg">
|
||||||
|
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (session('status') == 'verification-link-sent')
|
||||||
|
<div class="mb-4 font-medium text-sm text-green-600 dark:text-green-400">
|
||||||
|
{{ __('A new verification link has been sent to the email address you provided during registration.') }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="mt-4 flex items-center justify-between">
|
||||||
|
<form method="POST" action="{{ route('verification.send') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<x-primary-button>
|
||||||
|
{{ __('Resend Verification Email') }}
|
||||||
|
</x-primary-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('logout') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<button type="submit" class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">
|
||||||
|
{{ __('Log Out') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-guest-layout>
|
||||||
@@ -1 +1 @@
|
|||||||
<img class="h-10" src="/images/hs_banner.png">
|
<img class="h-16" src="/images/hs_banner.png">
|
||||||
@@ -1 +1 @@
|
|||||||
<a {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-left text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-neutral-100 dark:hover:bg-neutral-900 focus:outline-none focus:bg-neutral-100 dark:focus:bg-neutral-800 transition duration-150 ease-in-out']) }}>{{ $slot }}</a>
|
<a {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-neutral-100 dark:hover:bg-neutral-900 focus:outline-none focus:bg-neutral-100 dark:focus:bg-neutral-800 transition duration-150 ease-in-out']) }}>{{ $slot }}</a>
|
||||||
|
|||||||
@@ -1,24 +1,16 @@
|
|||||||
@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white dark:bg-neutral-800'])
|
@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white dark:bg-neutral-800'])
|
||||||
|
|
||||||
@php
|
@php
|
||||||
switch ($align) {
|
$alignmentClasses = match ($align) {
|
||||||
case 'left':
|
'left' => 'ltr:origin-top-left rtl:origin-top-right start-0',
|
||||||
$alignmentClasses = 'origin-top-left left-0';
|
'top' => 'origin-top',
|
||||||
break;
|
default => 'ltr:origin-top-right rtl:origin-top-left end-0',
|
||||||
case 'top':
|
};
|
||||||
$alignmentClasses = 'origin-top';
|
|
||||||
break;
|
|
||||||
case 'right':
|
|
||||||
default:
|
|
||||||
$alignmentClasses = 'origin-top-right right-0';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ($width) {
|
$width = match ($width) {
|
||||||
case '48':
|
'48' => 'w-48',
|
||||||
$width = 'w-48';
|
default => $width,
|
||||||
break;
|
};
|
||||||
}
|
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @close.stop="open = false">
|
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @close.stop="open = false">
|
||||||
@@ -28,11 +20,11 @@ switch ($width) {
|
|||||||
|
|
||||||
<div x-show="open"
|
<div x-show="open"
|
||||||
x-transition:enter="transition ease-out duration-200"
|
x-transition:enter="transition ease-out duration-200"
|
||||||
x-transition:enter-start="transform opacity-0 scale-95"
|
x-transition:enter-start="opacity-0 scale-95"
|
||||||
x-transition:enter-end="transform opacity-100 scale-100"
|
x-transition:enter-end="opacity-100 scale-100"
|
||||||
x-transition:leave="transition ease-in duration-75"
|
x-transition:leave="transition ease-in duration-75"
|
||||||
x-transition:leave-start="transform opacity-100 scale-100"
|
x-transition:leave-start="opacity-100 scale-100"
|
||||||
x-transition:leave-end="transform opacity-0 scale-95"
|
x-transition:leave-end="opacity-0 scale-95"
|
||||||
class="absolute z-50 mt-2 {{ $width }} rounded-md shadow-lg {{ $alignmentClasses }}"
|
class="absolute z-50 mt-2 {{ $width }} rounded-md shadow-lg {{ $alignmentClasses }}"
|
||||||
style="display: none;"
|
style="display: none;"
|
||||||
@click="open = false">
|
@click="open = false">
|
||||||
|
|||||||
@@ -40,12 +40,13 @@ $maxWidth = [
|
|||||||
}
|
}
|
||||||
})"
|
})"
|
||||||
x-on:open-modal.window="$event.detail == '{{ $name }}' ? show = true : null"
|
x-on:open-modal.window="$event.detail == '{{ $name }}' ? show = true : null"
|
||||||
|
x-on:close-modal.window="$event.detail == '{{ $name }}' ? show = false : null"
|
||||||
x-on:close.stop="show = false"
|
x-on:close.stop="show = false"
|
||||||
x-on:keydown.escape.window="show = false"
|
x-on:keydown.escape.window="show = false"
|
||||||
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
|
||||||
@@ -59,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"
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
@props(['active'])
|
@props(['active'])
|
||||||
|
|
||||||
@php
|
@php
|
||||||
$classes =
|
$classes = ($active ?? false)
|
||||||
$active ?? false
|
? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 dark:border-indigo-600 text-start text-base font-medium text-indigo-700 dark:text-indigo-300 bg-indigo-50 dark:bg-indigo-900/50 focus:outline-none focus:text-indigo-800 dark:focus:text-indigo-200 focus:bg-indigo-100 dark:focus:bg-indigo-900 focus:border-indigo-700 dark:focus:border-indigo-300 transition duration-150 ease-in-out'
|
||||||
? 'block w-full pl-3 pr-4 py-2 border-l-4 border-indigo-400 dark:border-indigo-600 text-left text-base font-medium text-indigo-700 dark:text-indigo-300 bg-indigo-50 dark:bg-indigo-900/50 focus:outline-none focus:text-indigo-800 dark:focus:text-indigo-200 focus:bg-indigo-100 dark:focus:bg-indigo-900 focus:border-indigo-700 dark:focus:border-indigo-300 transition duration-150 ease-in-out'
|
: 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-neutral-600 dark:text-neutral-400 hover:text-neutral-800 dark:hover:text-neutral-200 hover:bg-neutral-50 dark:hover:bg-neutral-700 hover:border-neutral-300 dark:hover:border-neutral-600 focus:outline-none focus:text-neutral-800 dark:focus:text-neutral-200 focus:bg-neutral-50 dark:focus:bg-neutral-700 focus:border-neutral-300 dark:focus:border-neutral-600 transition duration-150 ease-in-out';
|
||||||
: 'block w-full pl-3 pr-4 py-2 border-l-4 border-transparent text-left text-base font-medium text-neutral-600 dark:text-neutral-200 hover:text-neutral-800 dark:hover:text-neutral-200 hover:bg-neutral-50 dark:hover:bg-neutral-700 hover:border-neutral-300 dark:hover:border-neutral-600 focus:outline-none focus:text-neutral-800 dark:focus:text-neutral-200 focus:bg-neutral-50 dark:focus:bg-neutral-700 focus:border-neutral-300 dark:focus:border-neutral-600 transition duration-150 ease-in-out';
|
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<a {{ $attributes->merge(['class' => $classes]) }}>
|
<a {{ $attributes->merge(['class' => $classes]) }}>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
@props(['disabled' => false])
|
@props(['disabled' => false])
|
||||||
|
|
||||||
<input {{ $disabled ? 'disabled' : '' }} {!! $attributes->merge(['class' => 'border-gray-300 dark:border-gray-700 dark:bg-neutral-900 dark:text-gray-300 focus:border-rose-500 dark:focus:border-rose-600 focus:ring-rose-500 dark:focus:ring-rose-600 rounded-md shadow-sm']) !!}>
|
<input @disabled($disabled) {{ $attributes->merge(['class' => 'border-gray-300 dark:border-gray-700 dark:bg-neutral-900 dark:text-gray-300 focus:border-rose-500 dark:focus:border-rose-600 focus:ring-rose-500 dark:focus:ring-rose-600 rounded-md shadow-sm']) }}>
|
||||||
|
|||||||
@@ -33,19 +33,7 @@
|
|||||||
<x-input-error class="mt-2" :messages="$errors->get('message')" />
|
<x-input-error class="mt-2" :messages="$errors->get('message')" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<altcha-widget id="captcha" floating challengeurl="/altcha-challenge"></altcha-widget>
|
||||||
<x-input-label for="message" :value="__('Captcha')" />
|
|
||||||
<div class="flex pt-2">
|
|
||||||
<div id="captchaImg">
|
|
||||||
{!! captcha_img() !!}
|
|
||||||
</div>
|
|
||||||
<button type="button" class="inline-flex items-center ml-2 px-2 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" id="reloadcaptcha">
|
|
||||||
<i class="fa-solid fa-rotate-right"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
<x-text-input id="captcha" class="block " type="text" name="captcha" required />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<x-primary-button>{{ __('Submit') }}</x-primary-button>
|
<x-primary-button>{{ __('Submit') }}</x-primary-button>
|
||||||
@@ -65,18 +53,4 @@
|
|||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<script>
|
|
||||||
function reloadCaptcha() {
|
|
||||||
window.axios.get('/reload-captcha').then(function(response) {
|
|
||||||
if (response.status == 200) {
|
|
||||||
document.querySelector("#captchaImg").innerHTML = response.data.captcha;
|
|
||||||
}
|
|
||||||
}).catch(function(error) {
|
|
||||||
console.log(error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.querySelector("#reloadcaptcha").addEventListener("click", reloadCaptcha);
|
|
||||||
</script>
|
|
||||||
</section>
|
</section>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -3,17 +3,15 @@
|
|||||||
|
|
||||||
@include('partials.head')
|
@include('partials.head')
|
||||||
|
|
||||||
<body class="font-sans text-gray-900 antialiased">
|
<body class="font-sans antialiased">
|
||||||
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100 dark:bg-gray-900">
|
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100 dark:bg-neutral-900">
|
||||||
<div>
|
<div>
|
||||||
<a href="/">
|
<a href="/">
|
||||||
<x-application-logo class="w-20 h-20 fill-current text-gray-500" />
|
<x-application-logo class="w-24 h-24 fill-current text-gray-500" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-gray-800 shadow-md overflow-hidden sm:rounded-lg">
|
{{ $slot }}
|
||||||
{{ $slot }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -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()->getTagAttribute() }}" />
|
|
||||||
@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()->getTagAttribute() }}
|
{{ 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
|
||||||
@@ -165,7 +163,7 @@
|
|||||||
<i class="fa-solid fa-gear"></i> {{ __('nav.settings') }}
|
<i class="fa-solid fa-gear"></i> {{ __('nav.settings') }}
|
||||||
</x-dropdown-link>
|
</x-dropdown-link>
|
||||||
|
|
||||||
@if (Auth::user()->is_admin)
|
@if (Auth::user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
|
||||||
<x-dropdown-link href="{{ route('admin.upload.index') }}">
|
<x-dropdown-link href="{{ route('admin.upload.index') }}">
|
||||||
<i class="fa-solid fa-user-tie"></i> Admin
|
<i class="fa-solid fa-user-tie"></i> Admin
|
||||||
</x-dropdown-link>
|
</x-dropdown-link>
|
||||||
@@ -188,8 +186,8 @@
|
|||||||
@guest
|
@guest
|
||||||
<x-dropdown-link :href="route('login')">
|
<x-dropdown-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-dropdown-link>
|
</x-dropdown-link>
|
||||||
@endguest
|
@endguest
|
||||||
@@ -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()->getTagAttribute() }}" />
|
<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>
|
||||||
|
|||||||
@@ -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">
|
||||||
<a href="{{ route('user.index', ['username' => $comment->name]) }}">{{ $comment->name }}</a>
|
{{ $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
|
||||||
|
|||||||
@@ -6,12 +6,16 @@
|
|||||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-pink-700 dark:text-neutral-200 ">
|
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-pink-700 dark:text-neutral-200 ">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="px-6 py-3">
|
<th scope="col" class="px-6 py-3">
|
||||||
Discord-ID
|
ID
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="px-6 py-3">
|
||||||
|
Discord ID
|
||||||
<input
|
<input
|
||||||
class="w-4 h-4 ml-2 text-rose-600 bg-gray-100 border-gray-300 rounded focus:ring-rose-500 dark:focus:ring-rose-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
wire:model.live.debounce.600ms="discordId"
|
||||||
type="checkbox"
|
type="search"
|
||||||
wire:model.live="filtered"
|
id="discord-search"
|
||||||
value="true"
|
class="ml-2 w-32 h-7 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..."
|
||||||
>
|
>
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" class="px-6 py-3">
|
<th scope="col" class="px-6 py-3">
|
||||||
@@ -60,13 +64,16 @@
|
|||||||
{{ $user->id }}
|
{{ $user->id }}
|
||||||
</th>
|
</th>
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
{{ $user->discord_name ?? $user->name }}
|
{{ $user->discord_id ?? 'n/a' }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
{{ $user->is_patreon ? 'Yes' : 'No' }}
|
{{ $user->name }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
{{ $user->is_banned ? 'Yes' : 'No' }}
|
{{ $user->hasRole(\App\Enums\UserRole::SUPPORTER) ? 'Yes' : 'No' }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
{{ $user->hasRole(\App\Enums\UserRole::BANNED) ? 'Yes' : 'No' }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
{{ $user->created_at->format('Y-m-d') }}
|
{{ $user->created_at->format('Y-m-d') }}
|
||||||
@@ -78,9 +85,9 @@
|
|||||||
<form method="POST" action="{{ route('admin.user.update') }}">
|
<form method="POST" action="{{ route('admin.user.update') }}">
|
||||||
@csrf
|
@csrf
|
||||||
<input type="hidden" value="{{ $user->id }}" name="id">
|
<input type="hidden" value="{{ $user->id }}" name="id">
|
||||||
<input type="hidden" value="{{ $user->is_banned ? 'unban' : 'ban' }}" name="action">
|
<input type="hidden" value="{{ $user->hasRole(\App\Enums\UserRole::BANNED) ? 'unban' : 'ban' }}" name="action">
|
||||||
<button type="submit" class="inline-block w-full rounded bg-rose-600 pl-[4px] pr-[4px] p-[1px] text-xs font-medium uppercase leading-normal text-white transition duration-150 ease-in-out hover:bg-rose-700 focus:bg-rose-600">
|
<button type="submit" class="inline-block w-full rounded bg-rose-600 pl-[4px] pr-[4px] p-[1px] text-xs font-medium uppercase leading-normal text-white transition duration-150 ease-in-out hover:bg-rose-700 focus:bg-rose-600">
|
||||||
{{ $user->is_banned ? 'Unban' : 'Ban' }}
|
{{ $user->hasRole(\App\Enums\UserRole::BANNED) ? 'Unban' : 'Ban' }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<button wire:click="deleteUserComments('{{ $user->id }}')" class="inline-block w-full rounded bg-red-600 pl-[4px] pr-[4px] p-[1px] text-xs font-medium uppercase leading-normal text-white transition duration-150 ease-in-out hover:bg-rose-700 focus:bg-rose-600">
|
<button wire:click="deleteUserComments('{{ $user->id }}')" class="inline-block w-full rounded bg-red-600 pl-[4px] pr-[4px] p-[1px] text-xs font-medium uppercase leading-normal text-white transition duration-150 ease-in-out hover:bg-rose-700 focus:bg-rose-600">
|
||||||
|
|||||||
121
resources/views/livewire/comment.blade.php
Normal file
121
resources/views/livewire/comment.blade.php
Normal 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->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
|
||||||
|
<a data-te-toggle="tooltip" title="Admin"><i class="fa-solid fa-crown text-yellow-600"></i></a>
|
||||||
|
@endif
|
||||||
|
@if($comment->user->hasRole(\App\Enums\UserRole::SUPPORTER))
|
||||||
|
<a data-te-toggle="tooltip" title="Badge of appreciation for the horny people supporting us! :3"><i class="fa-solid fa-hand-holding-heart text-rose-600"></i></a>
|
||||||
|
@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>
|
||||||
60
resources/views/livewire/comments.blade.php
Normal file
60
resources/views/livewire/comments.blade.php
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -4,20 +4,14 @@
|
|||||||
|
|
||||||
<!-- 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">
|
||||||
<a href="{{ route('user.index', ['username' => $playlist->user->name]) }}">
|
<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">
|
</div>
|
||||||
@else
|
|
||||||
<img class="relative w-24 h-24 flex-none rounded-full shadow-lg" src="/images/default-avatar.webp">
|
|
||||||
@endif
|
|
||||||
</a>
|
|
||||||
<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>
|
||||||
<p class="font-light text-lg text-neutral-200">Episodes: {{ count($playlistEpisodes) }}</p>
|
<p class="font-light text-lg text-neutral-200">Episodes: {{ count($playlistEpisodes) }}</p>
|
||||||
<p class="font-light text-lg text-neutral-200">Creator: <a
|
<p class="font-light text-lg text-neutral-200">Creator: {{ $playlist->user->name }}</p>
|
||||||
href="{{ route('user.index', ['username' => $playlist->user->name]) }}">{{ $playlist->user->name }}</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col justify-center pl-4">
|
<div class="flex flex-col justify-center pl-4">
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
|
|||||||
130
resources/views/livewire/user-comments.blade.php
Normal file
130
resources/views/livewire/user-comments.blade.php
Normal 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>
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
<br>
|
<br>
|
||||||
@php $download = $episode->getDownloadByType('UHD'); @endphp
|
@php $download = $episode->getDownloadByType('UHD'); @endphp
|
||||||
@isset($download)
|
@isset($download)
|
||||||
@if (!Auth::user()->is_patreon)
|
@if (!Auth::user()->hasRole(\App\Enums\UserRole::SUPPORTER))
|
||||||
@if (config('hstream.free_downloads'))
|
@if (config('hstream.free_downloads'))
|
||||||
<p class="font-bold text-gray-800 dark:text-gray-200">
|
<p class="font-bold text-gray-800 dark:text-gray-200">
|
||||||
<i class="fa-solid fa-lock-open pr-[4px] text-yellow-600"></i> 4k
|
<i class="fa-solid fa-lock-open pr-[4px] text-yellow-600"></i> 4k
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
|
|
||||||
<br>
|
<br>
|
||||||
@if ($episode->interpolated_uhd)
|
@if ($episode->interpolated_uhd)
|
||||||
@if (!Auth::user()->is_patreon)
|
@if (!Auth::user()->hasRole(\App\Enums\UserRole::SUPPORTER))
|
||||||
@if (config('hstream.free_downloads'))
|
@if (config('hstream.free_downloads'))
|
||||||
<p class="font-bold text-gray-800 dark:text-gray-200">
|
<p class="font-bold text-gray-800 dark:text-gray-200">
|
||||||
<i class="fa-solid fa-lock-open pr-[4px] text-yellow-600"></i> 4k 48fps
|
<i class="fa-solid fa-lock-open pr-[4px] text-yellow-600"></i> 4k 48fps
|
||||||
|
|||||||
@@ -8,18 +8,7 @@
|
|||||||
<p id="message" class="text-red-600">
|
<p id="message" class="text-red-600">
|
||||||
</p>
|
</p>
|
||||||
<div class="flex pt-2">
|
<div class="flex pt-2">
|
||||||
<div id="captchaImg">
|
<altcha-widget id="altcha" challengeurl="/altcha-challenge"></altcha-widget>
|
||||||
{!! captcha_img() !!}
|
|
||||||
</div>
|
|
||||||
<button type="button" class="inline-flex items-center ml-2 px-2 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" id="reloadcaptcha" >
|
|
||||||
<i class="fa-solid fa-rotate-right"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="flex pt-2 mt-1">
|
|
||||||
<x-text-input id="captcha_text" class="block " type="text" name="captcha_text"/>
|
|
||||||
<button type="button" class="inline-flex items-center ml-2 px-2 -pt-1 bg-rose-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-rose-700 active:bg-rose-900 focus:outline-none focus:ring-2 focus:ring-rose-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150" id="submitcaptcha" >
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
<p class="text-gray-800 dark:text-gray-200 text-sm">
|
<p class="text-gray-800 dark:text-gray-200 text-sm">
|
||||||
@@ -51,21 +40,12 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
var downloadCounter = 0;
|
var downloadCounter = 0;
|
||||||
function reloadCaptcha() {
|
|
||||||
window.axios.get('/reload-captcha').then(function (response) {
|
|
||||||
if (response.status == 200) {
|
|
||||||
document.querySelector("#captchaImg").innerHTML = response.data.captcha;
|
|
||||||
}
|
|
||||||
}).catch(function (error) {
|
|
||||||
console.log(error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitCaptcha() {
|
function submitCaptcha(captchaToken) {
|
||||||
document.querySelector("#message").innerHTML = '';
|
document.querySelector("#message").innerHTML = '';
|
||||||
window.axios.post('/get-download', {
|
window.axios.post('/get-download', {
|
||||||
captcha: document.getElementById('captcha_text').value,
|
episode_id: document.getElementById('e_id').value,
|
||||||
episode_id: document.getElementById('e_id').value
|
captcha: captchaToken,
|
||||||
}).then(function (response) {
|
}).then(function (response) {
|
||||||
document.querySelector("#captcharequired").style.display = "none";
|
document.querySelector("#captcharequired").style.display = "none";
|
||||||
document.querySelector("#captchsolved").style.display = "block";
|
document.querySelector("#captchsolved").style.display = "block";
|
||||||
@@ -89,6 +69,16 @@
|
|||||||
|
|
||||||
document.querySelector("#downloadEpisode").addEventListener("click", increaseDownloadCounter);
|
document.querySelector("#downloadEpisode").addEventListener("click", increaseDownloadCounter);
|
||||||
|
|
||||||
document.querySelector("#reloadcaptcha").addEventListener("click", reloadCaptcha);
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
document.querySelector("#submitcaptcha").addEventListener("click", submitCaptcha);
|
const altcha = document.querySelector("#altcha");
|
||||||
|
|
||||||
|
altcha.addEventListener("statechange", (ev) => {
|
||||||
|
if (ev.detail.state === "verified") {
|
||||||
|
submitCaptcha(ev.detail.payload);
|
||||||
|
|
||||||
|
// Remove captcha from DOM
|
||||||
|
altcha.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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">
|
||||||
<a class="contents" href="{{ route('user.index', ['username' => $user->name]) }}">
|
<p class="font-medium text-gray-900 dark:text-gray-100">{{ $comment->user->name }}</p>
|
||||||
@if($user->discord_avatar)
|
@if($comment->user->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
|
||||||
<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->hasRole(\App\Enums\UserRole::SUPPORTER))
|
||||||
@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>
|
||||||
</a>
|
@endif
|
||||||
<div class="text-gray-800 dark:text-gray-200">
|
</div>
|
||||||
<a href="{{ route('user.index', ['username' => $user->name]) }}">
|
<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() }}
|
||||||
</a>
|
</span>
|
||||||
<div style="white-space: pre-wrap;">{!! $markdown->line($comment->comment) !!}</div>
|
</div>
|
||||||
<br />
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user