Major UI improvements and cleanup

 Features:
- Add movable theme button with drag functionality
- Theme button rotation animation on toggle
- Position persistence across page reloads
- Remove flash positioning on page load

🗑️ Cleanup:
- Remove URL shortener functionality completely
- Unify expiry dropdown (remove duplicate from pastebin)
- Fix import issues in cleanup scripts and app.py
- Remove unused CSS and JavaScript code

🔧 Technical:
- Improve drag vs click detection
- Add viewport boundary constraints for theme button
- Clean up JavaScript event handlers
- Optimize CSS animations and transitions
This commit is contained in:
2025-09-27 18:13:16 +01:00
parent b73af5bf11
commit 9ebd1331fb
6 changed files with 260 additions and 801 deletions

View File

@@ -9,12 +9,12 @@ import json
from datetime import datetime, timezone
from pathlib import Path
# Add src directory to Python path
src_path = Path(__file__).parent / "src"
sys.path.insert(0, str(src_path))
# Add the project root to Python path
project_root = Path(__file__).parent
sys.path.append(str(project_root))
from config import config
from storage import StorageManager
from src.config import config
from src.storage import StorageManager
def cleanup_local_storage(storage_manager):

View File

@@ -7,12 +7,17 @@ import hashlib
import re
from datetime import datetime, timedelta
from functools import wraps
from urllib.parse import urlparse
from flask import Flask, render_template, request, jsonify, redirect, url_for, send_from_directory, session, Response
import sys
import os
# Add project root to Python path
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(project_root)
# Import modules from current directory and project root
from config import config
from storage import StorageManager
import sys
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from expiry_db import ExpiryDatabase
# Flask app configuration
@@ -110,52 +115,7 @@ except Exception as e:
def generate_short_id(length=6):
return ''.join(random.choices(string.ascii_letters + string.digits, k=length))
# URL Shortener utility functions
def generate_short_code(length=6):
"""Generate a random short code for URL shortener"""
characters = string.ascii_letters + string.digits
while True:
code = ''.join(random.choices(characters, k=length))
# Make sure it doesn't conflict with existing content IDs
content_type, _ = detect_content_type(code)
if content_type is None and not expiry_db.get_redirect(code):
return code
def is_valid_url(url):
"""Validate if a URL is properly formatted"""
try:
result = urlparse(url)
return all([result.scheme, result.netloc]) and result.scheme in ['http', 'https']
except:
return False
def is_safe_url(url):
"""Basic safety check for URLs (can be extended)"""
# Block localhost, private IPs, and suspicious TLDs
parsed = urlparse(url)
hostname = parsed.hostname
if not hostname:
return False
# Block localhost and private IPs
if hostname.lower() in ['localhost', '127.0.0.1', '0.0.0.0']:
return False
# Block private IP ranges (basic check)
if hostname.startswith('192.168.') or hostname.startswith('10.') or hostname.startswith('172.'):
return False
# Block suspicious patterns
suspicious_patterns = [
r'bit\.ly', r'tinyurl', r'short\.link', r'malware', r'phishing'
]
for pattern in suspicious_patterns:
if re.search(pattern, url, re.IGNORECASE):
return False
return True
# Helper function to determine if an ID is a file or paste
def detect_content_type(content_id):
@@ -196,18 +156,11 @@ def index():
@check_maintenance
def get_content(content_id):
"""
Clean URL handler - checks for URL redirect first, then files/pastes
Clean URL handler for files and pastes
Examples: sharey.org/ABC123, sharey.org/XYZ789.png
"""
print(f"🔍 Processing request for ID: {content_id}")
# First check if it's a URL redirect
redirect_url = expiry_db.get_redirect(content_id)
if redirect_url:
print(f"🔗 Redirecting {content_id} to {redirect_url}")
return redirect(redirect_url, code=302)
# Not a redirect, try as file/paste
# Skip certain paths that should not be treated as content IDs
excluded_paths = ['admin', 'health', 'api', 'static', 'files', 'pastes', 'favicon.ico']
if content_id in excluded_paths:
@@ -447,121 +400,7 @@ def view_paste_raw(paste_id):
except Exception as e:
return jsonify({'error': 'Paste not found'}), 404
# URL Shortener API Routes
@app.route('/api/shorten', methods=['POST'])
@check_maintenance
def create_redirect():
"""Create a new URL redirect"""
try:
data = request.get_json()
if not data or 'url' not in data:
return jsonify({'error': 'URL is required'}), 400
target_url = data['url'].strip()
custom_code = data.get('code', '').strip()
expires_in_hours = data.get('expires_in_hours')
# Validate URL
if not is_valid_url(target_url):
return jsonify({'error': 'Invalid URL format'}), 400
if not is_safe_url(target_url):
return jsonify({'error': 'URL not allowed'}), 400
# Generate or validate short code
if custom_code:
# Custom code provided
if len(custom_code) < 3 or len(custom_code) > 20:
return jsonify({'error': 'Custom code must be 3-20 characters'}), 400
if not re.match(r'^[a-zA-Z0-9_-]+$', custom_code):
return jsonify({'error': 'Custom code can only contain letters, numbers, hyphens, and underscores'}), 400
# Check if code already exists
if expiry_db.get_redirect(custom_code) or detect_content_type(custom_code)[0]:
return jsonify({'error': 'Code already exists'}), 400
short_code = custom_code
else:
# Generate random code
short_code = generate_short_code()
# Calculate expiry
expires_at = None
if expires_in_hours and expires_in_hours > 0:
expires_at = (datetime.utcnow() + timedelta(hours=expires_in_hours)).isoformat() + 'Z'
# Get client IP for logging
client_ip = request.headers.get('X-Forwarded-For', request.remote_addr)
# Create redirect
success = expiry_db.add_redirect(
short_code=short_code,
target_url=target_url,
expires_at=expires_at,
created_by_ip=client_ip
)
if not success:
return jsonify({'error': 'Failed to create redirect'}), 500
# Return short URL
short_url = url_for('get_content', content_id=short_code, _external=True)
return jsonify({
'short_url': short_url,
'short_code': short_code,
'target_url': target_url,
'expires_at': expires_at
}), 201
except Exception as e:
return jsonify({'error': f'Failed to create redirect: {str(e)}'}), 500
@app.route('/api/redirect/<short_code>/info', methods=['GET'])
@check_maintenance
def redirect_info(short_code):
"""Get information about a redirect (for preview)"""
try:
import sqlite3
# Get redirect info without incrementing click count
conn = sqlite3.connect(expiry_db.db_path)
cursor = conn.cursor()
cursor.execute('''
SELECT target_url, created_at, expires_at, click_count, is_active
FROM url_redirects
WHERE short_code = ?
''', (short_code,))
result = cursor.fetchone()
conn.close()
if not result:
return jsonify({'error': 'Redirect not found'}), 404
target_url, created_at, expires_at, click_count, is_active = result
# Check if expired
is_expired = False
if expires_at:
current_time = datetime.utcnow().isoformat() + 'Z'
is_expired = expires_at <= current_time
return jsonify({
'short_code': short_code,
'target_url': target_url,
'created_at': created_at,
'expires_at': expires_at,
'click_count': click_count,
'is_active': bool(is_active),
'is_expired': is_expired
})
except Exception as e:
return jsonify({'error': f'Failed to get redirect info: {str(e)}'}), 500
# Admin Panel Routes
@app.route('/admin')

