id()) ->with(['anime.genres']) ->orderByDesc('created_at') ->get() ->groupBy('status'); $continues = ContinueWatching::where('user_id', auth()->id()) ->with(['anime', 'episode']) ->where('percent_complete', '<', 95) ->orderByDesc('updated_at') ->limit(20) ->get(); $achievements = UserAchievement::where('user_id', auth()->id()) ->with('achievement') ->orderByDesc('earned_at') ->get(); return view('frontend.profile', compact('items', 'continues', 'achievements')); } public function watchlistToggle(Request $request, Anime $anime) { $this->requireAuth(); $status = $request->input('status', 'plan'); if (!array_key_exists($status, Watchlist::STATUSES)) { $status = 'plan'; } $existing = Watchlist::where('user_id', auth()->id()) ->where('anime_id', $anime->id) ->first(); if ($existing) { if ($existing->status === $status) { $existing->delete(); $inList = false; $newStatus = null; } else { $existing->update(['status' => $status]); $inList = true; $newStatus = $status; } } else { Watchlist::create([ 'user_id' => auth()->id(), 'anime_id' => $anime->id, 'status' => $status, 'created_at' => now(), ]); $inList = true; $newStatus = $status; } $newlyEarned = AchievementService::check(auth()->user()); return response()->json([ 'in_list' => $inList, 'status' => $newStatus, 'status_label' => $newStatus ? (Watchlist::STATUSES[$newStatus] ?? '') : null, 'achievements' => array_map(fn($a) => ['title' => $a->title, 'icon' => $a->icon, 'color' => $a->color], $newlyEarned), ]); } // ── Watchlist Export ───────────────────────────────────────────────────── public function watchlistExport(Request $request) { $user = auth()->user(); if (!$user->hasPerk('watchlist_export')) { abort(403, 'Bu özellik için premium üyelik gerekiyor.'); } $format = in_array($request->query('format'), ['csv', 'json']) ? $request->query('format') : 'json'; $items = Watchlist::where('user_id', $user->id) ->with('anime:id,title,mal_score,genres') ->orderBy('status') ->orderByDesc('created_at') ->get() ->map(fn($w) => [ 'title' => $w->anime->title ?? '', 'status' => $w->status, 'added_at' => $w->created_at?->toDateString(), 'mal_score' => $w->anime->mal_score ?? null, ]); if ($format === 'csv') { $csv = "title,status,added_at,mal_score\n"; foreach ($items as $row) { $csv .= '"' . str_replace('"', '""', $row['title']) . '",' . $row['status'] . ',' . $row['added_at'] . ',' . $row['mal_score'] . "\n"; } return response($csv, 200, [ 'Content-Type' => 'text/csv; charset=utf-8', 'Content-Disposition' => 'attachment; filename="watchlist.csv"', ]); } return response()->json($items, 200, [ 'Content-Disposition' => 'attachment; filename="watchlist.json"', ]); } // ── Episode Vote ────────────────────────────────────────────────────────── public function episodeVote(Request $request, Episode $episode) { $this->requireAuth(); $vote = $request->input('vote') == 1 ? 1 : -1; $existing = EpisodeVote::where('user_id', auth()->id()) ->where('episode_id', $episode->id) ->first(); if ($existing) { if ($existing->vote === $vote) { $existing->delete(); // toggle off } else { $existing->update(['vote' => $vote]); } } else { EpisodeVote::create([ 'user_id' => auth()->id(), 'episode_id' => $episode->id, 'vote' => $vote, 'created_at' => now(), ]); } $likes = EpisodeVote::where('episode_id', $episode->id)->where('vote', 1)->count(); $dislikes = EpisodeVote::where('episode_id', $episode->id)->where('vote', -1)->count(); $myVote = EpisodeVote::where('user_id', auth()->id())->where('episode_id', $episode->id)->value('vote'); return response()->json([ 'likes' => $likes, 'dislikes' => $dislikes, 'my_vote' => $myVote, ]); } // ── Anime Rating ───────────────────────────────────────────────────────── public function animeRate(Request $request, Anime $anime) { $this->requireAuth(); $rating = (int) $request->input('rating'); if ($rating < 1 || $rating > 10) { return response()->json(['error' => 'Geçersiz puan'], 422); } AnimeRating::updateOrCreate( ['user_id' => auth()->id(), 'anime_id' => $anime->id], ['rating' => $rating] ); $avg = AnimeRating::where('anime_id', $anime->id)->avg('rating'); $count = AnimeRating::where('anime_id', $anime->id)->count(); // Anime tablosunu güncelle (ağırlıklı ortalama) $anime->update(['rating' => round($avg, 1)]); $newlyEarned = AchievementService::check(auth()->user()); return response()->json([ 'avg' => round($avg, 1), 'count' => $count, 'my_rating' => $rating, 'achievements' => array_map(fn($a) => ['title' => $a->title, 'icon' => $a->icon, 'color' => $a->color], $newlyEarned), ]); } // ── Continue Watching (güncelleme) ─────────────────────────────────────── public function continueWatchingUpdate(Request $request) { if (!auth()->check()) { return response()->json(['ok' => false]); } $data = $request->validate([ 'anime_id' => 'required|integer', 'episode_id' => 'required|integer', 'season_number' => 'required|integer', 'episode_number' => 'required|integer', 'seconds' => 'required|integer|min:0', 'total' => 'nullable|integer|min:0', 'percent' => 'nullable|integer|min:0|max:100', ]); $userId = auth()->id(); ContinueWatching::updateOrCreate( ['user_id' => $userId, 'anime_id' => $data['anime_id']], [ 'episode_id' => $data['episode_id'], 'season_number' => $data['season_number'], 'episode_number' => $data['episode_number'], 'seconds_watched' => $data['seconds'], 'total_seconds' => $data['total'] ?? 0, 'percent_complete'=> $data['percent'] ?? 0, 'updated_at' => now(), ] ); // stream_history perki yoksa en eski kayıtları silerek 30 limiti uygula if (!auth()->user()->hasPerk('stream_history')) { $count = ContinueWatching::where('user_id', $userId)->count(); if ($count > 30) { $idsToDelete = ContinueWatching::where('user_id', $userId) ->orderBy('updated_at') ->limit($count - 30) ->pluck('id'); ContinueWatching::whereIn('id', $idsToDelete)->delete(); } } // Başarım kontrolü (her 5 bölümde bir — performans için) if ($data['seconds'] % 300 < 35) { AchievementService::check(auth()->user()); } return response()->json(['ok' => true]); } // ── Anime İsteği ───────────────────────────────────────────────────────── public function requestIndex() { $requests = AnimeRequest::withCount('votes') ->whereIn('status', ['pending', 'approved', 'added']) ->orderByDesc('vote_count') ->orderByDesc('created_at') ->paginate(20); $myRequests = auth()->check() ? AnimeRequest::where('user_id', auth()->id())->orderByDesc('id')->limit(5)->get() : collect(); $votedIds = []; if (auth()->check()) { $votedIds = AnimeRequestVote::where('user_id', auth()->id()) ->pluck('anime_request_id')->toArray(); } return view('frontend.anime-request', compact('requests', 'myRequests', 'votedIds')); } public function requestStore(Request $request) { $this->requireAuth(); $data = $request->validate([ 'title' => 'required|string|max:200', 'original_title' => 'nullable|string|max:200', 'note' => 'nullable|string|max:1000', ]); // Benzer istek var mı? $existing = AnimeRequest::whereRaw('LOWER(title) = ?', [strtolower($data['title'])])->first(); if ($existing) { // Oy ekle $voted = AnimeRequestVote::where('anime_request_id', $existing->id) ->where('user_id', auth()->id()) ->exists(); if (!$voted) { AnimeRequestVote::create(['anime_request_id' => $existing->id, 'user_id' => auth()->id(), 'created_at' => now()]); $existing->increment('vote_count'); } return response()->json(['ok' => true, 'merged' => true, 'request_id' => $existing->id, 'vote_count' => $existing->fresh()->vote_count]); } $req = AnimeRequest::create([ 'user_id' => auth()->id(), 'title' => $data['title'], 'original_title' => $data['original_title'] ?? null, 'note' => $data['note'] ?? null, 'status' => 'pending', 'vote_count' => 1, ]); AnimeRequestVote::create(['anime_request_id' => $req->id, 'user_id' => auth()->id(), 'created_at' => now()]); AchievementService::check(auth()->user()); return response()->json(['ok' => true, 'merged' => false, 'request_id' => $req->id, 'vote_count' => 1]); } public function requestVote(AnimeRequest $animeRequest) { $this->requireAuth(); $voted = AnimeRequestVote::where('anime_request_id', $animeRequest->id) ->where('user_id', auth()->id()) ->exists(); if ($voted) { AnimeRequestVote::where('anime_request_id', $animeRequest->id) ->where('user_id', auth()->id()) ->delete(); $animeRequest->decrement('vote_count'); $isVoted = false; } else { AnimeRequestVote::create(['anime_request_id' => $animeRequest->id, 'user_id' => auth()->id(), 'created_at' => now()]); $animeRequest->increment('vote_count'); $isVoted = true; } return response()->json(['ok' => true, 'voted' => $isVoted, 'vote_count' => $animeRequest->fresh()->vote_count]); } // ── Anime Takip ────────────────────────────────────────────────────────── public function followToggle(Anime $anime) { $this->requireAuth(); $userId = auth()->id(); $existing = AnimeFollow::where('user_id', $userId)->where('anime_id', $anime->id)->first(); if ($existing) { $existing->delete(); $following = false; } else { AnimeFollow::create(['user_id' => $userId, 'anime_id' => $anime->id]); $following = true; } $count = AnimeFollow::where('anime_id', $anime->id)->count(); return response()->json(['following' => $following, 'count' => $count]); } // ── Bildirimler ─────────────────────────────────────────────────────────── public function notificationsIndex() { $this->requireAuth(); $notifications = UserNotification::where('user_id', auth()->id()) ->orderByDesc('created_at') ->paginate(30); // Görüntülenince hepsini okundu yap UserNotification::where('user_id', auth()->id()) ->whereNull('read_at') ->update(['read_at' => now()]); return view('frontend.notifications', compact('notifications')); } public function notificationsCount() { if (!auth()->check()) { return response()->json(['count' => 0]); } $count = UserNotification::where('user_id', auth()->id())->whereNull('read_at')->count(); return response()->json(['count' => $count]); } // ── Bölüm Notları ───────────────────────────────────────────────────────── public function noteStore(Request $request, Episode $episode) { $this->requireAuth(); $data = $request->validate([ 'content' => 'required|string|max:500', 'timestamp_at' => 'nullable|integer|min:0', ]); $note = EpisodeNote::create([ 'user_id' => auth()->id(), 'episode_id' => $episode->id, 'anime_id' => $episode->anime_id, 'content' => $data['content'], 'timestamp_at' => $data['timestamp_at'] ?? null, ]); return response()->json([ 'ok' => true, 'note' => [ 'id' => $note->id, 'content' => $note->content, 'timestamp_label' => $note->timestamp_label, 'timestamp_at' => $note->timestamp_at, 'created_at' => $note->created_at->format('d.m.Y H:i'), ], ]); } public function noteDelete(EpisodeNote $note) { $this->requireAuth(); if ($note->user_id !== auth()->id()) { abort(403); } $note->delete(); return response()->json(['ok' => true]); } public function episodeNotesList(Episode $episode) { $this->requireAuth(); $notes = EpisodeNote::where('user_id', auth()->id()) ->where('episode_id', $episode->id) ->orderBy('timestamp_at') ->orderBy('created_at') ->get() ->map(fn($n) => [ 'id' => $n->id, 'content' => $n->content, 'timestamp_label' => $n->timestamp_label, 'timestamp_at' => $n->timestamp_at, 'created_at' => $n->created_at->format('d.m.Y H:i'), ]); return response()->json(['notes' => $notes]); } // ── Helper ─────────────────────────────────────────────────────────────── private function requireAuth() { if (!auth()->check()) { abort(401); } } }