Initial commit: Animexe Laravel platform

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 00:01:48 +03:00
co-authored by Claude Opus 4.8
commit a63515cfc6
366 changed files with 74773 additions and 0 deletions
@@ -0,0 +1,287 @@
<?php
namespace App\Http\Controllers\Frontend;
use App\Http\Controllers\Controller;
use App\Models\Comment;
use App\Models\CommentLike;
use App\Models\Setting;
use App\Services\DeepSeekService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
class CommentController extends Controller
{
/**
* POST /comments yorum gönder
*/
public function store(Request $request)
{
$user = Auth::user();
$maxLength = ($user && $user->hasPerk('extended_comments')) ? 1000 : 500;
$request->validate([
'commentable_type' => 'required|in:episode,anime',
'commentable_id' => 'required|integer',
'content' => 'nullable|string|max:' . $maxLength,
'gif_url' => 'nullable|url|max:500',
'parent_id' => 'nullable|integer|exists:comments,id',
]);
if (empty(trim($request->content ?? '')) && empty($request->gif_url)) {
return response()->json(['error' => 'Yorum boş olamaz.'], 422);
}
if (!empty($request->gif_url) && (!$user || !$user->hasPerk('comment_gif'))) {
return response()->json(['error' => 'GIF eklemek için premium üyelik gerekiyor.'], 403);
}
$commentsEnabled = Setting::get('comments_enabled', '1') === '1';
if (!$commentsEnabled) {
return response()->json(['error' => 'Yorumlar şu an kapalı.'], 403);
}
$content = trim($request->content ?? '');
// AI moderasyon (sadece metin içeren yorumlar için, GIF yorumları direkt onaylanır)
$aiService = new DeepSeekService();
$pendingReason = null;
$status = 'approved';
if (!empty($content) && $aiService->isConfigured()) {
$mod = $aiService->moderateComment($content);
if ($mod['is_rude']) {
$status = 'pending';
$pendingReason = 'rude';
} elseif ($mod['is_spoiler']) {
$status = 'pending';
$pendingReason = 'spoiler';
}
} elseif (Setting::get('comments_require_approval', '0') === '1') {
$status = 'pending';
$pendingReason = 'manual';
}
$comment = Comment::create([
'user_id' => Auth::id(),
'commentable_type' => $request->commentable_type,
'commentable_id' => $request->commentable_id,
'parent_id' => $request->parent_id ?: null,
'content' => $content,
'gif_url' => $request->gif_url ?: null,
'status' => $status,
'like_count' => 0,
]);
$comment->load('user');
if ($status !== 'approved') {
return response()->json([
'ok' => true,
'pending' => true,
'pending_reason' => $pendingReason,
]);
}
return response()->json([
'ok' => true,
'pending' => false,
'comment' => $this->formatComment($comment, Auth::id()),
]);
}
/**
* POST /comments/{comment}/like beğen/beğenmekten vazgeç (toggle)
*/
public function like(Comment $comment)
{
$userId = Auth::id();
$existing = CommentLike::where('user_id', $userId)
->where('comment_id', $comment->id)
->first();
if ($existing) {
$existing->delete();
$comment->decrement('like_count');
$liked = false;
} else {
CommentLike::create(['user_id' => $userId, 'comment_id' => $comment->id]);
$comment->increment('like_count');
$liked = true;
}
return response()->json([
'ok' => true,
'liked' => $liked,
'like_count' => $comment->fresh()->like_count,
]);
}
/**
* GET /comments/gif-search GIF arama (Giphy öncelikli, Tenor fallback)
*/
public function gifSearch(Request $request)
{
$query = $request->query('q', 'anime reaction');
$giphyKey = Setting::get('giphy_api_key', '');
$tenorKey = Setting::get('tenor_api_key', '');
// Giphy
if (!empty($giphyKey)) {
return $this->searchGiphy($query, $giphyKey);
}
// Tenor
if (!empty($tenorKey)) {
return $this->searchTenor($query, $tenorKey);
}
return response()->json(['results' => [], 'error' => 'no_key']);
}
private function searchGiphy(string $query, string $apiKey)
{
try {
$res = Http::timeout(8)->get('https://api.giphy.com/v1/gifs/search', [
'api_key' => $apiKey,
'q' => $query,
'limit' => 24,
'rating' => 'pg-13',
'lang' => 'en',
]);
if (!$res->successful()) {
\Log::warning('Giphy API failed', ['status' => $res->status()]);
return response()->json(['results' => [], 'error' => 'giphy_fail']);
}
$gifs = collect($res->json('data', []))->map(function ($r) {
$images = $r['images'] ?? [];
$preview = $images['fixed_height_small']['url']
?? $images['fixed_height']['url']
?? $images['downsized']['url']
?? null;
$full = $images['downsized_medium']['url']
?? $images['fixed_height']['url']
?? $images['original']['url']
?? $preview;
if (!$preview || !$full) return null;
return [
'id' => $r['id'],
'preview' => $preview,
'url' => $full,
'title' => $r['title'] ?? '',
];
})->filter()->values();
return response()->json(['results' => $gifs]);
} catch (\Exception $e) {
\Log::error('Giphy error: ' . $e->getMessage());
return response()->json(['results' => [], 'error' => $e->getMessage()]);
}
}
private function searchTenor(string $query, string $apiKey)
{
try {
$res = Http::timeout(8)->get('https://tenor.googleapis.com/v2/search', [
'q' => $query,
'key' => $apiKey,
'limit' => 24,
'media_filter' => 'tinygif,gif',
'contentfilter' => 'medium',
]);
if (!$res->successful()) {
return response()->json(['results' => [], 'error' => 'tenor_fail']);
}
$gifs = collect($res->json('results', []))->map(function ($r) {
$formats = $r['media_formats'] ?? [];
$preview = $formats['tinygif']['url'] ?? $formats['mediumgif']['url'] ?? $formats['gif']['url'] ?? null;
$full = $formats['gif']['url'] ?? $formats['mediumgif']['url'] ?? $preview ?? null;
if (!$preview || !$full) return null;
return [
'id' => $r['id'],
'preview' => $preview,
'url' => $full,
'title' => $r['content_description'] ?? '',
];
})->filter()->values();
return response()->json(['results' => $gifs]);
} catch (\Exception $e) {
return response()->json(['results' => [], 'error' => $e->getMessage()]);
}
}
/**
* GET /comments bölüm yorumlarını getir (AJAX sayfalama)
*/
public function index(Request $request)
{
$userId = Auth::id();
$query = Comment::where('commentable_type', $request->type)
->where('commentable_id', $request->id)
->whereNull('parent_id')
->where('status', 'approved')
->with(['user', 'replies' => fn($q) => $q->where('status', 'approved')->with('user')->orderBy('created_at')])
->orderByDesc('is_pinned')
->orderByDesc('like_count')
->orderByDesc('created_at');
$comments = $query->paginate(20);
return response()->json([
'data' => $comments->map(fn($c) => $this->formatComment($c, $userId, true)),
'has_more' => $comments->hasMorePages(),
'next_page'=> $comments->currentPage() + 1,
]);
}
private function formatComment(Comment $c, ?int $userId, bool $withReplies = false): array
{
$data = [
'id' => $c->id,
'content' => $c->content,
'gif_url' => $c->gif_url,
'like_count' => $c->like_count,
'is_liked' => $userId ? $c->likes()->where('user_id', $userId)->exists() : false,
'is_pinned' => $c->is_pinned,
'parent_id' => $c->parent_id,
'created_at' => $c->created_at?->diffForHumans(),
'user' => $c->user ? [
'id' => $c->user->id,
'name' => $c->user->name,
'username' => $c->user->username,
'avatar' => $c->user->gif_avatar && $c->user->hasPerk('gif_avatar')
? $c->user->gif_avatar
: ($c->user->avatar ? \App\Support\MediaUrl::fromStoragePath($c->user->avatar) : null),
'role' => $c->user->role,
'is_following' => $userId && $userId !== $c->user->id
? \App\Models\UserFollow::where('follower_id', $userId)->where('following_id', $c->user->id)->exists()
: false,
'comment_bg' => $c->user->comment_bg,
'comment_glow' => $c->user->comment_glow,
'comment_signature' => $c->user->comment_signature,
'username_color' => $c->user->username_color,
'username_effect' => $c->user->username_effect,
'profile_frame' => $c->user->profile_frame,
'profile_badge' => $c->user->profile_badge,
'admin_badge' => $c->user->admin_badge,
'watch_rank' => $c->user->watchRank(),
] : null,
];
if ($withReplies && $c->relationLoaded('replies')) {
$data['replies'] = $c->replies->map(fn($r) => $this->formatComment($r, $userId))->toArray();
}
return $data;
}
}