final app for testing and deployment
This commit is contained in:
@@ -7,7 +7,7 @@ import io
|
||||
import base64
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from flask import Blueprint, request, jsonify, send_file
|
||||
from flask import Blueprint, request, jsonify, send_file, redirect, Response, current_app
|
||||
from app.utils.auth import login_required
|
||||
from app.utils.qr_generator import QRCodeGenerator
|
||||
from app.utils.link_manager import LinkPageManager
|
||||
@@ -20,9 +20,11 @@ qr_generator = QRCodeGenerator()
|
||||
link_manager = LinkPageManager()
|
||||
data_manager = QRDataManager()
|
||||
|
||||
# Configuration for file uploads
|
||||
UPLOAD_FOLDER = 'app/static/qr_codes'
|
||||
LOGOS_FOLDER = 'app/static/logos'
|
||||
# Configuration for file uploads - use paths relative to app root
|
||||
UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), '..', 'static', 'qr_codes')
|
||||
LOGOS_FOLDER = os.path.join(os.path.dirname(__file__), '..', 'static', 'logos')
|
||||
UPLOAD_FOLDER = os.path.abspath(UPLOAD_FOLDER)
|
||||
LOGOS_FOLDER = os.path.abspath(LOGOS_FOLDER)
|
||||
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
||||
os.makedirs(LOGOS_FOLDER, exist_ok=True)
|
||||
|
||||
@@ -107,7 +109,7 @@ END:VCARD"""
|
||||
@bp.route('/download/<qr_id>')
|
||||
@login_required
|
||||
def download_qr(qr_id):
|
||||
"""Download QR code"""
|
||||
"""Download QR code in PNG format"""
|
||||
try:
|
||||
img_path = os.path.join(UPLOAD_FOLDER, f'{qr_id}.png')
|
||||
if os.path.exists(img_path):
|
||||
@@ -117,6 +119,32 @@ def download_qr(qr_id):
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@bp.route('/download/<qr_id>/svg')
|
||||
@login_required
|
||||
def download_qr_svg(qr_id):
|
||||
"""Download QR code in SVG format"""
|
||||
try:
|
||||
# Get QR code data from database
|
||||
qr_data = data_manager.get_qr_code(qr_id)
|
||||
if not qr_data:
|
||||
return jsonify({'error': 'QR code not found'}), 404
|
||||
|
||||
# Regenerate QR code as SVG
|
||||
settings = qr_data.get('settings', {})
|
||||
content = qr_data.get('content', '')
|
||||
|
||||
# Generate SVG QR code
|
||||
svg_string = qr_generator.generate_qr_code_svg_string(content, settings)
|
||||
|
||||
# Create a response with SVG content
|
||||
response = Response(svg_string, mimetype='image/svg+xml')
|
||||
response.headers['Content-Disposition'] = f'attachment; filename=qr_code_{qr_id}.svg'
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@bp.route('/qr_codes')
|
||||
@login_required
|
||||
def list_qr_codes():
|
||||
@@ -195,8 +223,19 @@ def create_link_page():
|
||||
# Create the link page
|
||||
page_id = link_manager.create_link_page(title, description)
|
||||
|
||||
# Create QR code pointing to the link page
|
||||
page_url = f"{request.url_root}links/{page_id}"
|
||||
# Create the original page URL
|
||||
original_page_url = f"{request.url_root}links/{page_id}"
|
||||
|
||||
# Automatically create a short URL for the link page
|
||||
short_result = link_manager.create_standalone_short_url(
|
||||
original_page_url,
|
||||
title=f"Link Page: {title}",
|
||||
custom_code=None
|
||||
)
|
||||
short_page_url = short_result['short_url']
|
||||
|
||||
# Store the short URL info with the page
|
||||
link_manager.set_page_short_url(page_id, short_page_url, short_result['short_code'])
|
||||
|
||||
settings = {
|
||||
'size': data.get('size', 10),
|
||||
@@ -206,8 +245,8 @@ def create_link_page():
|
||||
'style': data.get('style', 'square')
|
||||
}
|
||||
|
||||
# Generate QR code
|
||||
qr_img = qr_generator.generate_qr_code(page_url, settings)
|
||||
# Generate QR code pointing to the SHORT URL (not the original long URL)
|
||||
qr_img = qr_generator.generate_qr_code(short_page_url, settings)
|
||||
|
||||
# Convert to base64
|
||||
img_buffer = io.BytesIO()
|
||||
@@ -215,8 +254,8 @@ def create_link_page():
|
||||
img_buffer.seek(0)
|
||||
img_base64 = base64.b64encode(img_buffer.getvalue()).decode()
|
||||
|
||||
# Save QR code record
|
||||
qr_id = data_manager.save_qr_record('link_page', page_url, settings, img_base64, page_id)
|
||||
# Save QR code record with the short URL
|
||||
qr_id = data_manager.save_qr_record('link_page', short_page_url, settings, img_base64, page_id)
|
||||
|
||||
# Save image file
|
||||
img_path = os.path.join(UPLOAD_FOLDER, f'{qr_id}.png')
|
||||
@@ -226,7 +265,9 @@ def create_link_page():
|
||||
'success': True,
|
||||
'qr_id': qr_id,
|
||||
'page_id': page_id,
|
||||
'page_url': page_url,
|
||||
'page_url': short_page_url, # Return the short URL as the main page URL
|
||||
'original_url': original_page_url, # Keep original for reference
|
||||
'short_code': short_result['short_code'],
|
||||
'edit_url': f"{request.url_root}edit/{page_id}",
|
||||
'image_data': f'data:image/png;base64,{img_base64}',
|
||||
'download_url': f'/api/download/{qr_id}'
|
||||
@@ -244,11 +285,17 @@ def add_link_to_page(page_id):
|
||||
title = data.get('title', '')
|
||||
url = data.get('url', '')
|
||||
description = data.get('description', '')
|
||||
enable_shortener = data.get('enable_shortener', False)
|
||||
custom_short_code = data.get('custom_short_code', None)
|
||||
|
||||
if not title or not url:
|
||||
return jsonify({'error': 'Title and URL are required'}), 400
|
||||
|
||||
success = link_manager.add_link(page_id, title, url, description)
|
||||
success = link_manager.add_link(
|
||||
page_id, title, url, description,
|
||||
enable_shortener=enable_shortener,
|
||||
custom_short_code=custom_short_code
|
||||
)
|
||||
|
||||
if success:
|
||||
return jsonify({'success': True})
|
||||
@@ -267,8 +314,14 @@ def update_link_in_page(page_id, link_id):
|
||||
title = data.get('title')
|
||||
url = data.get('url')
|
||||
description = data.get('description')
|
||||
enable_shortener = data.get('enable_shortener')
|
||||
custom_short_code = data.get('custom_short_code')
|
||||
|
||||
success = link_manager.update_link(page_id, link_id, title, url, description)
|
||||
success = link_manager.update_link(
|
||||
page_id, link_id, title, url, description,
|
||||
enable_shortener=enable_shortener,
|
||||
custom_short_code=custom_short_code
|
||||
)
|
||||
|
||||
if success:
|
||||
return jsonify({'success': True})
|
||||
@@ -302,3 +355,108 @@ def get_link_page(page_id):
|
||||
return jsonify(page_data)
|
||||
else:
|
||||
return jsonify({'error': 'Page not found'}), 404
|
||||
|
||||
# URL Shortener API Routes
|
||||
|
||||
@bp.route('/shorten', methods=['POST'])
|
||||
@login_required
|
||||
def create_short_url():
|
||||
"""Create a shortened URL"""
|
||||
try:
|
||||
data = request.json
|
||||
url = data.get('url', '')
|
||||
title = data.get('title', '')
|
||||
custom_code = data.get('custom_code', None)
|
||||
|
||||
if not url:
|
||||
return jsonify({'error': 'URL is required'}), 400
|
||||
|
||||
result = link_manager.create_standalone_short_url(url, title, custom_code)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'short_url': result['short_url'],
|
||||
'short_code': result['short_code'],
|
||||
'original_url': result['original_url']
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@bp.route('/short_urls')
|
||||
@login_required
|
||||
def list_short_urls():
|
||||
"""List all shortened URLs"""
|
||||
try:
|
||||
urls = link_manager.list_all_short_urls()
|
||||
return jsonify({'success': True, 'urls': urls})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@bp.route('/short_urls/<short_code>/stats')
|
||||
@login_required
|
||||
def get_short_url_stats(short_code):
|
||||
"""Get statistics for a short URL"""
|
||||
try:
|
||||
stats = link_manager.get_short_url_stats(short_code)
|
||||
if stats:
|
||||
return jsonify({'success': True, 'stats': stats})
|
||||
else:
|
||||
return jsonify({'error': 'Short URL not found'}), 404
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@bp.route('/generate_shortened_qr', methods=['POST'])
|
||||
@login_required
|
||||
def generate_shortened_qr():
|
||||
"""Generate QR code for a shortened URL"""
|
||||
try:
|
||||
data = request.json
|
||||
shortener_data = data.get('shortener', {})
|
||||
url = shortener_data.get('url', '')
|
||||
title = shortener_data.get('title', '')
|
||||
custom_code = shortener_data.get('custom_code', '').strip() or None
|
||||
|
||||
if not url:
|
||||
return jsonify({'error': 'URL is required'}), 400
|
||||
|
||||
# Create shortened URL
|
||||
result = link_manager.create_standalone_short_url(url, title, custom_code)
|
||||
short_url = result['short_url']
|
||||
|
||||
# Generate QR code for the short URL
|
||||
settings = {
|
||||
'size': data.get('size', 10),
|
||||
'border': data.get('border', 4),
|
||||
'foreground_color': data.get('foreground_color', '#000000'),
|
||||
'background_color': data.get('background_color', '#FFFFFF'),
|
||||
'style': data.get('style', 'square')
|
||||
}
|
||||
|
||||
qr_img = qr_generator.generate_qr_code(short_url, settings)
|
||||
|
||||
# Convert to base64
|
||||
img_buffer = io.BytesIO()
|
||||
qr_img.save(img_buffer, format='PNG')
|
||||
img_buffer.seek(0)
|
||||
img_base64 = base64.b64encode(img_buffer.getvalue()).decode()
|
||||
|
||||
# Save QR code record
|
||||
qr_id = data_manager.save_qr_record('url_shortener', short_url, settings, img_base64)
|
||||
|
||||
# Save image file
|
||||
img_path = os.path.join(UPLOAD_FOLDER, f'{qr_id}.png')
|
||||
qr_img.save(img_path)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'qr_id': qr_id,
|
||||
'short_url': short_url,
|
||||
'short_code': result['short_code'],
|
||||
'original_url': result['original_url'],
|
||||
'image_data': f'data:image/png;base64,{img_base64}',
|
||||
'download_url': f'/api/download/{qr_id}'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Main routes for QR Code Manager
|
||||
"""
|
||||
|
||||
from flask import Blueprint, render_template
|
||||
from flask import Blueprint, render_template, redirect, abort
|
||||
from app.utils.auth import login_required
|
||||
from app.utils.link_manager import LinkPageManager
|
||||
|
||||
@@ -44,3 +44,12 @@ def health_check():
|
||||
from datetime import datetime
|
||||
from flask import jsonify
|
||||
return jsonify({'status': 'healthy', 'timestamp': datetime.now().isoformat()})
|
||||
|
||||
@bp.route('/s/<short_code>')
|
||||
def redirect_short_url(short_code):
|
||||
"""Redirect short URL to original URL"""
|
||||
original_url = link_manager.resolve_short_url(short_code)
|
||||
if original_url:
|
||||
return redirect(original_url)
|
||||
else:
|
||||
abort(404)
|
||||
|
||||
@@ -141,12 +141,27 @@
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.link-item.editing {
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.link-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.link-logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.link-display {
|
||||
display: block;
|
||||
}
|
||||
@@ -253,6 +268,13 @@
|
||||
<div class="header">
|
||||
<h1>✏️ Edit Links</h1>
|
||||
<p>Manage your link collection: {{ page.title }}</p>
|
||||
{% if page.short_url %}
|
||||
<div style="margin-top: 15px; padding: 12px; background: rgba(255,255,255,0.2); border-radius: 8px; font-size: 0.9em;">
|
||||
<strong>🔗 Page Short URL:</strong>
|
||||
<a href="{{ page.short_url }}" target="_blank" style="color: #fff; text-decoration: underline;">{{ page.short_url }}</a>
|
||||
<button onclick="copyToClipboard('{{ page.short_url }}')" style="margin-left: 10px; padding: 4px 8px; background: rgba(255,255,255,0.3); color: white; border: 1px solid rgba(255,255,255,0.5); border-radius: 3px; cursor: pointer; font-size: 0.8em;">Copy</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success" id="success-alert">
|
||||
@@ -292,12 +314,21 @@
|
||||
{% if page.links %}
|
||||
{% for link in page.links %}
|
||||
<div class="link-item" data-link-id="{{ link.id }}">
|
||||
<div class="link-display">
|
||||
<div class="link-title">{{ link.title }}</div>
|
||||
{% if link.description %}
|
||||
<div class="link-description">{{ link.description }}</div>
|
||||
<div class="link-content">
|
||||
<div class="link-display">
|
||||
<div class="link-title">{{ link.title }}</div>
|
||||
{% if link.description %}
|
||||
<div class="link-description">{{ link.description }}</div>
|
||||
{% endif %}
|
||||
<div class="link-url" data-url="{{ link.url }}">{{ link.url }}</div>
|
||||
{% if link.short_url %}
|
||||
<div class="short-url-display" style="margin-top: 8px; padding: 8px; background: #e3f2fd; border-radius: 5px; border-left: 3px solid #2196f3;">
|
||||
<small style="color: #1976d2; font-weight: 600;">🔗 Short URL:</small>
|
||||
<br>
|
||||
<a href="{{ link.short_url }}" target="_blank" style="color: #1976d2; text-decoration: none; font-family: monospace;">{{ link.short_url }}</a>
|
||||
<button class="btn-copy" onclick="copyToClipboard('{{ link.short_url }}')" style="margin-left: 10px; padding: 2px 8px; background: #2196f3; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.8em;">Copy</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="link-url">{{ link.url }}</div>
|
||||
<div class="link-actions">
|
||||
<button class="btn btn-small btn-secondary" onclick="editLink('{{ link.id }}')">Edit</button>
|
||||
<button class="btn btn-small btn-danger" onclick="deleteLink('{{ link.id }}')">Delete</button>
|
||||
@@ -322,6 +353,7 @@
|
||||
<button class="btn btn-small btn-secondary" onclick="cancelEdit('{{ link.id }}')">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
<img class="link-logo" src="" alt="" style="display: none;" onerror="this.style.display='none'">
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
@@ -344,6 +376,87 @@
|
||||
<script>
|
||||
const pageId = '{{ page.id }}';
|
||||
|
||||
// Social media and website logo detection
|
||||
function getWebsiteLogo(url) {
|
||||
try {
|
||||
const urlObj = new URL(url.startsWith('http') ? url : 'https://' + url);
|
||||
const domain = urlObj.hostname.toLowerCase().replace('www.', '');
|
||||
|
||||
// Logo mapping for popular sites
|
||||
const logoMap = {
|
||||
'facebook.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/facebook.svg',
|
||||
'instagram.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/instagram.svg',
|
||||
'twitter.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/twitter.svg',
|
||||
'x.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/x.svg',
|
||||
'tiktok.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/tiktok.svg',
|
||||
'youtube.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/youtube.svg',
|
||||
'linkedin.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/linkedin.svg',
|
||||
'pinterest.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/pinterest.svg',
|
||||
'snapchat.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/snapchat.svg',
|
||||
'whatsapp.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/whatsapp.svg',
|
||||
'telegram.org': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/telegram.svg',
|
||||
'discord.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/discord.svg',
|
||||
'reddit.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/reddit.svg',
|
||||
'github.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/github.svg',
|
||||
'gmail.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/gmail.svg',
|
||||
'google.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/google.svg',
|
||||
'amazon.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/amazon.svg',
|
||||
'apple.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/apple.svg',
|
||||
'microsoft.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/microsoft.svg',
|
||||
'spotify.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/spotify.svg',
|
||||
'netflix.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/netflix.svg',
|
||||
'twitch.tv': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/twitch.svg',
|
||||
'dropbox.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/dropbox.svg',
|
||||
'zoom.us': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/zoom.svg'
|
||||
};
|
||||
|
||||
if (logoMap[domain]) {
|
||||
return logoMap[domain];
|
||||
}
|
||||
|
||||
// Fallback to favicon
|
||||
return `https://www.google.com/s2/favicons?domain=${domain}&sz=64`;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Set logos for existing links
|
||||
function setLogosForLinks() {
|
||||
document.querySelectorAll('.link-url[data-url]').forEach(linkElement => {
|
||||
const url = linkElement.getAttribute('data-url');
|
||||
const logoImg = linkElement.closest('.link-item').querySelector('.link-logo');
|
||||
const logoSrc = getWebsiteLogo(url);
|
||||
|
||||
if (logoSrc && logoImg) {
|
||||
logoImg.src = logoSrc;
|
||||
logoImg.style.display = 'block';
|
||||
logoImg.alt = new URL(url.startsWith('http') ? url : 'https://' + url).hostname;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize logos when page loads
|
||||
document.addEventListener('DOMContentLoaded', setLogosForLinks);
|
||||
|
||||
// Copy to clipboard function
|
||||
function copyToClipboard(text) {
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
// Show temporary success message
|
||||
const btn = event.target;
|
||||
const originalText = btn.textContent;
|
||||
btn.textContent = 'Copied!';
|
||||
btn.style.background = '#4caf50';
|
||||
setTimeout(() => {
|
||||
btn.textContent = originalText;
|
||||
btn.style.background = '#2196f3';
|
||||
}, 1500);
|
||||
}).catch(function(err) {
|
||||
console.error('Could not copy text: ', err);
|
||||
alert('Failed to copy to clipboard');
|
||||
});
|
||||
}
|
||||
|
||||
// Add new link
|
||||
document.getElementById('add-link-form').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
@@ -358,7 +471,11 @@
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ title, url, description })
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
url,
|
||||
description
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
@@ -399,7 +516,11 @@
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ title, url, description })
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
url,
|
||||
description
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
@@ -148,7 +148,8 @@
|
||||
.link-page-fields,
|
||||
.email-fields,
|
||||
.sms-fields,
|
||||
.vcard-fields {
|
||||
.vcard-fields,
|
||||
.url-shortener-fields {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -156,7 +157,8 @@
|
||||
.link-page-fields.active,
|
||||
.email-fields.active,
|
||||
.sms-fields.active,
|
||||
.vcard-fields.active {
|
||||
.vcard-fields.active,
|
||||
.url-shortener-fields.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -212,14 +214,9 @@
|
||||
}
|
||||
|
||||
.download-section {
|
||||
display: none;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.download-section.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
flex: 1;
|
||||
@@ -230,6 +227,15 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: linear-gradient(135deg, #20c997 0%, #17a2b8 100%);
|
||||
}
|
||||
|
||||
.qr-history {
|
||||
margin-top: 30px;
|
||||
padding: 25px;
|
||||
@@ -322,6 +328,7 @@
|
||||
<option value="text">Text</option>
|
||||
<option value="url">URL/Website</option>
|
||||
<option value="link_page">Dynamic Link Page</option>
|
||||
<option value="url_shortener">URL Shortener</option>
|
||||
<option value="wifi">WiFi</option>
|
||||
<option value="email">Email</option>
|
||||
<option value="phone">Phone</option>
|
||||
@@ -368,7 +375,32 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<p style="background: #e3f2fd; padding: 15px; border-radius: 8px; color: #1565c0; font-size: 0.9em;">
|
||||
<strong>💡 Dynamic Link Page:</strong> This creates a QR code that points to a web page where you can add, edit, and manage links. The QR code stays the same, but you can update the links anytime!
|
||||
<strong>💡 Dynamic Link Page with Short URL:</strong> This creates a QR code with a short URL that points to a web page where you can add, edit, and manage links. The QR code stays the same, but you can update the links anytime!
|
||||
<br><br>✨ <strong>Auto Short URL:</strong> Your link page will automatically get a short URL like <code>qr.moto-adv.com/s/abc123</code> making the QR code simpler and easier to scan!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- URL Shortener fields -->
|
||||
<div class="url-shortener-fields" id="url-shortener-fields">
|
||||
<div class="form-group">
|
||||
<label for="shortener-url">URL to Shorten</label>
|
||||
<input type="url" id="shortener-url" placeholder="https://example.com/very/long/url">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="shortener-title">Title (optional)</label>
|
||||
<input type="text" id="shortener-title" placeholder="My Website">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="shortener-custom-code">Custom Short Code (optional)</label>
|
||||
<input type="text" id="shortener-custom-code" placeholder="mylink" maxlength="20">
|
||||
<small style="color: #666; font-size: 0.8em;">
|
||||
Leave empty for random code. Only letters and numbers allowed.
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<p style="background: #e3f2fd; padding: 15px; border-radius: 8px; color: #1565c0; font-size: 0.9em;">
|
||||
<strong>🔗 URL Shortener:</strong> Creates a short URL that redirects to your original URL. The QR code will contain the short URL. Perfect for long URLs or tracking clicks!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -478,8 +510,9 @@
|
||||
</div>
|
||||
|
||||
<div class="download-section" id="download-section">
|
||||
<button class="btn btn-primary" onclick="downloadQR()">Download PNG</button>
|
||||
<button class="btn btn-secondary" onclick="copyToClipboard()">Copy Image</button>
|
||||
<button class="btn btn-primary" onclick="downloadQR('png')">📥 Download PNG</button>
|
||||
<button class="btn btn-success" onclick="downloadQR('svg')">🎨 Download SVG</button>
|
||||
<button class="btn btn-secondary" onclick="copyToClipboard()">📋 Copy Image</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -501,7 +534,7 @@
|
||||
|
||||
// Hide all specific fields
|
||||
document.getElementById('text-field').style.display = 'none';
|
||||
document.querySelectorAll('.wifi-fields, .link-page-fields, .email-fields, .sms-fields, .vcard-fields').forEach(el => {
|
||||
document.querySelectorAll('.wifi-fields, .link-page-fields, .email-fields, .sms-fields, .vcard-fields, .url-shortener-fields').forEach(el => {
|
||||
el.classList.remove('active');
|
||||
});
|
||||
|
||||
@@ -570,6 +603,12 @@
|
||||
email: document.getElementById('vcard-email').value,
|
||||
website: document.getElementById('vcard-website').value
|
||||
};
|
||||
} else if (type === 'url_shortener') {
|
||||
additionalData.shortener = {
|
||||
url: document.getElementById('shortener-url').value,
|
||||
title: document.getElementById('shortener-title').value,
|
||||
custom_code: document.getElementById('shortener-custom-code').value
|
||||
};
|
||||
}
|
||||
|
||||
const requestData = {
|
||||
@@ -583,7 +622,13 @@
|
||||
};
|
||||
|
||||
try {
|
||||
const endpoint = type === 'link_page' ? '/api/create_link_page' : '/api/generate';
|
||||
let endpoint = '/api/generate';
|
||||
if (type === 'link_page') {
|
||||
endpoint = '/api/create_link_page';
|
||||
} else if (type === 'url_shortener') {
|
||||
endpoint = '/api/generate_shortened_qr';
|
||||
}
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -606,10 +651,22 @@
|
||||
if (type === 'link_page') {
|
||||
previewHTML += `
|
||||
<div style="margin-top: 15px; padding: 15px; background: #e3f2fd; border-radius: 8px; text-align: left;">
|
||||
<h4 style="margin-bottom: 10px; color: #1565c0;">🎉 Dynamic Link Page Created!</h4>
|
||||
<p style="margin-bottom: 10px; font-size: 0.9em;"><strong>Public URL:</strong> <a href="${result.page_url}" target="_blank">${result.page_url}</a></p>
|
||||
<p style="margin-bottom: 10px; font-size: 0.9em;"><strong>Edit URL:</strong> <a href="${result.edit_url}" target="_blank">${result.edit_url}</a></p>
|
||||
<p style="font-size: 0.9em; color: #666;">Share the QR code - visitors will see your link collection. Use the edit URL to manage your links!</p>
|
||||
<h4 style="margin-bottom: 10px; color: #1565c0;">🎉 Dynamic Link Page Created with Short URL!</h4>
|
||||
<p style="margin-bottom: 10px; font-size: 0.9em;"><strong>🔗 Short URL:</strong> <a href="${result.page_url}" target="_blank">${result.page_url}</a>
|
||||
<button onclick="copyToClipboard('${result.page_url}')" style="margin-left: 10px; padding: 4px 8px; background: #1565c0; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.8em;">Copy</button></p>
|
||||
<p style="margin-bottom: 10px; font-size: 0.9em;"><strong>📝 Edit URL:</strong> <a href="${result.edit_url}" target="_blank">${result.edit_url}</a></p>
|
||||
${result.original_url ? `<p style="margin-bottom: 10px; font-size: 0.8em; color: #666;"><strong>Original URL:</strong> ${result.original_url}</p>` : ''}
|
||||
<p style="font-size: 0.9em; color: #666;">✨ QR code contains the short URL for easier scanning! Share it - visitors will see your link collection. Use the edit URL to manage your links!</p>
|
||||
</div>
|
||||
`;
|
||||
} else if (type === 'url_shortener') {
|
||||
previewHTML += `
|
||||
<div style="margin-top: 15px; padding: 15px; background: #e8f5e8; border-radius: 8px; text-align: left;">
|
||||
<h4 style="margin-bottom: 10px; color: #2e7d32;">🔗 Short URL Created!</h4>
|
||||
<p style="margin-bottom: 10px; font-size: 0.9em;"><strong>Short URL:</strong> <a href="${result.short_url}" target="_blank">${result.short_url}</a></p>
|
||||
<p style="margin-bottom: 10px; font-size: 0.9em;"><strong>Original URL:</strong> <a href="${result.original_url}" target="_blank">${result.original_url}</a></p>
|
||||
<p style="font-size: 0.9em; color: #666;">The QR code contains your short URL. When scanned, it will redirect to your original URL!</p>
|
||||
<button onclick="copyToClipboard('${result.short_url}')" style="margin-top: 10px; padding: 8px 15px; background: #2e7d32; color: white; border: none; border-radius: 5px; cursor: pointer;">Copy Short URL</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -629,9 +686,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadQR() {
|
||||
async function downloadQR(format = 'png') {
|
||||
if (currentQRId) {
|
||||
window.open(`/api/download/${currentQRId}`, '_blank');
|
||||
if (format === 'svg') {
|
||||
window.open(`/api/download/${currentQRId}/svg`, '_blank');
|
||||
} else {
|
||||
window.open(`/api/download/${currentQRId}`, '_blank');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -650,6 +711,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Copy to clipboard function
|
||||
function copyToClipboard(text) {
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
const btn = event.target;
|
||||
const originalText = btn.textContent;
|
||||
btn.textContent = 'Copied!';
|
||||
btn.style.background = '#4caf50';
|
||||
setTimeout(() => {
|
||||
btn.textContent = originalText;
|
||||
btn.style.background = '#2e7d32';
|
||||
}, 1500);
|
||||
}).catch(function(err) {
|
||||
console.error('Could not copy text: ', err);
|
||||
alert('Failed to copy to clipboard');
|
||||
});
|
||||
}
|
||||
|
||||
async function loadQRHistory() {
|
||||
try {
|
||||
const response = await fetch('/api/qr_codes');
|
||||
@@ -670,9 +748,10 @@
|
||||
<p>Created: ${new Date(qr.created_at).toLocaleDateString()}</p>
|
||||
</div>
|
||||
<div class="qr-item-actions">
|
||||
<button class="btn btn-small btn-primary" onclick="downloadQRById('${qr.id}')">Download</button>
|
||||
${qr.type === 'link_page' ? `<button class="btn btn-small" onclick="openLinkPage('${qr.id}')" style="background: #28a745;">Manage</button>` : ''}
|
||||
<button class="btn btn-small btn-secondary" onclick="deleteQR('${qr.id}')">Delete</button>
|
||||
<button class="btn btn-small btn-primary" onclick="downloadQRById('${qr.id}', 'png')" title="Download PNG">📥 PNG</button>
|
||||
<button class="btn btn-small btn-success" onclick="downloadQRById('${qr.id}', 'svg')" title="Download SVG">🎨 SVG</button>
|
||||
${qr.type === 'link_page' ? `<button class="btn btn-small" onclick="openLinkPage('${qr.id}')" style="background: #28a745;" title="Manage Links">📝 Manage</button>` : ''}
|
||||
<button class="btn btn-small btn-secondary" onclick="deleteQR('${qr.id}')" title="Delete QR Code">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
@@ -681,8 +760,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadQRById(qrId) {
|
||||
window.open(`/api/download/${qrId}`, '_blank');
|
||||
async function downloadQRById(qrId, format = 'png') {
|
||||
if (format === 'svg') {
|
||||
window.open(`/api/download/${qrId}/svg`, '_blank');
|
||||
} else {
|
||||
window.open(`/api/download/${qrId}`, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteQR(qrId) {
|
||||
|
||||
@@ -90,7 +90,10 @@
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
@@ -100,14 +103,22 @@
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.link-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.link-title {
|
||||
font-size: 1.3em;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.link-logo {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.link-icon {
|
||||
@@ -153,31 +164,6 @@
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.footer {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
border-top: 1px solid #e9ecef;
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.last-updated {
|
||||
margin-top: 10px;
|
||||
font-size: 0.8em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header {
|
||||
padding: 30px 20px;
|
||||
@@ -232,15 +218,19 @@
|
||||
{% if page.links %}
|
||||
<h2>📚 Available Links</h2>
|
||||
{% for link in page.links %}
|
||||
<a href="{{ link.url }}" target="_blank" class="link-item">
|
||||
<div class="link-title">
|
||||
<div class="link-icon">🔗</div>
|
||||
{{ link.title }}
|
||||
<a href="{{ link.short_url if link.short_url else link.url }}" target="_blank" class="link-item" data-url="{{ link.url }}">
|
||||
<div class="link-content">
|
||||
<div class="link-title">
|
||||
{{ link.title }}
|
||||
{% if link.short_url %}<span style="background: #2196f3; color: white; font-size: 0.7em; padding: 2px 6px; border-radius: 10px; margin-left: 8px;">SHORT</span>{% endif %}
|
||||
</div>
|
||||
{% if link.description %}
|
||||
<div class="link-description">{{ link.description }}</div>
|
||||
{% endif %}
|
||||
<div class="link-url">{{ link.short_url if link.short_url else link.url }}</div>
|
||||
</div>
|
||||
{% if link.description %}
|
||||
<div class="link-description">{{ link.description }}</div>
|
||||
{% endif %}
|
||||
<div class="link-url">{{ link.url }}</div>
|
||||
<img class="link-logo" src="" alt="" style="display: none;" onerror="this.style.display='none'">
|
||||
<div class="link-icon" style="display: block;">🔗</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
@@ -252,18 +242,81 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>Powered by <a href="/">QR Code Manager</a></p>
|
||||
{% if page.updated_at %}
|
||||
<div class="last-updated">
|
||||
Last updated: {{ page.updated_at[:10] }} at {{ page.updated_at[11:19] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Social media and website logo detection
|
||||
function getWebsiteLogo(url) {
|
||||
try {
|
||||
const urlObj = new URL(url.startsWith('http') ? url : 'https://' + url);
|
||||
const domain = urlObj.hostname.toLowerCase().replace('www.', '');
|
||||
|
||||
// Logo mapping for popular sites
|
||||
const logoMap = {
|
||||
'facebook.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/facebook.svg',
|
||||
'instagram.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/instagram.svg',
|
||||
'twitter.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/twitter.svg',
|
||||
'x.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/x.svg',
|
||||
'tiktok.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/tiktok.svg',
|
||||
'youtube.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/youtube.svg',
|
||||
'linkedin.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/linkedin.svg',
|
||||
'pinterest.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/pinterest.svg',
|
||||
'snapchat.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/snapchat.svg',
|
||||
'whatsapp.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/whatsapp.svg',
|
||||
'telegram.org': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/telegram.svg',
|
||||
'discord.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/discord.svg',
|
||||
'reddit.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/reddit.svg',
|
||||
'github.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/github.svg',
|
||||
'gmail.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/gmail.svg',
|
||||
'google.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/google.svg',
|
||||
'amazon.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/amazon.svg',
|
||||
'apple.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/apple.svg',
|
||||
'microsoft.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/microsoft.svg',
|
||||
'spotify.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/spotify.svg',
|
||||
'netflix.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/netflix.svg',
|
||||
'twitch.tv': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/twitch.svg',
|
||||
'dropbox.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/dropbox.svg',
|
||||
'zoom.us': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/zoom.svg'
|
||||
};
|
||||
|
||||
if (logoMap[domain]) {
|
||||
return logoMap[domain];
|
||||
}
|
||||
|
||||
// Fallback to favicon
|
||||
return `https://www.google.com/s2/favicons?domain=${domain}&sz=64`;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Set logos for links
|
||||
function setLogosForLinks() {
|
||||
document.querySelectorAll('.link-item[data-url]').forEach(linkElement => {
|
||||
const url = linkElement.getAttribute('data-url');
|
||||
const logoImg = linkElement.querySelector('.link-logo');
|
||||
const fallbackIcon = linkElement.querySelector('.link-icon');
|
||||
const logoSrc = getWebsiteLogo(url);
|
||||
|
||||
if (logoSrc && logoImg) {
|
||||
logoImg.src = logoSrc;
|
||||
logoImg.style.display = 'block';
|
||||
logoImg.alt = new URL(url.startsWith('http') ? url : 'https://' + url).hostname;
|
||||
// Hide the fallback icon when logo is shown
|
||||
logoImg.onload = function() {
|
||||
if (fallbackIcon) fallbackIcon.style.display = 'none';
|
||||
};
|
||||
logoImg.onerror = function() {
|
||||
this.style.display = 'none';
|
||||
if (fallbackIcon) fallbackIcon.style.display = 'flex';
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize logos when page loads
|
||||
document.addEventListener('DOMContentLoaded', setLogosForLinks);
|
||||
|
||||
// Add click tracking (optional)
|
||||
document.querySelectorAll('.link-item').forEach(link => {
|
||||
link.addEventListener('click', function() {
|
||||
|
||||
@@ -134,22 +134,6 @@
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.default-credentials {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
color: #856404;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.default-credentials strong {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.login-container {
|
||||
margin: 10px;
|
||||
@@ -199,12 +183,6 @@
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="default-credentials">
|
||||
<strong>Default Login Credentials:</strong>
|
||||
Username: admin<br>
|
||||
Password: admin123
|
||||
</div>
|
||||
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
@@ -222,9 +200,6 @@
|
||||
|
||||
<div class="login-footer">
|
||||
<p>🔒 Secure QR Code Management System</p>
|
||||
<p style="margin-top: 5px; font-size: 0.8em; opacity: 0.7;">
|
||||
Change default credentials in production
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,8 +4,9 @@ Utility modules for QR Code Manager
|
||||
|
||||
from .auth import init_admin, login_required, verify_password, get_admin_credentials
|
||||
from .qr_generator import QRCodeGenerator
|
||||
from .link_manager import LinkPageManager, link_pages_db
|
||||
from .data_manager import QRDataManager, qr_codes_db
|
||||
from .link_manager import LinkPageManager
|
||||
from .data_manager import QRDataManager
|
||||
from .url_shortener import URLShortener
|
||||
|
||||
__all__ = [
|
||||
'init_admin',
|
||||
@@ -14,7 +15,6 @@ __all__ = [
|
||||
'get_admin_credentials',
|
||||
'QRCodeGenerator',
|
||||
'LinkPageManager',
|
||||
'link_pages_db',
|
||||
'QRDataManager',
|
||||
'qr_codes_db'
|
||||
'URLShortener'
|
||||
]
|
||||
|
||||
@@ -3,14 +3,39 @@ Data storage utilities for QR codes
|
||||
"""
|
||||
|
||||
import uuid
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
# In-memory storage for QR codes (in production, use a database)
|
||||
qr_codes_db = {}
|
||||
# Data storage directory
|
||||
DATA_DIR = 'data'
|
||||
QR_CODES_FILE = os.path.join(DATA_DIR, 'qr_codes.json')
|
||||
|
||||
# Ensure data directory exists
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
|
||||
class QRDataManager:
|
||||
def __init__(self):
|
||||
pass
|
||||
self.qr_codes_db = self._load_qr_codes()
|
||||
|
||||
def _load_qr_codes(self):
|
||||
"""Load QR codes from JSON file"""
|
||||
try:
|
||||
if os.path.exists(QR_CODES_FILE):
|
||||
with open(QR_CODES_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
except Exception as e:
|
||||
print(f"Error loading QR codes: {e}")
|
||||
return {}
|
||||
|
||||
def _save_qr_codes(self):
|
||||
"""Save QR codes to JSON file"""
|
||||
try:
|
||||
with open(QR_CODES_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.qr_codes_db, f, indent=2, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
print(f"Error saving QR codes: {e}")
|
||||
|
||||
def save_qr_record(self, qr_type, content, settings, image_data, page_id=None):
|
||||
"""Save QR code record to database"""
|
||||
@@ -27,32 +52,45 @@ class QRDataManager:
|
||||
if page_id:
|
||||
qr_record['page_id'] = page_id
|
||||
|
||||
qr_codes_db[qr_id] = qr_record
|
||||
self.qr_codes_db[qr_id] = qr_record
|
||||
self._save_qr_codes() # Persist to file
|
||||
return qr_id
|
||||
|
||||
def get_qr_record(self, qr_id):
|
||||
"""Get QR code record"""
|
||||
return qr_codes_db.get(qr_id)
|
||||
# Reload data from file to ensure we have the latest data
|
||||
self.qr_codes_db = self._load_qr_codes()
|
||||
return self.qr_codes_db.get(qr_id)
|
||||
|
||||
def get_qr_code(self, qr_id):
|
||||
"""Get QR code record (alias for compatibility)"""
|
||||
return self.get_qr_record(qr_id)
|
||||
|
||||
def delete_qr_record(self, qr_id):
|
||||
"""Delete QR code record"""
|
||||
if qr_id in qr_codes_db:
|
||||
del qr_codes_db[qr_id]
|
||||
if qr_id in self.qr_codes_db:
|
||||
del self.qr_codes_db[qr_id]
|
||||
self._save_qr_codes() # Persist to file
|
||||
return True
|
||||
return False
|
||||
|
||||
def list_qr_codes(self):
|
||||
"""List all QR codes"""
|
||||
# Reload data from file to ensure we have the latest data
|
||||
self.qr_codes_db = self._load_qr_codes()
|
||||
qr_list = []
|
||||
for qr_id, qr_data in qr_codes_db.items():
|
||||
for qr_id, qr_data in self.qr_codes_db.items():
|
||||
qr_list.append({
|
||||
'id': qr_id,
|
||||
'type': qr_data['type'],
|
||||
'created_at': qr_data['created_at'],
|
||||
'preview': f'data:image/png;base64,{qr_data["image_data"]}'
|
||||
'preview': f'data:image/png;base64,{qr_data["image_data"]}',
|
||||
'page_id': qr_data.get('page_id') # Include page_id if it exists
|
||||
})
|
||||
return qr_list
|
||||
|
||||
def qr_exists(self, qr_id):
|
||||
"""Check if QR code exists"""
|
||||
return qr_id in qr_codes_db
|
||||
# Reload data from file to ensure we have the latest data
|
||||
self.qr_codes_db = self._load_qr_codes()
|
||||
return qr_id in self.qr_codes_db
|
||||
|
||||
@@ -3,14 +3,41 @@ Dynamic Link Page Manager utilities
|
||||
"""
|
||||
|
||||
import uuid
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from .url_shortener import URLShortener
|
||||
|
||||
# In-memory storage for dynamic link pages (in production, use a database)
|
||||
link_pages_db = {}
|
||||
# Data storage directory
|
||||
DATA_DIR = 'data'
|
||||
LINK_PAGES_FILE = os.path.join(DATA_DIR, 'link_pages.json')
|
||||
|
||||
# Ensure data directory exists
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
|
||||
class LinkPageManager:
|
||||
def __init__(self):
|
||||
pass
|
||||
self.url_shortener = URLShortener()
|
||||
self.link_pages_db = self._load_link_pages()
|
||||
|
||||
def _load_link_pages(self):
|
||||
"""Load link pages from JSON file"""
|
||||
try:
|
||||
if os.path.exists(LINK_PAGES_FILE):
|
||||
with open(LINK_PAGES_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
except Exception as e:
|
||||
print(f"Error loading link pages: {e}")
|
||||
return {}
|
||||
|
||||
def _save_link_pages(self):
|
||||
"""Save link pages to JSON file"""
|
||||
try:
|
||||
with open(LINK_PAGES_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.link_pages_db, f, indent=2, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
print(f"Error saving link pages: {e}")
|
||||
|
||||
def create_link_page(self, title="My Links", description="Collection of useful links"):
|
||||
"""Create a new dynamic link page"""
|
||||
@@ -22,34 +49,70 @@ class LinkPageManager:
|
||||
'links': [],
|
||||
'created_at': datetime.now().isoformat(),
|
||||
'updated_at': datetime.now().isoformat(),
|
||||
'view_count': 0
|
||||
'view_count': 0,
|
||||
'short_url': None, # Will be set when short URL is created
|
||||
'short_code': None
|
||||
}
|
||||
link_pages_db[page_id] = page_data
|
||||
self.link_pages_db[page_id] = page_data
|
||||
self._save_link_pages() # Persist to file
|
||||
return page_id
|
||||
|
||||
def add_link(self, page_id, title, url, description=""):
|
||||
"""Add a link to a page"""
|
||||
if page_id not in link_pages_db:
|
||||
def set_page_short_url(self, page_id, short_url, short_code):
|
||||
"""Set the short URL for a link page"""
|
||||
if page_id in self.link_pages_db:
|
||||
self.link_pages_db[page_id]['short_url'] = short_url
|
||||
self.link_pages_db[page_id]['short_code'] = short_code
|
||||
self.link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
|
||||
self._save_link_pages() # Persist to file
|
||||
return True
|
||||
return False
|
||||
|
||||
def add_link(self, page_id, title, url, description="", enable_shortener=False, custom_short_code=None):
|
||||
"""Add a link to a page with optional URL shortening"""
|
||||
if page_id not in self.link_pages_db:
|
||||
return False
|
||||
|
||||
# Ensure URL has protocol
|
||||
if not url.startswith(('http://', 'https://')):
|
||||
url = f'https://{url}'
|
||||
|
||||
# Create the link data
|
||||
link_data = {
|
||||
'id': str(uuid.uuid4()),
|
||||
'title': title,
|
||||
'url': url if url.startswith(('http://', 'https://')) else f'https://{url}',
|
||||
'url': url,
|
||||
'description': description,
|
||||
'created_at': datetime.now().isoformat()
|
||||
'created_at': datetime.now().isoformat(),
|
||||
'short_url': None,
|
||||
'short_code': None,
|
||||
'clicks': 0
|
||||
}
|
||||
|
||||
link_pages_db[page_id]['links'].append(link_data)
|
||||
link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
|
||||
# Generate short URL if enabled
|
||||
if enable_shortener:
|
||||
try:
|
||||
short_result = self.url_shortener.create_short_url(
|
||||
url,
|
||||
custom_code=custom_short_code,
|
||||
title=title
|
||||
)
|
||||
link_data['short_url'] = short_result['short_url']
|
||||
link_data['short_code'] = short_result['short_code']
|
||||
except Exception as e:
|
||||
# If shortening fails, continue without it
|
||||
print(f"URL shortening failed: {e}")
|
||||
|
||||
self.link_pages_db[page_id]['links'].append(link_data)
|
||||
self.link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
|
||||
self._save_link_pages() # Persist to file
|
||||
return True
|
||||
|
||||
def update_link(self, page_id, link_id, title=None, url=None, description=None):
|
||||
"""Update a specific link"""
|
||||
if page_id not in link_pages_db:
|
||||
def update_link(self, page_id, link_id, title=None, url=None, description=None, enable_shortener=None, custom_short_code=None):
|
||||
"""Update a specific link with optional URL shortening"""
|
||||
if page_id not in self.link_pages_db:
|
||||
return False
|
||||
|
||||
for link in link_pages_db[page_id]['links']:
|
||||
for link in self.link_pages_db[page_id]['links']:
|
||||
if link['id'] == link_id:
|
||||
if title is not None:
|
||||
link['title'] = title
|
||||
@@ -58,29 +121,79 @@ class LinkPageManager:
|
||||
if description is not None:
|
||||
link['description'] = description
|
||||
|
||||
link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
|
||||
# Handle URL shortening update
|
||||
if enable_shortener is not None:
|
||||
if enable_shortener and not link.get('short_url'):
|
||||
# Create new short URL
|
||||
try:
|
||||
short_result = self.url_shortener.create_short_url(
|
||||
link['url'],
|
||||
custom_code=custom_short_code,
|
||||
title=link['title']
|
||||
)
|
||||
link['short_url'] = short_result['short_url']
|
||||
link['short_code'] = short_result['short_code']
|
||||
except Exception as e:
|
||||
print(f"URL shortening failed: {e}")
|
||||
elif not enable_shortener and link.get('short_code'):
|
||||
# Remove short URL
|
||||
if link.get('short_code'):
|
||||
self.url_shortener.delete_url(link['short_code'])
|
||||
link['short_url'] = None
|
||||
link['short_code'] = None
|
||||
|
||||
self.link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
|
||||
self._save_link_pages() # Persist to file
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def delete_link(self, page_id, link_id):
|
||||
"""Delete a specific link"""
|
||||
if page_id not in link_pages_db:
|
||||
if page_id not in self.link_pages_db:
|
||||
return False
|
||||
|
||||
links = link_pages_db[page_id]['links']
|
||||
link_pages_db[page_id]['links'] = [link for link in links if link['id'] != link_id]
|
||||
link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
|
||||
links = self.link_pages_db[page_id]['links']
|
||||
for link in links:
|
||||
if link['id'] == link_id and link.get('short_code'):
|
||||
# Delete the short URL if it exists
|
||||
self.url_shortener.delete_url(link['short_code'])
|
||||
|
||||
self.link_pages_db[page_id]['links'] = [link for link in links if link['id'] != link_id]
|
||||
self.link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
|
||||
self._save_link_pages() # Persist to file
|
||||
return True
|
||||
|
||||
def increment_view_count(self, page_id):
|
||||
"""Increment view count for a page"""
|
||||
if page_id in link_pages_db:
|
||||
link_pages_db[page_id]['view_count'] += 1
|
||||
if page_id in self.link_pages_db:
|
||||
self.link_pages_db[page_id]['view_count'] += 1
|
||||
self._save_link_pages() # Persist to file
|
||||
|
||||
def get_page(self, page_id):
|
||||
"""Get page data"""
|
||||
return link_pages_db.get(page_id)
|
||||
# Reload data from file to ensure we have the latest data
|
||||
self.link_pages_db = self._load_link_pages()
|
||||
return self.link_pages_db.get(page_id)
|
||||
|
||||
def page_exists(self, page_id):
|
||||
"""Check if page exists"""
|
||||
return page_id in link_pages_db
|
||||
# Reload data from file to ensure we have the latest data
|
||||
self.link_pages_db = self._load_link_pages()
|
||||
return page_id in self.link_pages_db
|
||||
|
||||
# URL Shortener management methods
|
||||
def create_standalone_short_url(self, url, title="", custom_code=None):
|
||||
"""Create a standalone short URL (not tied to a link page)"""
|
||||
return self.url_shortener.create_short_url(url, custom_code, title)
|
||||
|
||||
def get_short_url_stats(self, short_code):
|
||||
"""Get statistics for a short URL"""
|
||||
return self.url_shortener.get_url_stats(short_code)
|
||||
|
||||
def list_all_short_urls(self):
|
||||
"""List all short URLs in the system"""
|
||||
return self.url_shortener.list_urls()
|
||||
|
||||
def resolve_short_url(self, short_code):
|
||||
"""Resolve a short URL to its original URL"""
|
||||
return self.url_shortener.get_original_url(short_code)
|
||||
|
||||
@@ -6,7 +6,9 @@ import os
|
||||
import qrcode
|
||||
from qrcode.image.styledpil import StyledPilImage
|
||||
from qrcode.image.styles.moduledrawers import RoundedModuleDrawer, CircleModuleDrawer, SquareModuleDrawer
|
||||
from qrcode.image.svg import SvgPathImage, SvgFragmentImage, SvgFillImage
|
||||
from PIL import Image
|
||||
import io
|
||||
|
||||
class QRCodeGenerator:
|
||||
def __init__(self):
|
||||
@@ -19,8 +21,8 @@ class QRCodeGenerator:
|
||||
'style': 'square'
|
||||
}
|
||||
|
||||
def generate_qr_code(self, data, settings=None):
|
||||
"""Generate QR code with custom settings"""
|
||||
def generate_qr_code(self, data, settings=None, format='PNG'):
|
||||
"""Generate QR code with custom settings in PNG or SVG format"""
|
||||
if settings is None:
|
||||
settings = self.default_settings.copy()
|
||||
else:
|
||||
@@ -28,6 +30,13 @@ class QRCodeGenerator:
|
||||
merged_settings.update(settings)
|
||||
settings = merged_settings
|
||||
|
||||
if format.upper() == 'SVG':
|
||||
return self._generate_svg_qr_code(data, settings)
|
||||
else:
|
||||
return self._generate_png_qr_code(data, settings)
|
||||
|
||||
def _generate_png_qr_code(self, data, settings):
|
||||
"""Generate PNG QR code (existing functionality)"""
|
||||
# Create QR code instance
|
||||
qr = qrcode.QRCode(
|
||||
version=1,
|
||||
@@ -64,6 +73,47 @@ class QRCodeGenerator:
|
||||
|
||||
return img
|
||||
|
||||
def _generate_svg_qr_code(self, data, settings):
|
||||
"""Generate SVG QR code"""
|
||||
# Create QR code instance
|
||||
qr = qrcode.QRCode(
|
||||
version=1,
|
||||
error_correction=settings['error_correction'],
|
||||
box_size=settings['size'],
|
||||
border=settings['border'],
|
||||
)
|
||||
|
||||
qr.add_data(data)
|
||||
qr.make(fit=True)
|
||||
|
||||
# Choose SVG image factory based on style
|
||||
if settings['style'] == 'circle':
|
||||
# Use SvgFillImage for better circle support
|
||||
factory = SvgFillImage
|
||||
else:
|
||||
# Use SvgPathImage for square and rounded styles
|
||||
factory = SvgPathImage
|
||||
|
||||
# Generate SVG image
|
||||
img = qr.make_image(
|
||||
image_factory=factory,
|
||||
fill_color=settings['foreground_color'],
|
||||
back_color=settings['background_color']
|
||||
)
|
||||
|
||||
return img
|
||||
|
||||
def generate_qr_code_svg_string(self, data, settings=None):
|
||||
"""Generate QR code as SVG string"""
|
||||
svg_img = self.generate_qr_code(data, settings, format='SVG')
|
||||
|
||||
# Convert SVG image to string
|
||||
svg_buffer = io.BytesIO()
|
||||
svg_img.save(svg_buffer)
|
||||
svg_buffer.seek(0)
|
||||
|
||||
return svg_buffer.getvalue().decode('utf-8')
|
||||
|
||||
def add_logo(self, qr_img, logo_path, logo_size_ratio=0.2):
|
||||
"""Add logo to QR code"""
|
||||
try:
|
||||
|
||||
122
app/utils/url_shortener.py
Normal file
122
app/utils/url_shortener.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""
|
||||
URL Shortener utilities for QR Code Manager
|
||||
"""
|
||||
|
||||
import os
|
||||
import uuid
|
||||
import string
|
||||
import random
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# Data storage directory
|
||||
DATA_DIR = 'data'
|
||||
SHORT_URLS_FILE = os.path.join(DATA_DIR, 'short_urls.json')
|
||||
|
||||
# Ensure data directory exists
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
|
||||
class URLShortener:
|
||||
def __init__(self):
|
||||
self.base_domain = os.environ.get('APP_DOMAIN', 'localhost:5000')
|
||||
# Ensure we have the protocol
|
||||
if not self.base_domain.startswith(('http://', 'https://')):
|
||||
# Use HTTPS for production domains, HTTP for localhost
|
||||
protocol = 'https://' if 'localhost' not in self.base_domain else 'http://'
|
||||
self.base_domain = f"{protocol}{self.base_domain}"
|
||||
|
||||
self.short_urls_db = self._load_short_urls()
|
||||
|
||||
def _load_short_urls(self):
|
||||
"""Load short URLs from JSON file"""
|
||||
try:
|
||||
if os.path.exists(SHORT_URLS_FILE):
|
||||
with open(SHORT_URLS_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
except Exception as e:
|
||||
print(f"Error loading short URLs: {e}")
|
||||
return {}
|
||||
|
||||
def _save_short_urls(self):
|
||||
"""Save short URLs to JSON file"""
|
||||
try:
|
||||
with open(SHORT_URLS_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.short_urls_db, f, indent=2, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
print(f"Error saving short URLs: {e}")
|
||||
|
||||
def generate_short_code(self, length=6):
|
||||
"""Generate a random short code"""
|
||||
characters = string.ascii_letters + string.digits
|
||||
while True:
|
||||
short_code = ''.join(random.choice(characters) for _ in range(length))
|
||||
# Ensure uniqueness
|
||||
if short_code not in self.short_urls_db:
|
||||
return short_code
|
||||
|
||||
def create_short_url(self, original_url, custom_code=None, title=""):
|
||||
"""Create a shortened URL"""
|
||||
# Generate or use custom short code
|
||||
if custom_code and custom_code not in self.short_urls_db:
|
||||
short_code = custom_code
|
||||
else:
|
||||
short_code = self.generate_short_code()
|
||||
|
||||
# Ensure original URL has protocol
|
||||
if not original_url.startswith(('http://', 'https://')):
|
||||
original_url = f'https://{original_url}'
|
||||
|
||||
# Create URL record
|
||||
url_data = {
|
||||
'id': str(uuid.uuid4()),
|
||||
'short_code': short_code,
|
||||
'original_url': original_url,
|
||||
'title': title,
|
||||
'clicks': 0,
|
||||
'created_at': datetime.now().isoformat(),
|
||||
'last_accessed': None
|
||||
}
|
||||
|
||||
self.short_urls_db[short_code] = url_data
|
||||
self._save_short_urls() # Persist to file
|
||||
|
||||
# Return the complete short URL
|
||||
short_url = f"{self.base_domain}/s/{short_code}"
|
||||
return {
|
||||
'short_url': short_url,
|
||||
'short_code': short_code,
|
||||
'original_url': original_url,
|
||||
'id': url_data['id']
|
||||
}
|
||||
|
||||
def get_original_url(self, short_code):
|
||||
"""Get original URL from short code and track click"""
|
||||
if short_code in self.short_urls_db:
|
||||
url_data = self.short_urls_db[short_code]
|
||||
# Track click
|
||||
url_data['clicks'] += 1
|
||||
url_data['last_accessed'] = datetime.now().isoformat()
|
||||
self._save_short_urls() # Persist to file
|
||||
return url_data['original_url']
|
||||
return None
|
||||
|
||||
def get_url_stats(self, short_code):
|
||||
"""Get statistics for a short URL"""
|
||||
return self.short_urls_db.get(short_code)
|
||||
|
||||
def list_urls(self):
|
||||
"""List all short URLs"""
|
||||
return list(self.short_urls_db.values())
|
||||
|
||||
def delete_url(self, short_code):
|
||||
"""Delete a short URL"""
|
||||
if short_code in self.short_urls_db:
|
||||
del self.short_urls_db[short_code]
|
||||
self._save_short_urls() # Persist to file
|
||||
return True
|
||||
return False
|
||||
|
||||
def url_exists(self, short_code):
|
||||
"""Check if short URL exists"""
|
||||
return short_code in self.short_urls_db
|
||||
Reference in New Issue
Block a user