From ad98c05c2f6ddf505e587b442c63f0d891a391f9 Mon Sep 17 00:00:00 2001 From: Junwei Zhao Date: Sun, 24 Nov 2024 15:19:50 +1100 Subject: [PATCH 1/2] Add iptv --- data/db.sqlite3 | Bin 290816 -> 290816 bytes links/forms.py | 10 +- links/iptv_urls.py | 14 + links/iptv_views.py | 33 +++ links/migrations/0021_iptvchannel.py | 31 +++ .../migrations/0022_alter_iptvchannel_user.py | 21 ++ .../0023_remove_iptvchannel_user.py | 17 ++ links/models.py | 13 + links/templates/links/collection_list.html | 2 +- .../templates/links/iptv/confirm_delete.html | 29 ++ links/templates/links/iptv/form.html | 63 +++++ links/templates/links/iptv/list.html | 258 ++++++++++++++++++ links/urls.py | 3 + new_theme/static/css/dist/styles.css | 74 +++++ templates/base.html | 9 + 15 files changed, 575 insertions(+), 2 deletions(-) create mode 100644 links/iptv_urls.py create mode 100644 links/iptv_views.py create mode 100644 links/migrations/0021_iptvchannel.py create mode 100644 links/migrations/0022_alter_iptvchannel_user.py create mode 100644 links/migrations/0023_remove_iptvchannel_user.py create mode 100644 links/templates/links/iptv/confirm_delete.html create mode 100644 links/templates/links/iptv/form.html create mode 100644 links/templates/links/iptv/list.html diff --git a/data/db.sqlite3 b/data/db.sqlite3 index 442ba714fcec92f6f0c9aec45a0c8cb9b289fb86..cdc815c638285db072608f45c1986ef3e10a6698 100644 GIT binary patch delta 2192 zcmah~ZEO=|9KYx8`bOL5>4a``fRq>m%edZq+q-paWeEDgI7I^p+TGT3J=Enfpo;HehWi+dqrk+oiExjagg2eDVBS;ZW zRAo`+gs2b`Wl8K`k39^|dl1rE-quV1=YUrwB`V2kJf_IfI_!a&_rSN>E<9HfR4x{k zVzR_@_5SvYzDN*Wr_s;oOY{M%pabYQ8bJy45V`~2MC;*oc&W}u&O#c!LGXuD?H%FO z6bt5JZ9T(Py%~MFU0qI7JwPkl*>q7ajT@F_6wGAx0DB)m)i2qemEFs54iWr@K(pu) z`WAhNP9p<(;Vk^7-pO5n?P0p(?xy#a?4kPX#&$p@-FvH^feEiC=>E?AmU}P8^`~p9 zC+Q*p6Gs(8pKL`msF{%lFVb)n`KJzSFIMJzQAW?}_JRsE)SKLe25QrwVL!IS0CRgW zdx1r_f-QPvq-8RT;b7JR)p}y~v88 zpyb@zJGZKHt5SdW`v)~D1Xeko0T2exwG22%I-d=K#3~Pcjkp7xBWi(R&;{Km+#$;6 zngHj(Mi9fd#RqDN25cvlBXZW1zBw zLh$RF=VP#%gkNJ+=VAK_L=zo%&vjr+k2p6z2OT`6c#j+{`E71^7NZkDi6EI1h)&0T^r~E2X+A5)g+J=geK?$}sTNJ`%}w;rS^b zHIc;PBF8&_h@^YNI)94rlLFxnc(?e@x=y=A`ZeEf@^|tRQh+}ME(b2-T;Z>_ewaK; z`%|Ed^Ev?>VT43JoJZ<4ynmGv_IK^{bg(RVZq(LB^7@h+yM&RfMMlZ+vVVtGH5HI+ zpJmB4wb!2{L$wRTWUzLxPVRcINP-vLV1`&r96aiwQY3K@mu}Ga#57t)bfi+Cb_h`2 z;ah_a51d9WIa!MFWQ`}OuA{Tm%QVUo!`N)M^CO_T?y9#9|AMN;P-obMV;jKK7{?ni z^Cnwy8|HWlm&+hnt&*|M3-8A1t}^ja$HmvVw&RT zMKNVMNP3Co)7==0na$nGG5(!w#L5(&DlSTGHgC<9fE74Slw^f%)n#;(HO!2@X9*zu zJSJ2=7L9Y_RGg2uB5D!EW8$;|FrfXQQTk1Tw*XZv1|Q56%T|_|oMh(eu(0HN`Xtuq zE0;`lWMot=8n{>R3tcfr^#&tt>XTNwF-%Q;XFi?Huy&uAkT|x|SFG?7JJKg}agO6- tEbb!ncIahQ;-U#z<~eB*_<4y`TRT86uSbTjqQgV*$UfSkuTVEn{SA{LZ|DF3 delta 1294 zcmZ9KTWnNC7{||C_FVRynK_GWuiMfZp;f@$?RIa{c6Y^4NmwhOwh8EVOG~=}dZ7jE zgW9ddm=F-$lw%qV4LszX2@;(n}?z(q#`6j1`qO>5f0{f0FvM<>*%P^G|@)f>0nj19B z@`aWXfFepHr^JUuWAk!8(t1@z5q&tPyl9gZr=tRcAjrj9-TBk|PK=-koyjTJY^|B* zXKuHp5k(7Ji32yt51OAszOWrcJ@!NxZj&6R@sGHTHPQ@87>D$4bf0rYdqJyIyVQ-! z8FU9uIc|a$5Xz@b4Or1M7!gS>LaVJC!nl1=7r#^uE8Re~z6!uP>s^XWhcDC^ zj`EcNl(*E`&B6wW5jswqjF0q$Zm2u7j~&x;0AJMh!+Y>+*Z`KT`c61$E%rgm#}2^9 zfgIZ_m+swa)n-6hU4PGsRO)c1|IlzMb0po{lk)$cJS)mv<89R54ga! z1E3Xf*Fo?G_+k)(vohbK;e!0Xu0DBl&iWOr&d3B9XLqwxAkbhwwp23?a0W ziP47wpK{<2A(kZYj0g+?Iv#fF$zC$9t;OA{#x=ZK#S|Eto~$L=?Q8r^4gYGW#pIT# zV7>3e3x=#G%Sm?I+QD5LaE$+5?bCaxz)(+? zl5BJXv@f+};~ic+TaI6nROzlnMxB4FDJ3ZD=qV@(a5tAWZ_{x{iJlCSZ2MYEt4*Rv zcM$(zxb/edit/', IPTVChannelUpdateView.as_view(), name='iptv-update'), + path('ui/iptv//delete/', IPTVChannelDeleteView.as_view(), name='iptv-delete'), +] diff --git a/links/iptv_views.py b/links/iptv_views.py new file mode 100644 index 0000000..dd7e0bb --- /dev/null +++ b/links/iptv_views.py @@ -0,0 +1,33 @@ +from django.urls import reverse_lazy +from django.views.generic import ListView, CreateView, UpdateView, DeleteView +from django.shortcuts import render +from .models import IPTVChannel +from .forms import IPTVChannelForm + +class IPTVChannelListView(ListView): + model = IPTVChannel + template_name = 'links/iptv/list.html' + context_object_name = 'channels' + + def get_queryset(self): + return IPTVChannel.objects.all() + +class IPTVChannelCreateView(CreateView): + model = IPTVChannel + form_class = IPTVChannelForm + template_name = 'links/iptv/form.html' + success_url = reverse_lazy('iptv-list') + + def form_valid(self, form): + return super().form_valid(form) + +class IPTVChannelUpdateView(UpdateView): + model = IPTVChannel + form_class = IPTVChannelForm + template_name = 'links/iptv/form.html' + success_url = reverse_lazy('iptv-list') + +class IPTVChannelDeleteView(DeleteView): + model = IPTVChannel + template_name = 'links/iptv/confirm_delete.html' + success_url = reverse_lazy('iptv-list') diff --git a/links/migrations/0021_iptvchannel.py b/links/migrations/0021_iptvchannel.py new file mode 100644 index 0000000..852d974 --- /dev/null +++ b/links/migrations/0021_iptvchannel.py @@ -0,0 +1,31 @@ +# Generated by Django 5.0.9 on 2024-11-24 03:42 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('links', '0020_alter_image_description'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='IPTVChannel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200)), + ('url', models.URLField(help_text='M3U or similar streaming URL')), + ('description', models.TextField(blank=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='iptv_channels', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/links/migrations/0022_alter_iptvchannel_user.py b/links/migrations/0022_alter_iptvchannel_user.py new file mode 100644 index 0000000..b8283d1 --- /dev/null +++ b/links/migrations/0022_alter_iptvchannel_user.py @@ -0,0 +1,21 @@ +# Generated by Django 5.0.9 on 2024-11-24 03:51 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('links', '0021_iptvchannel'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='iptvchannel', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='iptv_channels', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/links/migrations/0023_remove_iptvchannel_user.py b/links/migrations/0023_remove_iptvchannel_user.py new file mode 100644 index 0000000..520a87b --- /dev/null +++ b/links/migrations/0023_remove_iptvchannel_user.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.9 on 2024-11-24 03:52 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('links', '0022_alter_iptvchannel_user'), + ] + + operations = [ + migrations.RemoveField( + model_name='iptvchannel', + name='user', + ), + ] diff --git a/links/models.py b/links/models.py index 003be6d..9a4f03d 100644 --- a/links/models.py +++ b/links/models.py @@ -289,3 +289,16 @@ class Image(models.Model): height=height, fit='cover' ) + +class IPTVChannel(models.Model): + title = models.CharField(max_length=200) + url = models.URLField(help_text="M3U or similar streaming URL") + description = models.TextField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ['-created_at'] + + def __str__(self): + return self.title diff --git a/links/templates/links/collection_list.html b/links/templates/links/collection_list.html index 27c747d..6a10ee8 100644 --- a/links/templates/links/collection_list.html +++ b/links/templates/links/collection_list.html @@ -30,7 +30,7 @@ {% for image in images %}
- {{ image.title }}
diff --git a/links/templates/links/iptv/confirm_delete.html b/links/templates/links/iptv/confirm_delete.html new file mode 100644 index 0000000..9d5babf --- /dev/null +++ b/links/templates/links/iptv/confirm_delete.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+

