diff options
| author | Bobby <[email protected]> | 2024-09-15 12:47:50 -0400 |
|---|---|---|
| committer | Bobby <[email protected]> | 2024-09-15 12:47:50 -0400 |
| commit | 3a29e68d8baa9eec3d2f2483bfdb26b37d7ab2dd (patch) | |
| tree | eaca424bc1b74b854e760c5e0388a44dd516f463 | |
| parent | d237fe78518313579b5d49b73958c94effc10ee6 (diff) | |
| download | yugen-3a29e68d8baa9eec3d2f2483bfdb26b37d7ab2dd.tar.xz yugen-3a29e68d8baa9eec3d2f2483bfdb26b37d7ab2dd.zip | |
added watchlist
| -rw-r--r-- | homepage/urls.py | 1 | ||||
| -rw-r--r-- | homepage/views.py | 9 | ||||
| -rw-r--r-- | static/css/main.css | 22 | ||||
| -rw-r--r-- | templates/home/watchlist.html | 95 | ||||
| -rw-r--r-- | templates/partials/navbar.html | 8 | ||||
| -rw-r--r-- | watch/urls.py | 1 | ||||
| -rw-r--r-- | watch/views.py | 16 |
7 files changed, 148 insertions, 4 deletions
diff --git a/homepage/urls.py b/homepage/urls.py index d187864..6c2cb3f 100644 --- a/homepage/urls.py +++ b/homepage/urls.py @@ -7,5 +7,6 @@ urlpatterns = [ path("", views.index, name="index"), path("search", views.search, name="search"), path("schedule", views.schedule, name="schedule"), + path("watchlist", views.watchlist, name="watchlist"), path("q_search", views.search_json, name="q_search"), ] diff --git a/homepage/views.py b/homepage/views.py index ee94790..3906a52 100644 --- a/homepage/views.py +++ b/homepage/views.py @@ -125,6 +125,15 @@ def search_json(request): return JsonResponse(search_results) +def watchlist(request): + watchlist = gather_watch_history(request.user) + + context = { + "watchlist": watchlist, + } + + return render(request, "home/watchlist.html", context) + def schedule(request): start = request.GET.get("start") end = request.GET.get("end") diff --git a/static/css/main.css b/static/css/main.css index 4223bdf..cb8f962 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -701,6 +701,14 @@ video { top: 0.25rem; } +.right-2 { + right: 0.5rem; +} + +.top-2 { + top: 0.5rem; +} + .z-0 { z-index: 0; } @@ -1005,6 +1013,10 @@ video { width: 15rem; } +.w-6 { + width: 1.5rem; +} + .min-w-32 { min-width: 8rem; } @@ -2302,6 +2314,11 @@ video { color: rgb(209 213 219 / var(--tw-text-opacity)); } +.text-gray-700 { + --tw-text-opacity: 1; + color: rgb(55 65 81 / var(--tw-text-opacity)); +} + .text-opacity-30 { --tw-text-opacity: 0.3; } @@ -2737,6 +2754,11 @@ main { color: rgb(255 255 255 / var(--tw-text-opacity)); } +.hover\:text-red-600:hover { + --tw-text-opacity: 1; + color: rgb(220 38 38 / var(--tw-text-opacity)); +} + .hover\:text-opacity-100:hover { --tw-text-opacity: 1; } diff --git a/templates/home/watchlist.html b/templates/home/watchlist.html new file mode 100644 index 0000000..b0c7798 --- /dev/null +++ b/templates/home/watchlist.html @@ -0,0 +1,95 @@ +{% extends "partials/base.html" %} +{% block css %} +<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"/> +{% endblock css %} +{% block content %} +<h1 class="text-2xl font-bold mt-4">Currently Watching</h1> +<section class="flex flex-col lg:flex-row flex-wrap my-4 gap-4 justify-center"> + {% for history in watchlist %} + <a href="{% url "watch:watch_episode" history.anime.id history.episode.number %}" class="group rounded-lg aspect-video h-48 relative flex-shrink-0 overflow-hidden"> + <div class="absolute inset-0 bg-center bg-cover transition-transform group-hover:scale-110" style="background-image: url('{% if history.episode.image %}{{ history.episode.image }}{% else %}{{ history.anime.cover }}{% endif %}')"></div> + <div class="absolute inset-0 bg-{{ user.preferences.accent_colour }}-600 opacity-0 group-hover:opacity-30 transition-opacity"></div> + <div class="absolute inset-0" style="background: linear-gradient(45deg, rgb(8, 8, 8) 15%, transparent 60%), linear-gradient(0deg, rgb(8, 8, 8) 0%, transparent 60%);"></div> + <div class="flex flex-col justify-end h-full p-2 relative z-10"> + <button class="absolute top-2 right-2 bg-white p-1 rounded-full text-gray-700 hover:text-red-600 focus:outline-none" onclick="event.preventDefault(); removeAnime({{ history.anime.id }});"> + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6"> + <path fill-rule="evenodd" d="M16.5 4.478v.227a48.816 48.816 0 0 1 3.878.512.75.75 0 1 1-.256 1.478l-.209-.035-1.005 13.07a3 3 0 0 1-2.991 2.77H8.084a3 3 0 0 1-2.991-2.77L4.087 6.66l-.209.035a.75.75 0 0 1-.256-1.478A48.567 48.567 0 0 1 7.5 4.705v-.227c0-1.564 1.213-2.9 2.816-2.951a52.662 52.662 0 0 1 3.369 0c1.603.051 2.815 1.387 2.815 2.951Zm-6.136-1.452a51.196 51.196 0 0 1 3.273 0C14.39 3.05 15 3.684 15 4.478v.113a49.488 49.488 0 0 0-6 0v-.113c0-.794.609-1.428 1.364-1.452Zm-.355 5.945a.75.75 0 1 0-1.5.058l.347 9a.75.75 0 1 0 1.499-.058l-.346-9Zm5.48.058a.75.75 0 1 0-1.498-.058l-.347 9a.75.75 0 0 0 1.5.058l.345-9Z" clip-rule="evenodd" /> + </svg> + </button> + <h1 class="text-xl font-bold truncate max-w-full overflow-hidden text-ellipsis whitespace-nowrap"> + {% if user.preferences.title_language == "english" and history.anime.title.english %} + {{ history.anime.title.english }} + {% elif user.preferences.title_language == "native" and history.anime.title.native %} + {{ history.anime.title.native }} + {% else %} + {{ history.anime.title.romaji }} + {% endif %} + </h1> + <h2 class="font-bold truncate max-w-full overflow-hidden text-ellipsis whitespace-nowrap">{{ history.episode.title }}</h2> + <p>Episode {{ history.episode.number }}</p> + </div> + </a> + {% endfor %} +</section> +<div id="toastContainer" class="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-50 flex flex-col space-y-2"></div> +{% endblock content %} +{% block scripts %} +<script> + document.addEventListener('DOMContentLoaded', () => { + const toastMessage = sessionStorage.getItem('toastMessage'); + if (toastMessage) { + showToast(toastMessage, true); + sessionStorage.removeItem('toastMessage'); + } + }); + + function showToast(message, isSuccess) { + const toast = document.createElement('div'); + toast.className = `flex items-center p-4 rounded-md shadow-lg transition-opacity duration-500 ease-in-out animate__animated ${ + isSuccess ? 'bg-green-100 text-green-700 animate__fadeInUp' : 'bg-red-100 text-red-700 animate__fadeInUp' + }`; + + const checkSVG = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" /></svg>` + + const errorSVG = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" /></svg>` + + toast.innerHTML = ` + <div class="flex items-center"> + ${isSuccess ? checkSVG : errorSVG} + <span class="ml-2">${message}</span> + </div> + `; + + // Append the toast to the container + toastContainer.appendChild(toast); + + // Remove the toast after 3 seconds + setTimeout(() => { + toast.classList.add('animate__fadeOutDown'); + setTimeout(() => { + toastContainer.removeChild(toast); + }, 500); + }, 3000); + } + + function removeAnime(animeId) { + fetch("{% url "watch:remove_anime_from_watchlist" %}", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CSRFToken": "{{ csrf_token }}" + }, + body: JSON.stringify({ + "anime_id": animeId + }) + }).then(response => { + if (response.ok) { + sessionStorage.setItem('toastMessage', 'Anime removed from watchlist'); + location.reload(); + } else { + showToast('Failed to remove anime from watchlist', false); + } + }); + } +</script> +{% endblock scripts %} diff --git a/templates/partials/navbar.html b/templates/partials/navbar.html index 6c49fd2..67e931d 100644 --- a/templates/partials/navbar.html +++ b/templates/partials/navbar.html @@ -119,8 +119,8 @@ </a> <!-- Watchlist icon --> - <a href="#watchlist" class="flex flex-col gap-2 items-center"> - {% if request.path == '/watchlist/' %} + <a href="{% url "home:watchlist" %}" class="flex flex-col gap-2 items-center"> + {% if request.path == '/watchlist' %} <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" @@ -370,8 +370,8 @@ </a> <!-- Watchlist icon --> - <a href="#watchlist" class="flex flex-col gap-2 items-center"> - {% if request.path == '/watchlist/' %} + <a href="{% url "home:watchlist" %}" class="flex flex-col gap-2 items-center"> + {% if request.path == '/watchlist' %} <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" diff --git a/watch/urls.py b/watch/urls.py index c92bba3..9217c94 100644 --- a/watch/urls.py +++ b/watch/urls.py @@ -9,5 +9,6 @@ urlpatterns = [ path('/<int:anime_id>/<int:episode>', views.watch, name='watch_episode'), path('/zid:<str:zid>', views.watch_via_zid, name='watch_via_zid'), path('/update_watch_history', views.update_episode_watch_time, name='update_watch_history'), + path('/remove_anime_from_watchlist', views.remove_anime_from_watchlist, name='remove_anime_from_watchlist'), path('/malId:<int:mal_id>$zid:<str:zid>', views.watch_via_zid_mal_id, name='watch_via_zid_mal_id'), # if anilist id is not available ] diff --git a/watch/views.py b/watch/views.py index 928c3ce..3f87a55 100644 --- a/watch/views.py +++ b/watch/views.py @@ -7,6 +7,7 @@ import dotenv from django.shortcuts import render, redirect import requests from authentication.utils import get_single_anime_mal +from user_profile.models import UserHistory from watch.tmdbmapper import parse_title_and_season from watch.utils import attach_episode_metadata, get_anime_episodes, get_all_episode_metadata, get_anime_data, get_episodes_by_zid, get_from_redis_cache, get_info_by_zid, get_seasons_by_zid, store_in_redis_cache, update_anime_user_history, get_anime_user_history from watch.models import Anime, AnimeEpisode, AnimeTitle, AnimeTrailer, AnimeGenre, AnimeStudio @@ -656,3 +657,18 @@ def watch_via_zid_mal_id(request, mal_id, zid): context["mal_episode_range"] = range(1, anime_mal_info["num_episodes"] + 1) return render(request, "watch/watch.html", context) + +def remove_anime_from_watchlist(request): + if request.method != "POST": + return JsonResponse({"status": "error", "message": "Invalid request"}) + + data = json.loads(request.body) + anime_id = data.get("anime_id") + + if request.user.is_authenticated: + anime = Anime.objects.get(id=anime_id) + history = UserHistory.objects.filter(user=request.user, anime=anime) + history.delete() + return JsonResponse({"status": "success"}) + else: + return JsonResponse({"status": "error", "message": "User not authenticated"}) |
