aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--homepage/urls.py1
-rw-r--r--homepage/views.py9
-rw-r--r--static/css/main.css22
-rw-r--r--templates/home/watchlist.html95
-rw-r--r--templates/partials/navbar.html8
-rw-r--r--watch/urls.py1
-rw-r--r--watch/views.py16
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"})