+ Delete IPTV Channel +

+
+

Are you sure you want to delete "{{ object.title }}"? This action cannot be undone.

+
+
+
+ {% csrf_token %} +
+ + Cancel + + +
+
+
+
+
+
+{% endblock %} diff --git a/links/templates/links/iptv/form.html b/links/templates/links/iptv/form.html new file mode 100644 index 0000000..c9b8bc6 --- /dev/null +++ b/links/templates/links/iptv/form.html @@ -0,0 +1,63 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+

+ {% if form.instance.pk %}Edit{% else %}Add{% endif %} IPTV Channel +

+
+
+ {% csrf_token %} + +
+ +
+ {{ form.title }} +
+ {% if form.title.errors %} +

{{ form.title.errors.0 }}

+ {% endif %} +
+ +
+ +
+ {{ form.url }} +
+ {% if form.url.errors %} +

{{ form.url.errors.0 }}

+ {% endif %} +
+ +
+ +
+ {{ form.description }} +
+ {% if form.description.errors %} +

{{ form.description.errors.0 }}

+ {% endif %} +
+ +
+ + Cancel + + +
+
+
+
+
+
+{% endblock %} diff --git a/links/templates/links/iptv/list.html b/links/templates/links/iptv/list.html new file mode 100644 index 0000000..f5abb9d --- /dev/null +++ b/links/templates/links/iptv/list.html @@ -0,0 +1,258 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} +
+ +
+
+

