Compare commits
70 Commits
dca4924e9a
...
laravel-12
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| e972f8db41 | |||
| 98d36d6018 | |||
| 7eea8285ca | |||
| 9e8efbbe05 | |||
| 5461606857 | |||
| 9ca2f73714 | |||
| 59d63abd79 | |||
| efb3e4197b | |||
| 735dd693ca | |||
| 36f0126a21 | |||
| 50d8704560 | |||
| 7e382ffe1d | |||
| 6a25fd2700 | |||
| 71bcf277f6 | |||
| 6c44d83e6b | |||
| 444feac1e0 | |||
| c034c94db5 | |||
| ca52584da9 |
@@ -57,3 +57,8 @@ VITE_PUSHER_HOST="${PUSHER_HOST}"
|
||||
VITE_PUSHER_PORT="${PUSHER_PORT}"
|
||||
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
|
||||
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||
|
||||
SCOUT_QUEUE=true
|
||||
SCOUT_DRIVER=meilisearch
|
||||
MEILISEARCH_HOST=http://127.0.0.1:7700
|
||||
MEILISEARCH_KEY=masterKey
|
||||
|
||||
@@ -28,6 +28,10 @@ apt install php8.3-fpm
|
||||
apt install mariadb-server
|
||||
sudo mysql_secure_installation
|
||||
|
||||
# Install Meilisearch
|
||||
echo "deb [trusted=yes] https://apt.fury.io/meilisearch/ /" | sudo tee /etc/apt/sources.list.d/fury.list
|
||||
sudo apt update && sudo apt install meilisearch
|
||||
|
||||
# Clone Repo
|
||||
cd /var/www
|
||||
git clone https://gitea.hstream.moe/w33b/hstream.git
|
||||
|
||||
@@ -30,9 +30,9 @@ class AutoStats extends Command
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
PopularDaily::where('created_at', '<=', Carbon::now()->subMinutes(1440))->forceDelete();
|
||||
PopularWeekly::where('created_at', '<=', Carbon::now()->subMinutes(10080))->forceDelete();
|
||||
PopularMonthly::where('created_at', '<=', Carbon::now()->subMinutes(43200))->forceDelete();
|
||||
PopularDaily::where('created_at', '<=', Carbon::now()->subMinutes(1440))->delete();
|
||||
PopularWeekly::where('created_at', '<=', Carbon::now()->subMinutes(10080))->delete();
|
||||
PopularMonthly::where('created_at', '<=', Carbon::now()->subMinutes(43200))->delete();
|
||||
|
||||
$this->comment('Automated Purge Stats Complete');
|
||||
}
|
||||
|
||||
@@ -35,6 +35,6 @@ class ResetUserDownloads extends Command
|
||||
|
||||
// Clear old downloads which have expired
|
||||
UserDownload::where('created_at', '<=', Carbon::now()->subHour(6))
|
||||
->forceDelete();
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use App\Models\Comment;
|
||||
use App\Models\Episode;
|
||||
use App\Models\Hentai;
|
||||
use App\Models\PopularMonthly;
|
||||
@@ -126,7 +127,7 @@ class CacheHelper
|
||||
public static function getLatestComments()
|
||||
{
|
||||
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
|
||||
{
|
||||
Alert::where('id', $alert_id)->forceDelete();
|
||||
Alert::where('id', $alert_id)->delete();
|
||||
|
||||
cache()->forget('alerts');
|
||||
|
||||
|
||||
16
app/Http/Controllers/Admin/CommentsController.php
Normal file
16
app/Http/Controllers/Admin/CommentsController.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
class CommentsController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display Comments Page.
|
||||
*/
|
||||
public function index(): \Illuminate\View\View
|
||||
{
|
||||
return view('admin.comments.index');
|
||||
}
|
||||
}
|
||||
@@ -105,7 +105,7 @@ class SiteBackgroundController extends Controller
|
||||
DB::beginTransaction();
|
||||
|
||||
$bg = SiteBackground::where('id', $id)->firstOrFail();
|
||||
$bg->forceDelete();
|
||||
$bg->delete();
|
||||
|
||||
$resolutions = [1440, 1080, 720, 640];
|
||||
try {
|
||||
|
||||
@@ -38,7 +38,7 @@ class SubtitleController extends Controller
|
||||
|
||||
// Clear everything
|
||||
foreach($episode->subtitles as $sub) {
|
||||
$sub->forceDelete();
|
||||
$sub->delete();
|
||||
}
|
||||
|
||||
if (! $request->input('subtitles')) {
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Models\Hentai;
|
||||
use App\Models\PopularMonthly;
|
||||
use Carbon\Carbon;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Http\Controllers\Controller;
|
||||
@@ -46,6 +47,8 @@ class HentaiApiController extends Controller
|
||||
// Cache for 60 minutes
|
||||
$data = Cache::remember('api_monthly_views', now()->addMinutes(60), function () {
|
||||
return PopularMonthly::selectRaw('DATE(created_at) as date, COUNT(*) as count')
|
||||
->whereDate('created_at', '<', Carbon::today())
|
||||
->whereDate('created_at', '>=', Carbon::today()->subDays(28))
|
||||
->groupBy('date')
|
||||
->orderBy('date', 'asc')
|
||||
->get();
|
||||
|
||||
@@ -30,7 +30,7 @@ class StreamApiController extends Controller
|
||||
'poster' => $episode->gallery()->first()->image_url,
|
||||
'interpolated' => $episode->interpolated,
|
||||
'interpolated_uhd' => $episode->interpolated_uhd,
|
||||
'stream_url' => $episode->url,
|
||||
'stream_url' => $episode->dmca_takedown ? 'stuff/dmca' : $episode->url,
|
||||
'stream_domains' => config('hstream.stream_domain'),
|
||||
'asia_stream_domains' => config('hstream.asia_stream_domain'),
|
||||
'extra_subtitles' => $subtitles
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Auth\LoginRequest;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
@@ -29,7 +28,7 @@ class AuthenticatedSessionController extends Controller
|
||||
|
||||
$request->session()->regenerate();
|
||||
|
||||
return redirect()->intended(RouteServiceProvider::HOME);
|
||||
return redirect()->intended(route('home.index', absolute: false));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
@@ -36,6 +35,6 @@ class ConfirmablePasswordController extends Controller
|
||||
|
||||
$request->session()->put('auth.password_confirmed_at', time());
|
||||
|
||||
return redirect()->intended(RouteServiceProvider::HOME);
|
||||
return redirect()->intended(route('home.index', absolute: false));
|
||||
}
|
||||
}
|
||||
|
||||
130
app/Http/Controllers/Auth/DiscordAuthController.php
Normal file
130
app/Http/Controllers/Auth/DiscordAuthController.php
Normal file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
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->update([
|
||||
'is_patreon' => false,
|
||||
]);
|
||||
|
||||
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', []);
|
||||
|
||||
$isPatreon = false;
|
||||
foreach($patreonRoles as $patreonRole)
|
||||
{
|
||||
if (in_array($patreonRole, $discordRoles, true)) {
|
||||
$isPatreon = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Only update if something actually changed
|
||||
if ($user->is_patreon !== $isPatreon) {
|
||||
$user->update([
|
||||
'is_patreon' => $isPatreon,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@@ -15,7 +14,7 @@ class EmailVerificationNotificationController extends Controller
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
if ($request->user()->hasVerifiedEmail()) {
|
||||
return redirect()->intended(RouteServiceProvider::HOME);
|
||||
return redirect()->intended(route('home.index', absolute: false));
|
||||
}
|
||||
|
||||
$request->user()->sendEmailVerificationNotification();
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
@@ -16,7 +15,7 @@ class EmailVerificationPromptController extends Controller
|
||||
public function __invoke(Request $request): RedirectResponse|View
|
||||
{
|
||||
return $request->user()->hasVerifiedEmail()
|
||||
? redirect()->intended(RouteServiceProvider::HOME)
|
||||
? redirect()->intended(route('home.index', absolute: false))
|
||||
: view('auth.verify-email');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
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;
|
||||
@@ -40,7 +41,7 @@ class NewPasswordController extends Controller
|
||||
// database. Otherwise we will parse the error and return the response.
|
||||
$status = Password::reset(
|
||||
$request->only('email', 'password', 'password_confirmation', 'token'),
|
||||
function ($user) use ($request) {
|
||||
function (User $user) use ($request) {
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($request->password),
|
||||
'remember_token' => Str::random(60),
|
||||
@@ -56,6 +57,6 @@ class NewPasswordController extends Controller
|
||||
return $status == Password::PASSWORD_RESET
|
||||
? redirect()->route('login')->with('status', __($status))
|
||||
: back()->withInput($request->only('email'))
|
||||
->withErrors(['email' => __($status)]);
|
||||
->withErrors(['email' => __($status)]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,20 @@ class PasswordController extends Controller
|
||||
*/
|
||||
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'],
|
||||
|
||||
@@ -39,6 +39,6 @@ class PasswordResetLinkController extends Controller
|
||||
return $status == Password::RESET_LINK_SENT
|
||||
? back()->with('status', __($status))
|
||||
: back()->withInput($request->only('email'))
|
||||
->withErrors(['email' => __($status)]);
|
||||
->withErrors(['email' => __($status)]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,25 +4,15 @@ namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
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 Illuminate\View\View;
|
||||
|
||||
class RegisteredUserController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the registration view.
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('auth.register');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming registration request.
|
||||
*
|
||||
@@ -32,7 +22,7 @@ class RegisteredUserController extends Controller
|
||||
{
|
||||
$request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'email', 'max:255', 'unique:'.User::class],
|
||||
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
@@ -46,6 +36,6 @@ class RegisteredUserController extends Controller
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
return redirect(RouteServiceProvider::HOME);
|
||||
return redirect(route('home.index', absolute: false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Foundation\Auth\EmailVerificationRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@@ -16,13 +15,13 @@ class VerifyEmailController extends Controller
|
||||
public function __invoke(EmailVerificationRequest $request): RedirectResponse
|
||||
{
|
||||
if ($request->user()->hasVerifiedEmail()) {
|
||||
return redirect()->intended(RouteServiceProvider::HOME.'?verified=1');
|
||||
return redirect()->intended(route('home.index', absolute: false).'?verified=1');
|
||||
}
|
||||
|
||||
if ($request->user()->markEmailAsVerified()) {
|
||||
event(new Verified($request->user()));
|
||||
}
|
||||
|
||||
return redirect()->intended(RouteServiceProvider::HOME.'?verified=1');
|
||||
return redirect()->intended(route('home.index', absolute: false).'?verified=1');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,22 @@ class HomeController extends Controller
|
||||
return view('auth.banned');
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects to a random Hentai episode
|
||||
* Done due to performance reasons
|
||||
*/
|
||||
public function random(): \Illuminate\Http\RedirectResponse
|
||||
{
|
||||
$random = Episode::inRandomOrder()
|
||||
->limit(1)
|
||||
->pluck('slug')
|
||||
->first();
|
||||
|
||||
return redirect()->route('hentai.index', [
|
||||
'title' => $random,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display Search Page.
|
||||
*/
|
||||
@@ -95,11 +111,13 @@ class HomeController extends Controller
|
||||
*/
|
||||
public function updateLanguage(Request $request): \Illuminate\Http\RedirectResponse
|
||||
{
|
||||
if(! in_array($request->language, config('lang-detector.languages'))) {
|
||||
return redirect()->back();
|
||||
}
|
||||
abort_unless(in_array($request->language, config('app.supported_locales'), true), 404);
|
||||
|
||||
Cookie::queue(Cookie::forever('locale', $request->language));
|
||||
session(['locale' => $request->language]);
|
||||
|
||||
if (Auth::check()) {
|
||||
Auth::user()->update(['locale' => $request->language]);
|
||||
}
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ class NotificationController extends Controller
|
||||
->where('id', $request->input('id'))
|
||||
->firstOrFail();
|
||||
|
||||
$notification->forceDelete();
|
||||
$notification->delete();
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
@@ -105,13 +105,11 @@ class PlaylistController extends Controller
|
||||
|
||||
$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
|
||||
PlaylistEpisode::where('playlist_id', $playlist->id)->forceDelete();
|
||||
|
||||
// Delete Playlist
|
||||
$playlist->forceDelete();
|
||||
$playlist->delete();
|
||||
|
||||
return to_route('profile.playlists');
|
||||
}
|
||||
@@ -128,8 +126,14 @@ class PlaylistController extends Controller
|
||||
], 404);
|
||||
}
|
||||
|
||||
$playlist = Playlist::where('user_id', $request->user()->id)->where('id', (int) $request->input('playlist'))->firstOrFail();
|
||||
PlaylistEpisode::where('playlist_id', $playlist->id)->where('episode_id', (int) $request->input('episode'))->forceDelete();
|
||||
$playlist = Playlist::where('user_id', $request->user()->id)
|
||||
->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);
|
||||
|
||||
return response()->json([
|
||||
|
||||
@@ -3,12 +3,18 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Episode;
|
||||
use App\Models\User;
|
||||
use App\Http\Requests\ProfileUpdateRequest;
|
||||
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
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;
|
||||
|
||||
@@ -17,7 +23,7 @@ class ProfileController extends Controller
|
||||
/**
|
||||
* Display the user page.
|
||||
*/
|
||||
public function index(Request $request): \Illuminate\View\View
|
||||
public function index(Request $request): View
|
||||
{
|
||||
return view('profile.index', [
|
||||
'user' => $request->user(),
|
||||
@@ -27,7 +33,7 @@ class ProfileController extends Controller
|
||||
/**
|
||||
* 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();
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
public function watched(Request $request): \Illuminate\View\View
|
||||
public function watched(Request $request): View
|
||||
{
|
||||
return view('profile.watched', [
|
||||
'user' => $request->user(),
|
||||
@@ -50,17 +79,17 @@ class ProfileController extends Controller
|
||||
/**
|
||||
* Display the user's comments page.
|
||||
*/
|
||||
public function comments(Request $request): \Illuminate\View\View
|
||||
public function comments(Request $request): View
|
||||
{
|
||||
return view('profile.comments', [
|
||||
'user' => $request->user(),
|
||||
'user' => $request->user(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the user's likes page.
|
||||
*/
|
||||
public function likes(Request $request): \Illuminate\View\View
|
||||
public function likes(Request $request): View
|
||||
{
|
||||
return view('profile.likes', [
|
||||
'user' => $request->user(),
|
||||
@@ -70,7 +99,7 @@ class ProfileController extends Controller
|
||||
/**
|
||||
* Update user settings.
|
||||
*/
|
||||
public function saveSettings(Request $request): \Illuminate\Http\RedirectResponse
|
||||
public function saveSettings(Request $request): RedirectResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
$user->search_design = $request->input('searchDesign') == 'thumbnail';
|
||||
@@ -84,7 +113,7 @@ class ProfileController extends Controller
|
||||
/**
|
||||
* Update user tag blacklist.
|
||||
*/
|
||||
public function saveBlacklist(Request $request): \Illuminate\Http\RedirectResponse
|
||||
public function saveBlacklist(Request $request): RedirectResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
$tags = json_decode($request->input('tags'));
|
||||
@@ -112,19 +141,60 @@ class ProfileController extends Controller
|
||||
*/
|
||||
public function destroy(Request $request): \Illuminate\Http\RedirectResponse
|
||||
{
|
||||
$request->validateWithBag('userDeletion', [
|
||||
'password' => ['required', 'current_password'],
|
||||
]);
|
||||
|
||||
$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();
|
||||
|
||||
$user->delete();
|
||||
|
||||
$request->session()->invalidate();
|
||||
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
cache()->flush();
|
||||
|
||||
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('username', $username)
|
||||
->select('id', 'username', 'global_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,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
\App\Http\Middleware\IsBanned::class,
|
||||
\App\Http\Middleware\SetLocale::class,
|
||||
],
|
||||
|
||||
'api' => [
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ class LoginRequest extends FormRequest
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
@@ -80,6 +80,6 @@ class LoginRequest extends FormRequest
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['string', 'max:255'],
|
||||
'email' => ['email', 'max:255', Rule::unique(User::class)->ignore($this->user()->id)],
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'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),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
47
app/Livewire/AdminCommentSearch.php
Normal file
47
app/Livewire/AdminCommentSearch.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Comment;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AdminCommentSearch extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public $search = '';
|
||||
|
||||
public $userSearch = '';
|
||||
|
||||
public function updatingSearch(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatingUserSearch(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function deleteComment($commentId)
|
||||
{
|
||||
$comment = Comment::where('id', (int) $commentId)->firstOrFail();
|
||||
$comment->delete();
|
||||
|
||||
cache()->flush();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$comments = Comment::when($this->search !== '', fn ($query) => $query->where('body', 'LIKE', "%$this->search%"))
|
||||
->when($this->userSearch !== '', fn ($query) => $query->whereHas('user', fn ($query) => $query->where('name', 'LIKE', "%{$this->userSearch}%")))
|
||||
->orderBy('created_at', 'DESC')
|
||||
->paginate(12);
|
||||
|
||||
return view('livewire.admin-comment-search', [
|
||||
'comments' => $comments
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Comment;
|
||||
use App\Models\User;
|
||||
|
||||
use Livewire\Component;
|
||||
@@ -24,15 +25,23 @@ class AdminUserSearch extends Component
|
||||
#[Url(history: true)]
|
||||
public $banned = [];
|
||||
|
||||
public function deleteUserComments(int $userID)
|
||||
{
|
||||
$user = User::where('id', $userID)
|
||||
->firstOrFail();
|
||||
|
||||
Comment::where('user_id', $user->id)
|
||||
->delete();
|
||||
|
||||
cache()->flush();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$users = User::when($this->filtered !== [], fn ($query) => $query->where('id', '>=', 10000))
|
||||
->when($this->patreon !== [], fn ($query) => $query->where('is_patreon', 1))
|
||||
->when($this->banned !== [], fn ($query) => $query->where('is_banned', 1))
|
||||
->when($this->search !== '', fn ($query) => $query->where(function($query) {
|
||||
$query->where('username', 'like', '%'.$this->search.'%')
|
||||
->orWhere('global_name', 'like', '%'.$this->search.'%');
|
||||
}))
|
||||
->when($this->search !== '', fn ($query) => $query->where('name', 'like', '%'.$this->search.'%'))
|
||||
->paginate(20);
|
||||
|
||||
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\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class DownloadsFree extends Component
|
||||
{
|
||||
@@ -51,7 +50,7 @@ class DownloadsFree extends Component
|
||||
// Check timestamp
|
||||
if (Carbon::parse($alreadyDownloaded->created_at)->addHours(6) <= Carbon::now()) {
|
||||
// Already expired
|
||||
$alreadyDownloaded->forceDelete();
|
||||
$alreadyDownloaded->delete();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,15 +5,41 @@ namespace App\Livewire;
|
||||
use App\Models\Downloads;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Livewire\Attributes\Url;
|
||||
|
||||
class DownloadsSearch extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
#[Url(history: true)]
|
||||
public $fileSearch;
|
||||
|
||||
public $order = 'created_at_desc';
|
||||
|
||||
public $options = [
|
||||
'FHD' => true,
|
||||
'FHD 48fps' => true,
|
||||
];
|
||||
|
||||
public $isOpen = false;
|
||||
|
||||
#[Url(history: true)]
|
||||
public $studios = [];
|
||||
public $studiosCopy = [];
|
||||
|
||||
// To toggle individual option selection
|
||||
public function toggleOption($option)
|
||||
{
|
||||
$this->options[$option] = !$this->options[$option];
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
// To toggle dropdown visibility
|
||||
public function toggleDropdown()
|
||||
{
|
||||
$this->isOpen = !$this->isOpen;
|
||||
}
|
||||
|
||||
protected $queryString = [
|
||||
'fileSearch' => ['except' => '', 'as' => 'fS'],
|
||||
'order' => ['except' => '', 'as' => 'order'],
|
||||
@@ -24,6 +50,40 @@ class DownloadsSearch extends Component
|
||||
$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
|
||||
private function getSelectedTypes()
|
||||
{
|
||||
$types = [];
|
||||
|
||||
// Map the options to their corresponding database values
|
||||
foreach ($this->options as $label => $selected) {
|
||||
if ($selected) {
|
||||
if ($label === 'FHD') {
|
||||
$types[] = 'FHD';
|
||||
} elseif ($label === 'FHD 48fps') {
|
||||
$types[] = 'FHDi';
|
||||
} elseif ($label === 'UHD' && auth()->user()->is_patreon) {
|
||||
$types[] = 'UHD';
|
||||
} elseif ($label === 'UHD 48fps' && auth()->user()->is_patreon) {
|
||||
$types[] = 'UHDi';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $types;
|
||||
}
|
||||
|
||||
public function clicked($downloadId)
|
||||
{
|
||||
$download = Downloads::find($downloadId);
|
||||
@@ -36,6 +96,17 @@ class DownloadsSearch extends Component
|
||||
cache()->forget("episode_{$download->episode->id}_download_{$download->type}");
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
if (!auth()->user()->is_patreon) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add patreon options
|
||||
$this->options['UHD'] = true;
|
||||
$this->options['UHD 48fps'] = true;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$orderby = 'created_at';
|
||||
@@ -72,7 +143,8 @@ class DownloadsSearch extends Component
|
||||
}
|
||||
|
||||
$downloads = Downloads::when($this->fileSearch != '', fn ($query) => $query->where('url', 'like', '%'.$this->fileSearch.'%'))
|
||||
->when(!auth()->user()->is_patreon, fn ($query) => $query->whereIn('type', ['FHD', 'FHDi']))
|
||||
->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')
|
||||
->orderBy($orderby, $orderdirection)
|
||||
->paginate(20);
|
||||
@@ -80,6 +152,7 @@ class DownloadsSearch extends Component
|
||||
return view('livewire.downloads-search', [
|
||||
'downloads' => $downloads,
|
||||
'query' => $this->fileSearch,
|
||||
'studiocount' => is_array($this->studios) ? count($this->studios) : 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Episode;
|
||||
use App\Models\Gallery;
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
@@ -18,22 +17,15 @@ class NavLiveSearch extends Component
|
||||
public function render()
|
||||
{
|
||||
$episodes = [];
|
||||
$randomimage = null;
|
||||
if ($this->navSearch != '') {
|
||||
$episodes = Episode::with('gallery')->where('title', 'like', '%'.$this->navSearch.'%')
|
||||
->orWhere('title_jpn', 'like', '%'.$this->navSearch.'%')
|
||||
->when(Auth::guest(), fn ($query) => $query->withoutTags(['loli', 'shota']))
|
||||
->take(10)
|
||||
$episodes = Episode::search($this->navSearch)
|
||||
->when(Auth::guest(), fn ($query) => $query->whereNotIn('tags', ['Loli', 'Shota']))
|
||||
->take(7)
|
||||
->get();
|
||||
|
||||
$randomimage = Gallery::all()
|
||||
->random(1)
|
||||
->first();
|
||||
}
|
||||
|
||||
return view('livewire.nav-live-search', [
|
||||
'episodes' => $episodes,
|
||||
'randomimage' => $randomimage,
|
||||
'query' => $this->navSearch,
|
||||
'hide' => empty($this->navSearch),
|
||||
]);
|
||||
|
||||
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,7 @@ use App\Models\PopularWeekly;
|
||||
use App\Models\PopularDaily;
|
||||
|
||||
use Conner\Tagging\Taggable;
|
||||
use Laravelista\Comments\Commentable;
|
||||
use Laravel\Scout\Searchable;
|
||||
use Maize\Markable\Markable;
|
||||
use Maize\Markable\Models\Like;
|
||||
|
||||
@@ -17,6 +17,7 @@ use Spatie\Sitemap\Tags\Url;
|
||||
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@@ -24,13 +25,41 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Episode extends Model implements Sitemapable
|
||||
{
|
||||
use Commentable, Markable, Taggable;
|
||||
use Markable, Taggable;
|
||||
use HasFactory;
|
||||
use Searchable;
|
||||
|
||||
protected static $marks = [
|
||||
Like::class
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the name of the index associated with the model.
|
||||
*/
|
||||
public function searchableAs(): string
|
||||
{
|
||||
return 'episodes_index';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the indexable data array for the model.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toSearchableArray()
|
||||
{
|
||||
return [
|
||||
'title' => $this->title,
|
||||
'title_search' => $this->title_search,
|
||||
'title_jpn' => $this->title_jpn,
|
||||
'slug' => $this->slug,
|
||||
'description' => $this->description,
|
||||
'tags' => $this->tagNames(),
|
||||
'release_date' => $this->release_date,
|
||||
'created_at' => $this->created_at,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the studio for the Hentai.
|
||||
*/
|
||||
@@ -74,10 +103,11 @@ class Episode extends Model implements Sitemapable
|
||||
/**
|
||||
* Increment View Count.
|
||||
*/
|
||||
public function incrementViewCount(): bool
|
||||
public function incrementViewCount(): void
|
||||
{
|
||||
$this->view_count++;
|
||||
return $this->save();
|
||||
DB::table('episodes')
|
||||
->where('id', $this->id)
|
||||
->update(['view_count' => $this->view_count + 1]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,6 +160,11 @@ class Episode extends Model implements Sitemapable
|
||||
return cache()->remember('episodeComments' . $this->id, 300, fn() => $this->comments->count());
|
||||
}
|
||||
|
||||
public function comments()
|
||||
{
|
||||
return $this->morphMany(Comment::class, 'commentable');
|
||||
}
|
||||
|
||||
public function getProblematicTags(): string
|
||||
{
|
||||
$problematicTags = ['Gore', 'Scat', 'Horror'];
|
||||
|
||||
@@ -10,11 +10,10 @@ use Illuminate\Support\Carbon;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Conner\Tagging\Taggable;
|
||||
use Laravelista\Comments\Commentable;
|
||||
|
||||
class Hentai extends Model implements Sitemapable
|
||||
{
|
||||
use Commentable, Taggable;
|
||||
use Taggable;
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
@@ -37,6 +36,11 @@ class Hentai extends Model implements Sitemapable
|
||||
return $this->episodes->first()->title;
|
||||
}
|
||||
|
||||
public function comments()
|
||||
{
|
||||
return $this->morphMany(Comment::class, 'commentable');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,20 +2,18 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
//use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
use Jakyeru\Larascord\Traits\InteractsWithDiscord;
|
||||
use Laravelista\Comments\Commenter;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use HasApiTokens, HasFactory, Notifiable, InteractsWithDiscord, Commenter;
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
@@ -23,22 +21,14 @@ class User extends Authenticatable
|
||||
* @var string[]
|
||||
*/
|
||||
protected $fillable = [
|
||||
'id',
|
||||
'username',
|
||||
'global_name',
|
||||
'discriminator',
|
||||
'name',
|
||||
'email',
|
||||
'avatar',
|
||||
'verified',
|
||||
'banner',
|
||||
'banner_color',
|
||||
'accent_color',
|
||||
'password',
|
||||
'locale',
|
||||
'mfa_enabled',
|
||||
'premium_type',
|
||||
'public_flags',
|
||||
'roles',
|
||||
'is_banned',
|
||||
// Discord
|
||||
'discord_id',
|
||||
'discord_avatar',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -47,6 +37,7 @@ class User extends Authenticatable
|
||||
* @var array
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
@@ -56,24 +47,21 @@ class User extends Authenticatable
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'username' => 'string',
|
||||
'global_name' => 'string',
|
||||
'discriminator' => 'string',
|
||||
// Laravel defaults
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
// Other
|
||||
'name' => 'string',
|
||||
'email' => 'string',
|
||||
'avatar' => 'string',
|
||||
'verified' => 'boolean',
|
||||
'banner' => 'string',
|
||||
'banner_color' => 'string',
|
||||
'accent_color' => 'string',
|
||||
'locale' => 'string',
|
||||
'mfa_enabled' => 'boolean',
|
||||
'premium_type' => 'integer',
|
||||
'public_flags' => 'integer',
|
||||
'roles' => 'json',
|
||||
'tag_blacklist' => 'array',
|
||||
// Discord
|
||||
'discord_id' => 'integer',
|
||||
'discord_avatar' => 'string',
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Has Many Playlists.
|
||||
*/
|
||||
@@ -101,8 +89,34 @@ class User extends Authenticatable
|
||||
/**
|
||||
* Has Many Comments.
|
||||
*/
|
||||
public function comments()
|
||||
{
|
||||
return $this->hasMany(Comment::class, 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Comment Count.
|
||||
*/
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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->global_name ?? $user->username;
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Jakyeru\Larascord\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Jakyeru\Larascord\Http\Requests\StoreUserRequest;
|
||||
use Jakyeru\Larascord\Services\DiscordService;
|
||||
|
||||
use RealRashid\SweetAlert\Facades\Alert;
|
||||
|
||||
class DiscordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Handles the Discord OAuth2 login.
|
||||
*/
|
||||
public function handle(StoreUserRequest $request): RedirectResponse | JsonResponse
|
||||
{
|
||||
// Making sure the "guilds" scope was added to .env if there are any guilds specified in "larascord.guilds".
|
||||
if (count(config('larascord.guilds'))) {
|
||||
if (!in_array('guilds', explode('&', config('larascord.scopes')))) {
|
||||
return $this->throwError('missing_guilds_scope');
|
||||
}
|
||||
}
|
||||
|
||||
// Getting the accessToken from the Discord API.
|
||||
try {
|
||||
$accessToken = (new DiscordService())->getAccessTokenFromCode($request->get('code'));
|
||||
} catch (\Exception $e) {
|
||||
return $this->throwError('invalid_code', $e);
|
||||
}
|
||||
|
||||
// Get the user from the Discord API.
|
||||
try {
|
||||
$user = (new DiscordService())->getCurrentUser($accessToken);
|
||||
$user->setAccessToken($accessToken);
|
||||
} catch (\Exception $e) {
|
||||
return $this->throwError('authorization_failed', $e);
|
||||
}
|
||||
|
||||
// Making sure the user has an email if the email scope is set.
|
||||
if (in_array('email', explode('&', config('larascord.scopes')))) {
|
||||
if (empty($user->email)) {
|
||||
return $this->throwError('missing_email');
|
||||
}
|
||||
}
|
||||
|
||||
if (auth()->check()) {
|
||||
// Making sure the current logged-in user's ID is matching the ID retrieved from the Discord API.
|
||||
if (auth()->id() !== (int)$user->id) {
|
||||
auth()->logout();
|
||||
return $this->throwError('invalid_user');
|
||||
}
|
||||
|
||||
// Confirming the session in case the user was redirected from the password.confirm middleware.
|
||||
$request->session()->put('auth.password_confirmed_at', time());
|
||||
}
|
||||
|
||||
// Trying to create or update the user in the database.
|
||||
// Initiating a database transaction in case something goes wrong.
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$user = (new DiscordService())->createOrUpdateUser($user);
|
||||
$user->accessToken()->updateOrCreate([], $accessToken->toArray());
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return $this->throwError('database_error', $e);
|
||||
}
|
||||
|
||||
// Verifying if the user is soft-deleted.
|
||||
if (Schema::hasColumn('users', 'deleted_at')) {
|
||||
if ($user->trashed()) {
|
||||
DB::rollBack();
|
||||
return $this->throwError('user_deleted');
|
||||
}
|
||||
}
|
||||
|
||||
// Patreon check
|
||||
try {
|
||||
if (!$accessToken->hasScopes(['guilds', 'guilds.members.read'])) {
|
||||
DB::rollBack();
|
||||
return $this->throwError('missing_guilds_members_read_scope');
|
||||
}
|
||||
$guildMember = (new DiscordService())->getGuildMember($accessToken, config('discord.guild_id'));
|
||||
$patreonroles = config('discord.patreon_roles');
|
||||
$user->is_patreon = false;
|
||||
if ((new DiscordService())->hasRoleInGuild($guildMember, $patreonroles)) {
|
||||
$user->is_patreon = true;
|
||||
}
|
||||
$user->save();
|
||||
} catch (\Exception $e) {
|
||||
// Clearly not a patreon
|
||||
$user->is_patreon = false;
|
||||
$user->save();
|
||||
}
|
||||
|
||||
// Committing the database transaction.
|
||||
DB::commit();
|
||||
|
||||
// Authenticating the user if the user is not logged in.
|
||||
if (!auth()->check()) {
|
||||
auth()->login($user, config('larascord.remember_me', false));
|
||||
}
|
||||
|
||||
// Redirecting the user to the intended page or to the home page.
|
||||
return redirect()->intended(RouteServiceProvider::HOME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the throwing of an error.
|
||||
*/
|
||||
private function throwError(string $message, \Exception $exception = NULL): RedirectResponse | JsonResponse
|
||||
{
|
||||
if (app()->hasDebugModeEnabled()) {
|
||||
return response()->json([
|
||||
'larascord_message' => config('larascord.error_messages.' . $message),
|
||||
'message' => $exception?->getMessage(),
|
||||
'code' => $exception?->getCode()
|
||||
]);
|
||||
} else {
|
||||
if (config('larascord.error_messages.' . $message . '.redirect')) {
|
||||
Alert::error('Error', config('larascord.error_messages.' . $message . '.message', 'An error occurred while trying to log you in.'));
|
||||
return redirect(config('larascord.error_messages.' . $message . '.redirect'))->with('error', config('larascord.error_messages.' . $message . '.message', 'An error occurred while trying to log you in.'));
|
||||
} else {
|
||||
return redirect('/')->with('error', config('larascord.error_messages.' . $message, 'An error occurred while trying to log you in.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the deletion of the user.
|
||||
*/
|
||||
public function destroy(): RedirectResponse | JsonResponse
|
||||
{
|
||||
// Revoking the OAuth2 access token.
|
||||
try {
|
||||
(new DiscordService())->revokeAccessToken(auth()->user()->accessToken()->first()->refresh_token);
|
||||
} catch (\Exception $e) {
|
||||
return $this->throwError('revoke_token_failed', $e);
|
||||
}
|
||||
|
||||
// Deleting the user from the database.
|
||||
auth()->user()->delete();
|
||||
|
||||
// Showing the success message.
|
||||
if (config('larascord.success_messages.user_deleted.redirect')) {
|
||||
return redirect(config('larascord.success_messages.user_deleted.redirect'))->with('success', config('larascord.success_messages.user_deleted.message', 'Your account has been deleted.'));
|
||||
} else {
|
||||
return redirect('/')->with('success', config('larascord.success_messages.user_deleted', 'Your account has been deleted.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,273 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Jakyeru\Larascord\Services;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\OldUser;
|
||||
use App\Models\Playlist;
|
||||
use App\Models\PlaylistEpisode;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Jakyeru\Larascord\Types\AccessToken;
|
||||
use Jakyeru\Larascord\Types\GuildMember;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DiscordService
|
||||
{
|
||||
/**
|
||||
* The Discord OAuth2 token URL.
|
||||
*/
|
||||
protected string $tokenURL = "https://discord.com/api/oauth2/token";
|
||||
|
||||
/**
|
||||
* The Discord API base URL.
|
||||
*/
|
||||
protected string $baseApi = "https://discord.com/api";
|
||||
/**
|
||||
* The required data for the token request.
|
||||
*/
|
||||
protected array $tokenData = [
|
||||
"client_id" => NULL,
|
||||
"client_secret" => NULL,
|
||||
"grant_type" => "authorization_code",
|
||||
"code" => NULL,
|
||||
"redirect_uri" => NULL,
|
||||
"scope" => null
|
||||
];
|
||||
|
||||
/**
|
||||
* UserService constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->tokenData['client_id'] = config('larascord.client_id');
|
||||
$this->tokenData['client_secret'] = config('larascord.client_secret');
|
||||
$this->tokenData['grant_type'] = config('larascord.grant_type');
|
||||
$this->tokenData['redirect_uri'] = config('larascord.redirect_uri');
|
||||
$this->tokenData['scope'] = config('larascord.scopes');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the Discord OAuth2 callback and returns the access token.
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
public function getAccessTokenFromCode(string $code): AccessToken
|
||||
{
|
||||
$this->tokenData['code'] = $code;
|
||||
|
||||
$response = Http::asForm()->post($this->tokenURL, $this->tokenData);
|
||||
|
||||
$response->throw();
|
||||
|
||||
return new AccessToken(json_decode($response->body()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get access token from refresh token.
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
public function refreshAccessToken(string $refreshToken): AccessToken
|
||||
{
|
||||
$response = Http::asForm()->post($this->tokenURL, [
|
||||
'client_id' => config('larascord.client_id'),
|
||||
'client_secret' => config('larascord.client_secret'),
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $refreshToken,
|
||||
]);
|
||||
|
||||
$response->throw();
|
||||
|
||||
return new AccessToken(json_decode($response->body()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates the user with the access token and returns the user data.
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
public function getCurrentUser(AccessToken $accessToken): \Jakyeru\Larascord\Types\User
|
||||
{
|
||||
$response = Http::withToken($accessToken->access_token)->get($this->baseApi . '/users/@me');
|
||||
|
||||
$response->throw();
|
||||
|
||||
return new \Jakyeru\Larascord\Types\User(json_decode($response->body()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's guilds.
|
||||
*
|
||||
* @throws RequestException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getCurrentUserGuilds(AccessToken $accessToken, bool $withCounts = false): array
|
||||
{
|
||||
if (!$accessToken->hasScope('guilds')) throw new Exception(config('larascord.error_messages.missing_guilds_scope.message'));
|
||||
|
||||
$endpoint = '/users/@me/guilds';
|
||||
|
||||
if ($withCounts) {
|
||||
$endpoint .= '?with_counts=true';
|
||||
}
|
||||
|
||||
$response = Http::withToken($accessToken->access_token, $accessToken->token_type)->get($this->baseApi . $endpoint);
|
||||
|
||||
$response->throw();
|
||||
|
||||
return array_map(function ($guild) {
|
||||
return new \Jakyeru\Larascord\Types\Guild($guild);
|
||||
}, json_decode($response->body()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Guild Member object for a user.
|
||||
*
|
||||
* @throws RequestException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getGuildMember(AccessToken $accessToken, string $guildId): GuildMember
|
||||
{
|
||||
if (!$accessToken->hasScopes(['guilds', 'guilds.members.read'])) throw new Exception(config('larascord.error_messages.missing_guilds_members_read_scope.message'));
|
||||
|
||||
$response = Http::withToken($accessToken->access_token, $accessToken->token_type)->get($this->baseApi . '/users/@me/guilds/' . $guildId . '/member');
|
||||
|
||||
$response->throw();
|
||||
|
||||
return new GuildMember(json_decode($response->body()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the User's connections.
|
||||
*
|
||||
* @throws RequestException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getCurrentUserConnections(AccessToken $accessToken): array
|
||||
{
|
||||
if (!$accessToken->hasScope('connections')) throw new Exception('The "connections" scope is required.');
|
||||
|
||||
$response = Http::withToken($accessToken->access_token, $accessToken->token_type)->get($this->baseApi . '/users/@me/connections');
|
||||
|
||||
$response->throw();
|
||||
|
||||
return array_map(function ($connection) {
|
||||
return new \Jakyeru\Larascord\Types\Connection($connection);
|
||||
}, json_decode($response->body()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Join a guild.
|
||||
*
|
||||
* @throws RequestException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function joinGuild(AccessToken $accessToken, User $user, string $guildId, array $options = []): GuildMember
|
||||
{
|
||||
if (!config('larascord.access_token')) throw new Exception(config('larascord.error_messages.missing_access_token.message'));
|
||||
if (!$accessToken->hasScope('guilds.join')) throw new Exception('The "guilds" and "guilds.join" scopes are required.');
|
||||
|
||||
$response = Http::withToken(config('larascord.access_token'), 'Bot')->put($this->baseApi . '/guilds/' . $guildId . '/members/' . $user->id, array_merge([
|
||||
'access_token' => $accessToken->access_token,
|
||||
], $options));
|
||||
|
||||
$response->throw();
|
||||
|
||||
if ($response->status() === 204) return throw new Exception('User is already in the guild.');
|
||||
|
||||
return new GuildMember(json_decode($response->body()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update a user in the database.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function createOrUpdateUser(\Jakyeru\Larascord\Types\User $user): User
|
||||
{
|
||||
if (!$user->getAccessToken()) {
|
||||
throw new Exception('User access token is missing.');
|
||||
}
|
||||
|
||||
$forgottenUser = User::where('email', '=', $user->email)->where('id', '!=', $user->id)->first();
|
||||
if ($forgottenUser) {
|
||||
// This case should never happen (TM) - The discord id changed
|
||||
// The user probably re-created their discord account with the same email
|
||||
|
||||
// Delete Playlist
|
||||
$playlists = Playlist::where('user_id', $forgottenUser->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', '=', $forgottenUser->id)->update(['commenter_id' => 1]);
|
||||
|
||||
$forgottenUser->forceDelete();
|
||||
}
|
||||
|
||||
return User::updateOrCreate(
|
||||
[
|
||||
'id' => $user->id,
|
||||
],
|
||||
$user->toArray(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify if the user is in the specified guild(s).
|
||||
*/
|
||||
public function isUserInGuilds(array $guilds): bool
|
||||
{
|
||||
// Verify if the user is in all the specified guilds if strict mode is enabled.
|
||||
if (config('larascord.guilds_strict')) {
|
||||
return empty(array_diff(config('larascord.guilds'), array_column($guilds, 'id')));
|
||||
}
|
||||
|
||||
// Verify if the user is in any of the specified guilds if strict mode is disabled.
|
||||
return !empty(array_intersect(config('larascord.guilds'), array_column($guilds, 'id')));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify if the user has the specified role(s) in the specified guild.
|
||||
*/
|
||||
public function hasRoleInGuild(GuildMember $guildMember, array $roles): bool
|
||||
{
|
||||
// Verify if the user has any of the specified roles.
|
||||
return !empty(array_intersect($roles, $guildMember->roles));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the user's roles in the database.
|
||||
*/
|
||||
public function updateUserRoles(User $user, GuildMember $guildMember, int $guildId): void
|
||||
{
|
||||
// Updating the user's roles in the database.
|
||||
$updatedRoles = $user->roles;
|
||||
$updatedRoles[$guildId] = $guildMember->roles;
|
||||
$user->roles = $updatedRoles;
|
||||
$user->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke the user's access token.
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
public function revokeAccessToken(string $accessToken): object
|
||||
{
|
||||
$response = Http::asForm()->post($this->tokenURL . '/revoke', [
|
||||
'token' => $accessToken,
|
||||
'client_id' => config('larascord.client_id'),
|
||||
'client_secret' => config('larascord.client_secret'),
|
||||
]);
|
||||
|
||||
$response->throw();
|
||||
|
||||
return json_decode($response->body());
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
@@ -19,6 +20,8 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
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,
|
||||
Hentai $hentai,
|
||||
int $episodeNumber,
|
||||
Studios $studio = null,
|
||||
Episode $referenceEpisode = null
|
||||
?Studios $studio = null,
|
||||
?Episode $referenceEpisode = null
|
||||
): Episode
|
||||
{
|
||||
$episode = new Episode();
|
||||
@@ -74,6 +74,7 @@ class EpisodeService
|
||||
$episode->interpolated = $request->input('interpolated') == 'yes';
|
||||
$episode->interpolated_uhd = $request->input('downloadUHDi1') ? true : false;
|
||||
$episode->is_dvd_aspect = $request->input('dvd') == 'yes';
|
||||
$episode->dmca_takedown = $request->input('dmca_takedown') == 'true';
|
||||
$episode->save();
|
||||
|
||||
// Tagging
|
||||
|
||||
@@ -74,7 +74,7 @@ class GalleryService
|
||||
foreach ($oldGallery as $oldImage) {
|
||||
Storage::disk('public')->delete($oldImage->image_url);
|
||||
Storage::disk('public')->delete($oldImage->thumbnail_url);
|
||||
$oldImage->forceDelete();
|
||||
$oldImage->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,26 +11,29 @@
|
||||
"php": "^8.2",
|
||||
"guzzlehttp/guzzle": "^7.8.1",
|
||||
"hisorange/browser-detect": "^5.0",
|
||||
"intervention/image": "^3.9",
|
||||
"intervention/image-laravel": "^1.3",
|
||||
"jakyeru/larascord": "^6.0",
|
||||
"laravel/framework": "^11.0",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"http-interop/http-factory-guzzle": "^1.2",
|
||||
"intervention/image": "^3.11",
|
||||
"intervention/image-laravel": "^1.5",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/sanctum": "^4.2",
|
||||
"laravel/scout": "^10.20",
|
||||
"laravel/socialite": "^5.24",
|
||||
"laravel/tinker": "^2.10",
|
||||
"laravelista/comments": "dev-l11-compatibility",
|
||||
"livewire/livewire": "^3.6.4",
|
||||
"livewire/livewire": "^3.7.0",
|
||||
"maize-tech/laravel-markable": "^2.3.0",
|
||||
"mews/captcha": "3.4.4",
|
||||
"meilisearch/meilisearch-php": "^1.16",
|
||||
"mews/captcha": "^3.4.4",
|
||||
"predis/predis": "^2.2",
|
||||
"realrashid/sweet-alert": "^7.2",
|
||||
"rtconner/laravel-tagging": "^4.1",
|
||||
"spatie/laravel-discord-alerts": "^1.5",
|
||||
"spatie/laravel-sitemap": "^7.3",
|
||||
"vluzrmos/language-detector": "^2.3"
|
||||
"rtconner/laravel-tagging": "^5.0",
|
||||
"socialiteproviders/discord": "^4.2",
|
||||
"spatie/laravel-discord-alerts": "^1.8",
|
||||
"spatie/laravel-sitemap": "^7.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-debugbar": "^3.14.7",
|
||||
"barryvdh/laravel-debugbar": "^3.16",
|
||||
"fakerphp/faker": "^1.24.0",
|
||||
"laravel/breeze": "^2.3",
|
||||
"laravel/pint": "^1.18",
|
||||
"laravel/sail": "^1.38",
|
||||
"mockery/mockery": "^1.4.4",
|
||||
@@ -45,23 +48,13 @@
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"exclude-from-classmap": [
|
||||
"vendor/jakyeru/larascord/src/Http/Services/DiscordService.php",
|
||||
"vendor/jakyeru/larascord/src/Http/Controllers/DiscordController.php",
|
||||
"vendor/laravelista/comments/src/CommentPolicy.php",
|
||||
"vendor/laravelista/comments/src/CommentService.php"
|
||||
],
|
||||
"exclude-from-classmap": [],
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
},
|
||||
"files": [
|
||||
"app/Override/Discord/Services/DiscordService.php",
|
||||
"app/Override/Discord/DiscordController.php",
|
||||
"app/Override/Comments/CommentPolicy.php",
|
||||
"app/Override/Comments/CommentService.php"
|
||||
]
|
||||
"files": []
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
|
||||
3013
composer.lock
generated
3013
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -111,6 +111,18 @@ return [
|
||||
|
||||
'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
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'invite_link' => 'https://discord.gg/yAqgVKNgG5',
|
||||
|
||||
'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'),
|
||||
];
|
||||
@@ -1,247 +0,0 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application ID
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the ID of your Discord application.
|
||||
|
|
||||
*/
|
||||
|
||||
'client_id' => env('LARASCORD_CLIENT_ID', null),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Secret
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the secret of your Discord application.
|
||||
|
|
||||
*/
|
||||
|
||||
'client_secret' => env('LARASCORD_CLIENT_SECRET', null),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Access Token
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the access token of your Discord application.
|
||||
|
|
||||
*/
|
||||
|
||||
'access_token' => env('LARASCORD_ACCESS_TOKEN', null),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Grant Type
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the grant type of your Discord application. It must be set to
|
||||
| "authorization_code".
|
||||
|
|
||||
*/
|
||||
|
||||
'grant_type' => env('LARASCORD_GRANT_TYPE', 'authorization_code'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Redirect URI
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the URI that Discord will redirect to after the user authorizes
|
||||
| your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'redirect_uri' => env('APP_URL', 'http://localhost:8000') . '/' . env('LARASCORD_PREFIX', 'larascord') . '/callback',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Scopes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These are the OAuth2 scopes of your Discord application.
|
||||
|
|
||||
*/
|
||||
|
||||
'scopes' => env('LARASCORD_SCOPE', 'identify&email&guilds&guilds.members.read'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Route Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the prefix that Larascord will use for its routes. For example,
|
||||
| the prefix "larascord" will result in the route
|
||||
| "https://domain.com/larascord/login".
|
||||
|
|
||||
*/
|
||||
|
||||
'route_prefix' => env('LARASCORD_PREFIX', 'larascord'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| OAuth2 Prompt - "none" or "consent"
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The prompt controls how the authorization flow handles existing authorizations.
|
||||
| If a user has previously authorized your application with the requested scopes
|
||||
| and prompt is set to consent,it will request them to re-approve their
|
||||
| authorization. If set to none, it will skip the authorization screen
|
||||
| and redirect them back to your redirect URI without requesting
|
||||
| their authorization.
|
||||
|
|
||||
*/
|
||||
|
||||
'prompt' => 'none',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Restrict Access to Specific Guilds
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option restricts access to the application to users who are members
|
||||
| of specific Discord guilds. Users who are not members of the specified
|
||||
| guilds will not be able to use the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'guilds' => [],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Restrict Access to Specific Guilds - Strict Mode
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Enabling this option will require the user to be a member of ALL the
|
||||
| aforementioned guilds. If this option is disabled, the user will
|
||||
| only need to be a member of at least ONE of the guilds.
|
||||
|
|
||||
*/
|
||||
|
||||
'guilds_strict' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Restrict Access to Specific Roles
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When this option is enabled, the user will only be able to use the
|
||||
| application if they have at least one of the specified roles.
|
||||
|
|
||||
*/
|
||||
|
||||
// WARNING: This feature makes one request to the Discord API for each guild you specify. (Because you need to fetch the roles for each guild)
|
||||
// At the moment the database is not checked for roles when the user logs in. It will always fetch the roles from the Discord API.
|
||||
// Currently, the roles are only updated in the database when the user logs in. The roles from the database can be used in a middleware.
|
||||
// I'm working on a better way to do this, but for now, this will work.
|
||||
|
||||
'guild_roles' => [
|
||||
// 'guild_id' => [
|
||||
// 'role_id',
|
||||
// 'role_id',
|
||||
// ],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Remember Me
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Whether or not to remember the user after they log in.
|
||||
|
|
||||
*/
|
||||
|
||||
'remember_me' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Error Messages
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These are the error messages that will be displayed to the user if there
|
||||
| is an error.
|
||||
|
|
||||
*/
|
||||
|
||||
'error_messages' => [
|
||||
'missing_code' => [
|
||||
'message' => 'The authorization code is missing.',
|
||||
'redirect' => '/'
|
||||
],
|
||||
'invalid_code' => [
|
||||
'message' => 'The authorization code is invalid.',
|
||||
'redirect' => '/'
|
||||
],
|
||||
'authorization_failed' => [
|
||||
'message' => 'The authorization failed.',
|
||||
'redirect' => '/'
|
||||
],
|
||||
'missing_email' => [
|
||||
'message' => 'Couldn\'t get your e-mail address. Please add an e-mail address to your Discord account!',
|
||||
'redirect' => '/'
|
||||
],
|
||||
'invalid_user' => [
|
||||
'message' => 'The user ID doesn\'t match the logged-in user.',
|
||||
'redirect' => '/'
|
||||
],
|
||||
'database_error' => [
|
||||
'message' => 'There was an error with the database. Please try again later.',
|
||||
'redirect' => '/'
|
||||
],
|
||||
'missing_guilds_scope' => [
|
||||
'message' => 'The "guilds" scope is required.',
|
||||
'redirect' => '/'
|
||||
],
|
||||
'missing_guilds_members_read_scope' => [
|
||||
'message' => 'The "guilds" and "guilds.members.read" scopes are required.',
|
||||
'redirect' => '/'
|
||||
],
|
||||
'authorization_failed_guilds' => [
|
||||
'message' => 'Couldn\'t get the servers you\'re in.',
|
||||
'redirect' => '/'
|
||||
],
|
||||
'not_member_guild_only' => [
|
||||
'message' => 'You are not a member of the required guilds.',
|
||||
'redirect' => '/'
|
||||
],
|
||||
'missing_access_token' => [
|
||||
'message' => 'The access token is missing.',
|
||||
'redirect' => '/'
|
||||
],
|
||||
'authorization_failed_roles' => [
|
||||
'message' => 'Couldn\'t get the roles you have.',
|
||||
'redirect' => '/'
|
||||
],
|
||||
'missing_role' => [
|
||||
'message' => 'You don\'t have the required roles.',
|
||||
'redirect' => '/'
|
||||
],
|
||||
'revoke_token_failed' => [
|
||||
'message' => 'An error occurred while trying to revoke your access token.',
|
||||
'redirect' => '/'
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Success Messages
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These are the success messages that will be displayed to the user if there
|
||||
| is no error.
|
||||
|
|
||||
*/
|
||||
|
||||
'success_messages' => [
|
||||
'user_deleted' => [
|
||||
'message' => 'Your account has been deleted.',
|
||||
'redirect' => '/'
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
223
config/scout.php
Normal file
223
config/scout.php
Normal file
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Episode;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Search Engine
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default search connection that gets used while
|
||||
| using Laravel Scout. This connection is used when syncing all models
|
||||
| to the search service. You should adjust this based on your needs.
|
||||
|
|
||||
| Supported: "algolia", "meilisearch", "typesense",
|
||||
| "database", "collection", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'driver' => env('SCOUT_DRIVER', 'collection'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Index Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify a prefix that will be applied to all search index
|
||||
| names used by Scout. This prefix may be useful if you have multiple
|
||||
| "tenants" or applications sharing the same search infrastructure.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => env('SCOUT_PREFIX', ''),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Data Syncing
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to control if the operations that sync your data
|
||||
| with your search engines are queued. When this is set to "true" then
|
||||
| all automatic data syncing will get queued for better performance.
|
||||
|
|
||||
*/
|
||||
|
||||
'queue' => env('SCOUT_QUEUE', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database Transactions
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This configuration option determines if your data will only be synced
|
||||
| with your search indexes after every open database transaction has
|
||||
| been committed, thus preventing any discarded data from syncing.
|
||||
|
|
||||
*/
|
||||
|
||||
'after_commit' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Chunk Sizes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These options allow you to control the maximum chunk size when you are
|
||||
| mass importing data into the search engine. This allows you to fine
|
||||
| tune each of these chunk sizes based on the power of the servers.
|
||||
|
|
||||
*/
|
||||
|
||||
'chunk' => [
|
||||
'searchable' => 500,
|
||||
'unsearchable' => 500,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Soft Deletes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows to control whether to keep soft deleted records in
|
||||
| the search indexes. Maintaining soft deleted records can be useful
|
||||
| if your application still needs to search for the records later.
|
||||
|
|
||||
*/
|
||||
|
||||
'soft_delete' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Identify User
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to control whether to notify the search engine
|
||||
| of the user performing the search. This is sometimes useful if the
|
||||
| engine supports any analytics based on this application's users.
|
||||
|
|
||||
| Supported engines: "algolia"
|
||||
|
|
||||
*/
|
||||
|
||||
'identify' => env('SCOUT_IDENTIFY', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Algolia Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure your Algolia settings. Algolia is a cloud hosted
|
||||
| search engine which works great with Scout out of the box. Just plug
|
||||
| in your application ID and admin API key to get started searching.
|
||||
|
|
||||
*/
|
||||
|
||||
'algolia' => [
|
||||
'id' => env('ALGOLIA_APP_ID', ''),
|
||||
'secret' => env('ALGOLIA_SECRET', ''),
|
||||
'index-settings' => [
|
||||
// 'users' => [
|
||||
// 'searchableAttributes' => ['id', 'name', 'email'],
|
||||
// 'attributesForFaceting'=> ['filterOnly(email)'],
|
||||
// ],
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Meilisearch Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure your Meilisearch settings. Meilisearch is an open
|
||||
| source search engine with minimal configuration. Below, you can state
|
||||
| the host and key information for your own Meilisearch installation.
|
||||
|
|
||||
| See: https://www.meilisearch.com/docs/learn/configuration/instance_options#all-instance-options
|
||||
|
|
||||
*/
|
||||
|
||||
'meilisearch' => [
|
||||
'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'),
|
||||
'key' => env('MEILISEARCH_KEY'),
|
||||
'index-settings' => [
|
||||
Episode::class => [
|
||||
'filterableAttributes' => [
|
||||
'title',
|
||||
'title_search',
|
||||
'title_jpn',
|
||||
'slug',
|
||||
'description',
|
||||
'tags'
|
||||
],
|
||||
'sortableAttributes' => [
|
||||
'created_at',
|
||||
'release_date',
|
||||
'title'
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Typesense Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure your Typesense settings. Typesense is an open
|
||||
| source search engine using minimal configuration. Below, you will
|
||||
| state the host, key, and schema configuration for the instance.
|
||||
|
|
||||
*/
|
||||
|
||||
'typesense' => [
|
||||
'client-settings' => [
|
||||
'api_key' => env('TYPESENSE_API_KEY', 'xyz'),
|
||||
'nodes' => [
|
||||
[
|
||||
'host' => env('TYPESENSE_HOST', 'localhost'),
|
||||
'port' => env('TYPESENSE_PORT', '8108'),
|
||||
'path' => env('TYPESENSE_PATH', ''),
|
||||
'protocol' => env('TYPESENSE_PROTOCOL', 'http'),
|
||||
],
|
||||
],
|
||||
'nearest_node' => [
|
||||
'host' => env('TYPESENSE_HOST', 'localhost'),
|
||||
'port' => env('TYPESENSE_PORT', '8108'),
|
||||
'path' => env('TYPESENSE_PATH', ''),
|
||||
'protocol' => env('TYPESENSE_PROTOCOL', 'http'),
|
||||
],
|
||||
'connection_timeout_seconds' => env('TYPESENSE_CONNECTION_TIMEOUT_SECONDS', 2),
|
||||
'healthcheck_interval_seconds' => env('TYPESENSE_HEALTHCHECK_INTERVAL_SECONDS', 30),
|
||||
'num_retries' => env('TYPESENSE_NUM_RETRIES', 3),
|
||||
'retry_interval_seconds' => env('TYPESENSE_RETRY_INTERVAL_SECONDS', 1),
|
||||
],
|
||||
// 'max_total_results' => env('TYPESENSE_MAX_TOTAL_RESULTS', 1000),
|
||||
'model-settings' => [
|
||||
// User::class => [
|
||||
// 'collection-schema' => [
|
||||
// 'fields' => [
|
||||
// [
|
||||
// 'name' => 'id',
|
||||
// 'type' => 'string',
|
||||
// ],
|
||||
// [
|
||||
// 'name' => 'name',
|
||||
// 'type' => 'string',
|
||||
// ],
|
||||
// [
|
||||
// 'name' => 'created_at',
|
||||
// 'type' => 'int64',
|
||||
// ],
|
||||
// ],
|
||||
// 'default_sorting_field' => 'created_at',
|
||||
// ],
|
||||
// 'search-parameters' => [
|
||||
// 'query_by' => 'name'
|
||||
// ],
|
||||
// ],
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -31,4 +31,21 @@ return [
|
||||
'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
|
||||
],
|
||||
|
||||
|
||||
];
|
||||
|
||||
@@ -5,6 +5,7 @@ use App\Models\Downloads;
|
||||
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
|
||||
{
|
||||
@@ -14,7 +15,7 @@ return new class extends Migration
|
||||
public function up(): void
|
||||
{
|
||||
# Delete entries with "#" as URL
|
||||
Downloads::where('url', '#')->forceDelete();
|
||||
Downloads::where('url', '#')->delete();
|
||||
|
||||
# Remove duplicate entries
|
||||
$duplicates = DB::table('downloads')
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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('episodes', function (Blueprint $table) {
|
||||
$table->boolean('dmca_takedown')->default(0)->after('interpolated_uhd');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('episodes', function (Blueprint $table) {
|
||||
$table->dropColumn('dmca_takedown');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
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
|
||||
{
|
||||
// Remove tables from larascord
|
||||
Schema::dropIfExists('discord_access_tokens');
|
||||
Schema::dropIfExists('personal_access_tokens');
|
||||
|
||||
// Drop columns from larascord
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('discriminator');
|
||||
$table->dropColumn('remember_token');
|
||||
$table->dropColumn('banner');
|
||||
$table->dropColumn('banner_color');
|
||||
$table->dropColumn('accent_color');
|
||||
$table->dropColumn('premium_type');
|
||||
$table->dropColumn('public_flags');
|
||||
$table->dropColumn('verified');
|
||||
$table->dropColumn('mfa_enabled');
|
||||
$table->dropColumn('global_name');
|
||||
$table->dropColumn('locale');
|
||||
});
|
||||
|
||||
// Change & Add Columns
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
// Rename
|
||||
$table->renameColumn('username', 'name');
|
||||
$table->renameColumn('avatar', 'discord_avatar');
|
||||
$table->string('avatar')->nullable()->after('email');
|
||||
|
||||
// Re-Add Email verification
|
||||
$table->timestamp('email_verified_at')->nullable()->after('email');
|
||||
|
||||
// Re-Add Password Auth
|
||||
$table->string('password')->nullable()->after('email_verified_at');
|
||||
$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');
|
||||
});
|
||||
}
|
||||
};
|
||||
1331
package-lock.json
generated
1331
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -6,12 +6,13 @@
|
||||
"build": "vite build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"autoprefixer": "^10.4.18",
|
||||
"@tailwindcss/forms": "^0.5.2",
|
||||
"alpinejs": "^3.4.2",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"axios": "^1.6.8",
|
||||
"laravel-vite-plugin": "^2.0.0",
|
||||
"postcss": "^8.4.35",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"postcss": "^8.4.31",
|
||||
"tailwindcss": "^3.1.0",
|
||||
"vite": "^7.1.6",
|
||||
"vite-plugin-static-copy": "^3.0.1"
|
||||
},
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import './bootstrap';
|
||||
|
||||
// import { Alpine } from '../../vendor/livewire/livewire/dist/livewire.esm';
|
||||
// Alpine.start();
|
||||
|
||||
import 'hammerjs';
|
||||
import {
|
||||
Collapse,
|
||||
Carousel,
|
||||
@@ -15,6 +12,10 @@ import {
|
||||
initTE,
|
||||
} from "tw-elements";
|
||||
|
||||
initTE({ Collapse, Carousel, Clipboard, Modal, Tab, Lightbox, Tooltip, Ripple });
|
||||
// import Alpine from 'alpinejs';
|
||||
|
||||
import 'hammerjs';
|
||||
// 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 apiResponse = {};
|
||||
var volume = 0.5;
|
||||
var muted = false;
|
||||
var captions = true;
|
||||
var lastTime = 0.0;
|
||||
var streamServer = '';
|
||||
@@ -65,10 +66,16 @@ if (localStorage.hstreamCaptions) {
|
||||
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
|
||||
if (localStorage.hstreamServerFallback) {
|
||||
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
|
||||
@@ -224,6 +231,7 @@ function initPlayer() {
|
||||
};
|
||||
|
||||
player.volume = volume;
|
||||
player.muted = muted;
|
||||
//player.captions.languages = ['en'];
|
||||
player.captions.language = 'en';
|
||||
player.captions.active = captions;
|
||||
@@ -306,6 +314,8 @@ function initPlayer() {
|
||||
player.on('volumechange', () => {
|
||||
console.log('Saving Audio Volume to Local Storage: ' + player.volume);
|
||||
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', () => {
|
||||
|
||||
@@ -34,7 +34,7 @@ window.axios.get('/v1/monthly-views').then(function (response) {
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Views the last 30 days',
|
||||
text: 'Views the last 28 days',
|
||||
font: {
|
||||
size: 18
|
||||
}
|
||||
|
||||
@@ -19,4 +19,46 @@ if(localStorage.theme) {
|
||||
} else {
|
||||
// Default Dark Theme
|
||||
localStorage.theme = 'dark';
|
||||
}
|
||||
}
|
||||
|
||||
// Ability to disable blur effects for slower devices
|
||||
const LOCAL_STORAGE_KEY = 'blur';
|
||||
const blurCheckbox = document.querySelector("input[type='checkbox']#toggleBlur");
|
||||
|
||||
function setCSSFilter(selector, value) {
|
||||
document.querySelectorAll(selector).forEach(el => {
|
||||
el.style.backdropFilter = value
|
||||
});
|
||||
}
|
||||
|
||||
function applyBlur(enabled) {
|
||||
if (!enabled) {
|
||||
setCSSFilter('.backdrop-blur, .backdrop-blur-sm, .backdrop-blur-lg', 'none');
|
||||
return;
|
||||
}
|
||||
|
||||
setCSSFilter('.backdrop-blur-lg', 'blur(16px)');
|
||||
setCSSFilter('.backdrop-blur', 'blur(8px)');
|
||||
setCSSFilter('.backdrop-blur-sm', 'blur(4px)');
|
||||
}
|
||||
|
||||
function initBlurToggle() {
|
||||
const storedValue = localStorage.getItem(LOCAL_STORAGE_KEY);
|
||||
const enabled = storedValue === null ? true : storedValue === 'true';
|
||||
|
||||
// initialize UI and DOM
|
||||
applyBlur(enabled);
|
||||
if (blurCheckbox) blurCheckbox.checked = enabled;
|
||||
|
||||
// add event listener
|
||||
if (blurCheckbox) {
|
||||
blurCheckbox.addEventListener('click', (e) => {
|
||||
console.log("Received Event");
|
||||
const isEnabled = e.target.checked;
|
||||
applyBlur(isEnabled);
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, isEnabled ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
initBlurToggle();
|
||||
5
resources/views/admin/comments/index.blade.php
Normal file
5
resources/views/admin/comments/index.blade.php
Normal file
@@ -0,0 +1,5 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('content')
|
||||
@livewire('admin-comment-search')
|
||||
@endsection
|
||||
@@ -104,6 +104,13 @@
|
||||
v2 Re-Release Notification
|
||||
</label>
|
||||
</div>
|
||||
<div class="inline-block mr-2">
|
||||
<input class="w-4 h-4 text-rose-600 bg-gray-100 border-gray-300 rounded focus:ring-rose-500 dark:focus:ring-rose-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
||||
type="checkbox" value="true" id="dmca_takedown" name="dmca_takedown" />
|
||||
<label class="inline-block hover:cursor-pointer dark:text-white" for="dmca_takedown">
|
||||
DMCA Takedown
|
||||
</label>
|
||||
</div>
|
||||
<button type="button" class="inline-block px-6 pt-2.5 pb-2 text-xs font-medium leading-normal uppercase rounded transition duration-150 ease-in-out bg-primary-100 text-primary-700 hover:bg-primary-accent-100 focus:bg-primary-accent-100 focus:outline-none focus:ring-0 active:bg-primary-accent-200" data-te-modal-dismiss data-te-ripple-init data-te-ripple-color="light">
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
@@ -19,6 +19,12 @@
|
||||
<span class="ms-3">Users</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ route('admin.comments.index') }}" class="flex items-center p-2 text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-pink-700/40 group @if(Route::is('admin.comments.index')) bg-pink-700/40 @endif">
|
||||
<i class="fa-solid fa-comment"></i>
|
||||
<span class="ms-3">Comments</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ route('admin.contact.index') }}" class="flex items-center p-2 text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-pink-700/40 group @if(Route::is('admin.contact.index')) bg-pink-700/40 @endif">
|
||||
<i class="fa-solid fa-message"></i>
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
<x-guest-layout>
|
||||
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ __('This is a secure area of the application. Please confirm your session before continuing.') }}
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('larascord.refresh_token') }}">
|
||||
@csrf
|
||||
|
||||
<div class="flex justify-end mt-4">
|
||||
<x-primary-button>
|
||||
<svg style="margin-right: 10px;" width="30px" height="30px" viewBox="0 -28.5 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||
<g>
|
||||
<path d="M216.856339,16.5966031 C200.285002,8.84328665 182.566144,3.2084988 164.041564,0 C161.766523,4.11318106 159.108624,9.64549908 157.276099,14.0464379 C137.583995,11.0849896 118.072967,11.0849896 98.7430163,14.0464379 C96.9108417,9.64549908 94.1925838,4.11318106 91.8971895,0 C73.3526068,3.2084988 55.6133949,8.86399117 39.0420583,16.6376612 C5.61752293,67.146514 -3.4433191,116.400813 1.08711069,164.955721 C23.2560196,181.510915 44.7403634,191.567697 65.8621325,198.148576 C71.0772151,190.971126 75.7283628,183.341335 79.7352139,175.300261 C72.104019,172.400575 64.7949724,168.822202 57.8887866,164.667963 C59.7209612,163.310589 61.5131304,161.891452 63.2445898,160.431257 C105.36741,180.133187 151.134928,180.133187 192.754523,160.431257 C194.506336,161.891452 196.298154,163.310589 198.110326,164.667963 C191.183787,168.842556 183.854737,172.420929 176.223542,175.320965 C180.230393,183.341335 184.861538,190.991831 190.096624,198.16893 C211.238746,191.588051 232.743023,181.531619 254.911949,164.955721 C260.227747,108.668201 245.831087,59.8662432 216.856339,16.5966031 Z M85.4738752,135.09489 C72.8290281,135.09489 62.4592217,123.290155 62.4592217,108.914901 C62.4592217,94.5396472 72.607595,82.7145587 85.4738752,82.7145587 C98.3405064,82.7145587 108.709962,94.5189427 108.488529,108.914901 C108.508531,123.290155 98.3405064,135.09489 85.4738752,135.09489 Z M170.525237,135.09489 C157.88039,135.09489 147.510584,123.290155 147.510584,108.914901 C147.510584,94.5396472 157.658606,82.7145587 170.525237,82.7145587 C183.391518,82.7145587 193.761324,94.5189427 193.539891,108.914901 C193.539891,123.290155 183.391518,135.09489 170.525237,135.09489 Z" fill="#ffff" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</svg>
|
||||
{{ __('Confirm') }}
|
||||
</x-primary-button>
|
||||
<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>
|
||||
</x-guest-layout>
|
||||
|
||||
<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>
|
||||
138
resources/views/auth/login.blade.php
Normal file
138
resources/views/auth/login.blade.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<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="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="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'])
|
||||
|
||||
@php
|
||||
switch ($align) {
|
||||
case 'left':
|
||||
$alignmentClasses = 'origin-top-left left-0';
|
||||
break;
|
||||
case 'top':
|
||||
$alignmentClasses = 'origin-top';
|
||||
break;
|
||||
case 'right':
|
||||
default:
|
||||
$alignmentClasses = 'origin-top-right right-0';
|
||||
break;
|
||||
}
|
||||
$alignmentClasses = match ($align) {
|
||||
'left' => 'ltr:origin-top-left rtl:origin-top-right start-0',
|
||||
'top' => 'origin-top',
|
||||
default => 'ltr:origin-top-right rtl:origin-top-left end-0',
|
||||
};
|
||||
|
||||
switch ($width) {
|
||||
case '48':
|
||||
$width = 'w-48';
|
||||
break;
|
||||
}
|
||||
$width = match ($width) {
|
||||
'48' => 'w-48',
|
||||
default => $width,
|
||||
};
|
||||
@endphp
|
||||
|
||||
<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"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="transform opacity-0 scale-95"
|
||||
x-transition:enter-end="transform opacity-100 scale-100"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="transform opacity-100 scale-100"
|
||||
x-transition:leave-end="transform opacity-0 scale-95"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
class="absolute z-50 mt-2 {{ $width }} rounded-md shadow-lg {{ $alignmentClasses }}"
|
||||
style="display: none;"
|
||||
@click="open = false">
|
||||
|
||||
@@ -40,12 +40,13 @@ $maxWidth = [
|
||||
}
|
||||
})"
|
||||
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:keydown.escape.window="show = false"
|
||||
x-on:keydown.tab.prevent="$event.shiftKey || nextFocusable().focus()"
|
||||
x-on:keydown.shift.tab.prevent="prevFocusable().focus()"
|
||||
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' }};"
|
||||
>
|
||||
<div
|
||||
@@ -59,12 +60,12 @@ $maxWidth = [
|
||||
x-transition:leave-start="opacity-100"
|
||||
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
|
||||
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-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"
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
@props(['active'])
|
||||
|
||||
@php
|
||||
$classes =
|
||||
$active ?? false
|
||||
? '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 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';
|
||||
$classes = ($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 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';
|
||||
@endphp
|
||||
|
||||
<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 }}
|
||||
</button>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
@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']) }}>
|
||||
|
||||
@@ -24,9 +24,8 @@
|
||||
@include('home.partials.random')
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Comments -->
|
||||
<div class="mx-auto pt-6 sm:px-6 lg:px-8 space-y-6 max-w-[100%] xl:max-w-[95%] 2xl:max-w-[85%]">
|
||||
<div class="mx-auto pt-6 sm:px-6 lg:px-8 space-y-6 max-w-[100%] xl:max-w-[95%] 2xl:max-w-[85%] pb-2">
|
||||
@include('home.partials.comments')
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -2,14 +2,14 @@
|
||||
{{ __('home.latest-comments') }}
|
||||
</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)
|
||||
@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
|
||||
<div id="comments" class="flex p-4 bg-white rounded-lg dark:bg-neutral-950">
|
||||
<div
|
||||
class="w-[20vw] mr-5 p-1 md:p-2 mb-8 relative transition ease-in-out hover:-translate-y-1 hover:scale-110 duration-300">
|
||||
<a class="hidden hover:text-blue-600 xl:block"
|
||||
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 2xl:block"
|
||||
href="{{ route('hentai.index', ['title' => $episode->slug]) }}">
|
||||
<img alt="{{ $episode->title }} - {{ $episode->episode }}" loading="lazy" width="1000"
|
||||
class="block object-cover object-center relative z-20 rounded-lg aspect-video"
|
||||
@@ -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">
|
||||
{{ $episode->getResolution() }}</p>
|
||||
<div class="absolute w-[95%] grid grid-cols-1 text-center">
|
||||
<p class="text-sm text-center text-black dark:text-white">{{ $episode->title }} -
|
||||
<p class="text-sm text-center text-black dark:text-white truncate">{{ $episode->title }} -
|
||||
{{ $episode->episode }}</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a class="block hover:text-blue-600 xl:hidden"
|
||||
<a class="block 2xl:hidden"
|
||||
href="{{ route('hentai.index', ['title' => $episode->slug]) }}">
|
||||
<img alt="{{ $episode->title }} - {{ $episode->episode }}" loading="lazy" width="1000"
|
||||
class="block object-cover object-center relative z-20 rounded-lg"
|
||||
src="{{ $episode->cover_url }}"></img>
|
||||
</a>
|
||||
</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])
|
||||
</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
|
||||
<div id="comments" class="flex p-4 bg-white rounded-lg dark:bg-neutral-950">
|
||||
<div
|
||||
class="w-[20vw] mr-5 p-1 md:p-2 mb-8 relative transition ease-in-out hover:-translate-y-1 hover:scale-110 duration-300">
|
||||
<a class="hover:text-blue-600" href="{{ route('hentai.index', ['title' => $hentai->slug]) }}">
|
||||
class="w-[15vw] mr-5 p-1 md:p-2 mb-8 relative transition ease-in-out hover:-translate-y-1 hover:scale-110 duration-300">
|
||||
<a class="hidden 2xl:block" href="{{ route('hentai.index', ['title' => $hentai->slug]) }}">
|
||||
<img alt="{{ $hentai->episodes->first()->title }}" loading="lazy" width="1000"
|
||||
class="block object-cover object-center relative z-20 rounded-lg aspect-video"
|
||||
src="{{ $hentai->gallery->first()->thumbnail_url }}"></img>
|
||||
@@ -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">
|
||||
{{ $hentai->episodes->first()->getResolution() }}</p>
|
||||
<div class="absolute w-[95%] grid grid-cols-1 text-center">
|
||||
<p class="text-sm text-center text-black dark:text-white">
|
||||
<p class="text-sm text-center text-black dark:text-white truncate">
|
||||
{{ $hentai->episodes->first()->title }}</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a class="block 2xl:hidden"
|
||||
href="{{ route('hentai.index', ['title' => $hentai->slug]) }}">
|
||||
<img alt="{{ $hentai->episodes->first()->title }}" loading="lazy" width="1000"
|
||||
class="block object-cover object-center relative z-20 rounded-lg"
|
||||
src="{{ $hentai->episodes->first()->cover_url }}"></img>
|
||||
</a>
|
||||
</div>
|
||||
<div class="w-[60vw]">
|
||||
<div class="w-[60vw] pt-4 bg-neutral-100 dark:bg-neutral-800 rounded-lg pl-4">
|
||||
@include('partials.comment', ['comment' => $comment])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<p class="leading-normal font-bold text-lg text-neutral-800 dark:text-white">
|
||||
Random
|
||||
Random <span class="font-light text-xs text-neutral-800/60 dark:text-white/40">(Cached for 5 minutes)</span>
|
||||
</p>
|
||||
|
||||
@php $random = \App\Models\Episode::inRandomOrder()->limit(8)->get(); @endphp
|
||||
@php
|
||||
$random = \cache()->remember('random_home', 300, function () {
|
||||
return \App\Models\Episode::inRandomOrder()->limit(8)->get(); ;
|
||||
});
|
||||
@endphp
|
||||
|
||||
<div class="mb-6">
|
||||
@include('home.partials.tab.template', ['episodes' => $random, 'showThumbnails' => false])
|
||||
|
||||
@@ -45,6 +45,10 @@
|
||||
<a target="_blank" href="https://hentaisites.com/"
|
||||
class="hover:underline md:mr-6">hentaisites.com</a>
|
||||
</li>
|
||||
<li>
|
||||
<a target="_blank" href="https://zhentube.com/"
|
||||
class="hover:underline md:mr-6">zhentube.com</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="flex flex-wrap items-center mb-6 text-sm font-medium text-gray-500 sm:mb-0 dark:text-gray-400">
|
||||
<li>
|
||||
|
||||
@@ -3,17 +3,15 @@
|
||||
|
||||
@include('partials.head')
|
||||
|
||||
<body class="font-sans text-gray-900 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">
|
||||
<body class="font-sans antialiased">
|
||||
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100 dark:bg-neutral-900">
|
||||
<div>
|
||||
<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>
|
||||
</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 }}
|
||||
</div>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -59,16 +59,20 @@
|
||||
</div>
|
||||
</div>
|
||||
</x-dropdown-link>
|
||||
|
||||
{{-- Expiremental --}}
|
||||
<x-dropdown-link>
|
||||
@include('partials.blurswitcher')
|
||||
</x-dropdown-link>
|
||||
</x-slot>
|
||||
</x-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center invisible sm:visible">
|
||||
<div class="items-center hidden md:flex">
|
||||
@livewire('nav-live-search')
|
||||
<div class="hidden lg:block pl-4">
|
||||
<div class="flex flex-col items-center bg-gray-50/20 dark:bg-neutral-900/40 rounded-md">
|
||||
@php $random = App\Models\Episode::inRandomOrder()->limit(1)->pluck('slug')->first(); @endphp
|
||||
<a href="{{ route('hentai.index', ['title' => $random]) }}"
|
||||
<a href="{{ route('hentai.random') }}"
|
||||
class="cursor-pointer px-4 py-2 text-sm font-medium dark:hover:text-white text-gray-500 dark:text-white/90 focus:outline-none flex flex-col items-center md:flex-row">
|
||||
<i class="fa-solid fa-shuffle"></i>
|
||||
<p class="md:pl-1 pl-0">Random</p>
|
||||
@@ -90,11 +94,9 @@
|
||||
<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">
|
||||
@auth
|
||||
@if (Auth::user()->avatar)
|
||||
<img class="h-8 w-8 rounded-full object-cover mr-2"
|
||||
src="https://external-content.duckduckgo.com/iu/?u=https://cdn.discordapp.com/avatars/{{ Auth::user()->id }}/{{ Auth::user()->avatar }}.webp"
|
||||
alt="{{ Auth::user()->getTagAttribute() }}" />
|
||||
@endif
|
||||
<img class="h-8 w-8 rounded-full object-cover mr-2"
|
||||
src="{{ Auth::user()->getAvatar() }}"
|
||||
alt="{{ Auth::user()->name }}" />
|
||||
@else
|
||||
<img class="h-8 w-8 rounded-full object-cover mr-2" src="/images/default-avatar.webp"
|
||||
alt="Guest" />
|
||||
@@ -102,7 +104,7 @@
|
||||
|
||||
@auth
|
||||
<div style="display: flex; flex-direction: row; align-items: flex-start;">
|
||||
{{ Auth::user()->getTagAttribute() }}
|
||||
{{ Auth::user()->name }}
|
||||
@if ($notAvailable)
|
||||
<i class="fa-solid fa-bell text-rose-600"></i>
|
||||
@endif
|
||||
@@ -184,8 +186,8 @@
|
||||
@guest
|
||||
<x-dropdown-link :href="route('login')">
|
||||
<div
|
||||
class="relative bg-blue-700 hover:bg-blue-600 text-white font-bold px-4 h-10 rounded text-center p-[10px]">
|
||||
<i class="fa-brands fa-discord"></i> {{ __('nav.login') }}
|
||||
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-solid fa-arrow-right-to-bracket"></i> {{ __('nav.login') }}
|
||||
</div>
|
||||
</x-dropdown-link>
|
||||
@endguest
|
||||
@@ -227,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="flex justify-center">
|
||||
@if (Auth::user()->avatar)
|
||||
<img class="h-8 w-8 rounded-full object-cover mr-2"
|
||||
src="https://external-content.duckduckgo.com/iu/?u=https://cdn.discordapp.com/avatars/{{ Auth::user()->id }}/{{ Auth::user()->avatar }}.webp"
|
||||
alt="{{ Auth::user()->getTagAttribute() }}" />
|
||||
@else
|
||||
<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()->username }}
|
||||
<img class="h-8 w-8 rounded-full object-cover mr-2"
|
||||
src="{{ Auth::user()->getAvatar() }}"
|
||||
alt="{{ Auth::user()->name }}" />
|
||||
<span class="font-medium text-base text-gray-800 dark:text-neutral-200">
|
||||
{{ Auth::user()->name }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -298,8 +296,8 @@
|
||||
<div class="pb-1 text-center w-full">
|
||||
<x-responsive-nav-link :href="route('login')">
|
||||
<div
|
||||
class="relative bg-blue-700 hover:bg-blue-600 text-white font-bold px-4 h-10 rounded text-center p-[10px]">
|
||||
<i class="fa-brands fa-discord"></i> {{ __('nav.login') }}
|
||||
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-solid fa-arrow-right-to-bracket"></i> {{ __('nav.login') }}
|
||||
</div>
|
||||
</x-responsive-nav-link>
|
||||
</div>
|
||||
|
||||
60
resources/views/livewire/admin-comment-search.blade.php
Normal file
60
resources/views/livewire/admin-comment-search.blade.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<div>
|
||||
<div class="relative pt-5 text-gray-900 dark:text-white xl:max-w-[95%] 2xl:max-w-[90%]" wire:keydown.right.window="nextPage" wire:keydown.left.window="previousPage">
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="relative overflow-x-auto rounded-lg w-3/6">
|
||||
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-white">
|
||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-pink-700 dark:text-neutral-200 ">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-center">
|
||||
User
|
||||
<input
|
||||
wire:model.live.debounce.600ms="userSearch"
|
||||
type="search"
|
||||
id="live-search"
|
||||
class="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 scope="col" class="px-6 py-3">
|
||||
Comment
|
||||
<input
|
||||
wire:model.live.debounce.600ms="search"
|
||||
type="search"
|
||||
id="live-search"
|
||||
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 scope="col" class="px-6 py-3">
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($comments as $comment)
|
||||
<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">
|
||||
{{ $comment->user->name }}
|
||||
</td>
|
||||
<th scope="row" class="px-6 py-4 font-medium text-gray-900 dark:text-white max-w-lg">
|
||||
{{ $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>
|
||||
<td class="px-6 py-4">
|
||||
<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">
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{{ $comments->links('pagination::tailwind') }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -60,7 +60,7 @@
|
||||
{{ $user->id }}
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
{{ $user->global_name ?? $user->username }}
|
||||
{{ $user->name }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
{{ $user->is_patreon ? 'Yes' : 'No' }}
|
||||
@@ -74,15 +74,18 @@
|
||||
<td class="px-6 py-4">
|
||||
{{ $user->updated_at->format('Y-m-d') }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<td class="px-6 py-4 flex flex-col gap-1">
|
||||
<form method="POST" action="{{ route('admin.user.update') }}">
|
||||
@csrf
|
||||
<input type="hidden" value="{{ $user->id }}" name="id">
|
||||
<input type="hidden" value="{{ $user->is_banned ? 'unban' : 'ban' }}" name="action">
|
||||
<button type="submit" class="inline-block 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' }}
|
||||
</button>
|
||||
</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">
|
||||
Delete comments
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
|
||||
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->is_admin)
|
||||
<a data-te-toggle="tooltip" title="Admin"><i class="fa-solid fa-crown text-yellow-600"></i></a>
|
||||
@endif
|
||||
@if($comment->user->is_patreon)
|
||||
<a data-te-toggle="tooltip" title="Badge of appreciation for the horny people supporting us! :3"><i class="fa-solid fa-hand-holding-heart text-rose-600"></i></a>
|
||||
@endif
|
||||
</div>
|
||||
<div class="mt-1 flex-grow w-full">
|
||||
@if ($isEditing)
|
||||
<form wire:submit.prevent="editComment">
|
||||
<div>
|
||||
<label for="comment" class="sr-only">Comment body</label>
|
||||
<textarea id="comment" name="comment" rows="3"
|
||||
class="bg-white dark:bg-neutral-700 shadow-sm block w-full focus:ring-rose-500 focus:border-rose-500 border-gray-300 dark:border-gray-400/40 text-gray-900 dark:text-gray-200 placeholder:text-gray-400 rounded-md
|
||||
@error('editState.body') border-red-500 @enderror"
|
||||
placeholder="Write something" wire:model.defer="editState.body"></textarea>
|
||||
@error('editState.body')
|
||||
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
<div class="mt-3 flex items-center justify-between">
|
||||
<button type="submit"
|
||||
class="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md shadow-sm text-white bg-rose-600 hover:bg-rose-700 focus:outline-none focus:ring-2 focus:ring-rose-500">
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@else
|
||||
<div class="text-gray-700 dark:text-gray-200">{!! $comment->presenter()->markdownBody() !!}</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="mt-2 space-x-2 flex flex-row">
|
||||
<span class="text-gray-500 dark:text-gray-300">
|
||||
{{ $comment->presenter()->relativeCreatedAt() }}
|
||||
</span>
|
||||
|
||||
@guest
|
||||
<span data-te-toggle="tooltip" title="Please login to like the episode" class="text-gray-800 cursor-pointer dark:text-gray-200">
|
||||
<i class="fa-regular fa-heart"></i> {{ $comment->likeCount() }}
|
||||
</span>
|
||||
@endguest
|
||||
|
||||
@auth
|
||||
<!-- Like Button -->
|
||||
<button class="text-gray-800 dark:text-gray-200 leading-tight cursor-pointer whitespace-nowrap" wire:click="like">
|
||||
@if ($liked)
|
||||
<i class="fa-solid fa-heart text-rose-600"></i> {{ $likeCount }}
|
||||
@else
|
||||
<i class="fa-solid fa-heart"></i> {{ $likeCount }}
|
||||
@endif
|
||||
</button>
|
||||
@endauth
|
||||
|
||||
@auth
|
||||
@if ($comment->depth() < 2)
|
||||
<button wire:click="$toggle('isReplying')" type="button" class="text-gray-900 dark:text-gray-100 font-medium">
|
||||
Reply
|
||||
</button>
|
||||
@endif
|
||||
|
||||
@can ('update', $comment)
|
||||
<button wire:click="$toggle('isEditing')" type="button" class="text-gray-900 dark:text-gray-100 font-medium">
|
||||
Edit
|
||||
</button>
|
||||
@endcan
|
||||
|
||||
@can ('destroy', $comment)
|
||||
<button x-data="{
|
||||
confirmCommentDeletion () {
|
||||
if (window.confirm('Are you sure you want to delete this comment?')) {
|
||||
@this.call('deleteComment');
|
||||
}
|
||||
}
|
||||
}"
|
||||
@click="confirmCommentDeletion"
|
||||
type="button"
|
||||
class="text-gray-900 dark:text-gray-100 font-medium"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
@endcan
|
||||
@endauth
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ml-14 mt-6">
|
||||
@if ($isReplying)
|
||||
<form wire:submit.prevent="postReply" class="my-4">
|
||||
<div>
|
||||
<label for="comment" class="sr-only">Reply body</label>
|
||||
<textarea id="comment" name="comment" rows="3"
|
||||
class="bg-white dark:bg-neutral-700 shadow-sm block w-full focus:ring-rose-500 focus:border-rose-500 border-gray-300 dark:border-gray-400/40 text-gray-900 dark:text-gray-200 placeholder:text-gray-400 rounded-md
|
||||
@error('replyState.body') border-red-500 @enderror"
|
||||
placeholder="Write something" wire:model.defer="replyState.body"></textarea>
|
||||
@error('replyState.body')
|
||||
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
<div class="mt-3 flex items-center justify-between">
|
||||
<button type="submit"
|
||||
class="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md shadow-sm text-white bg-rose-600 hover:bg-rose-700 focus:outline-none focus:ring-2 focus:ring-rose-500">
|
||||
Comment
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
@foreach ($comment->children as $child)
|
||||
<livewire:comment :comment="$child" :key="$child->id"/>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
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>
|
||||
@@ -1,8 +1,8 @@
|
||||
<div class="py-3">
|
||||
<div class="mx-auto sm:px-6 lg:px-8 space-y-6 max-w-[100%] xl:max-w-[80%] 2xl:max-w-[50%]">
|
||||
<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-[60%] 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">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<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 md:grid-cols-4 gap-4 ">
|
||||
|
||||
<!-- Title -->
|
||||
<div>
|
||||
@@ -36,6 +36,61 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-data="{ open: @entangle('isOpen') }" x-on:click.away="open = false"
|
||||
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-filter text-gray-500 dark:text-gray-400"></i>
|
||||
</div>
|
||||
<button type="button"
|
||||
class="inline-flex 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"
|
||||
wire:click="toggleDropdown">
|
||||
Select Type
|
||||
<svg class="absolute -mr-1 h-5 w-5 text-gray-500 right-4 top-4"
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
|
||||
aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Dropdown Menu -->
|
||||
@if ($isOpen)
|
||||
<div x-show="open" x-transition @click.away="open = false"
|
||||
class="absolute mt-2 w-full rounded-lg bg-white dark:bg-neutral-900 border-[1px] border-gray-300 dark:border-neutral-600 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none z-[110]">
|
||||
<div class="py-1">
|
||||
@foreach ($options as $label => $selected)
|
||||
<label
|
||||
class="flex items-center px-4 py-2 text-sm text-gray-700 cursor-pointer hover:bg-gray-100 dark:hover:bg-neutral-800">
|
||||
<input type="checkbox" wire:click="toggleOption('{{ $label }}')"
|
||||
class="h-4 w-4 text-rose-600 bg-gray-100 border-gray-300 rounded focus:ring-rose-500 dark:focus:ring-rose-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
||||
{{ $selected ? 'checked' : '' }}>
|
||||
<span class="ml-2 text-gray-900 dark:text-white">{{ $label }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</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 -->
|
||||
<div>
|
||||
<div class="relative right-2 left-0 sm:left-2 transition-all">
|
||||
@@ -56,7 +111,8 @@
|
||||
</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%] xl:max-w-[80%] 2xl:max-w-[50%]">
|
||||
<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-[60%]">
|
||||
<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">
|
||||
@@ -68,12 +124,13 @@
|
||||
<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 hidden sm:block">Title</div>
|
||||
<div class="w-28 px-6 py-4 hidden sm:block">Size</div>
|
||||
<div class="w-32 px-6 py-4 hidden sm:block">Date</div>
|
||||
<div class="w-32 px-6 py-4">Download</div>
|
||||
<div class="w-20 px-6 py-4 hidden text-center sm:block">Type</div>
|
||||
<div class="w-28 px-6 py-4 hidden text-center sm:block">Size</div>
|
||||
<div class="w-32 px-6 py-4 hidden text-center sm:block">Date</div>
|
||||
<div class="w-32 px-6 py-4 text-center">Download</div>
|
||||
</div>
|
||||
|
||||
@php
|
||||
@php
|
||||
$dldomains = config('hstream.download_domain');
|
||||
$dlpdomains = config('hstream.download_domain_4k');
|
||||
@endphp
|
||||
@@ -91,35 +148,96 @@
|
||||
{{ $title }}
|
||||
</div>
|
||||
|
||||
<!-- Type -->
|
||||
<div class="w-20 whitespace-nowrap flex justify-center">
|
||||
<span class="block sm:hidden pr-2">
|
||||
Type:
|
||||
</span>
|
||||
<div class="flex justify-center">
|
||||
@if ($download->type === 'FHD' || $download->type === 'FHDi')
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
||||
class="text-sky-500" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="icon icon-tabler icons-tabler-outline icon-tabler-badge-hd">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M3 5m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" />
|
||||
<path d="M14 9v6h1a2 2 0 0 0 2 -2v-2a2 2 0 0 0 -2 -2h-1z" />
|
||||
<path d="M7 15v-6" />
|
||||
<path d="M10 15v-6" />
|
||||
<path d="M7 12h3" />
|
||||
</svg>
|
||||
@else
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
||||
class="text-rose-600" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="icon icon-tabler icons-tabler-outline icon-tabler-badge-4k">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M3 5m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" />
|
||||
<path d="M7 9v2a1 1 0 0 0 1 1h1" />
|
||||
<path d="M10 9v6" />
|
||||
<path d="M14 9v6" />
|
||||
<path d="M17 9l-2 3l2 3" />
|
||||
<path d="M15 12h-1" />
|
||||
</svg>
|
||||
@endif
|
||||
|
||||
@if ($download->type === 'FHDi' || $download->type === 'UHDi')
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" class="text-yellow-600"
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path
|
||||
d="M9.225 18.412a1.595 1.595 0 0 1 -1.225 .588c-.468 0 -.914 -.214 -1.225 -.588l-4.361 -5.248a1.844 1.844 0 0 1 0 -2.328l4.361 -5.248a1.595 1.595 0 0 1 1.225 -.588c.468 0 .914 .214 1.225 .588l4.361 5.248a1.844 1.844 0 0 1 0 2.328l-4.361 5.248z" />
|
||||
<path d="M17 5l4.586 5.836a1.844 1.844 0 0 1 0 2.328l-4.586 5.836" />
|
||||
<path d="M13 5l4.586 5.836a1.844 1.844 0 0 1 0 2.328l-4.586 5.836" />
|
||||
</svg>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Size -->
|
||||
<div class="w-28 whitespace-nowrap flex">
|
||||
<div class="w-28 whitespace-nowrap flex justify-center">
|
||||
<span class="block sm:hidden">
|
||||
Size:
|
||||
Size:
|
||||
</span>
|
||||
{{ $download->getFileSize() }}
|
||||
</div>
|
||||
|
||||
<!-- Date -->
|
||||
<div class="w-32 whitespace-nowrap flex">
|
||||
<div class="w-32 whitespace-nowrap flex justify-center">
|
||||
<span class="block sm:hidden">
|
||||
Date:
|
||||
Date:
|
||||
</span>
|
||||
{{ $download->created_at->format('Y-m-d') }}
|
||||
</div>
|
||||
|
||||
<!-- Download -->
|
||||
<div class="w-32 whitespace-nowrap">
|
||||
<div class="w-32 whitespace-nowrap flex justify-center">
|
||||
@php
|
||||
if (in_array($download->type, ['FHD', 'FHDi'])) {
|
||||
$downloadURL = $dldomains[array_rand($dldomains)].'/'.$download->url;
|
||||
$downloadURL =
|
||||
$dldomains[array_rand($dldomains)] . '/' . $download->url;
|
||||
} else {
|
||||
$now = \Illuminate\Support\Carbon::now();
|
||||
$expire = \Illuminate\Support\Facades\Crypt::encryptString($now->addHours(6));
|
||||
$file = \Illuminate\Support\Facades\Crypt::encryptString('hentai/'.$download->url);
|
||||
$downloadURL = $dlpdomains[array_rand($dlpdomains)].'/download/'.$file.'/'.$expire;
|
||||
$expire = \Illuminate\Support\Facades\Crypt::encryptString(
|
||||
$now->addHours(6),
|
||||
);
|
||||
$file = \Illuminate\Support\Facades\Crypt::encryptString(
|
||||
'hentai/' . $download->url,
|
||||
);
|
||||
$downloadURL =
|
||||
$dlpdomains[array_rand($dlpdomains)] .
|
||||
'/download/' .
|
||||
$file .
|
||||
'/' .
|
||||
$expire;
|
||||
}
|
||||
@endphp
|
||||
<a href="{{ $downloadURL }}" wire:click="clicked({{ $download->id }})" download>
|
||||
<a href="{{ $downloadURL }}" wire:click="clicked({{ $download->id }})"
|
||||
download>
|
||||
<button id="dl-{{ $download->id }}"
|
||||
class="group rounded-md bg-transparent border-[1px] border-white/20 shadow dark:text-white text-blac cursor-pointer flex justify-between items-center overflow-hidden transition-all w-[110px] h-[32px] mt-1 mb-1">
|
||||
<div
|
||||
@@ -146,4 +264,5 @@
|
||||
</div>
|
||||
{{ $downloads->links('pagination::tailwind') }}
|
||||
</div>
|
||||
@include('modals.filter-studios')
|
||||
</div>
|
||||
|
||||
@@ -1,63 +1,111 @@
|
||||
<div class="flex items-center">
|
||||
<form method="POST" action="{{ route('hentai.searchredirect') }}">
|
||||
<div class="flex items-center relative">
|
||||
<form method="POST" action="{{ route('hentai.searchredirect') }}" class="w-full">
|
||||
@csrf
|
||||
<label for="live-search" class="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white">Search</label>
|
||||
<div class="absolute right-2 left-2 sm:relative sm:min-w-[200px] md:min-w-[300px] lg:min-w-[400px] xl:min-w-[500px] transition-all">
|
||||
<div class="flex absolute inset-y-0 left-0 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="navSearch" type="search" id="live-search" name="live-search" class="block p-4 pl-10 w-full text-sm text-gray-900 rounded-lg border border-gray-300/50 bg-gray-50/20 focus:ring-rose-800 focus:border-rose-900 dark:bg-neutral-900/40 dark:border-neutral-600/50 dark:placeholder-gray-400 dark:text-white dark:focus:ring-rose-800 dark:focus:border-rose-900" placeholder="@if(request()->path() !== 'search'){{ __('search.search-hentai') }}@endif" required @if(request()->path() == 'search') disabled @endif>
|
||||
<button type="submit" class="absolute right-2.5 bottom-2.5 px-4 py-2 text-sm font-medium text-white bg-rose-700 rounded-lg hover:bg-rose-800 disabled:bg-gray-300 disabled:hover:bg-gray-300 disabled:dark:bg-gray-500 disabled:dark:hover:bg-gray-500 focus:ring-4 focus:outline-none focus:ring-rose-300 dark:bg-rose-600 dark:hover:bg-rose-700 dark:focus:ring-rose-800" @if(request()->path() == 'search') disabled @endif>{{ __('search.search') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="relative group">
|
||||
<label for="live-search" class="sr-only">Search</label>
|
||||
<div class="relative w-full sm:min-w-[200px] md:min-w-[300px] lg:min-w-[400px] xl:min-w-[500px]">
|
||||
{{-- Search Icon --}}
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 pl-3 flex items-center">
|
||||
<svg class="w-4 h-4 text-gray-400 dark:text-gray-300" 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>
|
||||
|
||||
@if((! $hide) && request()->path() != 'search' && request()->path() != 'download-search')
|
||||
<!-- BG Blur and BG Size -->
|
||||
<div class="absolute left-0 sm:top-[65px] w-[100%] h-[calc(100vh-60px)] z-40 text-gray-900 dark:text-white bg-neutral-100/80 dark:bg-neutral-900/80">
|
||||
<div class="flex justify-center items-center">
|
||||
<!-- Padding for Grid -->
|
||||
<div class="flex justify-center w-5/6">
|
||||
<div class="grid grid-cols-1 gap-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
|
||||
@foreach($episodes as $episode)
|
||||
<div class="relative p-1 mb-8 w-full transition duration-300 ease-in-out md:p-2 hover:-translate-y-1 hover:scale-110">
|
||||
<a class="hover:text-blue-600" href="{{ route('hentai.index', ['title' => $episode->slug ]) }}">
|
||||
<div class="absolute w-[95%] top-[38%] text-center z-10">
|
||||
<svg aria-hidden="true" class="inline mr-2 w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-pink-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
|
||||
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
|
||||
{{-- Search Input --}}
|
||||
<input
|
||||
wire:model.live.debounce.600ms="navSearch"
|
||||
type="search"
|
||||
id="live-search"
|
||||
name="live-search"
|
||||
class="block w-full pl-10 pr-28 py-3 text-sm rounded-2xl border border-gray-200 bg-white/80 dark:bg-neutral-900/50 dark:border-neutral-700 placeholder-gray-400 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-rose-600 focus:border-rose-700 transition"
|
||||
placeholder="@if(request()->path() !== 'search'){{ __('search.search-hentai') }}@endif"
|
||||
required
|
||||
@if(request()->path() == 'search') disabled @endif
|
||||
aria-autocomplete="list"
|
||||
aria-label="Search"
|
||||
>
|
||||
|
||||
{{-- Submit button to redirect to advanced search --}}
|
||||
<button
|
||||
type="submit"
|
||||
class="absolute right-1 top-1/2 -translate-y-1/2 px-4 py-2 text-sm font-medium rounded-xl bg-rose-700 text-white hover:bg-rose-800 focus:outline-none focus:ring-2 focus:ring-rose-300 disabled:bg-gray-300 disabled:hover:bg-gray-300 disabled:dark:bg-gray-500 disabled:dark:hover:bg-gray-500"
|
||||
@if(request()->path() == 'search') disabled @endif
|
||||
>{{ __('search.search') }}</button>
|
||||
</div>
|
||||
|
||||
@if((! $hide) && request()->path() != 'search' && request()->path() != 'download-search')
|
||||
<div
|
||||
class="pointer-events-auto absolute left-0 right-0 mt-3 z-50 flex justify-center"
|
||||
aria-live="polite"
|
||||
>
|
||||
<div
|
||||
class="max-h-[70vh] overflow-auto rounded-2xl bg-white dark:bg-neutral-900 border border-gray-200 dark:border-neutral-700 shadow-lg transition-all transform hidden group-focus-within:block group-focus-within:translate-y-0">
|
||||
<div class="flex items-center justify-between p-3 border-b border-gray-100 dark:border-neutral-800">
|
||||
<div class="text-sm text-gray-700 dark:text-gray-200 font-medium">
|
||||
@if($episodes->count())
|
||||
{{ __('Search result for ') }} “{{ $query ?: $navSearch }}”
|
||||
@else
|
||||
{{ __('No results') }}
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Loading indicator using Livewire --}}
|
||||
<div class="flex items-center gap-2">
|
||||
<div wire:loading.class.remove="hidden" class="hidden">
|
||||
<svg class="w-5 h-5 animate-spin text-rose-600" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-opacity="0.2" stroke-width="4"></circle>
|
||||
<path d="M22 12a10 10 0 0 1-10 10" stroke="currentColor" stroke-width="4" stroke-linecap="round"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<img
|
||||
alt="{{ $episode->title }} - {{ $episode->episode }}"
|
||||
loading="lazy"
|
||||
width="500"
|
||||
class="block object-cover object-center relative z-20 rounded-lg aspect-video"
|
||||
src="{{ $episode->gallery->first()->thumbnail_url }}">
|
||||
</img>
|
||||
<p class="absolute right-4 top-4 bg-white/80 dark:bg-neutral-700/80 !text-gray-900 dark:!text-white rounded-bl-lg rounded-lg p-1 pr-2 pl-2 font-semibold text-sm">{{ $episode->getResolution() }}</p>
|
||||
<p class="absolute left-4 bottom-4 bg-white/80 dark:bg-neutral-700/80 !text-gray-900 dark:!text-white rounded-bl-lg rounded-lg p-1 pr-2 pl-2 font-semibold text-sm"><i class="fa-regular fa-eye"></i> {{ $episode->view_count }} <i class="fa-regular fa-heart"></i> {{ count($episode->likes) }}</p>
|
||||
<p class="absolute w-[95%] text-center text-sm">{{ $episode->title }} - {{ $episode->episode }}</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
<div class="relative p-1 mb-8 w-full transition duration-300 ease-in-out md:p-2 hover:-translate-y-1 hover:scale-110">
|
||||
<a class="hover:text-blue-600" href="{{ route('hentai.search', ['s' => $query]) }}">
|
||||
<img
|
||||
alt="gallery"
|
||||
loading="lazy"
|
||||
width="500"
|
||||
class="block object-cover object-center rounded-lg aspect-video"
|
||||
src="{{ $randomimage->thumbnail_url }}">
|
||||
</img>
|
||||
<p class="absolute left-2 top-2 w-[95%] h-[91.5%] bg-white/10 dark:bg-neutral-700/10 !text-gray-900 dark:!text-white rounded-bl-lg rounded-lg font-semibold p-4 pr-8 pl-8 text-center"></p>
|
||||
<p class="absolute left-[20%] top-[35%] bg-white/80 dark:bg-neutral-700/80 !text-gray-900 dark:!text-white rounded-bl-lg rounded-lg font-semibold p-4 pr-8 pl-8 text-center">Advanced Search...</p>
|
||||
</a>
|
||||
|
||||
{{-- content area: responsive grid --}}
|
||||
<div class="p-4">
|
||||
@if($episodes->count())
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-1 lg:grid-cols-2">
|
||||
@foreach($episodes as $episode)
|
||||
<a href="{{ route('hentai.index', ['title' => $episode->slug ]) }}" class="group block rounded-xl overflow-hidden bg-neutral-50 dark:bg-neutral-950 border border-transparent hover:border-gray-200 dark:hover:border-neutral-700 shadow-sm hover:shadow-md transition">
|
||||
<div class="relative aspect-video">
|
||||
<img
|
||||
alt="{{ $episode->title }} - {{ $episode->episode }}"
|
||||
loading="lazy"
|
||||
class="object-cover w-full h-full"
|
||||
src="{{ $episode->gallery->first()->thumbnail_url }}"
|
||||
>
|
||||
<span class="absolute right-0 top-0 bg-white/90 dark:bg-neutral-800/80 dark:text-white text-xs font-semibold rounded-tr rounded-bl-xl px-2 py-1">{{ $episode->getResolution() }}</span>
|
||||
<div class="absolute left-0 bottom-0 bg-white/90 dark:bg-neutral-800/80 dark:text-white text-xs rounded-tr-xl px-2 py-1 font-medium">
|
||||
<i class="fa-regular fa-eye mr-1"></i> {{ $episode->viewCountFormatted() }}
|
||||
<i class="fa-regular fa-heart ml-2"></i> {{ $episode->likeCount() }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3">
|
||||
<h3 class="text-sm font-semibold truncate text-gray-900 dark:text-white">{{ $episode->title }} - {{ $episode->episode }}</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1 truncate"> {{ \Illuminate\Support\Str::limit($episode->description ?? '', 80) }}</p>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
|
||||
{{-- Advanced Search card --}}
|
||||
<a href="{{ route('hentai.search', ['search' => $query]) }}" class="flex items-center justify-center rounded-xl border border-dashed border-gray-200 dark:border-neutral-700 p-6 hover:bg-gray-50 dark:hover:bg-neutral-900 transition">
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-rose-600 mb-1">🔎</div>
|
||||
<div class="font-semibold text-sm dark:text-white">Advanced Search</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">View more results</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@else
|
||||
{{-- Empty state --}}
|
||||
<div class="py-12 text-center text-sm text-gray-600 dark:text-gray-300">
|
||||
<div class="mb-3">No results found for “{{ $query ?: $navSearch }}”</div>
|
||||
<a href="{{ route('hentai.search', ['search' => $navSearch ?: $query]) }}" class="inline-block px-4 py-2 rounded-lg bg-rose-700 text-white text-sm hover:bg-rose-800">Try advanced search</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -4,20 +4,14 @@
|
||||
|
||||
<!-- Header -->
|
||||
<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->username]) }}">
|
||||
@if ($playlist->user->avatar)
|
||||
<img class="relative w-24 h-24 flex-none rounded-full shadow-lg"
|
||||
src="https://external-content.duckduckgo.com/iu/?u=https://cdn.discordapp.com/avatars/{{ $playlist->user->id }}/{{ $playlist->user->avatar }}.webp">
|
||||
@else
|
||||
<img class="relative w-24 h-24 flex-none rounded-full shadow-lg" src="/images/default-avatar.webp">
|
||||
@endif
|
||||
</a>
|
||||
<div>
|
||||
<img class="relative w-24 h-24 flex-none rounded-full shadow-lg"
|
||||
src="{{ $playlist->user->getAvatar() }}">
|
||||
</div>
|
||||
<div class="flex flex-col justify-center flex-1 pl-4">
|
||||
<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">Creator: <a
|
||||
href="{{ route('user.index', ['username' => $playlist->user->username]) }}">{{ $playlist->user->username }}</a>
|
||||
</p>
|
||||
<p class="font-light text-lg text-neutral-200">Creator: {{ $playlist->user->name }}</p>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center pl-4">
|
||||
<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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user