forked from w33b/hstream
Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b4d3d435e | |||
| 564f816fb9 | |||
| 3709e378c3 | |||
| 4a45dae593 | |||
| 2b0448d517 | |||
| 3bb6af73c3 | |||
| 57cf153560 | |||
| e45fd4b148 | |||
|
d479369770
|
|||
|
af739e3c88
|
|||
| 273ed65a8d | |||
| ccfd5b996b | |||
| e5ef197ed6 | |||
| c0be2e294a | |||
| b8ba17b33f | |||
| 5a8dd12cb8 | |||
| 3a77c4320d | |||
| 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 |
@@ -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
|
||||
|
||||
18
README.md
18
README.md
@@ -2,13 +2,13 @@
|
||||
|
||||
## hstream Website
|
||||
|
||||
### Install
|
||||
### Install (Ubuntu)
|
||||
|
||||
```bash
|
||||
# Install PHP
|
||||
sudo add-apt-repository ppa:ondrej/php
|
||||
apt update && apt upgrade
|
||||
apt install php8.3 php8.3-xml php8.3-mysql php8.3-gd php8.3-zip php8.3-curl php8.3-mbstring
|
||||
apt install php8.4 php8.4-xml php8.4-mysql php8.4-gd php8.4-zip php8.4-curl php8.4-mbstring
|
||||
|
||||
# Install NodeJS
|
||||
curl -sL https://deb.nodesource.com/setup_20.x -o /tmp/nodesource_setup.sh
|
||||
@@ -22,12 +22,16 @@ mv composer.phar composer
|
||||
|
||||
# Install NGINX (skip for local dev)
|
||||
apt install nginx
|
||||
apt install php8.3-fpm
|
||||
apt install php8.4-fpm
|
||||
|
||||
# Install MariaDB
|
||||
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
|
||||
@@ -50,7 +54,7 @@ nano /etc/supervisor/conf.d/laravel-queue.conf :
|
||||
|
||||
[program:laravel-queue]
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
command=php /var/www/hstream/artisan queue:work --queue=default --sleep=3 --tries=3 --max-time=3600
|
||||
command=php84 /var/www/hstream/artisan queue:work --queue=default --sleep=3 --tries=3 --max-time=3600
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stopasgroup=true
|
||||
@@ -79,9 +83,9 @@ zip -r hstream_2023_11_30.zip hstream/
|
||||
|
||||
### Update
|
||||
```bash
|
||||
php artisan down
|
||||
php84 artisan down
|
||||
git pull
|
||||
npm run build
|
||||
php artisan view:clear && php artisan optimize:clear && php artisan cache:clear && service php8.4-fpm restart
|
||||
php artisan up
|
||||
php84 artisan view:clear && php84 artisan optimize:clear && php84 artisan cache:clear && service php8.4-fpm restart
|
||||
php84 artisan up
|
||||
```
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
11
app/Enums/UserRole.php
Normal file
11
app/Enums/UserRole.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum UserRole: string
|
||||
{
|
||||
case ADMINISTRATOR = 'admin';
|
||||
case MODERATOR = 'moderator';
|
||||
case SUPPORTER = 'supporter';
|
||||
case BANNED = 'banned';
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
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')) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Enums\UserRole;
|
||||
use App\Models\User;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -31,11 +32,11 @@ class UserController extends Controller
|
||||
|
||||
switch ($validated['action']) {
|
||||
case 'ban':
|
||||
$user->update(['is_banned' => 1]);
|
||||
$user->addRole(UserRole::BANNED);
|
||||
alert()->success('Banned', 'User has been banned.');
|
||||
break;
|
||||
case 'unban':
|
||||
$user->update(['is_banned' => 0]);
|
||||
$user->removeRole(UserRole::BANNED);
|
||||
alert()->success('Unbanned', 'User has been unbanned.');
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -8,6 +8,8 @@ use App\Models\Episode;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
use GrantHolle\Altcha\Rules\ValidAltcha;
|
||||
|
||||
class DownloadApiController extends Controller
|
||||
{
|
||||
/**
|
||||
@@ -16,11 +18,12 @@ class DownloadApiController extends Controller
|
||||
public function getDownload(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'episode_id' => 'required',
|
||||
'captcha' => 'required|captcha'
|
||||
'episode_id' => ['required'],
|
||||
'captcha' => ['required', new ValidAltcha],
|
||||
]);
|
||||
|
||||
$episode = Episode::where('id', $request->input('episode_id'))->firstOrFail();
|
||||
$episode = Episode::where('id', $request->input('episode_id'))
|
||||
->firstOrFail();
|
||||
|
||||
// Increase download count, as we assume the user
|
||||
// downloads after submitting the captcha
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
122
app/Http/Controllers/Auth/DiscordAuthController.php
Normal file
122
app/Http/Controllers/Auth/DiscordAuthController.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Enums\UserRole;
|
||||
use App\Models\User;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Laravel\Socialite\Facades\Socialite;
|
||||
|
||||
class DiscordAuthController extends Controller
|
||||
{
|
||||
/**
|
||||
* Redirect to Discord
|
||||
*/
|
||||
public function redirect(): RedirectResponse
|
||||
{
|
||||
return Socialite::driver('discord')->redirect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback received from Discord
|
||||
*/
|
||||
public function callback(): RedirectResponse
|
||||
{
|
||||
$discordUser = Socialite::driver('discord')->user();
|
||||
|
||||
$user = User::where('discord_id', $discordUser->id)->first();
|
||||
|
||||
if (!$user) {
|
||||
// link by email if it already exists
|
||||
$user = User::where('email', $discordUser->email)->first();
|
||||
|
||||
if ($user) {
|
||||
$user->update([
|
||||
'discord_id' => $discordUser->id,
|
||||
'discord_avatar' => $discordUser->avatar,
|
||||
]);
|
||||
} else {
|
||||
// Create new user
|
||||
$user = User::create([
|
||||
'name' => $discordUser->name,
|
||||
'email' => $discordUser->email,
|
||||
'discord_id' => $discordUser->id,
|
||||
'discord_avatar' => $discordUser->avatar,
|
||||
'password' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->checkDiscordAvatar($discordUser, $user);
|
||||
$this->checkDiscordRoles($user);
|
||||
|
||||
Auth::login($user, true);
|
||||
|
||||
return redirect()->route('home.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if discord avatar changed
|
||||
*/
|
||||
private function checkDiscordAvatar(\Laravel\Socialite\Contracts\User $socialiteUser, User $user): void
|
||||
{
|
||||
if ($socialiteUser->avatar != $user->discord_avatar) {
|
||||
$user->update(['discord_avatar' => $socialiteUser->avatar]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Discord Roles if user is Patreon member
|
||||
*/
|
||||
private function checkDiscordRoles(User $user): void
|
||||
{
|
||||
// Should not ever happen
|
||||
if (!$user->discord_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$guildId = config('discord.guild_id');
|
||||
|
||||
$response = Http::withToken(config('discord.discord_bot_token'), 'Bot')
|
||||
->timeout(5)
|
||||
->get("https://discord.com/api/v10/guilds/{$guildId}/members/{$user->discord_id}");
|
||||
|
||||
// User is not in the guild
|
||||
if ($response->status() === 404) {
|
||||
$user->removeRole(UserRole::SUPPORTER);
|
||||
return;
|
||||
}
|
||||
|
||||
// Something else failed
|
||||
if ($response->failed()) {
|
||||
Log::warning('Discord role check failed', [
|
||||
'user_id' => $user->id,
|
||||
'discord_id' => $user->discord_id,
|
||||
'status' => $response->status(),
|
||||
'body' => $response->body(),
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$discordRoles = $response->json('roles', []);
|
||||
$patreonRoles = config('discord.patreon_roles', []);
|
||||
|
||||
// If intersect of array is empty, then the user doesn't have the role
|
||||
$hasSupporterRole = !empty(array_intersect($discordRoles, $patreonRoles));
|
||||
|
||||
if (!$hasSupporterRole) {
|
||||
// Remove role if not found
|
||||
$user->removeRole(UserRole::SUPPORTER);
|
||||
return;
|
||||
}
|
||||
|
||||
$user->addRole(UserRole::SUPPORTER);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -4,25 +4,17 @@ 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;
|
||||
|
||||
use GrantHolle\Altcha\Rules\ValidAltcha;
|
||||
|
||||
class RegisteredUserController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the registration view.
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('auth.register');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming registration request.
|
||||
*
|
||||
@@ -32,8 +24,9 @@ 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()],
|
||||
'altcha' => ['required', new ValidAltcha],
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
@@ -46,6 +39,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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace App\Http\Controllers;
|
||||
use App\Models\Contact;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use GrantHolle\Altcha\Rules\ValidAltcha;
|
||||
|
||||
class ContactController extends Controller
|
||||
{
|
||||
/**
|
||||
@@ -25,7 +27,7 @@ class ContactController extends Controller
|
||||
'email' => 'required|max:50',
|
||||
'message' => 'required|max:1000',
|
||||
'subject' => 'required|max:50',
|
||||
'captcha' => 'required|captcha',
|
||||
'altcha' => ['required', new ValidAltcha],
|
||||
]);
|
||||
|
||||
$contact = new Contact();
|
||||
@@ -37,9 +39,4 @@ class ContactController extends Controller
|
||||
|
||||
return back()->with('status', 'contact-submitted');
|
||||
}
|
||||
|
||||
public function reloadCaptcha(): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
return response()->json(['captcha'=> captcha_img()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,11 +111,13 @@ class HomeController extends Controller
|
||||
*/
|
||||
public function updateLanguage(Request $request): \Illuminate\Http\RedirectResponse
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
57
app/Http/Controllers/MatrixController.php
Normal file
57
app/Http/Controllers/MatrixController.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\MatrixRegisterRequest;
|
||||
use App\Services\MatrixRegistrationService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class MatrixController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the user page.
|
||||
*/
|
||||
public function index(Request $request): \Illuminate\View\View
|
||||
{
|
||||
$rooms = [
|
||||
['name' => '🏠 General', 'description' => 'Our main chat.', 'alias' => 'https://matrix.to/#/#general:hstream.moe'],
|
||||
['name' => '📡 Releases', 'description' => 'Were we @everyone for new releases.', 'alias' => 'https://matrix.to/#/#releases:hstream.moe'],
|
||||
['name' => '👗 NSFW 2D', 'description' => 'Channel for R18 2D Media.', 'alias' => 'https://matrix.to/#/#nsfw:hstream.moe'],
|
||||
['name' => '👗 NSFW IRL', 'description' => 'Channel for R18 IRL Media.', 'alias' => 'https://matrix.to/#/#nsfw-irl:hstream.moe']
|
||||
];
|
||||
|
||||
return view('matrix.index', [
|
||||
'user' => $request->user(),
|
||||
'rooms' => $rooms,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create matrix user
|
||||
*/
|
||||
public function store(
|
||||
MatrixRegisterRequest $request,
|
||||
MatrixRegistrationService $matrixService
|
||||
) {
|
||||
try {
|
||||
$result = $matrixService->registerUser(
|
||||
$request->username,
|
||||
$request->password
|
||||
);
|
||||
|
||||
$user = $request->user();
|
||||
$user->matrix_id = $result['user_id'];
|
||||
$user->save();
|
||||
|
||||
return redirect()
|
||||
->back()
|
||||
->with('success', 'Matrix user created successfully.');
|
||||
} catch (\Exception $e) {
|
||||
return back()
|
||||
->withErrors([
|
||||
'username' => $e->getMessage()
|
||||
])
|
||||
->withInput();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,7 +79,7 @@ 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(),
|
||||
@@ -60,7 +89,7 @@ class ProfileController extends Controller
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
// Verify password if user has password
|
||||
if (!is_null($user->password)) {
|
||||
$request->validateWithBag('userDeletion', [
|
||||
'password' => ['required', 'current_password'],
|
||||
]);
|
||||
}
|
||||
|
||||
$user = $request->user();
|
||||
// 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' => [
|
||||
@@ -58,6 +59,7 @@ class Kernel extends HttpKernel
|
||||
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
|
||||
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
|
||||
'auth.admin' => \App\Http\Middleware\IsAdmin::class,
|
||||
'auth.moderator' => \App\Http\Middleware\IsModerator::class,
|
||||
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
|
||||
'can' => \Illuminate\Auth\Middleware\Authorize::class,
|
||||
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
||||
|
||||
@@ -1,28 +1,14 @@
|
||||
<?php namespace app\Http\Middleware;
|
||||
|
||||
use App\Enums\UserRole;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Auth\Guard;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class IsAdmin {
|
||||
|
||||
/**
|
||||
* The Guard implementation.
|
||||
*
|
||||
* @var Guard
|
||||
*/
|
||||
protected $auth;
|
||||
|
||||
/**
|
||||
* Create a new filter instance.
|
||||
*
|
||||
* @param Guard $auth
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Guard $auth)
|
||||
{
|
||||
$this->auth = $auth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
@@ -30,15 +16,14 @@ class IsAdmin {
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if( ! $this->auth->user()->is_admin)
|
||||
if(Auth::check() && Auth::user()->hasRole(UserRole::ADMINISTRATOR))
|
||||
{
|
||||
session()->flash('error_msg','This resource is restricted to Administrators!');
|
||||
return redirect()->route('home.index');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
session()->flash('error_msg','This resource is restricted to Administrators!');
|
||||
return redirect()->route('home.index');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<?php namespace app\Http\Middleware;
|
||||
|
||||
use App\Enums\UserRole;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Contracts\Auth\Guard;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class IsBanned {
|
||||
|
||||
@@ -13,9 +16,9 @@ class IsBanned {
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if(auth()->check() && auth()->user()->is_banned == 1)
|
||||
if(Auth::check() && Auth::user()->hasRole(UserRole::BANNED))
|
||||
{
|
||||
Auth::logout();
|
||||
$request->session()->invalidate();
|
||||
|
||||
29
app/Http/Middleware/IsModerator.php
Normal file
29
app/Http/Middleware/IsModerator.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Enums\UserRole;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class IsModerator
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (Auth::check() && Auth::user()->hasRole(UserRole::MODERATOR))
|
||||
{
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
session()->flash('error_msg','This resource is restricted to Administrators!');
|
||||
return redirect()->route('home.index');
|
||||
}
|
||||
}
|
||||
41
app/Http/Middleware/SetLocale.php
Normal file
41
app/Http/Middleware/SetLocale.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class SetLocale
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// 1. Logged-in user preference
|
||||
if (Auth::check() && Auth::user()->locale) {
|
||||
App::setLocale(Auth::user()->locale);
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// 2. Session (guest or user override)
|
||||
if (session()->has('locale') && in_array($request->language, config('app.supported_locales'), true)) {
|
||||
App::setLocale(session('locale'));
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// 3. Browser language
|
||||
$locale = $request->getPreferredLanguage(config('app.supported_locales'));
|
||||
|
||||
if ($locale) {
|
||||
App::setLocale($locale);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
use GrantHolle\Altcha\Rules\ValidAltcha;
|
||||
|
||||
class LoginRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
@@ -22,13 +24,14 @@ 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
|
||||
{
|
||||
return [
|
||||
'email' => ['required', 'string', 'email'],
|
||||
'password' => ['required', 'string'],
|
||||
'altcha' => ['required', new ValidAltcha],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -80,6 +83,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());
|
||||
}
|
||||
}
|
||||
|
||||
49
app/Http/Requests/MatrixRegisterRequest.php
Normal file
49
app/Http/Requests/MatrixRegisterRequest.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class MatrixRegisterRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
$isOldEnough = $this->user()->created_at->lt(now()->subMonth());
|
||||
$noAccount = !$this->user()->matrix_id;
|
||||
return $isOldEnough && $noAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'username' => [
|
||||
'required',
|
||||
'string',
|
||||
'min:3',
|
||||
'max:32',
|
||||
'regex:/^[a-z0-9._=-]+$/', // Valid Matrix localpart
|
||||
],
|
||||
'password' => [
|
||||
'required',
|
||||
'string',
|
||||
'min:8',
|
||||
'confirmed',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'username.regex' => 'Username may only contain lowercase letters, numbers and . _ = -',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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,8 @@
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Enums\UserRole;
|
||||
use App\Models\Comment;
|
||||
use App\Models\User;
|
||||
|
||||
use Livewire\Component;
|
||||
@@ -16,7 +18,7 @@ class AdminUserSearch extends Component
|
||||
public $search = '';
|
||||
|
||||
#[Url(history: true)]
|
||||
public $filtered = ['true'];
|
||||
public $discordId = '';
|
||||
|
||||
#[Url(history: true)]
|
||||
public $patreon = [];
|
||||
@@ -24,15 +26,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.'%');
|
||||
}))
|
||||
$users = User::when($this->patreon !== [], fn ($query) => $query->whereJsonContains('roles', UserRole::SUPPORTER->value))
|
||||
->when($this->banned !== [], fn ($query) => $query->whereJsonContains('roles', UserRole::BANNED->value))
|
||||
->when($this->search !== '', fn ($query) => $query->where('name', 'like', '%'.$this->search.'%'))
|
||||
->when($this->discordId !== '', fn ($query) => $query->where('discord_id', '=', $this->discordId))
|
||||
->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
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,15 @@ class DownloadButton extends Component
|
||||
|
||||
public $background = 'bg-rose-600';
|
||||
|
||||
public $fileExtension = 'HEVC';
|
||||
|
||||
public function mount()
|
||||
{
|
||||
if (str_contains($this->downloadUrl, 'AV1')) {
|
||||
$this->fileExtension = 'AV1';
|
||||
}
|
||||
}
|
||||
|
||||
public function clicked($downloadId)
|
||||
{
|
||||
$download = Downloads::find($downloadId);
|
||||
|
||||
@@ -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()->hasRole(\App\Enums\UserRole::SUPPORTER)) {
|
||||
$types[] = 'UHD';
|
||||
} elseif ($label === 'UHD 48fps' && auth()->user()->hasRole(\App\Enums\UserRole::SUPPORTER)) {
|
||||
$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()->hasRole(\App\Enums\UserRole::SUPPORTER)) {
|
||||
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,20 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
//use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
|
||||
use App\Enums\UserRole;
|
||||
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 +23,13 @@ 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 +38,7 @@ class User extends Authenticatable
|
||||
* @var array
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
@@ -56,24 +48,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',
|
||||
'roles' => 'array',
|
||||
'tag_blacklist' => 'array',
|
||||
// Discord
|
||||
'discord_id' => 'integer',
|
||||
'discord_avatar' => 'string',
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Has Many Playlists.
|
||||
*/
|
||||
@@ -101,8 +90,74 @@ 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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has a specific role
|
||||
*/
|
||||
public function hasRole(UserRole $role): bool
|
||||
{
|
||||
return in_array($role->value, $this->roles ?? [], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Role to User
|
||||
*/
|
||||
public function addRole(UserRole $role): void
|
||||
{
|
||||
if ($this->hasRole($role)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all current roles
|
||||
$roles = $this->roles ?? [];
|
||||
|
||||
// Add new role
|
||||
$roles[] = $role->value;
|
||||
|
||||
$this->roles = $roles;
|
||||
$this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove Role from User
|
||||
*/
|
||||
public function removeRole(UserRole $role): void
|
||||
{
|
||||
if (!$this->hasRole($role)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->roles = array_diff($this->roles, [$role->value]);
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Laravelista\Comments;
|
||||
|
||||
use Laravelista\Comments\Comment;
|
||||
|
||||
class CommentPolicy
|
||||
{
|
||||
/**
|
||||
* Can user create the comment
|
||||
*
|
||||
* @param $user
|
||||
* @return bool
|
||||
*/
|
||||
public function create($user) : bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can user delete the comment
|
||||
*
|
||||
* @param $user
|
||||
* @param Comment $comment
|
||||
* @return bool
|
||||
*/
|
||||
public function delete($user, Comment $comment) : bool
|
||||
{
|
||||
return ($user->getKey() == $comment->commenter_id) || $user->is_admin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can user update the comment
|
||||
*
|
||||
* @param $user
|
||||
* @param Comment $comment
|
||||
* @return bool
|
||||
*/
|
||||
public function update($user, Comment $comment) : bool
|
||||
{
|
||||
return $user->getKey() == $comment->commenter_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can user reply to the comment
|
||||
*
|
||||
* @param $user
|
||||
* @param Comment $comment
|
||||
* @return bool
|
||||
*/
|
||||
public function reply($user, Comment $comment) : bool
|
||||
{
|
||||
return $user->getKey() != $comment->commenter_id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Laravelista\Comments;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
use App\Notifications\CommentNotification;
|
||||
use App\Models\User;
|
||||
use App\Models\Episode;
|
||||
|
||||
class CommentService
|
||||
{
|
||||
/**
|
||||
* Handles creating a new comment for given model.
|
||||
* @return mixed the configured comment-model
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
// If guest commenting is turned off, authorize this action.
|
||||
if (Config::get('comments.guest_commenting') == false) {
|
||||
Gate::authorize('create-comment', Comment::class);
|
||||
}
|
||||
|
||||
// Define guest rules if user is not logged in.
|
||||
if (!Auth::check()) {
|
||||
$guest_rules = [
|
||||
'guest_name' => 'required|string|max:255',
|
||||
'guest_email' => 'required|string|email|max:255',
|
||||
];
|
||||
}
|
||||
|
||||
// Merge guest rules, if any, with normal validation rules.
|
||||
Validator::make($request->all(), array_merge($guest_rules ?? [], [
|
||||
'commentable_type' => 'required|string',
|
||||
'commentable_id' => 'required|string|min:1',
|
||||
'message' => 'required|string'
|
||||
]))->validate();
|
||||
|
||||
$model = $request->commentable_type::findOrFail($request->commentable_id);
|
||||
|
||||
$commentClass = Config::get('comments.model');
|
||||
$comment = new $commentClass;
|
||||
|
||||
if (!Auth::check()) {
|
||||
$comment->guest_name = $request->guest_name;
|
||||
$comment->guest_email = $request->guest_email;
|
||||
} else {
|
||||
$comment->commenter()->associate(Auth::user());
|
||||
}
|
||||
|
||||
$comment->commentable()->associate($model);
|
||||
$comment->comment = $request->message;
|
||||
$comment->approved = !Config::get('comments.approval_required');
|
||||
$comment->save();
|
||||
|
||||
return $comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles updating the message of the comment.
|
||||
* @return mixed the configured comment-model
|
||||
*/
|
||||
public function update(Request $request, Comment $comment)
|
||||
{
|
||||
Gate::authorize('edit-comment', $comment);
|
||||
|
||||
Validator::make($request->all(), [
|
||||
'message' => 'required|string'
|
||||
])->validate();
|
||||
|
||||
$comment->update([
|
||||
'comment' => $request->message
|
||||
]);
|
||||
|
||||
return $comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles deleting a comment.
|
||||
* @return mixed the configured comment-model
|
||||
*/
|
||||
public function destroy(Comment $comment): void
|
||||
{
|
||||
Gate::authorize('delete-comment', $comment);
|
||||
|
||||
if (Config::get('comments.soft_deletes') == true) {
|
||||
$comment->delete();
|
||||
} else {
|
||||
$comment->forceDelete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles creating a reply "comment" to a comment.
|
||||
* @return mixed the configured comment-model
|
||||
*/
|
||||
public function reply(Request $request, Comment $comment)
|
||||
{
|
||||
Gate::authorize('reply-to-comment', $comment);
|
||||
|
||||
Validator::make($request->all(), [
|
||||
'message' => 'required|string'
|
||||
])->validate();
|
||||
|
||||
$commentClass = Config::get('comments.model');
|
||||
$reply = new $commentClass;
|
||||
$reply->commenter()->associate(Auth::user());
|
||||
$reply->commentable()->associate($comment->commentable);
|
||||
$reply->parent()->associate($comment);
|
||||
$reply->comment = $request->message;
|
||||
$reply->approved = !Config::get('comments.approval_required');
|
||||
$reply->save();
|
||||
|
||||
// Notify
|
||||
if ($comment->commentable_type == 'App\Models\Episode') {
|
||||
$episode = Episode::where('id', $comment->commentable_id)->firstOrFail();
|
||||
$url = '/hentai/' . $episode->slug . '#comment-' . $reply->id;
|
||||
|
||||
$user = Auth::user();
|
||||
$username = $user->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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
49
app/Services/MatrixRegistrationService.php
Normal file
49
app/Services/MatrixRegistrationService.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class MatrixRegistrationService
|
||||
{
|
||||
public function registerUser(string $username, string $password)
|
||||
{
|
||||
$server = config('services.matrix.server');
|
||||
$secret = config('services.matrix.shared_secret');
|
||||
|
||||
// Get nonce from Synapse
|
||||
$nonceResponse = Http::get("$server/_synapse/admin/v1/register");
|
||||
|
||||
if (!$nonceResponse->ok()) {
|
||||
throw new \Exception("Could not fetch nonce from Matrix.");
|
||||
}
|
||||
|
||||
$nonce = $nonceResponse->json()['nonce'];
|
||||
|
||||
// Generate MAC
|
||||
$mac = hash_hmac(
|
||||
'sha1',
|
||||
$nonce . "\0" .
|
||||
$username . "\0" .
|
||||
$password . "\0" .
|
||||
"notadmin",
|
||||
$secret
|
||||
);
|
||||
|
||||
// Send registration request
|
||||
$response = Http::post("$server/_synapse/admin/v1/register", [
|
||||
'nonce' => $nonce,
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
'admin' => false,
|
||||
'mac' => $mac,
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
$error = $response->json()['error'] ?? $response->body();
|
||||
throw new \Exception($error);
|
||||
}
|
||||
|
||||
return $response->json();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "laravel/laravel",
|
||||
"name": "w33b/hstream",
|
||||
"type": "project",
|
||||
"description": "The skeleton application for the Laravel framework.",
|
||||
"description": "The website of hstream.moe",
|
||||
"keywords": [
|
||||
"laravel",
|
||||
"framework"
|
||||
@@ -9,59 +9,46 @@
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"grantholle/laravel-altcha": "^2.1",
|
||||
"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",
|
||||
"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",
|
||||
"nunomaduro/collision": "^8.1",
|
||||
"phpunit/phpunit": "^11.4",
|
||||
"spatie/laravel-ignition": "^2.0"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/renatokira/comments.git"
|
||||
}
|
||||
],
|
||||
"repositories": [],
|
||||
"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": {
|
||||
|
||||
3228
composer.lock
generated
3228
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,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'characters' => ['2', '3', '4', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'M', 'N', 'P', 'Q', 'R', 'T', 'U', 'X', 'Y', 'Z'],
|
||||
'default' => [
|
||||
'length' => 5,
|
||||
'width' => 120,
|
||||
'height' => 36,
|
||||
'quality' => 90,
|
||||
'math' => false,
|
||||
'expire' => 60,
|
||||
'encrypt' => false,
|
||||
],
|
||||
'math' => [
|
||||
'length' => 9,
|
||||
'width' => 120,
|
||||
'height' => 36,
|
||||
'quality' => 90,
|
||||
'math' => true,
|
||||
],
|
||||
|
||||
'flat' => [
|
||||
'length' => 6,
|
||||
'width' => 160,
|
||||
'height' => 46,
|
||||
'quality' => 90,
|
||||
'lines' => 6,
|
||||
'bgImage' => false,
|
||||
'bgColor' => '#ecf2f4',
|
||||
'fontColors' => ['#2c3e50', '#c0392b', '#16a085', '#c0392b', '#8e44ad', '#303f9f', '#f57c00', '#795548'],
|
||||
'contrast' => -5,
|
||||
],
|
||||
'mini' => [
|
||||
'length' => 3,
|
||||
'width' => 60,
|
||||
'height' => 32,
|
||||
],
|
||||
'inverse' => [
|
||||
'length' => 5,
|
||||
'width' => 120,
|
||||
'height' => 36,
|
||||
'quality' => 90,
|
||||
'sensitive' => true,
|
||||
'angle' => 12,
|
||||
'sharpen' => 10,
|
||||
'blur' => 2,
|
||||
'invert' => true,
|
||||
'contrast' => -5,
|
||||
]
|
||||
];
|
||||
@@ -1,10 +1,19 @@
|
||||
<?php
|
||||
|
||||
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,27 @@ 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
|
||||
],
|
||||
|
||||
/**
|
||||
* Matrix Registration
|
||||
*/
|
||||
'matrix' => [
|
||||
'server' => env('MATRIX_SERVER'),
|
||||
'shared_secret' => env('MATRIX_SHARED_SECRET'),
|
||||
],
|
||||
];
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// Migrate supporters
|
||||
DB::table('users')->where('is_patreon', 1)->update([
|
||||
'roles' => DB::raw("JSON_ARRAY('supporter')")
|
||||
]);
|
||||
|
||||
// Migrate banned
|
||||
DB::table('users')->where('is_banned', 1)->update([
|
||||
'roles' => DB::raw("JSON_ARRAY('banned')")
|
||||
]);
|
||||
|
||||
// Migrate admins
|
||||
DB::table('users')->where('is_admin', 1)->update([
|
||||
'roles' => DB::raw("JSON_ARRAY('admin')")
|
||||
]);
|
||||
|
||||
// Drop columns
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('is_admin');
|
||||
$table->dropColumn('is_patreon');
|
||||
$table->dropColumn('is_banned');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('users')->update(['roles' => null]);
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->boolean('is_admin')->default(0);
|
||||
$table->boolean('is_patreon')->default(0);
|
||||
$table->boolean('is_banned')->default(0);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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('matrix_id')
|
||||
->nullable()
|
||||
->after('discord_id');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('matrix_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,74 +0,0 @@
|
||||
services:
|
||||
laravel.test:
|
||||
build:
|
||||
context: './vendor/laravel/sail/runtimes/8.3'
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
WWWGROUP: '${WWWGROUP}'
|
||||
MYSQL_CLIENT: mariadb-client
|
||||
image: 'sail-8.3/app'
|
||||
extra_hosts:
|
||||
- 'host.docker.internal:host-gateway'
|
||||
ports:
|
||||
- '${APP_PORT:-80}:80'
|
||||
- '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
|
||||
environment:
|
||||
WWWUSER: '${WWWUSER}'
|
||||
LARAVEL_SAIL: 1
|
||||
XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
|
||||
XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
|
||||
IGNITION_LOCAL_SITES_PATH: '${PWD}'
|
||||
volumes:
|
||||
- '.:/var/www/html'
|
||||
networks:
|
||||
- sail
|
||||
depends_on:
|
||||
- mariadb
|
||||
- redis
|
||||
mariadb:
|
||||
image: 'mariadb:11'
|
||||
ports:
|
||||
- '${FORWARD_DB_PORT:-3306}:3306'
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
|
||||
MYSQL_ROOT_HOST: '%'
|
||||
MYSQL_DATABASE: '${DB_DATABASE}'
|
||||
MYSQL_USER: '${DB_USERNAME}'
|
||||
MYSQL_PASSWORD: '${DB_PASSWORD}'
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
|
||||
volumes:
|
||||
- 'sail-mariadb:/var/lib/mysql'
|
||||
- './vendor/laravel/sail/database/mariadb/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
|
||||
networks:
|
||||
- sail
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- healthcheck.sh
|
||||
- '--connect'
|
||||
- '--innodb_initialized'
|
||||
retries: 3
|
||||
timeout: 5s
|
||||
redis:
|
||||
image: 'redis:alpine'
|
||||
ports:
|
||||
- '${FORWARD_REDIS_PORT:-6379}:6379'
|
||||
volumes:
|
||||
- 'sail-redis:/data'
|
||||
networks:
|
||||
- sail
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- redis-cli
|
||||
- ping
|
||||
retries: 3
|
||||
timeout: 5s
|
||||
networks:
|
||||
sail:
|
||||
driver: bridge
|
||||
volumes:
|
||||
sail-mariadb:
|
||||
driver: local
|
||||
sail-redis:
|
||||
driver: local
|
||||
1352
package-lock.json
generated
1352
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -6,12 +6,13 @@
|
||||
"build": "vite build"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
@@ -19,6 +20,7 @@
|
||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||
"@jellyfin/libass-wasm": "^4.1.1",
|
||||
"@yaireo/tagify": "^4.21.2",
|
||||
"altcha": "^2.3.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"dashjs": "^5.0.0",
|
||||
"hammerjs": "^2.0.8",
|
||||
|
||||
@@ -123,3 +123,32 @@ input:checked~.dot {
|
||||
src: url(https://fonts.bunny.net/figtree/files/figtree-latin-ext-600-normal.woff2) format('woff2'), url(https://fonts.bunny.net/figtree/files/figtree-latin-ext-600-normal.woff) format('woff');
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* Captcha */
|
||||
:root {
|
||||
--altcha-border-width: 1px;
|
||||
--altcha-border-radius: 0.375rem;
|
||||
--altcha-color-base: #333;
|
||||
--altcha-color-border: #a0a0a0;
|
||||
--altcha-color-text: #fff;
|
||||
--altcha-color-border-focus: currentColor;
|
||||
--altcha-color-error-text: #f23939;
|
||||
--altcha-color-footer-bg: #141414;
|
||||
--altcha-max-width: 260px;
|
||||
}
|
||||
|
||||
.altcha-footer {
|
||||
border-bottom-left-radius: 0.375rem;
|
||||
border-bottom-right-radius: 0.375rem;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
background-color: #ffffff;
|
||||
border-color: #a0a0a0;
|
||||
color: rgb(225,29,72);
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked {
|
||||
background-color: rgb(225,29,72);
|
||||
box-shadow: 0 0 0 0px #fff, 0 0 0 calc(2px + 0px) rgba(246, 59, 118, 0.5), 0 0 #0000;
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
import './bootstrap';
|
||||
|
||||
// import { Alpine } from '../../vendor/livewire/livewire/dist/livewire.esm';
|
||||
// Alpine.start();
|
||||
|
||||
import 'hammerjs';
|
||||
import {
|
||||
Collapse,
|
||||
Carousel,
|
||||
@@ -15,6 +12,13 @@ import {
|
||||
initTE,
|
||||
} from "tw-elements";
|
||||
|
||||
initTE({ Collapse, Carousel, Clipboard, Modal, Tab, Lightbox, Tooltip, Ripple });
|
||||
// Captcha
|
||||
import 'altcha';
|
||||
|
||||
import 'hammerjs';
|
||||
// import Alpine from 'alpinejs';
|
||||
|
||||
// window.Alpine = Alpine;
|
||||
|
||||
// Alpine.start();
|
||||
|
||||
initTE({ Collapse, Carousel, Clipboard, Modal, Tab, Lightbox, Tooltip, Ripple });
|
||||
|
||||
@@ -28,6 +28,7 @@ var av1Supported = (!!document.createElement('video').canPlayType('video/webm; c
|
||||
var dashSupported = dashjs.supportsMediaSource();
|
||||
var 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
|
||||
}
|
||||
|
||||
@@ -20,3 +20,45 @@ if(localStorage.theme) {
|
||||
// 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
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="mb-4 rounded-lg bg-success-400 px-6 py-5 text-base text-success-800 mt-5" role="alert">
|
||||
{{ $alert->text }}
|
||||
@auth
|
||||
@if(Auth::user()->is_admin)
|
||||
@if(Auth::user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
|
||||
<form method="POST" action="{{ route('admin.alert.delete', $alert->id) }}" class="float-right hover:text-success-900">
|
||||
@csrf
|
||||
@method('delete')
|
||||
@@ -21,7 +21,7 @@
|
||||
<div class="mb-4 rounded-lg bg-danger-400 px-6 py-5 text-base text-danger-800 mt-5" role="alert">
|
||||
{{ $alert->text }}
|
||||
@auth
|
||||
@if(Auth::user()->is_admin)
|
||||
@if(Auth::user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
|
||||
<form method="POST" action="{{ route('admin.alert.delete', $alert->id) }}" class="float-right hover:text-danger-900">
|
||||
@csrf
|
||||
@method('delete')
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
@auth
|
||||
@if(Auth::user()->is_admin)
|
||||
@if(Auth::user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
|
||||
<div class="relative p-5 bg-white dark:bg-neutral-700/40 rounded-lg overflow-hidden z-10">
|
||||
<div class="float-left">
|
||||
<a data-te-toggle="modal" data-te-target="#modalUploadEpisode" class="text-xl text-gray-800 dark:text-gray-200 leading-tight cursor-pointer whitespace-nowrap">
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
<x-guest-layout>
|
||||
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-neutral-950/50 shadow-md overflow-hidden sm:rounded-lg">
|
||||
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ __('This is a secure area of the application. Please confirm your session before continuing.') }}
|
||||
{{ __('This is a secure area of the application. Please confirm your password before continuing.') }}
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('larascord.refresh_token') }}">
|
||||
<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>
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
</x-guest-layout>
|
||||
27
resources/views/auth/forgot-password.blade.php
Normal file
27
resources/views/auth/forgot-password.blade.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<x-guest-layout>
|
||||
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-neutral-950/50 shadow-md overflow-hidden sm:rounded-lg">
|
||||
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }}
|
||||
</div>
|
||||
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="mb-4" :status="session('status')" />
|
||||
|
||||
<form method="POST" action="{{ route('password.email') }}">
|
||||
@csrf
|
||||
|
||||
<!-- Email Address -->
|
||||
<div>
|
||||
<x-input-label for="email" :value="__('Email')" />
|
||||
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus />
|
||||
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<x-primary-button>
|
||||
{{ __('Send Password Reset Link') }}
|
||||
</x-primary-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</x-guest-layout>
|
||||
148
resources/views/auth/login.blade.php
Normal file
148
resources/views/auth/login.blade.php
Normal file
@@ -0,0 +1,148 @@
|
||||
<x-guest-layout>
|
||||
<!-- Tabs -->
|
||||
<div class="w-full sm:max-w-md mt-6">
|
||||
<ul class="flex list-none flex-row flex-wrap border-b-0 pl-0 relative " role="tablist" data-te-nav-ref>
|
||||
<li role="presentation" class="flex-auto text-center">
|
||||
<a href="#tabs-login" class="rounded-l-lg my-2 block border-x-0 border-b-2 border-t-0 border-transparent px-7 pb-3.5 pt-4 text-xs font-medium uppercase leading-tight text-neutral-500 hover:isolate hover:border-transparent hover:bg-neutral-50 focus:isolate focus:border-transparent data-[te-nav-active]:border-rose-600 data-[te-nav-active]:text-black dark:text-neutral-400 bg-white/50 dark:bg-neutral-950/50 backdrop-blur-sm dark:hover:bg-neutral-800 dark:data-[te-nav-active]:text-white"
|
||||
data-te-toggle="pill" data-te-target="#tabs-login" data-te-nav-active role="tab" aria-controls="tabs-login" aria-selected="true">
|
||||
{{ __('Login') }}
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation" class="flex-auto text-center">
|
||||
<a href="#tabs-register" class="rounded-r-lg my-2 block border-x-0 border-b-2 border-t-0 border-transparent px-7 pb-3.5 pt-4 text-xs font-medium uppercase leading-tight text-neutral-500 hover:isolate hover:border-transparent hover:bg-neutral-50 focus:isolate focus:border-transparent data-[te-nav-active]:border-rose-600 data-[te-nav-active]:text-black dark:text-neutral-400 bg-white/50 dark:bg-neutral-950/50 backdrop-blur-sm dark:hover:bg-neutral-800 dark:data-[te-nav-active]:text-white"
|
||||
data-te-toggle="pill" data-te-target="#tabs-register" role="tab" aria-controls="tabs-register" aria-selected="false">
|
||||
{{ __('Register') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Login -->
|
||||
<div class="w-full sm:max-w-md hidden opacity-100 transition-opacity duration-150 ease-linear data-[te-tab-active]:block" id="tabs-login" role="tabpanel" aria-labelledby="tabs-login" data-te-tab-active>
|
||||
<div class="px-6 py-4 bg-white dark:bg-neutral-950/50 shadow-md overflow-hidden sm:rounded-lg">
|
||||
<div class="w-full text-center text-white mb-3">
|
||||
<a href="{{ route('discord.login') }}">
|
||||
<div
|
||||
class="relative bg-blue-700 hover:bg-blue-600 text-white font-bold px-4 h-10 rounded text-center p-[10px] mb-4">
|
||||
<i class="fa-brands fa-discord"></i> {{ __('Use Discord Account') }}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Or -->
|
||||
<div class="grid grid-cols-3">
|
||||
<hr class="self-center border-neutral-600">
|
||||
<p>OR</p>
|
||||
<hr class="self-center border-neutral-600">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="mb-4" :status="session('status')" />
|
||||
|
||||
<form method="POST" action="{{ route('login') }}">
|
||||
@csrf
|
||||
|
||||
<!-- Email Address -->
|
||||
<div>
|
||||
<x-input-label for="email" :value="__('Email')" />
|
||||
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus autocomplete="username" />
|
||||
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="mt-4">
|
||||
<x-input-label for="password" :value="__('Password')" />
|
||||
|
||||
<x-text-input id="password" class="block mt-1 w-full"
|
||||
type="password"
|
||||
name="password"
|
||||
required autocomplete="current-password" />
|
||||
|
||||
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Remember Me -->
|
||||
<div class="block mt-4">
|
||||
<label for="remember_me" class="inline-flex items-center">
|
||||
<input id="remember_me" type="checkbox" class="rounded dark:bg-neutral-900 border-neutral-300 dark:border-neutral-700 text-rose-600 shadow-sm focus:ring-rose-500 dark:focus:ring-rose-600 dark:focus:ring-offset-neutral-800" name="remember">
|
||||
<span class="ms-2 text-sm text-neutral-600 dark:text-neutral-400">{{ __('Remember me') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<altcha-widget id="captcha" floating challengeurl="/altcha-challenge"></altcha-widget>
|
||||
<x-input-error :messages="$errors->get('altcha')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
@if (Route::has('password.request'))
|
||||
<a class="underline text-sm text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-neutral-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-rose-500 dark:focus:ring-offset-neutral-800" href="{{ route('password.request') }}">
|
||||
{{ __('Forgot your password?') }}
|
||||
</a>
|
||||
@endif
|
||||
|
||||
<x-primary-button class="ms-3">
|
||||
{{ __('Log in') }}
|
||||
</x-primary-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Register -->
|
||||
<div class="w-full sm:max-w-md hidden opacity-0 transition-opacity duration-150 ease-linear data-[te-tab-active]:block" id="tabs-register" role="tabpanel" aria-labelledby="tabs-register">
|
||||
<div class="px-6 py-4 bg-white dark:bg-neutral-950/50 shadow-md overflow-hidden sm:rounded-lg">
|
||||
<form method="POST" action="{{ route('register') }}">
|
||||
@csrf
|
||||
|
||||
<!-- Name -->
|
||||
<div>
|
||||
<x-input-label for="name" :value="__('Name')" />
|
||||
<x-text-input id="name" class="block mt-1 w-full" type="text" name="name" :value="old('name')" required autofocus autocomplete="name" />
|
||||
<x-input-error :messages="$errors->get('name')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Email Address -->
|
||||
<div class="mt-4">
|
||||
<x-input-label for="email" :value="__('Email')" />
|
||||
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autocomplete="username" />
|
||||
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="mt-4">
|
||||
<x-input-label for="password" :value="__('Password')" />
|
||||
|
||||
<x-text-input id="password" class="block mt-1 w-full"
|
||||
type="password"
|
||||
name="password"
|
||||
required autocomplete="new-password" />
|
||||
|
||||
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<div class="mt-4">
|
||||
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
|
||||
|
||||
<x-text-input id="password_confirmation" class="block mt-1 w-full"
|
||||
type="password"
|
||||
name="password_confirmation" required autocomplete="new-password" />
|
||||
|
||||
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<altcha-widget id="captcha" floating challengeurl="/altcha-challenge"></altcha-widget>
|
||||
<x-input-error :messages="$errors->get('altcha')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<x-primary-button class="ms-4">
|
||||
{{ __('Register') }}
|
||||
</x-primary-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</x-guest-layout>
|
||||
41
resources/views/auth/reset-password.blade.php
Normal file
41
resources/views/auth/reset-password.blade.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<x-guest-layout>
|
||||
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-neutral-950/50 shadow-md overflow-hidden sm:rounded-lg">
|
||||
<form method="POST" action="{{ route('password.store') }}">
|
||||
@csrf
|
||||
|
||||
<!-- Password Reset Token -->
|
||||
<input type="hidden" name="token" value="{{ $request->route('token') }}">
|
||||
|
||||
<!-- Email Address -->
|
||||
<div>
|
||||
<x-input-label for="email" :value="__('Email')" />
|
||||
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email', $request->email)" required autofocus autocomplete="username" />
|
||||
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="mt-4">
|
||||
<x-input-label for="password" :value="__('Password')" />
|
||||
<x-text-input id="password" class="block mt-1 w-full" type="password" name="password" required autocomplete="new-password" />
|
||||
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<div class="mt-4">
|
||||
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
|
||||
|
||||
<x-text-input id="password_confirmation" class="block mt-1 w-full"
|
||||
type="password"
|
||||
name="password_confirmation" required autocomplete="new-password" />
|
||||
|
||||
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<x-primary-button>
|
||||
{{ __('Reset Password') }}
|
||||
</x-primary-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</x-guest-layout>
|
||||
33
resources/views/auth/verify-email.blade.php
Normal file
33
resources/views/auth/verify-email.blade.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<x-guest-layout>
|
||||
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-neutral-950/50 shadow-md overflow-hidden sm:rounded-lg">
|
||||
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }}
|
||||
</div>
|
||||
|
||||
@if (session('status') == 'verification-link-sent')
|
||||
<div class="mb-4 font-medium text-sm text-green-600 dark:text-green-400">
|
||||
{{ __('A new verification link has been sent to the email address you provided during registration.') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<form method="POST" action="{{ route('verification.send') }}">
|
||||
@csrf
|
||||
|
||||
<div>
|
||||
<x-primary-button>
|
||||
{{ __('Resend Verification Email') }}
|
||||
</x-primary-button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
|
||||
<button type="submit" class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">
|
||||
{{ __('Log Out') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</x-guest-layout>
|
||||
@@ -1 +1 @@
|
||||
<img class="h-10" src="/images/hs_banner.png">
|
||||
<img class="h-16" src="/images/hs_banner.png">
|
||||
@@ -1 +1 @@
|
||||
<a {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-left text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-neutral-100 dark:hover:bg-neutral-900 focus:outline-none focus:bg-neutral-100 dark:focus:bg-neutral-800 transition duration-150 ease-in-out']) }}>{{ $slot }}</a>
|
||||
<a {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-neutral-100 dark:hover:bg-neutral-900 focus:outline-none focus:bg-neutral-100 dark:focus:bg-neutral-800 transition duration-150 ease-in-out']) }}>{{ $slot }}</a>
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white dark:bg-neutral-800'])
|
||||
|
||||
@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"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user