IPTV Channels

+ + Add Channel + +
+ +
+ + +
+
+
+ + + +

No channel selected

+

Select a channel from the list to start watching

+
+ + + + + +
+
+
+{% endblock %} + +{% block extra_js %} + + +{% endblock %} diff --git a/links/urls.py b/links/urls.py index 5189a73..0067c48 100644 --- a/links/urls.py +++ b/links/urls.py @@ -57,6 +57,9 @@ urlpatterns = [ # Include collection URLs path('', include('links.collection_urls')), + # Include IPTV URLs - must be before alias patterns + path('', include('links.iptv_urls')), + # Aliases - these should always be last path('/', views.redirect_to_original, name='redirect_to_original'), path('//', views.redirect_to_original, name='redirect_to_original_with_param'), diff --git a/new_theme/static/css/dist/styles.css b/new_theme/static/css/dist/styles.css index 2cd519e..327a740 100644 --- a/new_theme/static/css/dist/styles.css +++ b/new_theme/static/css/dist/styles.css @@ -868,6 +868,10 @@ video { height: 280px; } +.h-\[calc\(100vh-4rem\)\] { + height: calc(100vh - 4rem); +} + .h-auto { height: auto; } @@ -880,6 +884,10 @@ video { height: 100vh; } +.min-h-0 { + min-height: 0px; +} + .w-12 { width: 3rem; } @@ -916,6 +924,10 @@ video { width: 2rem; } +.w-80 { + width: 20rem; +} + .w-full { width: 100%; } @@ -928,6 +940,10 @@ video { min-width: 100%; } +.max-w-2xl { + max-width: 42rem; +} + .max-w-3xl { max-width: 48rem; } @@ -988,6 +1004,10 @@ video { cursor: not-allowed; } +.cursor-pointer { + cursor: pointer; +} + .select-all { -webkit-user-select: all; -moz-user-select: all; @@ -1158,6 +1178,10 @@ video { overflow-x: auto; } +.overflow-y-auto { + overflow-y: auto; +} + .truncate { overflow: hidden; text-overflow: ellipsis; @@ -1218,6 +1242,10 @@ video { border-left-width: 4px; } +.border-r { + border-right-width: 1px; +} + .border-t { border-top-width: 1px; } @@ -1265,6 +1293,11 @@ video { border-color: rgb(234 179 8 / var(--tw-border-opacity)); } +.border-indigo-500 { + --tw-border-opacity: 1; + border-color: rgb(99 102 241 / var(--tw-border-opacity)); +} + .bg-blue-100 { --tw-bg-opacity: 1; background-color: rgb(219 234 254 / var(--tw-bg-opacity)); @@ -1325,6 +1358,11 @@ video { background-color: rgb(22 163 74 / var(--tw-bg-opacity)); } +.bg-indigo-600 { + --tw-bg-opacity: 1; + background-color: rgb(79 70 229 / var(--tw-bg-opacity)); +} + .bg-purple-100 { --tw-bg-opacity: 1; background-color: rgb(243 232 255 / var(--tw-bg-opacity)); @@ -1374,10 +1412,28 @@ video { background-color: rgb(254 249 195 / var(--tw-bg-opacity)); } +.bg-black { + --tw-bg-opacity: 1; + background-color: rgb(0 0 0 / var(--tw-bg-opacity)); +} + +.bg-indigo-50 { + --tw-bg-opacity: 1; + background-color: rgb(238 242 255 / var(--tw-bg-opacity)); +} + .bg-opacity-75 { --tw-bg-opacity: 0.75; } +.bg-opacity-20 { + --tw-bg-opacity: 0.2; +} + +.bg-opacity-50 { + --tw-bg-opacity: 0.5; +} + .bg-gradient-to-b { background-image: linear-gradient(to bottom, var(--tw-gradient-stops)); } @@ -1959,6 +2015,11 @@ video { background-color: rgb(21 128 61 / var(--tw-bg-opacity)); } +.hover\:bg-indigo-700:hover { + --tw-bg-opacity: 1; + background-color: rgb(67 56 202 / var(--tw-bg-opacity)); +} + .hover\:bg-purple-600:hover { --tw-bg-opacity: 1; background-color: rgb(147 51 234 / var(--tw-bg-opacity)); @@ -1984,6 +2045,10 @@ video { background-color: rgb(185 28 28 / var(--tw-bg-opacity)); } +.hover\:bg-opacity-30:hover { + --tw-bg-opacity: 0.3; +} + .hover\:text-blue-200:hover { --tw-text-opacity: 1; color: rgb(191 219 254 / var(--tw-text-opacity)); @@ -2121,6 +2186,11 @@ video { --tw-ring-color: rgb(34 197 94 / var(--tw-ring-opacity)); } +.focus\:ring-indigo-500:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity)); +} + .focus\:ring-red-500:focus { --tw-ring-opacity: 1; --tw-ring-color: rgb(239 68 68 / var(--tw-ring-opacity)); @@ -2313,6 +2383,10 @@ video { margin-bottom: calc(0px * var(--tw-space-y-reverse)); } + .sm\:rounded-lg { + border-radius: 0.5rem; + } + .sm\:rounded-md { border-radius: 0.375rem; } diff --git a/templates/base.html b/templates/base.html index 8e95414..e9817f0 100644 --- a/templates/base.html +++ b/templates/base.html @@ -122,6 +122,15 @@ {% trans "Images" %} + +
+ + + + {% trans "IPTV" %} +
+
From ecbbc1da203beb1a3f12dedd9fd7f093d3d1b875 Mon Sep 17 00:00:00 2001 From: Junwei Zhao Date: Sun, 24 Nov 2024 16:09:03 +1100 Subject: [PATCH 2/2] add ability to export iptv channels --- data/db.sqlite3 | Bin 290816 -> 290816 bytes links/iptv_urls.py | 3 + links/iptv_views.py | 23 +++ links/templates/links/iptv/list.html | 265 +++++++++++++++++++++++---- new_theme/static/css/dist/styles.css | 130 +++++++++++++ 5 files changed, 390 insertions(+), 31 deletions(-) diff --git a/data/db.sqlite3 b/data/db.sqlite3 index cdc815c638285db072608f45c1986ef3e10a6698..2d862a470fe9e333ddb4e9d28baba7ccdcce271d 100644 GIT binary patch delta 623 zcmZp8AlUFgaDp_W-$WT_M!$^-v-KF+C;!o_WOQgQ)8AgE&)9c?*`EE(RCcCf9*#ne z01kZ)KK8flXEqxuxUtvEJFqY~G8!2endllC>Kd6S7?@ZY8(A5d=ouSXm>XC+%G+a6 zU|?lxq-SAnVrBwVV24$Kfw7*6v7w2vv7@{#rUD~#D^n9aGXq~UBMS>hc^fP$Os$N~ z^b9R5Obm=1<*l(QFf`LMGPf`>GI5l*0x6jO(3nYd`&tgBaCT;UzM|>u0!)THd=vP( z_*(et_$v5{wp$1=>G7$sGBYwdI)F)gFlh%SZNa1sn6%zLO^&HRkg<0|+8AnFLTc)u}|9zQ(f3{1?7jM=CEHfGY^-el$4p4m1uX4jT>%4f+l74d@Ns4b=*24a^F44ZIDt z4XzEN4Vn#&3SSL=4Q>r&4O$IM4Mey7Fajw95N$yU02M(&2^9bUTWN4~c9)Sf0u~}R zG&V3YFfuePF)=MNG$1fDIy5&rGcqnXHa9acF-%ELOP6gQ11E/edit/', IPTVChannelUpdateView.as_view(), name='iptv-update'), path('ui/iptv//delete/', IPTVChannelDeleteView.as_view(), name='iptv-delete'), + path('api/iptv/export/', export_m3u_playlist, name='iptv-export'), ] diff --git a/links/iptv_views.py b/links/iptv_views.py index dd7e0bb..84d98f9 100644 --- a/links/iptv_views.py +++ b/links/iptv_views.py @@ -1,6 +1,7 @@ from django.urls import reverse_lazy from django.views.generic import ListView, CreateView, UpdateView, DeleteView from django.shortcuts import render +from django.http import HttpResponse from .models import IPTVChannel from .forms import IPTVChannelForm @@ -31,3 +32,25 @@ class IPTVChannelDeleteView(DeleteView): model = IPTVChannel template_name = 'links/iptv/confirm_delete.html' success_url = reverse_lazy('iptv-list') + +def export_m3u_playlist(request): + """Export all channels as M3U playlist""" + channels = IPTVChannel.objects.all() + + # Start the M3U file content + content = ['#EXTM3U'] + + for channel in channels: + # Add channel info line + content.append(f'#EXTINF:-1 tvg-id="{channel.id}",{channel.title}') + # Add URL line + content.append(channel.url) + + # Join all lines with newlines + playlist_content = '\n'.join(content) + + # Create the response with appropriate headers + response = HttpResponse(playlist_content, content_type='audio/x-mpegurl') + response['Content-Disposition'] = 'attachment; filename="iptv_channels.m3u"' + + return response diff --git a/links/templates/links/iptv/list.html b/links/templates/links/iptv/list.html index f5abb9d..9f8dbb9 100644 --- a/links/templates/links/iptv/list.html +++ b/links/templates/links/iptv/list.html @@ -2,43 +2,51 @@ {% load static %} {% block content %} -
- -
-
-

IPTV Channels

-
+
+ +
+
- -
-
-
+ +
+
+
-

No channel selected

+

No channel selected

Select a channel from the list to start watching

- + +
+
+
+ + + + + + @@ -254,5 +334,128 @@ function cleanupPlayer() { emptyState.classList.remove('hidden'); playOverlay.classList.add('hidden'); } + +// Modal handling functions +let currentDeleteId = null; +let lastFocusedElement = null; + +function showAddModal() { + lastFocusedElement = document.activeElement; + document.getElementById('modalTitle').textContent = 'Add Channel'; + document.getElementById('channelId').value = ''; + document.getElementById('channelForm').reset(); + const modal = document.getElementById('channelModal'); + modal.classList.remove('hidden'); + document.getElementById('title').focus(); +} + +function showEditModal(id, title, url, description) { + lastFocusedElement = document.activeElement; + document.getElementById('modalTitle').textContent = 'Edit Channel'; + document.getElementById('channelId').value = id; + document.getElementById('title').value = title; + document.getElementById('url').value = url; + document.getElementById('description').value = description; + const modal = document.getElementById('channelModal'); + modal.classList.remove('hidden'); + document.getElementById('title').focus(); +} + +function hideChannelModal() { + document.getElementById('channelModal').classList.add('hidden'); + if (lastFocusedElement) { + lastFocusedElement.focus(); + } +} + +function showDeleteModal(id, title) { + lastFocusedElement = document.activeElement; + currentDeleteId = id; + document.getElementById('deleteChannelTitle').textContent = title; + document.getElementById('deleteModal').classList.remove('hidden'); +} + +function hideDeleteModal() { + currentDeleteId = null; + document.getElementById('deleteModal').classList.add('hidden'); + if (lastFocusedElement) { + lastFocusedElement.focus(); + } +} + +async function submitChannelForm() { + const form = document.getElementById('channelForm'); + const formData = new FormData(form); + const id = formData.get('id'); + const url = id ? `/ui/iptv/${id}/edit/` : '/ui/iptv/create/'; + + try { + const response = await fetch(url, { + method: 'POST', + headers: { + 'X-CSRFToken': getCookie('csrftoken'), + }, + body: formData, + credentials: 'same-origin' + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + hideChannelModal(); + window.location.reload(); + } catch (error) { + console.error('Error:', error); + alert('Error saving channel. Please try again.'); + } +} + +async function confirmDelete() { + if (!currentDeleteId) return; + + try { + const response = await fetch(`/ui/iptv/${currentDeleteId}/delete/`, { + method: 'POST', + headers: { + 'X-CSRFToken': getCookie('csrftoken'), + }, + credentials: 'same-origin' + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + hideDeleteModal(); + window.location.reload(); + } catch (error) { + console.error('Error:', error); + alert('Error deleting channel. Please try again.'); + } +} + +// Add keyboard event listeners for modal accessibility +document.addEventListener('keydown', function(e) { + if (e.key === 'Escape') { + hideChannelModal(); + hideDeleteModal(); + } +}); + +function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; +} {% endblock %} diff --git a/new_theme/static/css/dist/styles.css b/new_theme/static/css/dist/styles.css index 327a740..5596bf9 100644 --- a/new_theme/static/css/dist/styles.css +++ b/new_theme/static/css/dist/styles.css @@ -588,6 +588,18 @@ video { } } +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + .pointer-events-none { pointer-events: none; } @@ -608,6 +620,10 @@ video { position: relative; } +.sticky { + position: sticky; +} + .inset-0 { inset: 0px; } @@ -888,6 +904,14 @@ video { min-height: 0px; } +.min-h-full { + min-height: 100%; +} + +.min-h-\[50vh\] { + min-height: 50vh; +} + .w-12 { width: 3rem; } @@ -1008,6 +1032,10 @@ video { cursor: pointer; } +.touch-manipulation { + touch-action: manipulation; +} + .select-all { -webkit-user-select: all; -moz-user-select: all; @@ -1580,6 +1608,11 @@ video { padding-bottom: 2rem; } +.py-6 { + padding-top: 1.5rem; + padding-bottom: 1.5rem; +} + .pl-3 { padding-left: 0.75rem; } @@ -1600,6 +1633,14 @@ video { padding-top: 1.25rem; } +.pb-4 { + padding-bottom: 1rem; +} + +.pt-4 { + padding-top: 1rem; +} + .text-left { text-align: left; } @@ -1866,6 +1907,21 @@ video { outline-style: solid; } +.ring-1 { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.ring-inset { + --tw-ring-inset: inset; +} + +.ring-gray-300 { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(209 213 219 / var(--tw-ring-opacity)); +} + .blur { --tw-blur: blur(8px); filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); @@ -2045,6 +2101,16 @@ video { background-color: rgb(185 28 28 / var(--tw-bg-opacity)); } +.hover\:bg-indigo-500:hover { + --tw-bg-opacity: 1; + background-color: rgb(99 102 241 / var(--tw-bg-opacity)); +} + +.hover\:bg-red-500:hover { + --tw-bg-opacity: 1; + background-color: rgb(239 68 68 / var(--tw-bg-opacity)); +} + .hover\:bg-opacity-30:hover { --tw-bg-opacity: 0.3; } @@ -2114,6 +2180,11 @@ video { color: rgb(153 27 27 / var(--tw-text-opacity)); } +.hover\:text-gray-600:hover { + --tw-text-opacity: 1; + color: rgb(75 85 99 / var(--tw-text-opacity)); +} + .hover\:underline:hover { text-decoration-line: underline; } @@ -2143,6 +2214,11 @@ video { border-color: transparent; } +.focus\:border-indigo-500:focus { + --tw-border-opacity: 1; + border-color: rgb(99 102 241 / var(--tw-border-opacity)); +} + .focus\:outline-none:focus { outline: 2px solid transparent; outline-offset: 2px; @@ -2241,6 +2317,11 @@ video { margin-right: 0px; } + .sm\:my-8 { + margin-top: 2rem; + margin-bottom: 2rem; + } + .sm\:mb-6 { margin-bottom: 1.5rem; } @@ -2325,6 +2406,14 @@ video { width: auto; } + .sm\:w-full { + width: 100%; + } + + .sm\:max-w-lg { + max-width: 32rem; + } + .sm\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } @@ -2411,6 +2500,10 @@ video { padding: 1.5rem; } + .sm\:p-0 { + padding: 0px; + } + .sm\:px-4 { padding-left: 1rem; padding-right: 1rem; @@ -2476,6 +2569,30 @@ video { display: table-cell; } + .md\:h-20 { + height: 5rem; + } + + .md\:h-\[calc\(100vh-4rem\)\] { + height: calc(100vh - 4rem); + } + + .md\:h-full { + height: 100%; + } + + .md\:min-h-0 { + min-height: 0px; + } + + .md\:w-20 { + width: 5rem; + } + + .md\:w-80 { + width: 20rem; + } + .md\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } @@ -2484,9 +2601,22 @@ video { grid-template-columns: repeat(4, minmax(0, 1fr)); } + .md\:flex-row { + flex-direction: row; + } + .md\:gap-8 { gap: 2rem; } + + .md\:border-r { + border-right-width: 1px; + } + + .md\:text-2xl { + font-size: 1.5rem; + line-height: 2rem; + } } @media (min-width: 1024px) {