mirror of
https://github.com/rudy3333/eversync.git
synced 2025-07-01 16:46:02 +00:00
Compare commits
4 commits
6cbb408022
...
bced97db38
Author | SHA1 | Date | |
---|---|---|---|
bced97db38 | |||
970fe28e1e | |||
3c03787863 | |||
c76d6f31ef |
11 changed files with 925 additions and 27 deletions
|
@ -1,7 +1,7 @@
|
|||
from django import forms
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
|
||||
from .models import Document, Event, Notes, Task
|
||||
from .models import Document, Event, Notes, Task, WebArchive
|
||||
from django_recaptcha.fields import ReCaptchaField
|
||||
from django_recaptcha.widgets import ReCaptchaV2Checkbox
|
||||
|
||||
|
@ -57,3 +57,19 @@ class TaskForm(forms.ModelForm):
|
|||
|
||||
class EmailUpdateForm(forms.Form):
|
||||
email = forms.EmailField(label="Your email", required=True)
|
||||
|
||||
class WebArchiveForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = WebArchive
|
||||
fields = ['url']
|
||||
widgets = {
|
||||
'url': forms.URLInput(attrs={'placeholder': 'Enter URL to archive'})
|
||||
}
|
||||
|
||||
def save(self, commit=True, user=None):
|
||||
instance = super().save(commit=False)
|
||||
if user:
|
||||
instance.user = user
|
||||
if commit:
|
||||
instance.save()
|
||||
return instance
|
27
eversyncc/migrations/0028_webarchive.py
Normal file
27
eversyncc/migrations/0028_webarchive.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Generated by Django 5.2.1 on 2025-06-08 01:46
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('eversyncc', '0027_usernotifs'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='WebArchive',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('url', models.URLField()),
|
||||
('title', models.CharField(max_length=255)),
|
||||
('screenshot', models.ImageField(upload_to='web_archives/')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
18
eversyncc/migrations/0029_webarchive_content.py
Normal file
18
eversyncc/migrations/0029_webarchive_content.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 5.2.1 on 2025-06-08 02:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('eversyncc', '0028_webarchive'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='webarchive',
|
||||
name='content',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
|
@ -114,3 +114,11 @@ class Stroke(models.Model):
|
|||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
data = models.JSONField()
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class WebArchive(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
url = models.URLField()
|
||||
title = models.CharField(max_length=255)
|
||||
screenshot = models.ImageField(upload_to='web_archives/')
|
||||
content = models.TextField(null=True, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
@ -171,6 +171,10 @@ background-color: #03a9f4;
|
|||
background-color: white;
|
||||
}
|
||||
|
||||
.archive {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.service-button i {
|
||||
font-size: 70px;
|
||||
margin-bottom: 5px;
|
||||
|
|
|
@ -70,12 +70,10 @@ urlpatterns = [
|
|||
path('whiteboard/<int:whiteboard_id>/upload-image/', views.upload_image, name='upload_image'),
|
||||
path('whiteboard/<int:whiteboard_id>/delete-image/', views.delete_image, name='delete_image'),
|
||||
path('api/update_device_token/', views.update_device_token, name='update_device_token'),
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
path('web_archive/', views.web_archive, name='web_archive'),
|
||||
path('web_archive/save/', views.save_web_archive, name='save_web_archive'),
|
||||
path('web_archive/delete/<int:archive_id>', views.delete_web_archive, name='delete_web_archive'),
|
||||
path('web_archive/view/<int:archive_id>', views.view_web_archive, name='view_web_archive')
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
|
|
|
@ -3,12 +3,15 @@ from django.http import HttpResponse, JsonResponse, FileResponse
|
|||
from django.contrib.auth.decorators import login_required
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, PasswordChangeForm
|
||||
from django.core.files import File
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
# Create your views here.
|
||||
from django.contrib.auth import logout
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.views import PasswordChangeView
|
||||
from .forms import UsernameChangeForm, DocumentForm, EventForm, NoteForm, TaskForm
|
||||
from .models import Document, Event, Notes, Embed, Task, RichDocument, Message
|
||||
from .models import Document, Event, Notes, Embed, Task, RichDocument, Message, WebArchive
|
||||
from django.contrib import messages
|
||||
from allauth.account.views import LoginView as AllauthLoginView
|
||||
import os
|
||||
|
@ -17,6 +20,7 @@ from eversyncc.models import UserNotifs
|
|||
import requests
|
||||
from allauth.account.models import EmailAddress
|
||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from webpush import send_user_notification
|
||||
from icalendar import Calendar, Event as IcalEvent
|
||||
from django.utils.text import slugify
|
||||
|
@ -29,7 +33,7 @@ from .embed_utils import get_embed_info
|
|||
from .models import Whiteboard, Stroke
|
||||
from eversyncc.email import verify_token
|
||||
from django.contrib.auth import get_user_model
|
||||
from .forms import EmailUpdateForm
|
||||
from .forms import EmailUpdateForm, WebArchiveForm
|
||||
from allauth.account.utils import send_email_confirmation
|
||||
from functools import wraps
|
||||
from .forms import RegisterForm
|
||||
|
@ -41,6 +45,9 @@ from .models import UserNotifs
|
|||
import aiohttp
|
||||
import asyncio
|
||||
from asgiref.sync import sync_to_async
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.common.by import By
|
||||
import selenium
|
||||
|
||||
def email_verified_required(view_func):
|
||||
@wraps(view_func)
|
||||
|
@ -1014,3 +1021,135 @@ def update_device_token(request):
|
|||
return JsonResponse({"message": "Device token updated"})
|
||||
except Exception as e:
|
||||
return JsonResponse({"error": str(e)}, status=500)
|
||||
|
||||
@email_verified_required
|
||||
@login_required
|
||||
def save_web_archive(request):
|
||||
if request.method == 'POST':
|
||||
form = WebArchiveForm(request.POST)
|
||||
if form.is_valid():
|
||||
driver = None
|
||||
try:
|
||||
# Set up Chrome options
|
||||
chrome_options = Options()
|
||||
chrome_options.add_argument('--headless=new')
|
||||
chrome_options.add_argument('--no-sandbox')
|
||||
chrome_options.add_argument('--disable-dev-shm-usage')
|
||||
chrome_options.add_argument('--disable-gpu')
|
||||
chrome_options.add_argument("--lang=en-US")
|
||||
chrome_options.add_experimental_option("prefs", {
|
||||
"intl.accept_languages": "en,en_US",
|
||||
"profile.default_content_setting_values.geolocation": 2,
|
||||
})
|
||||
|
||||
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
|
||||
chrome_options.add_argument(f"user-agent={user_agent}")
|
||||
chrome_options.add_argument("--disable-blink-features=AutomationControlled")
|
||||
|
||||
# Initialize Chrome driver
|
||||
driver = webdriver.Chrome(options=chrome_options)
|
||||
url = form.cleaned_data['url']
|
||||
print(f"Attempting to access URL: {url}")
|
||||
|
||||
driver.get(url)
|
||||
|
||||
try:
|
||||
cookie_button = WebDriverWait(driver, 10).until(
|
||||
EC.element_to_be_clickable((
|
||||
By.XPATH,
|
||||
"//button[contains(translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'accept') or " +
|
||||
"contains(translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'agree') or " +
|
||||
"contains(translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'allow')]"))
|
||||
)
|
||||
cookie_button.click()
|
||||
|
||||
WebDriverWait(driver, 5).until_not(EC.presence_of_element_located((By.XPATH,
|
||||
"//button[contains(translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'accept') or " +
|
||||
"contains(translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'agree') or " +
|
||||
"contains(translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'allow')]"))
|
||||
)
|
||||
except:
|
||||
print("No cookie banner found or clickable :3")
|
||||
|
||||
WebDriverWait(driver, 10).until(
|
||||
lambda driver: driver.execute_script('return document.readyState') == 'complete'
|
||||
)
|
||||
|
||||
# Get page title and content
|
||||
title = driver.title or url
|
||||
|
||||
content = driver.execute_script("""
|
||||
return new XMLSerializer().serializeToString(document);
|
||||
""")
|
||||
|
||||
print(f"Page title: {title}")
|
||||
|
||||
# Take screenshot
|
||||
screenshot = driver.get_screenshot_as_png()
|
||||
print("Screenshot taken successfully")
|
||||
|
||||
# Create archive instance with user
|
||||
archive = form.save(commit=False)
|
||||
archive.user = request.user
|
||||
archive.title = title
|
||||
archive.content = content
|
||||
archive.save()
|
||||
print("Archive saved successfully")
|
||||
|
||||
# Now save the screenshot
|
||||
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
|
||||
tmp.write(screenshot)
|
||||
print(f"Saving screenshot to: {tmp.name}")
|
||||
archive.screenshot.save(f'archive_{archive.id}.png', File(open(tmp.name, 'rb')))
|
||||
archive.save()
|
||||
|
||||
messages.success(request, "Page archived successfully!")
|
||||
return redirect('web_archive')
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in save_web_archive: {str(e)}")
|
||||
messages.error(request, f"Error archiving page: {str(e)}")
|
||||
return redirect('web_archive')
|
||||
finally:
|
||||
if driver:
|
||||
try:
|
||||
driver.quit()
|
||||
print("Chrome driver closed successfully")
|
||||
except Exception as e:
|
||||
print(f"Error closing Chrome driver: {str(e)}")
|
||||
else:
|
||||
form = WebArchiveForm()
|
||||
return render(request, 'save_web_archive.html', {'form': form})
|
||||
|
||||
@email_verified_required
|
||||
@login_required
|
||||
def web_archive(request):
|
||||
archives = WebArchive.objects.filter(user=request.user)
|
||||
return render(request, 'web_archive.html', {'archives': archives})
|
||||
|
||||
@email_verified_required
|
||||
@login_required
|
||||
def delete_web_archive(request, archive_id):
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
archive = get_object_or_404(WebArchive, id=archive_id, user=request.user)
|
||||
|
||||
if archive.screenshot:
|
||||
try:
|
||||
archive.screenshot.delete(save=False)
|
||||
except:
|
||||
pass
|
||||
|
||||
archive.delete()
|
||||
|
||||
except Exception as e:
|
||||
messages.error(request, f"Error deleting archive: {str(e)}")
|
||||
|
||||
return redirect('web_archive')
|
||||
|
||||
@email_verified_required
|
||||
@login_required
|
||||
def view_web_archive(request, archive_id):
|
||||
archive = get_object_or_404(WebArchive, id=archive_id, user=request.user)
|
||||
return render(request, 'view_web_archive.html', {'archive': archive})
|
||||
|
||||
|
|
|
@ -192,7 +192,13 @@
|
|||
<i class="fa-solid fa-chalkboard-user"></i>
|
||||
whiteboard
|
||||
</div></a>
|
||||
|
||||
</div>
|
||||
<div class="button-container">
|
||||
<a href="/web_archive" style="text-decoration: none;"><div class="service-button archive">
|
||||
<i class="fa-solid fa-camera"></i>
|
||||
web archive
|
||||
</div></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
|
191
templates/save_web_archive.html
Normal file
191
templates/save_web_archive.html
Normal file
|
@ -0,0 +1,191 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Eversync</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||
{% load static %}
|
||||
<link rel="icon" href="{% static 'favicon.ico' %}">
|
||||
<link rel="stylesheet" href="{% static 'index-style.css' %}">
|
||||
{% block scripts %}
|
||||
{% include "sentry_replay.html" %}
|
||||
{% endblock %}
|
||||
|
||||
<style>
|
||||
.container {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
align-items: flex-start;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.welcome-box {
|
||||
flex: 1 1 0;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
max-width: 100%;
|
||||
background: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.form-container {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.archive-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #dc3545;
|
||||
font-size: 0.9em;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 1em;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* On mobile */
|
||||
@media (max-width: 700px) {
|
||||
.container {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-width: 100vw;
|
||||
padding: 10px;
|
||||
}
|
||||
.welcome-box {
|
||||
flex: none;
|
||||
width: auto;
|
||||
max-width: 90vw;
|
||||
margin: 0 0 20px 0;
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<div class="header-content">
|
||||
<a href="/"><img src="{% static 'eversync2.png' %}" alt="Eversync Logo" style="height: 80px; margin-right: 10px; display: flex; align-items: center; gap: 5px;"></a>
|
||||
<a href="/" class="logo" >eversync</a>
|
||||
<div class="nav-links" style="position: relative;">
|
||||
<div class="dropdown">
|
||||
<button class="dropdown-toggle" style="background: none; border: none; color: white; font-size: 16px; cursor: pointer;">
|
||||
Welcome, {{ user.username }} <i class="fas fa-caret-down"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu" style="display: none; position: absolute; right: 0; background-color: #333; border: 1px solid #444; border-radius: 4px; padding: 10px; width: 184px;">
|
||||
<form action="{% url 'manage' %}" method="post" style="margin: 0;">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="logout-button" style="background-color: transparent; color: white; border: none; cursor: pointer;">Manage Account</button>
|
||||
</form>
|
||||
<form action="{% url 'logoutz' %}" method="post" style="margin: 0;">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="logout-button" style="background-color: transparent; color: white; border: none; cursor: pointer;">Log Out</button>
|
||||
</form>
|
||||
<button id="themeToggle" class="logout-button" style="background-color: transparent; color: white; border: none; cursor: pointer;">Toggle Dark Mode</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="welcome-box">
|
||||
<div class="container">
|
||||
<h1>Archive Web Page</h1>
|
||||
<a href="{% url 'web_archive' %}" class="btn btn-secondary">
|
||||
<i class="fa-solid fa-arrow-left"></i> Back to Archives
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="form-container">
|
||||
<form method="POST" class="archive-form">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.url.id_for_label }}">URL to Archive</label>
|
||||
{{ form.url }}
|
||||
{% if form.url.errors %}
|
||||
<div class="error-message">{{ form.url.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fa-solid fa-camera"></i> Take Screenshot
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const toggle = document.querySelector('.dropdown-toggle');
|
||||
const menu = document.querySelector('.dropdown-menu');
|
||||
toggle.addEventListener('click', function () {
|
||||
menu.style.display = menu.style.display === 'block' ? 'none' : 'block';
|
||||
});
|
||||
});
|
||||
|
||||
const toggle = document.getElementById("themeToggle");
|
||||
const root = document.documentElement;
|
||||
|
||||
if (localStorage.getItem("theme") === "dark") {
|
||||
root.classList.add("dark");
|
||||
}
|
||||
|
||||
toggle.addEventListener("click", () => {
|
||||
root.classList.toggle("dark");
|
||||
if (root.classList.contains("dark")) {
|
||||
localStorage.setItem("theme", "dark");
|
||||
} else {
|
||||
localStorage.setItem("theme", "light");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
264
templates/view_web_archive.html
Normal file
264
templates/view_web_archive.html
Normal file
|
@ -0,0 +1,264 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Eversync</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
|
||||
{% load static %}
|
||||
<link rel="icon" href="{% static 'favicon.ico' %}">
|
||||
<link rel="stylesheet" href="{% static 'index-style.css' %}">
|
||||
<style>
|
||||
.archive-container {
|
||||
max-width: 1200px;
|
||||
margin: 20px auto;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.archive-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.archive-title {
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
}
|
||||
.archive-meta {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
.archive-content {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.archive-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.btn {
|
||||
padding: 8px 15px;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 0.9em;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
.archive-frame {
|
||||
width: 100%;
|
||||
height: 800px;
|
||||
border: none;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.view-toggle {
|
||||
margin: 10px 0;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.raw-html {
|
||||
display: none;
|
||||
background: #282c34;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
margin-top: 20px;
|
||||
position: relative;
|
||||
}
|
||||
.raw-html.active {
|
||||
display: block;
|
||||
}
|
||||
.archive-frame.active {
|
||||
display: block;
|
||||
}
|
||||
.archive-frame.hidden {
|
||||
display: none;
|
||||
}
|
||||
.html-controls {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
.html-controls button {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.html-controls button:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
.code-block {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.hljs {
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<div class="header-content">
|
||||
<a href="/"><img src="{% static 'eversync2.png' %}" alt="Eversync Logo" style="height: 80px; margin-right: 10px; display: flex; align-items: center; gap: 5px;"></a>
|
||||
<a href="/" class="logo" >eversync</a>
|
||||
<div class="nav-links" style="position: relative;">
|
||||
<div class="dropdown">
|
||||
<button class="dropdown-toggle" style="background: none; border: none; color: white; font-size: 16px; cursor: pointer;">
|
||||
Welcome, {{ user.username }} <i class="fas fa-caret-down"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu" style="display: none; position: absolute; right: 0; background-color: #333; border: 1px solid #444; border-radius: 4px; padding: 10px; width: 184px;">
|
||||
|
||||
<form action="{% url 'manage' %}" method="post" style="margin: 0;">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="logout-button" style="background-color: transparent; color: white; border: none; cursor: pointer;">Manage Account</button>
|
||||
</form>
|
||||
|
||||
<form action="{% url 'logoutz' %}" method="post" style="margin: 0;">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="logout-button" style="background-color: transparent; color: white; border: none; cursor: pointer;">Log Out</button>
|
||||
</form>
|
||||
<button id="themeToggle" class="logout-button" style="background-color: transparent; color: white; border: none; cursor: pointer;">Toggle Dark Mode</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="archive-container">
|
||||
<div class="archive-header">
|
||||
<div>
|
||||
<h1 class="archive-title">{{ archive.title }}</h1>
|
||||
<p class="archive-meta">
|
||||
Archived on {{ archive.created_at|date:"F j, Y" }} from
|
||||
<a href="{{ archive.url }}" target="_blank">{{ archive.url }}</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="archive-actions">
|
||||
<a href="{% url 'web_archive' %}" class="btn btn-secondary">
|
||||
<i class="fa-solid fa-arrow-left"></i> Back to Archives
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if archive.screenshot %}
|
||||
<img src="{{ archive.screenshot.url }}" alt="Archive screenshot" style="max-width: 100%; height: auto; margin-bottom: 20px;">
|
||||
{% endif %}
|
||||
|
||||
<div class="view-toggle">
|
||||
<button class="btn btn-primary" onclick="toggleView()">
|
||||
<i class="fa-solid fa-code"></i> Toggle Raw HTML
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="copyHtml()">
|
||||
<i class="fa-solid fa-copy"></i> Copy HTML
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="archive-content">
|
||||
<iframe srcdoc="{{ archive.content }}" class="archive-frame"></iframe>
|
||||
<div class="raw-html">
|
||||
<div class="html-controls">
|
||||
<button onclick="copyHtml()">
|
||||
<i class="fa-solid fa-copy"></i> Copy
|
||||
</button>
|
||||
<button onclick="downloadHtml()">
|
||||
<i class="fa-solid fa-download"></i> Download
|
||||
</button>
|
||||
</div>
|
||||
<pre class="code-block"><code class="language-html">{{ archive.content }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script>
|
||||
function toggleView() {
|
||||
const iframe = document.querySelector('.archive-frame');
|
||||
const rawHtml = document.querySelector('.raw-html');
|
||||
|
||||
iframe.classList.toggle('active');
|
||||
iframe.classList.toggle('hidden');
|
||||
rawHtml.classList.toggle('active');
|
||||
|
||||
if (rawHtml.classList.contains('active')) {
|
||||
hljs.highlightAll();
|
||||
}
|
||||
}
|
||||
|
||||
function copyHtml() {
|
||||
const htmlContent = `{{ archive.content|escapejs }}`;
|
||||
navigator.clipboard.writeText(htmlContent).then(() => {
|
||||
alert('HTML copied to clipboard!');
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy HTML:', err);
|
||||
});
|
||||
}
|
||||
|
||||
function downloadHtml() {
|
||||
const htmlContent = `{{ archive.content|escapejs }}`;
|
||||
const blob = new Blob([htmlContent], { type: 'text/html' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'archive.html';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
|
||||
// Initialize syntax highlighting when the page loads
|
||||
document.addEventListener('DOMContentLoaded', (event) => {
|
||||
hljs.highlightAll();
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const toggle = document.querySelector('.dropdown-toggle');
|
||||
const menu = document.querySelector('.dropdown-menu');
|
||||
toggle.addEventListener('click', function () {
|
||||
menu.style.display = menu.style.display === 'block' ? 'none' : 'block';
|
||||
});
|
||||
});
|
||||
|
||||
const toggle = document.getElementById("themeToggle");
|
||||
const root = document.documentElement;
|
||||
|
||||
if (localStorage.getItem("theme") === "dark") {
|
||||
root.classList.add("dark");
|
||||
}
|
||||
|
||||
toggle.addEventListener("click", () => {
|
||||
root.classList.toggle("dark");
|
||||
if (root.classList.contains("dark")) {
|
||||
localStorage.setItem("theme", "dark");
|
||||
} else {
|
||||
localStorage.setItem("theme", "light");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
227
templates/web_archive.html
Normal file
227
templates/web_archive.html
Normal file
|
@ -0,0 +1,227 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Eversync</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||
{% load static %}
|
||||
<link rel="icon" href="{% static 'favicon.ico' %}">
|
||||
<link rel="stylesheet" href="{% static 'index-style.css' %}">
|
||||
{% block scripts %}
|
||||
{% include "sentry_replay.html" %}
|
||||
{% endblock %}
|
||||
|
||||
<style>
|
||||
.container {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
align-items: flex-start;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.welcome-box {
|
||||
flex: 1 1 0;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
max-width: 900px;
|
||||
background: none;
|
||||
}
|
||||
|
||||
|
||||
.archives-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.archive-card {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.archive-screenshot {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.archive-content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.archive-url {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
margin: 5px 0;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.archive-date {
|
||||
color: #888;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.archive-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 15px;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 0.9em;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.no-archives {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* On mobile */
|
||||
@media (max-width: 700px) {
|
||||
.container {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.welcome-box {
|
||||
flex: none;
|
||||
width: auto;
|
||||
max-width: 90vw;
|
||||
margin: 0 0 20px 0;
|
||||
padding: 15px;
|
||||
}
|
||||
.archives-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<div class="header-content">
|
||||
<a href="/"><img src="{% static 'eversync2.png' %}" alt="Eversync Logo" style="height: 80px; margin-right: 10px; display: flex; align-items: center; gap: 5px;"></a>
|
||||
<a href="/" class="logo" >eversync</a>
|
||||
<div class="nav-links" style="position: relative;">
|
||||
<div class="dropdown">
|
||||
<button class="dropdown-toggle" style="background: none; border: none; color: white; font-size: 16px; cursor: pointer;">
|
||||
Welcome, {{ user.username }} <i class="fas fa-caret-down"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu" style="display: none; position: absolute; right: 0; background-color: #333; border: 1px solid #444; border-radius: 4px; padding: 10px; width: 184px;">
|
||||
|
||||
<form action="{% url 'manage' %}" method="post" style="margin: 0;">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="logout-button" style="background-color: transparent; color: white; border: none; cursor: pointer;">Manage Account</button>
|
||||
</form>
|
||||
|
||||
<form action="{% url 'logoutz' %}" method="post" style="margin: 0;">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="logout-button" style="background-color: transparent; color: white; border: none; cursor: pointer;">Log Out</button>
|
||||
</form>
|
||||
<button id="themeToggle" class="logout-button" style="background-color: transparent; color: white; border: none; cursor: pointer;">Toggle Dark Mode</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="welcome-box">
|
||||
<div class="container">
|
||||
<h1>Web Archives</h1>
|
||||
<a href="{% url 'save_web_archive' %}" class="btn btn-primary">
|
||||
<i class="fa-solid fa-plus"></i> New Archive
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="archives-grid">
|
||||
{% for archive in archives %}
|
||||
<div class="archive-card">
|
||||
{% if archive.screenshot %}
|
||||
<img src="{{ archive.screenshot.url }}" alt="Archived page" class="archive-screenshot">
|
||||
{% endif %}
|
||||
<div class="archive-content">
|
||||
<p class="archive-url">{{ archive.url }}</p>
|
||||
<p class="archive-date">{{ archive.created_at|date:"F j, Y" }}</p>
|
||||
<div class="archive-actions">
|
||||
<a href="{% url 'view_web_archive' archive.id %}" class="btn btn-primary">
|
||||
<i class="fa-solid fa-eye"></i> View
|
||||
</a>
|
||||
<a href="{{ archive.url }}" target="_blank" class="btn btn-secondary">
|
||||
<i class="fa-solid fa-external-link"></i> Visit
|
||||
</a>
|
||||
<form action="{% url 'delete_web_archive' archive.id %}" method="post" style="display: inline;">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete this archive?')">
|
||||
<i class="fa-solid fa-trash"></i> Delete
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="no-archives">
|
||||
<p>No archived pages yet. Start by archiving your first page!</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const toggle = document.querySelector('.dropdown-toggle');
|
||||
const menu = document.querySelector('.dropdown-menu');
|
||||
toggle.addEventListener('click', function () {
|
||||
menu.style.display = menu.style.display === 'block' ? 'none' : 'block';
|
||||
});
|
||||
});
|
||||
|
||||
const toggle = document.getElementById("themeToggle");
|
||||
const root = document.documentElement;
|
||||
|
||||
if (localStorage.getItem("theme") === "dark") {
|
||||
root.classList.add("dark");
|
||||
}
|
||||
|
||||
toggle.addEventListener("click", () => {
|
||||
root.classList.toggle("dark");
|
||||
if (root.classList.contains("dark")) {
|
||||
localStorage.setItem("theme", "dark");
|
||||
} else {
|
||||
localStorage.setItem("theme", "light");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
Add table
Add a link
Reference in a new issue