View File

@@ -13,26 +13,16 @@ document.addEventListener("DOMContentLoaded", function() {
const fileModeButton = document.getElementById('fileMode');
const pasteModeButton = document.getElementById('pasteMode');
const urlModeButton = document.getElementById('urlMode');
const faqButton = document.getElementById('faqButton');
const fileSharingSection = document.getElementById('fileSharingSection');
const pastebinSection = document.getElementById('pastebinSection');
const urlShortenerSection = document.getElementById('urlShortenerSection');
const faqSection = document.getElementById('faqSection');
const pasteContent = document.getElementById('pasteContent');
const submitPasteButton = document.getElementById('submitPaste');
const pasteResult = document.getElementById('pasteResult');
// URL Shortener elements
const urlInput = document.getElementById('urlInput');
const customCode = document.getElementById('customCode');
const urlExpiry = document.getElementById('urlExpiry');
const shortenUrlButton = document.getElementById('shortenUrl');
const urlResult = document.getElementById('urlResult');
const urlPreviewContainer = document.getElementById('urlPreviewContainer');
// Mobile clipboard elements
const mobileClipboardSection = document.getElementById('mobileClipboardSection');
const mobilePasteButton = document.getElementById('mobilePasteButton');
@@ -40,11 +30,9 @@ document.addEventListener("DOMContentLoaded", function() {
let filesToUpload = [];
let currentMode = 'file'; // Track current mode: 'file', 'paste', or 'faq'
// Expiry elements
// Expiry elements (shared for both files and pastes)
const expirySelect = document.getElementById('expirySelect');
const customExpiry = document.getElementById('customExpiry');
const pasteExpirySelect = document.getElementById('pasteExpirySelect');
const customPasteExpiry = document.getElementById('customPasteExpiry');
// Helper function to check if file is an image
function isImageFile(file) {
@@ -65,29 +53,15 @@ document.addEventListener("DOMContentLoaded", function() {
currentMode = 'paste';
showSection(pastebinSection);
hideSection(fileSharingSection);
hideSection(urlShortenerSection);
hideSection(faqSection);
activateButton(pasteModeButton);
});
urlModeButton.addEventListener('click', () => {
currentMode = 'url';
showSection(urlShortenerSection);
hideSection(fileSharingSection);
hideSection(pastebinSection);
hideSection(faqSection);
activateButton(urlModeButton);
// Initialize URL shortener after section is visible
initializeUrlShortener();
});
faqButton.addEventListener('click', () => {
currentMode = 'faq';
showSection(faqSection);
hideSection(fileSharingSection);
hideSection(pastebinSection);
hideSection(urlShortenerSection);
activateButton(faqButton);
});
@@ -103,7 +77,7 @@ document.addEventListener("DOMContentLoaded", function() {
// Helper function to activate a button
function activateButton(button) {
const buttons = [fileModeButton, pasteModeButton, urlModeButton, faqButton];
const buttons = [fileModeButton, pasteModeButton, faqButton];
buttons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
}
@@ -125,16 +99,7 @@ document.addEventListener("DOMContentLoaded", function() {
});
}
if (pasteExpirySelect) {
pasteExpirySelect.addEventListener('change', (e) => {
if (e.target.value === 'custom') {
customPasteExpiry.style.display = 'block';
customPasteExpiry.focus();
} else {
customPasteExpiry.style.display = 'none';
}
});
}
// Helper function to calculate expiry datetime
function calculateExpiryTime(expiryValue, customValue = null) {
@@ -702,9 +667,9 @@ document.addEventListener("DOMContentLoaded", function() {
try {
pasteResult.innerHTML = '<p>📝 Uploading paste...</p>';
// Calculate paste expiry
const expiryValue = pasteExpirySelect?.value || 'never';
const customExpiryValue = customPasteExpiry?.value;
// Calculate paste expiry (using shared expiry selector)
const expiryValue = expirySelect?.value || 'never';
const customExpiryValue = customExpiry?.value;
const expiryTime = calculateExpiryTime(expiryValue, customExpiryValue);
// Prepare request body
@@ -769,9 +734,27 @@ document.addEventListener("DOMContentLoaded", function() {
console.log('🔍 Theme toggle button found:', themeToggle ? 'YES' : 'NO');
if (themeToggle) {
console.log('🎯 Adding click listener to theme button');
// Apply saved position immediately if available
if (window.shareyThemeButtonPosition) {
const pos = window.shareyThemeButtonPosition;
const maxX = window.innerWidth - themeToggle.offsetWidth;
const maxY = window.innerHeight - themeToggle.offsetHeight;
const x = Math.max(0, Math.min(pos.x, maxX));
const y = Math.max(0, Math.min(pos.y, maxY));
themeToggle.style.left = x + 'px';
themeToggle.style.top = y + 'px';
themeToggle.style.right = 'auto';
themeToggle.style.bottom = 'auto';
}
themeToggle.addEventListener('click', toggleTheme);
// Show the current theme setting on button
updateThemeButton(savedTheme);
// Make theme button draggable
makeDraggable(themeToggle);
} else {
console.log('⚠️ Theme toggle button not found on this page');
}
@@ -791,6 +774,18 @@ document.addEventListener("DOMContentLoaded", function() {
}
console.log('🎨 Switching to theme:', newTheme);
// Add animation to theme button
const themeToggle = document.getElementById('themeToggle');
if (themeToggle) {
themeToggle.classList.add('switching');
// Remove animation class after animation completes
setTimeout(() => {
themeToggle.classList.remove('switching');
}, 600); // Match animation duration
}
applyTheme(newTheme);
setCookie('sharey-theme', newTheme, 365); // Save for 1 year
updateThemeButton(newTheme);
@@ -823,6 +818,138 @@ document.addEventListener("DOMContentLoaded", function() {
}
}
// Draggable functionality for theme button
function makeDraggable(element) {
let isDragging = false;
let hasMoved = false;
let dragOffset = { x: 0, y: 0 };
let startPos = { x: 0, y: 0 };
const moveThreshold = 5; // pixels - minimum movement to be considered a drag
// Mouse events
element.addEventListener('mousedown', startDrag);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', stopDrag);
// Touch events for mobile
element.addEventListener('touchstart', startDrag, { passive: false });
document.addEventListener('touchmove', drag, { passive: false });
document.addEventListener('touchend', stopDrag);
function startDrag(e) {
isDragging = true;
hasMoved = false;
const clientX = e.clientX || (e.touches && e.touches[0].clientX);
const clientY = e.clientY || (e.touches && e.touches[0].clientY);
const rect = element.getBoundingClientRect();
startPos.x = clientX;
startPos.y = clientY;
dragOffset.x = clientX - rect.left;
dragOffset.y = clientY - rect.top;
// Only prevent default for touch events to avoid issues
if (e.type.startsWith('touch')) {
e.preventDefault();
}
}
function drag(e) {
if (!isDragging) return;
const clientX = e.clientX || (e.touches && e.touches[0].clientX);
const clientY = e.clientY || (e.touches && e.touches[0].clientY);
// Check if we've moved enough to be considered a drag
const deltaX = Math.abs(clientX - startPos.x);
const deltaY = Math.abs(clientY - startPos.y);
if (!hasMoved && (deltaX > moveThreshold || deltaY > moveThreshold)) {
hasMoved = true;
element.classList.add('dragging');
}
// Only move the element if we're actually dragging
if (hasMoved) {
let newX = clientX - dragOffset.x;
let newY = clientY - dragOffset.y;
// Keep button within viewport bounds
const maxX = window.innerWidth - element.offsetWidth;
const maxY = window.innerHeight - element.offsetHeight;
newX = Math.max(0, Math.min(newX, maxX));
newY = Math.max(0, Math.min(newY, maxY));
element.style.left = newX + 'px';
element.style.top = newY + 'px';
element.style.right = 'auto';
element.style.bottom = 'auto';
e.preventDefault();
}
}
function stopDrag(e) {
if (!isDragging) return;
isDragging = false;
element.classList.remove('dragging');
if (hasMoved) {
// Save position to localStorage only if we actually moved
const rect = element.getBoundingClientRect();
saveThemeButtonPosition(rect.left, rect.top);
// Prevent the click event since this was a drag
e.preventDefault();
e.stopPropagation();
}
// If we didn't move, let the click event fire normally
}
// Only prevent click if we actually dragged
element.addEventListener('click', function(e) {
if (hasMoved) {
e.preventDefault();
e.stopPropagation();
return false;
}
// If we didn't drag, allow the click to proceed normally
}, true);
}
// Save theme button position to localStorage
function saveThemeButtonPosition(x, y) {
const position = { x: x, y: y };
localStorage.setItem('sharey-theme-button-position', JSON.stringify(position));
}
// Restore theme button position from localStorage
function restoreThemeButtonPosition(element) {
try {
const saved = localStorage.getItem('sharey-theme-button-position');
if (saved) {
const position = JSON.parse(saved);
// Validate position is within current viewport
const maxX = window.innerWidth - element.offsetWidth;
const maxY = window.innerHeight - element.offsetHeight;
const x = Math.max(0, Math.min(position.x, maxX));
const y = Math.max(0, Math.min(position.y, maxY));
element.style.left = x + 'px';
element.style.top = y + 'px';
element.style.right = 'auto';
element.style.bottom = 'auto';
}
} catch (error) {
console.log('Could not restore theme button position:', error);
}
}
// Cookie management functions
function setCookie(name, value, days) {
const expires = new Date();
@@ -1088,218 +1215,4 @@ function showToast(message, type = "success", duration = 3000) {
}, duration);
}
// URL Shortener initialization function
function initializeUrlShortener() {
console.log('🔗 Initializing URL shortener...');
// Re-query elements now that section is visible
const urlInput = document.getElementById('urlInput');
const customCode = document.getElementById('customCode');
const urlExpiry = document.getElementById('urlExpiry');
const shortenUrlButton = document.getElementById('shortenUrl');
const urlResult = document.getElementById('urlResult');
const urlPreviewContainer = document.getElementById('urlPreviewContainer');
console.log('URL shortener elements:', {
urlInput: !!urlInput,
customCode: !!customCode,
urlExpiry: !!urlExpiry,
shortenUrlButton: !!shortenUrlButton,
urlResult: !!urlResult,
urlPreviewContainer: !!urlPreviewContainer
});
if (shortenUrlButton && urlInput && urlResult) {
console.log('✅ URL shortener elements found, adding event listeners');
// Remove any existing listeners to avoid duplicates
shortenUrlButton.replaceWith(shortenUrlButton.cloneNode(true));
const newShortenButton = document.getElementById('shortenUrl');
newShortenButton.addEventListener('click', () => shortenUrl(urlInput, customCode, urlExpiry, urlResult, urlPreviewContainer, newShortenButton));
// Enter key support for URL input
urlInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
shortenUrl(urlInput, customCode, urlExpiry, urlResult, urlPreviewContainer, newShortenButton);
}
});
// Copy button functionality
document.addEventListener('click', (e) => {
if (e.target && e.target.id === 'copyUrlButton') {
const shortUrlInput = document.getElementById('shortUrlInput');
if (shortUrlInput) {
shortUrlInput.select();
shortUrlInput.setSelectionRange(0, 99999); // For mobile
navigator.clipboard.writeText(shortUrlInput.value).then(() => {
showToast('Short URL copied to clipboard! 📋', 'success');
}).catch(() => {
// Fallback for older browsers
document.execCommand('copy');
showToast('Short URL copied to clipboard! <20>', 'success');
});
}
}
});
} else {
console.log('❌ URL shortener elements not found');
}
}
// URL Shortener functionality
console.log('<27>🔗 Setting up URL shortener...');
console.log('shortenUrlButton:', shortenUrlButton);
console.log('urlInput:', urlInput);
console.log('urlResult:', urlResult);
if (shortenUrlButton && urlInput && urlResult) {
console.log('✅ URL shortener elements found, adding event listeners');
shortenUrlButton.addEventListener('click', shortenUrl);
// Enter key support for URL input
urlInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
shortenUrl();
}
});
// Copy button functionality
document.addEventListener('click', (e) => {
if (e.target && e.target.id === 'copyUrlButton') {
const shortUrlInput = document.getElementById('shortUrlInput');
if (shortUrlInput) {
shortUrlInput.select();
shortUrlInput.setSelectionRange(0, 99999); // For mobile
navigator.clipboard.writeText(shortUrlInput.value).then(() => {
showToast('Short URL copied to clipboard! 📋', 'success');
}).catch(() => {
// Fallback for older browsers
document.execCommand('copy');
showToast('Short URL copied to clipboard! 📋', 'success');
});
}
}
});
} else {
console.log('❌ URL shortener elements not found');
console.log('Missing elements:', {
shortenUrlButton: !shortenUrlButton,
urlInput: !urlInput,
urlResult: !urlResult
});
}
async function shortenUrl(urlInputEl, customCodeEl, urlExpiryEl, urlResultEl, urlPreviewContainerEl, shortenButtonEl) {
// Use passed elements or fallback to global ones
const urlInput = urlInputEl || document.getElementById('urlInput');
const customCode = customCodeEl || document.getElementById('customCode');
const urlExpiry = urlExpiryEl || document.getElementById('urlExpiry');
const urlResult = urlResultEl || document.getElementById('urlResult');
const urlPreviewContainer = urlPreviewContainerEl || document.getElementById('urlPreviewContainer');
const shortenUrlButton = shortenButtonEl || document.getElementById('shortenUrl');
console.log('🔗 shortenUrl function called');
const url = urlInput.value.trim();
const code = customCode.value.trim();
const expiryHours = urlExpiry.value === 'never' ? null : parseInt(urlExpiry.value);
console.log('URL:', url, 'Code:', code, 'Expiry:', expiryHours);
if (!url) {
showError('Please enter a URL to shorten', urlResult);
return;
}
// Basic URL validation
try {
new URL(url);
} catch (e) {
showError('Please enter a valid URL (include http:// or https://)', urlResult);
return;
}
// Show loading state
shortenUrlButton.disabled = true;
shortenUrlButton.textContent = '⏳ Shortening...';
showInfo('Creating short URL...', urlResult);
try {
const payload = {
url: url
};
if (code) {
payload.code = code;
}
if (expiryHours) {
payload.expires_in_hours = expiryHours;
}
const response = await fetch('/api/shorten', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
const data = await response.json();
if (response.ok) {
showUrlSuccess(data, urlResult, urlPreviewContainer, urlInput, customCode, urlExpiry);
} else {
showError(data.error || 'Failed to create short URL', urlResult);
}
} catch (error) {
console.error('URL shortening error:', error);
showError('Failed to create short URL. Please try again.', urlResult);
} finally {
// Reset button state
shortenUrlButton.disabled = false;
shortenUrlButton.textContent = '✨ Shorten URL';
}
}
function showUrlSuccess(data, urlResult, urlPreviewContainer, urlInput, customCode, urlExpiry) {
urlResult.className = 'result-message success';
urlResult.textContent = '✅ Short URL created successfully!';
// Show preview container
urlPreviewContainer.style.display = 'block';
// Update preview elements
const shortUrlInput = document.getElementById('shortUrlInput');
const targetUrl = document.getElementById('targetUrl');
const clickCount = document.getElementById('clickCount');
const expiryInfo = document.getElementById('expiryInfo');
const expiryDate = document.getElementById('expiryDate');
if (shortUrlInput) {
shortUrlInput.value = data.short_url;
}
if (targetUrl) {
targetUrl.textContent = data.target_url;
}
if (clickCount) {
clickCount.textContent = '0';
}
if (data.expires_at && expiryInfo && expiryDate) {
expiryDate.textContent = new Date(data.expires_at).toLocaleString();
expiryInfo.style.display = 'block';
} else if (expiryInfo) {
expiryInfo.style.display = 'none';
}
// Clear form
urlInput.value = '';
customCode.value = '';
urlExpiry.value = '24';
}

View File

@@ -88,16 +88,16 @@ h1 {
/* Theme toggle button */
.theme-toggle {
position: absolute;
position: fixed;
top: clamp(15px, 3vw, 20px);
right: clamp(15px, 3vw, 20px);
background: var(--bg-secondary);
border: 2px solid var(--border-color);
border-radius: 50px;
padding: clamp(10px, 2vw, 16px);
cursor: pointer;
cursor: grab;
font-size: clamp(16px, 3vw, 18px);
transition: all 0.3s ease;
transition: box-shadow 0.3s ease, transform 0.3s ease;
color: var(--text-primary);
box-shadow: 0 2px 6px var(--shadow);
min-width: 44px; /* Minimum touch target */
@@ -105,6 +105,61 @@ h1 {
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
touch-action: none;
}
.theme-toggle:active {
cursor: grabbing;
transform: scale(1.05);
}
.theme-toggle.dragging {
transition: none;
cursor: grabbing;
transform: scale(1.05);
box-shadow: 0 8px 25px var(--shadow);
}
.theme-toggle.switching {
animation: themeSwitch 0.6s ease-in-out;
}
@keyframes themeSwitch {
0% {
transform: rotate(0deg) scale(1);
}
25% {
transform: rotate(90deg) scale(0.8);
}
50% {
transform: rotate(180deg) scale(0.6);
opacity: 0.7;
}
75% {
transform: rotate(270deg) scale(0.8);
}
100% {
transform: rotate(360deg) scale(1);
}
}
/* Alternative spinning animation */
@keyframes themeSpin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.theme-toggle.spinning {
animation: themeSpin 0.5s ease-in-out;
}
.theme-toggle:hover {
@@ -1389,191 +1444,4 @@ pre {
}
}
/* URL Shortener Styles */
.url-input-container {
display: flex;
flex-direction: column;
gap: 20px;
margin-bottom: 25px;
}
.url-label {
font-size: 18px;
font-weight: bold;
color: var(--text-primary);
margin-bottom: 8px;
}
.url-input {
width: 100%;
padding: 15px;
font-size: 16px;
border: 2px solid var(--border-color);
border-radius: 8px;
background-color: var(--bg-secondary);
color: var(--text-primary);
transition: all 0.3s ease;
box-sizing: border-box;
}
.url-input:focus {
outline: none;
border-color: #388e3c;
box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.2);
}
.url-options {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-bottom: 20px;
}
.option-group {
flex: 1;
min-width: 200px;
}
.option-label {
display: block;
font-weight: bold;
color: var(--text-primary);
margin-bottom: 5px;
font-size: 14px;
}
.custom-code-input {
width: 100%;
padding: 10px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 6px;
background-color: var(--bg-secondary);
color: var(--text-primary);
transition: all 0.3s ease;
box-sizing: border-box;
}
.custom-code-input:focus {
outline: none;
border-color: var(--border-color);
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
}
.url-expiry-select {
width: 100%;
padding: 10px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 6px;
background-color: var(--bg-secondary);
color: var(--text-primary);
cursor: pointer;
}
.option-hint {
font-size: 12px;
color: #666;
margin-top: 3px;
font-style: italic;
}
/* URL Preview Container */
.url-preview-container {
background-color: var(--bg-accent);
border: 2px solid var(--border-color);
border-radius: 12px;
padding: 20px;
margin-top: 20px;
animation: slideDown 0.3s ease-out;
}
.url-preview h3 {
color: var(--text-primary);
margin-bottom: 15px;
font-size: 18px;
}
.short-url-display {
display: flex;
gap: 10px;
margin-bottom: 15px;
align-items: center;
}
.short-url-input {
flex: 1;
padding: 12px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 6px;
background-color: var(--bg-secondary);
color: var(--text-primary);
font-family: 'Courier New', monospace;
font-weight: bold;
}
.copy-button {
padding: 12px 16px;
font-size: 16px;
background-color: var(--border-color);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
}
.copy-button:hover {
background-color: #388e3c;
transform: translateY(-2px);
}
.url-details {
font-size: 14px;
color: var(--text-primary);
line-height: 1.6;
}
.url-details p {
margin: 8px 0;
}
.url-details strong {
color: var(--text-secondary);
}
/* Responsive adjustments for URL shortener */
@media only screen and (max-width: 768px) {
.url-options {
flex-direction: column;
gap: 15px;
}
.option-group {
min-width: unset;
}
.short-url-display {
flex-direction: column;
gap: 10px;
}
.copy-button {
align-self: stretch;
text-align: center;
}
}
/* Animation for URL preview */
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

View File

@@ -11,9 +11,9 @@
<!-- QR Code generation library -->
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js"></script>
<!-- Immediate theme application to prevent flash -->
<!-- Immediate theme and position application to prevent flash -->
<script>
// Apply theme immediately before page renders
// Apply theme and button position immediately before page renders
(function() {
function getCookie(name) {
const nameEQ = name + "=";
@@ -31,6 +31,22 @@
if (savedTheme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
}
// Restore theme button position immediately
try {
const saved = localStorage.getItem('sharey-theme-button-position');
if (saved) {
const position = JSON.parse(saved);
// Store position data to be applied when DOM loads
window.shareyThemeButtonPosition = {
x: position.x,
y: position.y
};
}
} catch (error) {
// Silently fail if there's an issue with saved position
}
})();
</script>
</head>
@@ -45,7 +61,6 @@
<div class="toggle-container">
<button id="fileMode" class="toggle-button active">File Sharing</button>
<button id="pasteMode" class="toggle-button">Pastebin</button>
<button id="urlMode" class="toggle-button">URL Shortener</button>
<button id="faqButton" class="toggle-button">FAQ</button>
<!-- File Expiry Selection (inline) -->
@@ -103,71 +118,11 @@
<div id="pastebinSection" class="section" style="display: none;">
<textarea id="pasteContent" placeholder="Enter your text here..." rows="10" cols="50"></textarea>
<!-- Paste Expiry Selection -->
<div class="expiry-section">
<label for="pasteExpirySelect" class="expiry-label">⏰ Paste Expiry:</label>
<select id="pasteExpirySelect" class="expiry-select">
<option value="never">Never expires</option>
<option value="1h">1 Hour</option>
<option value="24h" selected>24 Hours</option>
<option value="7d">7 Days</option>
<option value="30d">30 Days</option>
<option value="custom">Custom...</option>
</select>
<input type="datetime-local" id="customPasteExpiry" class="custom-expiry" style="display: none;">
<p class="expiry-hint">Pastes will be automatically deleted after the expiry time</p>
</div>
<button id="submitPaste" class="submit-button">Submit Paste</button>
<div id="pasteResult" class="result-message"></div>
</div>
<!-- URL Shortener Area (hidden by default) -->
<div id="urlShortenerSection" class="section" style="display: none;">
<div class="url-input-container">
<label for="urlInput" class="url-label">🔗 Enter URL to shorten:</label>
<input type="url" id="urlInput" class="url-input" placeholder="https://example.com/very-long-url" required>
<div class="url-options">
<div class="option-group">
<label for="customCode" class="option-label">Custom code (optional):</label>
<input type="text" id="customCode" class="custom-code-input" placeholder="my-link" maxlength="20">
<small class="option-hint">3-20 characters, letters, numbers, hyphens, underscores only</small>
</div>
<div class="option-group">
<label for="urlExpiry" class="option-label">⏰ Expires in:</label>
<select id="urlExpiry" class="url-expiry-select">
<option value="never">Never</option>
<option value="1">1 Hour</option>
<option value="24" selected>24 Hours</option>
<option value="168">7 Days</option>
<option value="720">30 Days</option>
</select>
</div>
</div>
<button id="shortenUrl" class="submit-button">✨ Shorten URL</button>
</div>
<div id="urlResult" class="result-message"></div>
<!-- URL Preview Container -->
<div id="urlPreviewContainer" class="url-preview-container" style="display: none;">
<h3>🔗 Short URL Created:</h3>
<div id="urlPreview" class="url-preview">
<div class="short-url-display">
<input type="text" id="shortUrlInput" class="short-url-input" readonly>
<button id="copyUrlButton" class="copy-button" title="Copy to clipboard">📋</button>
</div>
<div class="url-details">
<p><strong>Target:</strong> <span id="targetUrl"></span></p>
<p><strong>Clicks:</strong> <span id="clickCount">0</span></p>
<p id="expiryInfo" style="display: none;"><strong>Expires:</strong> <span id="expiryDate"></span></p>
</div>
</div>
</div>
</div>
<!-- FAQ Section (hidden by default) -->
<div id="faqSection" class="section" style="display: none;">
@@ -263,21 +218,7 @@
<p><em>Perfect for temporary shares or sensitive content that shouldn't persist!</em></p>
</div>
<div class="faq-section">
<h3>🔗 How do I shorten a URL?</h3>
<p>Use the URL Shortener tab to create short links:</p>
<ul>
<li><strong>Basic shortening:</strong> Enter any URL and get a short link like <code>sharey.org/abc123</code></li>
<li><strong>Custom codes:</strong> Create memorable links like <code>sharey.org/my-project</code></li>
<li><strong>Expiring links:</strong> Set links to expire automatically for security</li>
<li><strong>Click tracking:</strong> See how many times your link has been clicked</li>
</ul>
<p><strong>API Usage:</strong></p>
<pre><code>curl -X POST https://sharey.org/api/shorten \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "code": "my-link", "expires_in_hours": 24}'</code></pre>
<p><em>Perfect for sharing long URLs, tracking clicks, or creating temporary access links!</em></p>
</div>
</div>
</div>

View File

@@ -1,102 +0,0 @@
<!DOCTYPE html>
<html lang="en">
< <!-- Theme toggle button -->
<button id="themeToggle" class="theme-toggle" title="Switch to dark mode">🌙</button>ad>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sharey</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<!-- QR Code generation library -->
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js"></script>
<!-- Immediate theme application to prevent flash -->
<script>
// Apply theme immediately before page renders
(function() {
function getCookie(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
const savedTheme = getCookie('sharey-theme') || 'light';
if (savedTheme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
}
})();
</script>
</head>
<body>
<!-- Theme toggle button -->
<button id="themeToggle" class="theme-toggle" title="Switch to dark mode"><EFBFBD></button>
<div class="container">
<h1>Sharey~</h1>
<!-- Toggle button to switch between modes -->
<div class="toggle-container">
<button id="fileMode" class="toggle-button active">File Sharing</button>
<button id="pasteMode" class="toggle-button">Pastebin</button>
<button id="faqButton" class="toggle-button">FAQ</button> <!-- Updated FAQ button -->
</div>
<!-- File Sharing Area -->
<div id="fileSharingSection" class="section">
<div id="drop-area" class="drop-area">
<p>Drag & Drop Files Here or Click to Select</p>
<input type="file" id="fileInput" multiple style="display: none;">
</div>
<div id="progressContainer" class="progress-bar" style="display: none;">
<div id="progressBar" class="progress"></div>
</div>
<p id="progressText">0%</p>
<div id="result" class="result-message"></div>
<!-- File preview container -->
<div id="filePreviewContainer" class="file-preview-container" style="display: none;">
<h3>File Previews:</h3>
<div id="filePreviews" class="file-previews"></div>
</div>
</div>
<!-- Pastebin Area (hidden by default) -->
<div id="pastebinSection" class="section" style="display: none;">
<textarea id="pasteContent" placeholder="Enter your text here..." rows="10" cols="50"></textarea>
<button id="submitPaste" class="submit-button">Submit Paste</button>
<div id="pasteResult" class="result-message"></div>
</div>
<!-- FAQ Section (hidden by default) -->
<div id="faqSection" class="section" style="display: none;">
<h2>Frequently Asked Questions</h2>
<div class="faq-section">
<h3>How do I upload a file?</h3>
<p>To upload a file, send a POST request to the /api/upload endpoint. Example:</p>
<pre><code>curl -X POST https://sharey.org/api/upload \
-F "files[]=@/path/to/your/file.txt"</code></pre>
</div>
<div class="faq-section">
<h3>How do I create a paste?</h3>
<p>To create a paste, send a POST request to the /api/paste endpoint. Example:</p>
<pre><code>curl -X POST https://sharey.org/api/paste \
-H "Content-Type: application/json" \
-d '{"content": "This is the content of my paste."}'</code></pre>
</div>
<!-- Add more FAQ items as needed -->
</div>
</div>
<!-- QR Code library for sharing -->
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js"></script>
<script src="{{ url_for('static', filename='script.js') }}"></script>
</body>
</html>