Initial commit: Animexe Laravel platform
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\UserActivityLog;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ActivityLogger
|
||||
{
|
||||
public static function log(
|
||||
string $action,
|
||||
?int $userId = null,
|
||||
?string $subjectType = null,
|
||||
?int $subjectId = null,
|
||||
?array $meta = null,
|
||||
?Request $request = null
|
||||
): void {
|
||||
try {
|
||||
$req = $request ?? request();
|
||||
$ip = $req->ip();
|
||||
$ua = $req->userAgent() ?? '';
|
||||
$isBot = (bool) $req->attributes->get('is_bot', false);
|
||||
$geo = cache()->remember("geo_{$ip}", 3600, fn() => self::geoIp($ip));
|
||||
|
||||
UserActivityLog::create([
|
||||
'user_id' => $userId ?? auth()->id(),
|
||||
'session_id' => session()->getId(),
|
||||
'action' => $action,
|
||||
'subject_type' => $subjectType,
|
||||
'subject_id' => $subjectId,
|
||||
'ip' => $ip,
|
||||
'country' => $geo['country'] ?? null,
|
||||
'city' => $geo['city'] ?? null,
|
||||
'device' => self::device($ua),
|
||||
'browser' => self::browser($ua),
|
||||
'user_agent' => mb_substr($ua, 0, 500),
|
||||
'is_bot' => $isBot,
|
||||
'meta' => $meta,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
} catch (\Throwable) {
|
||||
// never break the app
|
||||
}
|
||||
}
|
||||
|
||||
private static function geoIp(string $ip): array
|
||||
{
|
||||
try {
|
||||
if (in_array($ip, ['127.0.0.1', '::1']) || str_starts_with($ip, '192.168.') || str_starts_with($ip, '10.')) {
|
||||
return ['country' => 'Local', 'city' => null];
|
||||
}
|
||||
$r = \Illuminate\Support\Facades\Http::timeout(2)->get("http://ip-api.com/json/{$ip}?fields=country,city,status");
|
||||
$d = $r->json();
|
||||
return ($d['status'] ?? '') === 'success' ? ['country' => $d['country'], 'city' => $d['city']] : [];
|
||||
} catch (\Throwable) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private static function device(string $ua): string
|
||||
{
|
||||
$ua = strtolower($ua);
|
||||
if (str_contains($ua, 'mobile') || str_contains($ua, 'android') || str_contains($ua, 'iphone')) return 'mobile';
|
||||
if (str_contains($ua, 'tablet') || str_contains($ua, 'ipad')) return 'tablet';
|
||||
return 'desktop';
|
||||
}
|
||||
|
||||
private static function browser(string $ua): string
|
||||
{
|
||||
if (str_contains($ua, 'Chrome')) return 'Chrome';
|
||||
if (str_contains($ua, 'Firefox')) return 'Firefox';
|
||||
if (str_contains($ua, 'Safari')) return 'Safari';
|
||||
if (str_contains($ua, 'Edge')) return 'Edge';
|
||||
if (str_contains($ua, 'Opera')) return 'Opera';
|
||||
return 'Other';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ImageOptimizer
|
||||
{
|
||||
private const PRESETS = [
|
||||
'cover' => ['w' => 600, 'h' => 900, 'quality' => 92],
|
||||
'banner' => ['w' => 1920, 'h' => 1080, 'quality' => 90],
|
||||
'thumbnail' => ['w' => 854, 'h' => 480, 'quality' => 88],
|
||||
'site_banner'=> ['w' => 1920, 'h' => 600, 'quality' => 90],
|
||||
'avatar' => ['w' => 400, 'h' => 400, 'quality' => 92],
|
||||
'default' => ['w' => 1920, 'h' => 1920, 'quality' => 92],
|
||||
];
|
||||
|
||||
public static function store(UploadedFile $file, string $folder, string $preset = 'default'): string
|
||||
{
|
||||
if (!extension_loaded('gd')) {
|
||||
return $file->store($folder, 'public');
|
||||
}
|
||||
|
||||
try {
|
||||
$cfg = self::PRESETS[$preset] ?? self::PRESETS['default'];
|
||||
$path = $file->getRealPath();
|
||||
$mime = mime_content_type($path);
|
||||
|
||||
$src = match (true) {
|
||||
str_contains($mime, 'jpeg'), str_contains($mime, 'jpg') => @imagecreatefromjpeg($path),
|
||||
str_contains($mime, 'png') => @imagecreatefrompng($path),
|
||||
str_contains($mime, 'webp') => @imagecreatefromwebp($path),
|
||||
str_contains($mime, 'gif') => @imagecreatefromgif($path),
|
||||
default => false,
|
||||
};
|
||||
|
||||
if (!$src) {
|
||||
return $file->store($folder, 'public');
|
||||
}
|
||||
|
||||
[$origW, $origH] = [imagesx($src), imagesy($src)];
|
||||
|
||||
// Scale down only — never upscale
|
||||
$ratio = min(1.0, $cfg['w'] / $origW, $cfg['h'] / $origH);
|
||||
$newW = (int) round($origW * $ratio);
|
||||
$newH = (int) round($origH * $ratio);
|
||||
|
||||
$dst = imagecreatetruecolor($newW, $newH);
|
||||
|
||||
// Preserve transparency for PNG
|
||||
if (str_contains($mime, 'png')) {
|
||||
imagealphablending($dst, false);
|
||||
imagesavealpha($dst, true);
|
||||
$transparent = imagecolorallocatealpha($dst, 0, 0, 0, 127);
|
||||
imagefilledrectangle($dst, 0, 0, $newW, $newH, $transparent);
|
||||
}
|
||||
|
||||
imagecopyresampled($dst, $src, 0, 0, 0, 0, $newW, $newH, $origW, $origH);
|
||||
imagedestroy($src);
|
||||
|
||||
// Output as WebP if supported, else JPEG
|
||||
$ext = function_exists('imagewebp') ? 'webp' : 'jpg';
|
||||
$filename = $folder . '/' . Str::uuid() . '.' . $ext;
|
||||
|
||||
ob_start();
|
||||
if ($ext === 'webp') {
|
||||
imagewebp($dst, null, $cfg['quality']);
|
||||
} else {
|
||||
imagejpeg($dst, null, $cfg['quality']);
|
||||
}
|
||||
$bytes = ob_get_clean();
|
||||
imagedestroy($dst);
|
||||
|
||||
Storage::disk('public')->put($filename, $bytes);
|
||||
|
||||
return $filename;
|
||||
} catch (\Throwable) {
|
||||
return $file->store($folder, 'public');
|
||||
}
|
||||
}
|
||||
|
||||
public static function delete(?string $path): void
|
||||
{
|
||||
if ($path && !MediaUrl::isExternal($path)) {
|
||||
Storage::disk('public')->delete($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class MediaUrl
|
||||
{
|
||||
public static function fromStoragePath(?string $path): ?string
|
||||
{
|
||||
if (!$path) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (self::isDirectUrl($path)) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
if (Str::startsWith($path, '/')) {
|
||||
return url($path);
|
||||
}
|
||||
|
||||
$normalized = self::normalize($path);
|
||||
|
||||
if ($normalized === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return url('media/' . self::encodePath($normalized));
|
||||
}
|
||||
|
||||
public static function normalize(string $path): string
|
||||
{
|
||||
$normalized = str_replace('\\', '/', $path);
|
||||
$normalized = preg_replace('~/+~', '/', $normalized) ?? $normalized;
|
||||
|
||||
return ltrim($normalized, '/');
|
||||
}
|
||||
|
||||
private static function encodePath(string $path): string
|
||||
{
|
||||
$segments = array_filter(
|
||||
explode('/', self::normalize($path)),
|
||||
static fn (string $segment): bool => $segment !== ''
|
||||
);
|
||||
|
||||
return implode('/', array_map('rawurlencode', $segments));
|
||||
}
|
||||
|
||||
public static function isExternal(?string $path): bool
|
||||
{
|
||||
return $path !== null && Str::startsWith($path, ['http://', 'https://', '//', 'data:']);
|
||||
}
|
||||
|
||||
private static function isDirectUrl(string $path): bool
|
||||
{
|
||||
return self::isExternal($path);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user