Compare commits
5 Commits
418ba6e4ba
...
f71720c156
| Author | SHA1 | Date | |
|---|---|---|---|
| f71720c156 | |||
| 511c9bee48 | |||
| 866bfe4d6d | |||
| 742ff0e553 | |||
| 4ec1feb2a9 |
22
app.py
22
app.py
@@ -8,8 +8,8 @@ from flask import Flask, render_template, abort, jsonify, request
|
|||||||
from comics_data import (
|
from comics_data import (
|
||||||
COMICS, COMIC_NAME, COPYRIGHT_NAME, SITE_URL, FULL_WIDTH_DEFAULT, PLAIN_DEFAULT, LOGO_IMAGE, LOGO_MODE,
|
COMICS, COMIC_NAME, COPYRIGHT_NAME, SITE_URL, FULL_WIDTH_DEFAULT, PLAIN_DEFAULT, LOGO_IMAGE, LOGO_MODE,
|
||||||
HEADER_IMAGE, FOOTER_IMAGE, BANNER_IMAGE, COMPACT_FOOTER, ARCHIVE_FULL_WIDTH, SECTIONS_ENABLED,
|
HEADER_IMAGE, FOOTER_IMAGE, BANNER_IMAGE, COMPACT_FOOTER, ARCHIVE_FULL_WIDTH, SECTIONS_ENABLED,
|
||||||
USE_COMIC_NAV_ICONS, USE_HEADER_NAV_ICONS, USE_FOOTER_SOCIAL_ICONS, NEWSLETTER_ENABLED,
|
USE_COMIC_NAV_ICONS, USE_HEADER_NAV_ICONS, USE_FOOTER_SOCIAL_ICONS, USE_SHARE_ICONS, NEWSLETTER_ENABLED,
|
||||||
SOCIAL_INSTAGRAM, SOCIAL_YOUTUBE, SOCIAL_EMAIL, API_SPEC_LINK
|
SOCIAL_INSTAGRAM, SOCIAL_YOUTUBE, SOCIAL_EMAIL, API_SPEC_LINK, EMBED_ENABLED, PERMALINK_ENABLED
|
||||||
)
|
)
|
||||||
import markdown
|
import markdown
|
||||||
|
|
||||||
@@ -45,11 +45,14 @@ def inject_global_settings():
|
|||||||
'use_comic_nav_icons': USE_COMIC_NAV_ICONS,
|
'use_comic_nav_icons': USE_COMIC_NAV_ICONS,
|
||||||
'use_header_nav_icons': USE_HEADER_NAV_ICONS,
|
'use_header_nav_icons': USE_HEADER_NAV_ICONS,
|
||||||
'use_footer_social_icons': USE_FOOTER_SOCIAL_ICONS,
|
'use_footer_social_icons': USE_FOOTER_SOCIAL_ICONS,
|
||||||
|
'use_share_icons': USE_SHARE_ICONS,
|
||||||
'newsletter_enabled': NEWSLETTER_ENABLED,
|
'newsletter_enabled': NEWSLETTER_ENABLED,
|
||||||
'social_instagram': SOCIAL_INSTAGRAM,
|
'social_instagram': SOCIAL_INSTAGRAM,
|
||||||
'social_youtube': SOCIAL_YOUTUBE,
|
'social_youtube': SOCIAL_YOUTUBE,
|
||||||
'social_email': SOCIAL_EMAIL,
|
'social_email': SOCIAL_EMAIL,
|
||||||
'api_spec_link': API_SPEC_LINK
|
'api_spec_link': API_SPEC_LINK,
|
||||||
|
'embed_enabled': EMBED_ENABLED,
|
||||||
|
'permalink_enabled': PERMALINK_ENABLED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -168,6 +171,19 @@ def comic(comic_id):
|
|||||||
comic=comic, total_comics=len(COMICS))
|
comic=comic, total_comics=len(COMICS))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/embed/<int:comic_id>')
|
||||||
|
def embed(comic_id):
|
||||||
|
"""Embeddable comic view - minimal layout for iframes"""
|
||||||
|
if not EMBED_ENABLED:
|
||||||
|
abort(404)
|
||||||
|
comic = get_comic_by_number(comic_id)
|
||||||
|
if not comic:
|
||||||
|
abort(404)
|
||||||
|
# Use comic title if present, otherwise use #X format
|
||||||
|
page_title = comic.get('title', f"#{comic_id}")
|
||||||
|
return render_template('embed.html', title=page_title, comic=comic)
|
||||||
|
|
||||||
|
|
||||||
def group_comics_by_section(comics_list):
|
def group_comics_by_section(comics_list):
|
||||||
"""Group comics by section. Returns list of (section_title, comics) tuples"""
|
"""Group comics by section. Returns list of (section_title, comics) tuples"""
|
||||||
if not SECTIONS_ENABLED:
|
if not SECTIONS_ENABLED:
|
||||||
|
|||||||
@@ -73,6 +73,10 @@ USE_HEADER_NAV_ICONS = True
|
|||||||
# Uses instagram.png, youtube.png, and mail.png from static/images/icons/
|
# Uses instagram.png, youtube.png, and mail.png from static/images/icons/
|
||||||
USE_FOOTER_SOCIAL_ICONS = True
|
USE_FOOTER_SOCIAL_ICONS = True
|
||||||
|
|
||||||
|
# Global setting: Set to True to show icons in share buttons (permalink and embed)
|
||||||
|
# Uses link.png for permalink and embed.png for embed from static/images/icons/
|
||||||
|
USE_SHARE_ICONS = True
|
||||||
|
|
||||||
# Global setting: Set to True to show newsletter section in footer
|
# Global setting: Set to True to show newsletter section in footer
|
||||||
NEWSLETTER_ENABLED = False
|
NEWSLETTER_ENABLED = False
|
||||||
|
|
||||||
@@ -85,6 +89,14 @@ SOCIAL_EMAIL = None # e.g., 'mailto:your@email.com'
|
|||||||
# Path is relative to static/ directory
|
# Path is relative to static/ directory
|
||||||
API_SPEC_LINK = None # Set to 'openapi.yml' to enable
|
API_SPEC_LINK = None # Set to 'openapi.yml' to enable
|
||||||
|
|
||||||
|
# Global setting: Set to True to enable comic embed functionality
|
||||||
|
# When enabled, users can get embed codes to display comics on other websites
|
||||||
|
EMBED_ENABLED = True
|
||||||
|
|
||||||
|
# Global setting: Set to True to enable permalink copy button
|
||||||
|
# When enabled, users can easily copy a direct link to the current comic
|
||||||
|
PERMALINK_ENABLED = True
|
||||||
|
|
||||||
COMICS = [
|
COMICS = [
|
||||||
{
|
{
|
||||||
'number': 1,
|
'number': 1,
|
||||||
|
|||||||
@@ -1067,4 +1067,237 @@ footer.compact-footer .footer-section:not(:last-child)::after {
|
|||||||
content: '|';
|
content: '|';
|
||||||
margin-left: var(--space-md);
|
margin-left: var(--space-md);
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
SHARE/EMBED FEATURE STYLES
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
/* Share section (contains permalink and embed buttons) */
|
||||||
|
.comic-share-section {
|
||||||
|
margin-top: var(--space-lg);
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-md);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Permalink button */
|
||||||
|
.btn-permalink {
|
||||||
|
padding: var(--space-sm) var(--space-lg);
|
||||||
|
background-color: var(--color-background);
|
||||||
|
border: var(--border-width-thin) solid var(--color-border);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: var(--letter-spacing-tight);
|
||||||
|
transition: background-color var(--transition-speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-permalink:hover {
|
||||||
|
background-color: var(--color-hover-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-permalink:focus {
|
||||||
|
outline: 3px solid var(--color-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-permalink.copied {
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
color: var(--color-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Embed button */
|
||||||
|
.btn-embed {
|
||||||
|
padding: var(--space-sm) var(--space-lg);
|
||||||
|
background-color: var(--color-background);
|
||||||
|
border: var(--border-width-thin) solid var(--color-border);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: var(--letter-spacing-tight);
|
||||||
|
transition: background-color var(--transition-speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-embed:hover {
|
||||||
|
background-color: var(--color-hover-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-embed:focus {
|
||||||
|
outline: 3px solid var(--color-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Share button icons */
|
||||||
|
.btn-with-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-with-icon .btn-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-permalink.copied .btn-icon {
|
||||||
|
filter: invert(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal overlay */
|
||||||
|
.modal {
|
||||||
|
display: none; /* Hidden by default */
|
||||||
|
position: fixed;
|
||||||
|
z-index: 10000;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal content box */
|
||||||
|
.modal-content {
|
||||||
|
background-color: var(--color-background);
|
||||||
|
margin: 10% auto;
|
||||||
|
padding: 0;
|
||||||
|
border: var(--border-width-thick) solid var(--color-border);
|
||||||
|
max-width: 600px;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal header */
|
||||||
|
.modal-header {
|
||||||
|
padding: var(--space-md) var(--space-lg);
|
||||||
|
border-bottom: var(--border-width-thin) solid var(--color-border);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: var(--letter-spacing-tight);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal close button */
|
||||||
|
.modal-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: var(--font-size-3xl);
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-text);
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close:hover,
|
||||||
|
.modal-close:focus {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal body */
|
||||||
|
.modal-body {
|
||||||
|
padding: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body p {
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Embed code textarea */
|
||||||
|
#embed-code {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100px;
|
||||||
|
padding: var(--space-md);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
border: var(--border-width-thin) solid var(--color-border);
|
||||||
|
background-color: var(--color-background);
|
||||||
|
color: var(--color-text);
|
||||||
|
resize: vertical;
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
#embed-code:focus {
|
||||||
|
outline: 3px solid var(--color-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Copy button */
|
||||||
|
.btn-copy {
|
||||||
|
padding: var(--space-sm) var(--space-lg);
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
border: var(--border-width-thin) solid var(--color-border);
|
||||||
|
color: var(--color-background);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: var(--letter-spacing-tight);
|
||||||
|
width: 100%;
|
||||||
|
transition: background-color var(--transition-speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-copy:hover {
|
||||||
|
background-color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-copy:focus {
|
||||||
|
outline: 3px solid var(--color-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-copy.copied {
|
||||||
|
background-color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Embed preview link */
|
||||||
|
.embed-preview-link {
|
||||||
|
margin-top: var(--space-md);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed-preview-link a {
|
||||||
|
color: var(--color-text);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed-preview-link a:hover {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsive modal */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.modal-content {
|
||||||
|
margin: 20% auto;
|
||||||
|
width: 95%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h2 {
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#embed-code {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
BIN
static/images/icons/embed.png
LFS
Normal file
BIN
static/images/icons/embed.png
LFS
Normal file
Binary file not shown.
BIN
static/images/icons/link.png
LFS
Normal file
BIN
static/images/icons/link.png
LFS
Normal file
Binary file not shown.
@@ -125,6 +125,11 @@
|
|||||||
// Update URL without reload
|
// Update URL without reload
|
||||||
history.pushState({ comicId: comic.number }, '', `/comic/${comic.number}`);
|
history.pushState({ comicId: comic.number }, '', `/comic/${comic.number}`);
|
||||||
|
|
||||||
|
// Dispatch custom event for other features (like embed button)
|
||||||
|
window.dispatchEvent(new CustomEvent('comicUpdated', {
|
||||||
|
detail: { comicNumber: comic.number }
|
||||||
|
}));
|
||||||
|
|
||||||
// Move focus to comic image for keyboard navigation accessibility
|
// Move focus to comic image for keyboard navigation accessibility
|
||||||
const comicImageFocus = document.getElementById('comic-image-focus');
|
const comicImageFocus = document.getElementById('comic-image-focus');
|
||||||
if (comicImageFocus) {
|
if (comicImageFocus) {
|
||||||
@@ -202,10 +207,22 @@
|
|||||||
firstBtn.className = isIconNav ? 'btn-icon-nav' : 'btn btn-nav';
|
firstBtn.className = isIconNav ? 'btn-icon-nav' : 'btn btn-nav';
|
||||||
firstBtn.onclick = (e) => { e.preventDefault(); loadComic(1); };
|
firstBtn.onclick = (e) => { e.preventDefault(); loadComic(1); };
|
||||||
firstBtn.removeAttribute('aria-disabled');
|
firstBtn.removeAttribute('aria-disabled');
|
||||||
|
if (firstBtn.tagName === 'SPAN') {
|
||||||
|
firstBtn.setAttribute('tabindex', '0');
|
||||||
|
firstBtn.setAttribute('role', 'button');
|
||||||
|
firstBtn.onkeydown = (e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
loadComic(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
firstBtn.className = isIconNav ? 'btn-icon-nav btn-icon-disabled' : 'btn btn-nav btn-disabled';
|
firstBtn.className = isIconNav ? 'btn-icon-nav btn-icon-disabled' : 'btn btn-nav btn-disabled';
|
||||||
firstBtn.onclick = null;
|
firstBtn.onclick = null;
|
||||||
|
firstBtn.onkeydown = null;
|
||||||
firstBtn.setAttribute('aria-disabled', 'true');
|
firstBtn.setAttribute('aria-disabled', 'true');
|
||||||
|
firstBtn.removeAttribute('tabindex');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Previous button
|
// Previous button
|
||||||
@@ -214,10 +231,22 @@
|
|||||||
prevBtn.className = isIconNav ? 'btn-icon-nav' : 'btn btn-nav';
|
prevBtn.className = isIconNav ? 'btn-icon-nav' : 'btn btn-nav';
|
||||||
prevBtn.onclick = (e) => { e.preventDefault(); loadComic(currentNumber - 1); };
|
prevBtn.onclick = (e) => { e.preventDefault(); loadComic(currentNumber - 1); };
|
||||||
prevBtn.removeAttribute('aria-disabled');
|
prevBtn.removeAttribute('aria-disabled');
|
||||||
|
if (prevBtn.tagName === 'SPAN') {
|
||||||
|
prevBtn.setAttribute('tabindex', '0');
|
||||||
|
prevBtn.setAttribute('role', 'button');
|
||||||
|
prevBtn.onkeydown = (e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
loadComic(currentNumber - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
prevBtn.className = isIconNav ? 'btn-icon-nav btn-icon-disabled' : 'btn btn-nav btn-disabled';
|
prevBtn.className = isIconNav ? 'btn-icon-nav btn-icon-disabled' : 'btn btn-nav btn-disabled';
|
||||||
prevBtn.onclick = null;
|
prevBtn.onclick = null;
|
||||||
|
prevBtn.onkeydown = null;
|
||||||
prevBtn.setAttribute('aria-disabled', 'true');
|
prevBtn.setAttribute('aria-disabled', 'true');
|
||||||
|
prevBtn.removeAttribute('tabindex');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Comic date display
|
// Comic date display
|
||||||
@@ -231,10 +260,22 @@
|
|||||||
nextBtn.className = isIconNav ? 'btn-icon-nav' : 'btn btn-nav';
|
nextBtn.className = isIconNav ? 'btn-icon-nav' : 'btn btn-nav';
|
||||||
nextBtn.onclick = (e) => { e.preventDefault(); loadComic(currentNumber + 1); };
|
nextBtn.onclick = (e) => { e.preventDefault(); loadComic(currentNumber + 1); };
|
||||||
nextBtn.removeAttribute('aria-disabled');
|
nextBtn.removeAttribute('aria-disabled');
|
||||||
|
if (nextBtn.tagName === 'SPAN') {
|
||||||
|
nextBtn.setAttribute('tabindex', '0');
|
||||||
|
nextBtn.setAttribute('role', 'button');
|
||||||
|
nextBtn.onkeydown = (e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
loadComic(currentNumber + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
nextBtn.className = isIconNav ? 'btn-icon-nav btn-icon-disabled' : 'btn btn-nav btn-disabled';
|
nextBtn.className = isIconNav ? 'btn-icon-nav btn-icon-disabled' : 'btn btn-nav btn-disabled';
|
||||||
nextBtn.onclick = null;
|
nextBtn.onclick = null;
|
||||||
|
nextBtn.onkeydown = null;
|
||||||
nextBtn.setAttribute('aria-disabled', 'true');
|
nextBtn.setAttribute('aria-disabled', 'true');
|
||||||
|
nextBtn.removeAttribute('tabindex');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Latest button
|
// Latest button
|
||||||
@@ -243,10 +284,22 @@
|
|||||||
latestBtn.className = isIconNav ? 'btn-icon-nav' : 'btn btn-nav';
|
latestBtn.className = isIconNav ? 'btn-icon-nav' : 'btn btn-nav';
|
||||||
latestBtn.onclick = (e) => { e.preventDefault(); loadComic(totalComics); };
|
latestBtn.onclick = (e) => { e.preventDefault(); loadComic(totalComics); };
|
||||||
latestBtn.removeAttribute('aria-disabled');
|
latestBtn.removeAttribute('aria-disabled');
|
||||||
|
if (latestBtn.tagName === 'SPAN') {
|
||||||
|
latestBtn.setAttribute('tabindex', '0');
|
||||||
|
latestBtn.setAttribute('role', 'button');
|
||||||
|
latestBtn.onkeydown = (e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
loadComic(totalComics);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
latestBtn.className = isIconNav ? 'btn-icon-nav btn-icon-disabled' : 'btn btn-nav btn-disabled';
|
latestBtn.className = isIconNav ? 'btn-icon-nav btn-icon-disabled' : 'btn btn-nav btn-disabled';
|
||||||
latestBtn.onclick = null;
|
latestBtn.onclick = null;
|
||||||
|
latestBtn.onkeydown = null;
|
||||||
latestBtn.setAttribute('aria-disabled', 'true');
|
latestBtn.setAttribute('aria-disabled', 'true');
|
||||||
|
latestBtn.removeAttribute('tabindex');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
129
static/js/embed.js
Normal file
129
static/js/embed.js
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
// Embed functionality for Sunday Comics
|
||||||
|
// Handles showing embed code modal and copying to clipboard
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const modal = document.getElementById('embed-modal');
|
||||||
|
const embedButton = document.getElementById('embed-button');
|
||||||
|
const closeButton = modal ? modal.querySelector('.modal-close') : null;
|
||||||
|
const embedCodeTextarea = document.getElementById('embed-code');
|
||||||
|
const copyButton = document.getElementById('copy-embed-code');
|
||||||
|
const previewLink = document.getElementById('embed-preview-link');
|
||||||
|
|
||||||
|
if (!modal || !embedButton) {
|
||||||
|
// Embed feature not enabled or elements not found
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the site URL from the page (we'll add it as a data attribute)
|
||||||
|
const siteUrl = document.body.getAttribute('data-site-url') || window.location.origin;
|
||||||
|
|
||||||
|
// Open modal when embed button is clicked
|
||||||
|
embedButton.addEventListener('click', function() {
|
||||||
|
const comicNumber = this.getAttribute('data-comic-number');
|
||||||
|
if (!comicNumber) return;
|
||||||
|
|
||||||
|
// Generate embed code
|
||||||
|
const embedUrl = `${siteUrl}/embed/${comicNumber}`;
|
||||||
|
const embedCode = `<iframe src="${embedUrl}" width="800" height="600" frameborder="0" scrolling="no" style="max-width: 100%;" title="Comic #${comicNumber}"></iframe>`;
|
||||||
|
|
||||||
|
// Set the embed code in the textarea
|
||||||
|
embedCodeTextarea.value = embedCode;
|
||||||
|
|
||||||
|
// Set the preview link
|
||||||
|
previewLink.href = embedUrl;
|
||||||
|
|
||||||
|
// Show the modal
|
||||||
|
modal.style.display = 'block';
|
||||||
|
modal.setAttribute('aria-hidden', 'false');
|
||||||
|
|
||||||
|
// Focus on the textarea
|
||||||
|
embedCodeTextarea.focus();
|
||||||
|
embedCodeTextarea.select();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close modal when close button is clicked
|
||||||
|
if (closeButton) {
|
||||||
|
closeButton.addEventListener('click', function() {
|
||||||
|
closeModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal when clicking outside the modal content
|
||||||
|
modal.addEventListener('click', function(event) {
|
||||||
|
if (event.target === modal) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close modal with Escape key
|
||||||
|
document.addEventListener('keydown', function(event) {
|
||||||
|
if (event.key === 'Escape' && modal.style.display === 'block') {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copy embed code to clipboard
|
||||||
|
if (copyButton) {
|
||||||
|
copyButton.addEventListener('click', function() {
|
||||||
|
embedCodeTextarea.select();
|
||||||
|
embedCodeTextarea.setSelectionRange(0, 99999); // For mobile devices
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Modern clipboard API
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard.writeText(embedCodeTextarea.value).then(function() {
|
||||||
|
showCopyFeedback();
|
||||||
|
}).catch(function() {
|
||||||
|
// Fallback to execCommand
|
||||||
|
fallbackCopy();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Fallback for older browsers
|
||||||
|
fallbackCopy();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
fallbackCopy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fallbackCopy() {
|
||||||
|
try {
|
||||||
|
document.execCommand('copy');
|
||||||
|
showCopyFeedback();
|
||||||
|
} catch (err) {
|
||||||
|
alert('Failed to copy. Please select and copy manually.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCopyFeedback() {
|
||||||
|
const originalText = copyButton.textContent;
|
||||||
|
copyButton.textContent = 'Copied!';
|
||||||
|
copyButton.classList.add('copied');
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
copyButton.textContent = originalText;
|
||||||
|
copyButton.classList.remove('copied');
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
modal.setAttribute('aria-hidden', 'true');
|
||||||
|
|
||||||
|
// Return focus to the embed button
|
||||||
|
if (embedButton) {
|
||||||
|
embedButton.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update embed button when comic changes via client-side navigation
|
||||||
|
// This integrates with the existing comic-nav.js functionality
|
||||||
|
window.addEventListener('comicUpdated', function(event) {
|
||||||
|
if (event.detail && event.detail.comicNumber && embedButton) {
|
||||||
|
embedButton.setAttribute('data-comic-number', event.detail.comicNumber);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
84
static/js/permalink.js
Normal file
84
static/js/permalink.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
// Permalink functionality for Sunday Comics
|
||||||
|
// Handles copying comic permalinks to clipboard
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const permalinkButton = document.getElementById('permalink-button');
|
||||||
|
|
||||||
|
if (!permalinkButton) {
|
||||||
|
// Permalink feature not enabled or button not found
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the site URL from the page
|
||||||
|
const siteUrl = document.body.getAttribute('data-site-url') || window.location.origin;
|
||||||
|
|
||||||
|
// Copy permalink when button is clicked
|
||||||
|
permalinkButton.addEventListener('click', function() {
|
||||||
|
const comicNumber = this.getAttribute('data-comic-number');
|
||||||
|
if (!comicNumber) return;
|
||||||
|
|
||||||
|
// Generate permalink URL
|
||||||
|
const permalink = `${siteUrl}/comic/${comicNumber}`;
|
||||||
|
|
||||||
|
// Copy to clipboard
|
||||||
|
copyToClipboard(permalink);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copy text to clipboard
|
||||||
|
function copyToClipboard(text) {
|
||||||
|
// Modern clipboard API
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard.writeText(text).then(function() {
|
||||||
|
showCopyFeedback();
|
||||||
|
}).catch(function() {
|
||||||
|
// Fallback for older browsers
|
||||||
|
fallbackCopy(text);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Fallback for older browsers
|
||||||
|
fallbackCopy(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback copy method for older browsers
|
||||||
|
function fallbackCopy(text) {
|
||||||
|
// Create temporary textarea
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.value = text;
|
||||||
|
textarea.style.position = 'fixed';
|
||||||
|
textarea.style.opacity = '0';
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.focus();
|
||||||
|
textarea.select();
|
||||||
|
|
||||||
|
try {
|
||||||
|
document.execCommand('copy');
|
||||||
|
showCopyFeedback();
|
||||||
|
} catch (err) {
|
||||||
|
alert('Failed to copy. Please copy manually: ' + text);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show visual feedback that the permalink was copied
|
||||||
|
function showCopyFeedback() {
|
||||||
|
const originalText = permalinkButton.textContent;
|
||||||
|
permalinkButton.textContent = 'Copied!';
|
||||||
|
permalinkButton.classList.add('copied');
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
permalinkButton.textContent = originalText;
|
||||||
|
permalinkButton.classList.remove('copied');
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update permalink button when comic changes via client-side navigation
|
||||||
|
window.addEventListener('comicUpdated', function(event) {
|
||||||
|
if (event.detail && event.detail.comicNumber && permalinkButton) {
|
||||||
|
permalinkButton.setAttribute('data-comic-number', event.detail.comicNumber);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
<link rel="alternate" type="application/rss+xml" title="{{ comic_name }} RSS Feed" href="{{ url_for('static', filename='feed.rss') }}">
|
<link rel="alternate" type="application/rss+xml" title="{{ comic_name }} RSS Feed" href="{{ url_for('static', filename='feed.rss') }}">
|
||||||
{% block extra_css %}{% endblock %}
|
{% block extra_css %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body data-site-url="{{ site_url }}">
|
||||||
<!-- Skip to main content link for keyboard navigation -->
|
<!-- Skip to main content link for keyboard navigation -->
|
||||||
<a href="#main-content" class="skip-to-main">Skip to main content</a>
|
<a href="#main-content" class="skip-to-main">Skip to main content</a>
|
||||||
|
|
||||||
@@ -185,7 +185,31 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Embed Code Modal -->
|
||||||
|
{% if embed_enabled %}
|
||||||
|
<div id="embed-modal" class="modal" role="dialog" aria-labelledby="embed-modal-title" aria-hidden="true">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 id="embed-modal-title">Embed This Comic</h2>
|
||||||
|
<button class="modal-close" aria-label="Close embed modal">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Copy this code to embed the comic on your website:</p>
|
||||||
|
<textarea id="embed-code" readonly onfocus="this.select()" onclick="this.select()" aria-label="Embed code"></textarea>
|
||||||
|
<button id="copy-embed-code" class="btn-copy" aria-label="Copy embed code to clipboard">Copy Code</button>
|
||||||
|
<p class="embed-preview-link">Preview: <a id="embed-preview-link" href="#" target="_blank" rel="noopener noreferrer">Open embed in new window</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<script src="{{ url_for('static', filename='js/comic-nav.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/comic-nav.js') }}"></script>
|
||||||
|
{% if embed_enabled %}
|
||||||
|
<script src="{{ url_for('static', filename='js/embed.js') }}"></script>
|
||||||
|
{% endif %}
|
||||||
|
{% if permalink_enabled %}
|
||||||
|
<script src="{{ url_for('static', filename='js/permalink.js') }}"></script>
|
||||||
|
{% endif %}
|
||||||
{% block extra_js %}{% endblock %}
|
{% block extra_js %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -131,6 +131,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if embed_enabled or permalink_enabled %}
|
||||||
|
<div class="comic-share-section">
|
||||||
|
{% if permalink_enabled %}
|
||||||
|
<button class="btn-permalink{% if use_share_icons %} btn-with-icon{% endif %}" id="permalink-button" data-comic-number="{{ comic.number }}" aria-label="Copy permalink to this comic">
|
||||||
|
{% if use_share_icons %}<img src="{{ url_for('static', filename='images/icons/link.png') }}" alt="" class="btn-icon" aria-hidden="true">{% endif %}Copy Permalink
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% if embed_enabled %}
|
||||||
|
<button class="btn-embed{% if use_share_icons %} btn-with-icon{% endif %}" id="embed-button" data-comic-number="{{ comic.number }}" aria-label="Get embed code for this comic">
|
||||||
|
{% if use_share_icons %}<img src="{{ url_for('static', filename='images/icons/embed.png') }}" alt="" class="btn-icon" aria-hidden="true">{% endif %}Share/Embed
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if comic.author_note %}
|
{% if comic.author_note %}
|
||||||
<div class="comic-transcript">
|
<div class="comic-transcript">
|
||||||
<h3>Author Note</h3>
|
<h3>Author Note</h3>
|
||||||
|
|||||||
156
templates/embed.html
Normal file
156
templates/embed.html
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{ title }} - {{ comic_name }}</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
background: #fff;
|
||||||
|
color: #000;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.embed-container {
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
text-align: center;
|
||||||
|
border: 2px solid #000;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.embed-header {
|
||||||
|
padding: 15px;
|
||||||
|
border-bottom: 2px solid #000;
|
||||||
|
background: #fff;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
.embed-header-text {
|
||||||
|
flex: 1;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.embed-logo {
|
||||||
|
max-width: 120px;
|
||||||
|
max-height: 60px;
|
||||||
|
height: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.embed-title {
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
color: #000;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
.embed-date {
|
||||||
|
font-size: 0.75em;
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.embed-image-wrapper {
|
||||||
|
padding: 0;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 2px solid #000;
|
||||||
|
}
|
||||||
|
.embed-image-link {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
.embed-image {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.embed-footer {
|
||||||
|
padding: 12px 15px;
|
||||||
|
font-size: 0.75em;
|
||||||
|
background: #f8f8f8;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
.embed-footer a {
|
||||||
|
color: #000;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
.embed-footer a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.embed-alt-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
/* Mobile responsive */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.embed-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.embed-logo {
|
||||||
|
max-width: 100px;
|
||||||
|
max-height: 50px;
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
.embed-title {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
.embed-date {
|
||||||
|
font-size: 0.7em;
|
||||||
|
}
|
||||||
|
.embed-footer {
|
||||||
|
font-size: 0.7em;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="embed-container">
|
||||||
|
<div class="embed-header">
|
||||||
|
<div class="embed-header-text">
|
||||||
|
<h1 class="embed-title">{{ comic.title if comic.title else '#' ~ comic.number }}</h1>
|
||||||
|
<p class="embed-date">{{ comic.formatted_date }}</p>
|
||||||
|
</div>
|
||||||
|
{% if logo_image %}
|
||||||
|
<img src="{{ site_url }}/static/images/{{ logo_image }}" alt="{{ comic_name }}" class="embed-logo">
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="embed-image-wrapper">
|
||||||
|
<a href="{{ site_url }}/comic/{{ comic.number }}" class="embed-image-link" target="_blank" rel="noopener noreferrer" aria-label="View {{ comic.title if comic.title else 'comic #' ~ comic.number }} on {{ comic_name }}">
|
||||||
|
{% if comic.mobile_filename %}
|
||||||
|
<picture>
|
||||||
|
<source media="(max-width: 768px)" srcset="{{ url_for('static', filename='images/comics/' + comic.mobile_filename) }}">
|
||||||
|
<img src="{{ url_for('static', filename='images/comics/' + comic.filename) }}"
|
||||||
|
alt="{{ comic.title if comic.title else '#' ~ comic.number }}"
|
||||||
|
title="{{ comic.alt_text }}"
|
||||||
|
class="embed-image">
|
||||||
|
</picture>
|
||||||
|
{% else %}
|
||||||
|
<img src="{{ url_for('static', filename='images/comics/' + comic.filename) }}"
|
||||||
|
alt="{{ comic.title if comic.title else '#' ~ comic.number }}"
|
||||||
|
title="{{ comic.alt_text }}"
|
||||||
|
class="embed-image">
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="embed-footer">
|
||||||
|
<a href="{{ site_url }}/comic/{{ comic.number }}" target="_blank" rel="noopener noreferrer">View on {{ comic_name }} →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -91,6 +91,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if embed_enabled or permalink_enabled %}
|
||||||
|
<div class="comic-share-section">
|
||||||
|
{% if permalink_enabled %}
|
||||||
|
<button class="btn-permalink{% if use_share_icons %} btn-with-icon{% endif %}" id="permalink-button" data-comic-number="{{ comic.number }}" aria-label="Copy permalink to this comic">
|
||||||
|
{% if use_share_icons %}<img src="{{ url_for('static', filename='images/icons/link.png') }}" alt="" class="btn-icon" aria-hidden="true">{% endif %}Copy Permalink
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% if embed_enabled %}
|
||||||
|
<button class="btn-embed{% if use_share_icons %} btn-with-icon{% endif %}" id="embed-button" data-comic-number="{{ comic.number }}" aria-label="Get embed code for this comic">
|
||||||
|
{% if use_share_icons %}<img src="{{ url_for('static', filename='images/icons/embed.png') }}" alt="" class="btn-icon" aria-hidden="true">{% endif %}Share/Embed
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if comic.author_note %}
|
{% if comic.author_note %}
|
||||||
<div class="comic-transcript">
|
<div class="comic-transcript">
|
||||||
<h3>Author Note</h3>
|
<h3>Author Note</h3>
|
||||||
|
|||||||
Reference in New Issue
Block a user