preview: remove screenshot export and restore UI animations
Validate / Validate (push) Successful in 2m52s
Validate / Validate (push) Successful in 2m52s
This commit is contained in:
1 parent
6733e81356
commit
bcf1b87e82
8 files changed
+60
-227
No files matched your search
@@ -21,9 +21,6 @@ REQUIRED_FILES = (
|
|||||||
"docs/README.zh-CN.md",
|
"docs/README.zh-CN.md",
|
||||||
"docs/CONTRIBUTING.zh-CN.md",
|
"docs/CONTRIBUTING.zh-CN.md",
|
||||||
"preview/index.html",
|
"preview/index.html",
|
||||||
"preview/lib/html2canvas/html2canvas.min.js",
|
|
||||||
"preview/lib/html2canvas/LICENSE",
|
|
||||||
"preview/lib/html2canvas/README.md",
|
|
||||||
)
|
)
|
||||||
OPTIONAL_ROOT_DOCUMENTS = ("AGENTS.md", "THIRD_PARTY_NOTICES.md")
|
OPTIONAL_ROOT_DOCUMENTS = ("AGENTS.md", "THIRD_PARTY_NOTICES.md")
|
||||||
DOCUMENTATION_DIRECTORIES = ("docs", ".github/release-notes")
|
DOCUMENTATION_DIRECTORIES = ("docs", ".github/release-notes")
|
||||||
|
|||||||
@@ -30,9 +30,6 @@ class PackagingTests(unittest.TestCase):
|
|||||||
"docs/README.zh-CN.md",
|
"docs/README.zh-CN.md",
|
||||||
"docs/CONTRIBUTING.zh-CN.md",
|
"docs/CONTRIBUTING.zh-CN.md",
|
||||||
"preview/index.html",
|
"preview/index.html",
|
||||||
"preview/lib/html2canvas/html2canvas.min.js",
|
|
||||||
"preview/lib/html2canvas/LICENSE",
|
|
||||||
"preview/lib/html2canvas/README.md",
|
|
||||||
"docs/images/README.md",
|
"docs/images/README.md",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -133,15 +130,8 @@ class PackagingTests(unittest.TestCase):
|
|||||||
self.assertEqual(expected_members, zipped)
|
self.assertEqual(expected_members, zipped)
|
||||||
self.assertEqual(expected_members, tarred)
|
self.assertEqual(expected_members, tarred)
|
||||||
self.assertIn(prefix + "themes/demo/mail/base/head.tmpl", zipped)
|
self.assertIn(prefix + "themes/demo/mail/base/head.tmpl", zipped)
|
||||||
for name in ("html2canvas.min.js", "LICENSE", "README.md"):
|
|
||||||
self.assertIn(prefix + "preview/lib/html2canvas/" + name, zipped)
|
|
||||||
self.assertFalse(any("stale" in name or "/build/" in name for name in zipped))
|
self.assertFalse(any("stale" in name or "/build/" in name for name in zipped))
|
||||||
|
|
||||||
def test_missing_screenshot_library_is_rejected(self):
|
|
||||||
(self.root / "preview/lib/html2canvas/html2canvas.min.js").unlink()
|
|
||||||
with self.assertRaisesRegex(ValueError, "Expected regular file"):
|
|
||||||
PACKER.collect(self.root, self.VERSION)
|
|
||||||
|
|
||||||
def test_existing_archives_are_not_overwritten(self):
|
def test_existing_archives_are_not_overwritten(self):
|
||||||
PACKER.package(self.root, self.VERSION, self.output)
|
PACKER.package(self.root, self.VERSION, self.output)
|
||||||
original_archives = {
|
original_archives = {
|
||||||
|
|||||||
@@ -203,7 +203,6 @@ Contributions to themes, tools, documentation and translations are welcome. The
|
|||||||
This project is licensed under [MIT](LICENSE). Generated release archives retain Gitea's license and snapshot provenance.
|
This project is licensed under [MIT](LICENSE). Generated release archives retain Gitea's license and snapshot provenance.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Notes
|
### Notes
|
||||||
|
|
||||||
[1](#notes): [Mail templates | Gitea Documentation](https://docs.gitea.com/administration/mail-templates/)
|
[1](#notes): [Mail templates | Gitea Documentation](https://docs.gitea.com/administration/mail-templates/)
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ preview/rendered/<LOCALE>.js # 各语言的预览数据
|
|||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
本项目采用 [MIT 许可证](../LICENSE)。生成的发行包保留 Gitea 许可证和快照来源信息,详见[第三方声明](../THIRD_PARTY_NOTICES.md)。
|
本项目采用 [MIT 许可证](../LICENSE)。生成的发行包保留 Gitea 许可证和快照来源信息。
|
||||||
|
|
||||||
---
|
---
|
||||||
### 注释
|
### 注释
|
||||||
|
|||||||
+59
-159
@@ -29,10 +29,6 @@ body{font-family:system-ui,-apple-system,sans-serif;background:var(--bg);color:v
|
|||||||
#topbar .btn-group button+button{border-left:none}
|
#topbar .btn-group button+button{border-left:none}
|
||||||
#topbar .btn-group button.on{background:var(--accent);color:#000;border-color:var(--accent);font-weight:600}
|
#topbar .btn-group button.on{background:var(--accent);color:#000;border-color:var(--accent);font-weight:600}
|
||||||
#topbar .hint{font-size:10px;color:var(--muted);opacity:.5;white-space:nowrap}
|
#topbar .hint{font-size:10px;color:var(--muted);opacity:.5;white-space:nowrap}
|
||||||
#btn-screenshot{display:inline-flex;align-items:center;gap:6px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:var(--rad);padding:6px 12px;font-size:12px;cursor:pointer}
|
|
||||||
#btn-screenshot svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.8;flex-shrink:0}
|
|
||||||
#btn-screenshot:hover:not(:disabled){border-color:var(--accent)}
|
|
||||||
#btn-screenshot:disabled{opacity:.45;cursor:wait}
|
|
||||||
button:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
|
button:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
|
||||||
|
|
||||||
/* Topbar responsive — progressively compact as width shrinks */
|
/* Topbar responsive — progressively compact as width shrinks */
|
||||||
@@ -129,12 +125,15 @@ section.collapsed .section-chevron{transform:rotate(-90deg)}
|
|||||||
|
|
||||||
/* Collapsible content wrapper */
|
/* Collapsible content wrapper */
|
||||||
.section-content{
|
.section-content{
|
||||||
margin-top:10px;
|
display:grid;grid-template-rows:1fr;opacity:1;margin-top:10px;
|
||||||
|
transition:grid-template-rows .3s ease,opacity .25s ease,margin-top .3s ease;
|
||||||
}
|
}
|
||||||
|
.section-content-inner{min-height:0;overflow:hidden}
|
||||||
section.collapsed .section-content{
|
section.collapsed .section-content{
|
||||||
display:none;
|
grid-template-rows:0fr;opacity:0;margin-top:0;
|
||||||
}
|
}
|
||||||
#sec-params .section-content{max-height:50vh;overflow:auto;overscroll-behavior:contain}
|
#sec-params .section-content-inner{max-height:50vh;overflow:auto;overscroll-behavior:contain}
|
||||||
|
section.collapsed .section-content-inner{overflow:hidden}
|
||||||
|
|
||||||
#panel .card{background:var(--bg);border-radius:var(--rad);padding:10px 12px;margin-bottom:8px}
|
#panel .card{background:var(--bg);border-radius:var(--rad);padding:10px 12px;margin-bottom:8px}
|
||||||
#panel .card:last-child{margin-bottom:0}
|
#panel .card:last-child{margin-bottom:0}
|
||||||
@@ -151,12 +150,12 @@ section.collapsed .section-content{
|
|||||||
#panel .stat{font-size:10px;color:var(--muted);margin-top:6px}
|
#panel .stat{font-size:10px;color:var(--muted);margin-top:6px}
|
||||||
|
|
||||||
/* Viewport */
|
/* Viewport */
|
||||||
#viewport{border-radius:var(--rad);box-shadow:0 8px 32px rgba(0,0,0,.45);flex-shrink:0;max-width:100%;overflow:hidden;background:#fff}
|
#viewport{border:0 solid #1e293b;border-radius:var(--rad);box-shadow:0 8px 32px rgba(0,0,0,.45);flex-shrink:0;max-width:100%;overflow:hidden;background:#fff;display:flex;flex-direction:column;transition:width .3s ease,border-radius .3s ease,border-width .3s ease,box-shadow .3s ease}
|
||||||
#viewport.desktop{width:100%;max-width:1386px;height:780px}
|
#viewport.desktop{width:100%;max-width:1386px;height:780px}
|
||||||
#viewport.mobile{width:390px;height:780px;border-radius:28px;border:3px solid #1e293b;box-shadow:0 8px 32px rgba(0,0,0,.5),inset 0 0 0 1px #0f172a;display:flex;flex-direction:column}
|
#viewport.mobile{width:390px;height:780px;border-radius:28px;border:3px solid #1e293b;box-shadow:0 8px 32px rgba(0,0,0,.5),inset 0 0 0 1px #0f172a;display:flex;flex-direction:column}
|
||||||
#viewport.mobile #preview-frame{flex:1;min-height:0}
|
#viewport::before{content:'';display:block;height:0;background:#1e293b;flex-shrink:0;transition:height .3s ease}
|
||||||
#viewport.mobile::before{content:'';display:block;height:24px;background:#1e293b;flex-shrink:0}
|
#viewport.mobile::before{height:24px}
|
||||||
#preview-frame{width:100%;height:100%;border:none;display:block;background:#fff}
|
#preview-frame{width:100%;height:100%;flex:1;min-height:0;border:none;display:block;background:#fff}
|
||||||
#viewport.desktop #preview-frame{border-radius:var(--rad)}
|
#viewport.desktop #preview-frame{border-radius:var(--rad)}
|
||||||
|
|
||||||
/* Loading */
|
/* Loading */
|
||||||
@@ -171,7 +170,7 @@ section.collapsed .section-content{
|
|||||||
.message code{background:rgba(0,0,0,0.12);padding:1px 6px;border-radius:3px}
|
.message code{background:rgba(0,0,0,0.12);padding:1px 6px;border-radius:3px}
|
||||||
|
|
||||||
/* Scrollbar — neutral gray, consistent across all browsers */
|
/* Scrollbar — neutral gray, consistent across all browsers */
|
||||||
html,body,#preview-area,#panel-body,#sec-params .section-content{
|
html,body,#preview-area,#panel-body,#sec-params .section-content-inner{
|
||||||
scrollbar-width:thin;
|
scrollbar-width:thin;
|
||||||
scrollbar-color:rgba(107,125,153,.25) transparent;
|
scrollbar-color:rgba(107,125,153,.25) transparent;
|
||||||
}
|
}
|
||||||
@@ -179,30 +178,30 @@ html,body,#preview-area,#panel-body,#sec-params .section-content{
|
|||||||
-ms-overflow-style:-ms-autohiding-scrollbar;
|
-ms-overflow-style:-ms-autohiding-scrollbar;
|
||||||
}
|
}
|
||||||
#preview-area::-webkit-scrollbar,
|
#preview-area::-webkit-scrollbar,
|
||||||
#sec-params .section-content::-webkit-scrollbar,
|
#sec-params .section-content-inner::-webkit-scrollbar,
|
||||||
#panel-body::-webkit-scrollbar { width:6px; height:6px }
|
#panel-body::-webkit-scrollbar { width:6px; height:6px }
|
||||||
#preview-area::-webkit-scrollbar-track,
|
#preview-area::-webkit-scrollbar-track,
|
||||||
#sec-params .section-content::-webkit-scrollbar-track,
|
#sec-params .section-content-inner::-webkit-scrollbar-track,
|
||||||
#panel-body::-webkit-scrollbar-track { background:transparent }
|
#panel-body::-webkit-scrollbar-track { background:transparent }
|
||||||
#preview-area::-webkit-scrollbar-thumb,
|
#preview-area::-webkit-scrollbar-thumb,
|
||||||
#sec-params .section-content::-webkit-scrollbar-thumb,
|
#sec-params .section-content-inner::-webkit-scrollbar-thumb,
|
||||||
#panel-body::-webkit-scrollbar-thumb {
|
#panel-body::-webkit-scrollbar-thumb {
|
||||||
background-color:rgba(107,125,153,.25);
|
background-color:rgba(107,125,153,.25);
|
||||||
border-radius:3px;
|
border-radius:3px;
|
||||||
border:1px solid rgba(255,255,255,.06);
|
border:1px solid rgba(255,255,255,.06);
|
||||||
}
|
}
|
||||||
#preview-area::-webkit-scrollbar-thumb:hover,
|
#preview-area::-webkit-scrollbar-thumb:hover,
|
||||||
#sec-params .section-content::-webkit-scrollbar-thumb:hover,
|
#sec-params .section-content-inner::-webkit-scrollbar-thumb:hover,
|
||||||
#panel-body::-webkit-scrollbar-thumb:hover { background-color:rgba(107,125,153,.45) }
|
#panel-body::-webkit-scrollbar-thumb:hover { background-color:rgba(107,125,153,.45) }
|
||||||
#preview-area::-webkit-scrollbar-corner,
|
#preview-area::-webkit-scrollbar-corner,
|
||||||
#sec-params .section-content::-webkit-scrollbar-corner,
|
#sec-params .section-content-inner::-webkit-scrollbar-corner,
|
||||||
#panel-body::-webkit-scrollbar-corner { background:transparent }
|
#panel-body::-webkit-scrollbar-corner { background:transparent }
|
||||||
|
|
||||||
@media(max-width:700px){
|
@media(max-width:700px){
|
||||||
#panel{--panel-w:50vw;margin-top:6px;margin-bottom:6px;margin-right:6px}
|
#panel{--panel-w:50vw;margin-top:6px;margin-bottom:6px;margin-right:6px}
|
||||||
#preview-area{padding:12px}
|
#preview-area{padding:12px}
|
||||||
}
|
}
|
||||||
@media(prefers-reduced-motion:reduce){#panel,#panel-handle svg{transition:none}}
|
@media(prefers-reduced-motion:reduce){#panel,#panel-handle svg,.section-content,.section-chevron,#viewport,#viewport::before{transition:none}}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -237,10 +236,6 @@ html,body,#preview-area,#panel-body,#sec-params .section-content{
|
|||||||
<button id="btn-desk" class="on" onclick="setViewport('desktop')">Desktop</button>
|
<button id="btn-desk" class="on" onclick="setViewport('desktop')">Desktop</button>
|
||||||
<button id="btn-mob" onclick="setViewport('mobile')">Mobile</button>
|
<button id="btn-mob" onclick="setViewport('mobile')">Mobile</button>
|
||||||
</div>
|
</div>
|
||||||
<button id="btn-screenshot" type="button" onclick="capturePreview()" disabled>
|
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5l1.5-2h5L16 5h4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2z"/><circle cx="12" cy="13" r="4"/></svg>
|
|
||||||
<span id="screenshot-label">Screenshot</span>
|
|
||||||
</button>
|
|
||||||
<span class="hint">keys: d/m ←→ tab ↑↓ select p toggle panel</span>
|
<span class="hint">keys: d/m ←→ tab ↑↓ select p toggle panel</span>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -270,6 +265,7 @@ html,body,#preview-area,#panel-body,#sec-params .section-content{
|
|||||||
<svg class="section-chevron" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg>
|
<svg class="section-chevron" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-content">
|
<div class="section-content">
|
||||||
|
<div class="section-content-inner">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="label">Type</div>
|
<div class="label">Type</div>
|
||||||
<div class="value" id="panel-name">-</div>
|
<div class="value" id="panel-name">-</div>
|
||||||
@@ -280,6 +276,7 @@ html,body,#preview-area,#panel-body,#sec-params .section-content{
|
|||||||
</div>
|
</div>
|
||||||
<div class="stat" id="panel-stats"></div>
|
<div class="stat" id="panel-stats"></div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="sec-params">
|
<section id="sec-params">
|
||||||
@@ -288,8 +285,10 @@ html,body,#preview-area,#panel-body,#sec-params .section-content{
|
|||||||
<svg class="section-chevron" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg>
|
<svg class="section-chevron" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-content">
|
<div class="section-content">
|
||||||
|
<div class="section-content-inner">
|
||||||
<table id="panel-params"><tr><td colspan="2" style="color:var(--muted)">-</td></tr></table>
|
<table id="panel-params"><tr><td colspan="2" style="color:var(--muted)">-</td></tr></table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="sec-source">
|
<section id="sec-source">
|
||||||
@@ -298,14 +297,15 @@ html,body,#preview-area,#panel-body,#sec-params .section-content{
|
|||||||
<svg class="section-chevron" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg>
|
<svg class="section-chevron" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-content">
|
<div class="section-content">
|
||||||
|
<div class="section-content-inner">
|
||||||
<div class="card" style="font-family:monospace;font-size:10px;color:var(--muted);word-break:break-all" id="panel-path">-</div>
|
<div class="card" style="font-family:monospace;font-size:10px;color:var(--muted);word-break:break-all" id="panel-path">-</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="lib/html2canvas/html2canvas.min.js"></script>
|
|
||||||
<script src="rendered.js"></script>
|
<script src="rendered.js"></script>
|
||||||
<script>
|
<script>
|
||||||
//
|
//
|
||||||
@@ -320,122 +320,6 @@ let localeRequest = 0;
|
|||||||
let renderRequest = 0, renderTimer;
|
let renderRequest = 0, renderTimer;
|
||||||
const localeLoads = new Map();
|
const localeLoads = new Map();
|
||||||
let devMode = false; // true once SSE connects
|
let devMode = false; // true once SSE connects
|
||||||
let previewReady = false;
|
|
||||||
let capturing = false;
|
|
||||||
|
|
||||||
function updateCaptureButton() {
|
|
||||||
document.getElementById('btn-screenshot').disabled = !previewReady || capturing;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Capture only the visible viewport, including the mobile frame when selected.
|
|
||||||
async function capturePreview() {
|
|
||||||
if (!previewReady || capturing) return;
|
|
||||||
const label = document.getElementById('screenshot-label');
|
|
||||||
const selection = { ...state };
|
|
||||||
const request = renderRequest;
|
|
||||||
const frame = document.getElementById('preview-frame');
|
|
||||||
const viewport = document.getElementById('viewport');
|
|
||||||
capturing = true;
|
|
||||||
label.textContent = 'Capturing…';
|
|
||||||
updateCaptureButton();
|
|
||||||
document.getElementById('capture-message')?.remove();
|
|
||||||
try {
|
|
||||||
if (typeof html2canvas !== 'function') throw new Error('Screenshot library unavailable; restore preview/lib/html2canvas.');
|
|
||||||
const doc = frame.contentDocument;
|
|
||||||
if (!doc) throw new Error('Preview document is not accessible.');
|
|
||||||
if (doc.fonts) await doc.fonts.ready;
|
|
||||||
await Promise.all(Array.from(doc.images, image => image.decode().catch(() => {})));
|
|
||||||
if (request !== renderRequest || !previewReady) throw new Error('Preview changed; capture again after it finishes loading.');
|
|
||||||
const width = viewport.offsetWidth;
|
|
||||||
const height = viewport.offsetHeight;
|
|
||||||
const frameWidth = frame.clientWidth;
|
|
||||||
const frameHeight = frame.clientHeight;
|
|
||||||
const scrollX = frame.contentWindow.scrollX;
|
|
||||||
const scrollY = frame.contentWindow.scrollY;
|
|
||||||
const viewportBounds = viewport.getBoundingClientRect();
|
|
||||||
const frameBounds = frame.getBoundingClientRect();
|
|
||||||
const frameLeft = frameBounds.left - viewportBounds.left;
|
|
||||||
const frameTop = frameBounds.top - viewportBounds.top;
|
|
||||||
const scale = Math.min(window.devicePixelRatio || 1, 2);
|
|
||||||
// Render in the mail document's own coordinates. Rendering nested iframes
|
|
||||||
// with the outer document's context can shift centered tables and text.
|
|
||||||
const mailCanvas = await html2canvas(doc.documentElement, {
|
|
||||||
backgroundColor: '#fff',
|
|
||||||
scale,
|
|
||||||
width: frameWidth,
|
|
||||||
height: frameHeight,
|
|
||||||
windowWidth: frameWidth,
|
|
||||||
windowHeight: frameHeight,
|
|
||||||
x: scrollX,
|
|
||||||
y: scrollY,
|
|
||||||
scrollX,
|
|
||||||
scrollY,
|
|
||||||
useCORS: true,
|
|
||||||
logging: false
|
|
||||||
});
|
|
||||||
const canvas = await html2canvas(viewport, {
|
|
||||||
backgroundColor: null,
|
|
||||||
scale,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
useCORS: true,
|
|
||||||
logging: false,
|
|
||||||
ignoreElements: element => element.id === 'preview-frame',
|
|
||||||
onclone: clone => {
|
|
||||||
const clonedViewport = clone.getElementById('viewport');
|
|
||||||
clonedViewport.style.width = width + 'px';
|
|
||||||
clonedViewport.style.maxWidth = 'none';
|
|
||||||
clonedViewport.style.height = height + 'px';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (request !== renderRequest || !previewReady) throw new Error('Preview changed; capture again after it finishes loading.');
|
|
||||||
const context = canvas.getContext('2d');
|
|
||||||
if (!context) throw new Error('Could not create the screenshot canvas.');
|
|
||||||
const style = getComputedStyle(viewport);
|
|
||||||
const border = parseFloat(style.borderLeftWidth) || 0;
|
|
||||||
const radius = Math.max(0, (parseFloat(style.borderTopLeftRadius) || 0) - border);
|
|
||||||
// Clip the composite to the container's inner rounded border, including
|
|
||||||
// the mobile frame. No top bar or sidebar enters the exported image.
|
|
||||||
const left = border, top = border;
|
|
||||||
const right = width - border, bottom = height - border;
|
|
||||||
const rounding = Math.min(radius, (right - left) / 2, (bottom - top) / 2);
|
|
||||||
context.save();
|
|
||||||
context.scale(scale, scale);
|
|
||||||
context.beginPath();
|
|
||||||
context.moveTo(left + rounding, top);
|
|
||||||
context.arcTo(right, top, right, bottom, rounding);
|
|
||||||
context.arcTo(right, bottom, left, bottom, rounding);
|
|
||||||
context.arcTo(left, bottom, left, top, rounding);
|
|
||||||
context.arcTo(left, top, right, top, rounding);
|
|
||||||
context.closePath();
|
|
||||||
context.clip();
|
|
||||||
context.drawImage(mailCanvas, frameLeft, frameTop, frameWidth, frameHeight);
|
|
||||||
context.restore();
|
|
||||||
const blob = await new Promise((resolve, reject) => canvas.toBlob(
|
|
||||||
value => value ? resolve(value) : reject(new Error('Could not encode the screenshot.')),
|
|
||||||
'image/png'
|
|
||||||
));
|
|
||||||
const filename = [selection.theme, selection.template, selection.locale]
|
|
||||||
.map(value => value.replace(/[<>:"/\\|?*\x00-\x1f]/g, '-')).join('_') + '.png';
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.download = filename;
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
link.remove();
|
|
||||||
// Give the browser time to start reading the download before releasing it.
|
|
||||||
setTimeout(() => URL.revokeObjectURL(url), 60000);
|
|
||||||
} catch (error) {
|
|
||||||
const message = showMessage('', 'error');
|
|
||||||
message.id = 'capture-message';
|
|
||||||
message.textContent = 'Screenshot failed: ' + String(error.message || error);
|
|
||||||
} finally {
|
|
||||||
capturing = false;
|
|
||||||
label.textContent = 'Screenshot';
|
|
||||||
updateCaptureButton();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Client transforms
|
// Client transforms
|
||||||
@@ -470,10 +354,7 @@ function render() {
|
|||||||
const loading = document.getElementById('loading');
|
const loading = document.getElementById('loading');
|
||||||
if(!rendered) return;
|
if(!rendered) return;
|
||||||
|
|
||||||
// Same-origin DOM access is required for capture; scripts remain disabled.
|
frame.setAttribute('sandbox', '');
|
||||||
frame.setAttribute('sandbox', 'allow-same-origin');
|
|
||||||
previewReady = false;
|
|
||||||
updateCaptureButton();
|
|
||||||
|
|
||||||
frame.style.display = 'none';
|
frame.style.display = 'none';
|
||||||
loading.style.display = 'flex';
|
loading.style.display = 'flex';
|
||||||
@@ -490,8 +371,6 @@ function render() {
|
|||||||
loading.style.display = 'none';
|
loading.style.display = 'none';
|
||||||
frame.style.display = 'block';
|
frame.style.display = 'block';
|
||||||
frame.onload = null;
|
frame.onload = null;
|
||||||
previewReady = true;
|
|
||||||
updateCaptureButton();
|
|
||||||
}
|
}
|
||||||
frame.onload = finish;
|
frame.onload = finish;
|
||||||
frame.srcdoc = content;
|
frame.srcdoc = content;
|
||||||
@@ -514,7 +393,8 @@ function togglePanel() {
|
|||||||
|
|
||||||
function toggleSection(id) {
|
function toggleSection(id) {
|
||||||
const sec = document.getElementById(id);
|
const sec = document.getElementById(id);
|
||||||
sec.classList.toggle('collapsed');
|
const collapsed = sec.classList.toggle('collapsed');
|
||||||
|
sec.querySelector('.section-content').inert = collapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -556,11 +436,11 @@ function switchTheme(v) { state.theme=v; render(); }
|
|||||||
function switchTemplate(v) { state.template=v; switchLocale(state.locale); }
|
function switchTemplate(v) { state.template=v; switchLocale(state.locale); }
|
||||||
function switchClient(v) { state.client=v; render(); }
|
function switchClient(v) { state.client=v; render(); }
|
||||||
function setViewport(v) {
|
function setViewport(v) {
|
||||||
|
if (state.viewport === v) return;
|
||||||
state.viewport=v;
|
state.viewport=v;
|
||||||
document.getElementById('btn-desk').classList.toggle('on',v==='desktop');
|
document.getElementById('btn-desk').classList.toggle('on',v==='desktop');
|
||||||
document.getElementById('btn-mob').classList.toggle('on',v==='mobile');
|
document.getElementById('btn-mob').classList.toggle('on',v==='mobile');
|
||||||
document.getElementById('viewport').className=v;
|
document.getElementById('viewport').className=v;
|
||||||
render();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -602,6 +482,37 @@ function fillSelect(id, entries, selected) {
|
|||||||
select.value=selected;
|
select.value=selected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stable native names also cover browsers with incomplete ICU locale data.
|
||||||
|
const LOCALE_NAMES = {
|
||||||
|
'cs-CZ':'čeština (Česko)', 'de-DE':'Deutsch (Deutschland)',
|
||||||
|
'el-GR':'Ελληνικά (Ελλάδα)', 'en-US':'English (United States)',
|
||||||
|
'es-ES':'español (España)', 'fa-IR':'فارسی (ایران)',
|
||||||
|
'fi-FI':'suomi (Suomi)', 'fr-FR':'français (France)',
|
||||||
|
'ga-IE':'Gaeilge (Éire)', 'hu-HU':'magyar (Magyarország)',
|
||||||
|
'id-ID':'Indonesia (Indonesia)', 'is-IS':'íslenska (Ísland)',
|
||||||
|
'it-IT':'italiano (Italia)', 'ja-JP':'日本語 (日本)',
|
||||||
|
'ko-KR':'한국어 (대한민국)', 'lv-LV':'latviešu (Latvija)',
|
||||||
|
'nl-NL':'Nederlands (Nederland)', 'pl-PL':'polski (Polska)',
|
||||||
|
'pt-BR':'português (Brasil)', 'pt-PT':'português (Portugal)',
|
||||||
|
'ru-RU':'русский (Россия)', 'si-LK':'සිංහල (ශ්රී ලංකාව)',
|
||||||
|
'sk-SK':'slovenčina (Slovensko)',
|
||||||
|
'sv-SE':'svenska (Sverige)', 'tr-TR':'Türkçe (Türkiye)',
|
||||||
|
'uk-UA':'українська (Україна)', 'zh-CN':'中文 (简体,中国)',
|
||||||
|
'zh-TW':'中文 (繁體,台灣)'
|
||||||
|
};
|
||||||
|
|
||||||
|
function localeLabel(locale) {
|
||||||
|
let name = LOCALE_NAMES[locale];
|
||||||
|
if (!name) {
|
||||||
|
try {
|
||||||
|
if (Intl.DisplayNames.supportedLocalesOf([locale]).length) {
|
||||||
|
name = new Intl.DisplayNames([locale], {type:'language', fallback:'none'}).of(locale);
|
||||||
|
}
|
||||||
|
} catch (_) { /* Unknown languages remain selectable by their locale code. */ }
|
||||||
|
}
|
||||||
|
return name && name !== locale ? locale + ' — ' + name : locale;
|
||||||
|
}
|
||||||
|
|
||||||
function applyManifest(data) {
|
function applyManifest(data) {
|
||||||
if (!data || data.schema !== 1 || !data.languages.length || !data.themes.length) {
|
if (!data || data.schema !== 1 || !data.languages.length || !data.themes.length) {
|
||||||
throw new Error('Invalid preview manifest; regenerate with go run . preview all');
|
throw new Error('Invalid preview manifest; regenerate with go run . preview all');
|
||||||
@@ -612,16 +523,7 @@ function applyManifest(data) {
|
|||||||
if (!data.languages.includes(state.locale)) state.locale=data.languages.includes('en-US') ? 'en-US' : data.languages[0];
|
if (!data.languages.includes(state.locale)) state.locale=data.languages.includes('en-US') ? 'en-US' : data.languages[0];
|
||||||
fillSelect('sel-theme',data.themes.map(t=>[t,t.charAt(0).toUpperCase()+t.slice(1)]),state.theme);
|
fillSelect('sel-theme',data.themes.map(t=>[t,t.charAt(0).toUpperCase()+t.slice(1)]),state.theme);
|
||||||
fillSelect('sel-template',Object.entries(REGISTRY).map(([id,t])=>[id,t.name]),state.template);
|
fillSelect('sel-template',Object.entries(REGISTRY).map(([id,t])=>[id,t.name]),state.template);
|
||||||
fillSelect('sel-locale',data.languages.map(locale=>{
|
fillSelect('sel-locale',data.languages.map(locale=>[locale,localeLabel(locale)]),state.locale);
|
||||||
let name;
|
|
||||||
try {
|
|
||||||
// Show each language in its own language, independent of the browser UI.
|
|
||||||
if (Intl.DisplayNames.supportedLocalesOf([locale]).length) {
|
|
||||||
name=new Intl.DisplayNames([locale],{type:'language',fallback:'none'}).of(locale);
|
|
||||||
}
|
|
||||||
} catch (_) { /* Older browsers can still select languages by locale code. */ }
|
|
||||||
return [locale,name && name!==locale ? locale+' — '+name : locale];
|
|
||||||
}),state.locale);
|
|
||||||
document.querySelectorAll('#sel-locale option').forEach(option=>{
|
document.querySelectorAll('#sel-locale option').forEach(option=>{
|
||||||
option.lang=option.value;
|
option.lang=option.value;
|
||||||
option.dir='auto';
|
option.dir='auto';
|
||||||
@@ -650,8 +552,6 @@ function loadLocale(lang) {
|
|||||||
async function switchLocale(lang) {
|
async function switchLocale(lang) {
|
||||||
if (!manifest?.languages.includes(lang)) return;
|
if (!manifest?.languages.includes(lang)) return;
|
||||||
state.locale=lang; rendered=null;
|
state.locale=lang; rendered=null;
|
||||||
previewReady=false;
|
|
||||||
updateCaptureButton();
|
|
||||||
const request=++localeRequest;
|
const request=++localeRequest;
|
||||||
document.getElementById('preview-frame').style.display='none';
|
document.getElementById('preview-frame').style.display='none';
|
||||||
document.getElementById('loading').style.display='flex';
|
document.getElementById('loading').style.display='flex';
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
Copyright (c) 2012 Niklas von Hertzen
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person
|
|
||||||
obtaining a copy of this software and associated documentation
|
|
||||||
files (the "Software"), to deal in the Software without
|
|
||||||
restriction, including without limitation the rights to use,
|
|
||||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the
|
|
||||||
Software is furnished to do so, subject to the following
|
|
||||||
conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be
|
|
||||||
included in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
||||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
||||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
||||||
OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# html2canvas
|
|
||||||
|
|
||||||
Version: **1.4.1**, distributed under the [MIT license](LICENSE).
|
|
||||||
|
|
||||||
The preview uses this local copy to export the visible email container as PNG without a CDN connection. Release archives include the library and its license.
|
|
||||||
|
|
||||||
- Project: https://github.com/niklasvh/html2canvas
|
|
||||||
- Distribution: https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js
|
|
||||||
- SHA-256: `e87e550794322e574a1fda0c1549a3c70dae5a93d9113417a429016838eab8cb`
|
|
||||||
|
|
||||||
Capture uses DOM rendering, so CSS support and cross-origin images follow [html2canvas's documented limitations](https://html2canvas.hertzen.com/documentation). The iframe permits same-origin DOM access while keeping scripts disabled. Keep the version, license and checksum together when updating the library.
|
|
||||||
-20
@@ -1,20 +0,0 @@
|
|||||||
/*!
|
|
||||||
* html2canvas 1.4.1 <https://html2canvas.hertzen.com>
|
|
||||||
* Copyright (c) 2022 Niklas von Hertzen <https://hertzen.com>
|
|
||||||
* Released under MIT License
|
|
||||||
*/
|
|
||||||
!function(A,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(A="undefined"!=typeof globalThis?globalThis:A||self).html2canvas=e()}(this,function(){"use strict";
|
|
||||||
/*! *****************************************************************************
|
|
||||||
Copyright (c) Microsoft Corporation.
|
|
||||||
|
|
||||||
Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
purpose with or without fee is hereby granted.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
***************************************************************************** */var r=function(A,e){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(A,e){A.__proto__=e}||function(A,e){for(var t in e)Object.prototype.hasOwnProperty.call(e,t)&&(A[t]=e[t])})(A,e)};function A(A,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function t(){this.constructor=A}r(A,e),A.prototype=null===e?Object.create(e):(t.prototype=e.prototype,new t)}var h=function(){return(h=Object.assign||function(A){for(var e,t=1,r=arguments.length;t<r;t++)for(var B in e=arguments[t])Object.prototype.hasOwnProperty.call(e,B)&&(A[B]=e[B]);return A}).apply(this,arguments)};function a(A,s,o,i){return new(o=o||Promise)(function(t,e){function r(A){try{n(i.next(A))}catch(A){e(A)}}function B(A){try{n(i.throw(A))}catch(A){e(A)}}function n(A){var e;A.done?t(A.value):((e=A.value)instanceof o?e:new o(function(A){A(e)})).then(r,B)}n((i=i.apply(A,s||[])).next())})}function H(t,r){var B,n,s,o={label:0,sent:function(){if(1&s[0])throw s[1];return s[1]},trys:[],ops:[]},A={next:e(0),throw:e(1),return:e(2)};return"function"==typeof Symbol&&(A[Symbol.iterator]=function(){return this}),A;function e(e){return function(A){return function(e){if(B)throw new TypeError("Generator is already executing.");for(;o;)try{if(B=1,n&&(s=2&e[0]?n.return:e[0]?n.throw||((s=n.return)&&s.call(n),0):n.next)&&!(s=s.call(n,e[1])).done)return s;switch(n=0,(e=s?[2&e[0],s.value]:e)[0]){case 0:case 1:s=e;break;case 4:return o.label++,{value:e[1],done:!1};case 5:o.label++,n=e[1],e=[0];continue;case 7:e=o.ops.pop(),o.trys.pop();continue;default:if(!(s=0<(s=o.trys).length&&s[s.length-1])&&(6===e[0]||2===e[0])){o=0;continue}if(3===e[0]&&(!s||e[1]>s[0]&&e[1]<s[3])){o.label=e[1];break}if(6===e[0]&&o.label<s[1]){o.label=s[1],s=e;break}if(s&&o.label<s[2]){o.label=s[2],o.ops.push(e);break}s[2]&&o.ops.pop(),o.trys.pop();continue}e=r.call(t,o)}catch(A){e=[6,A],n=0}finally{B=s=0}if(5&e[0])throw e[1];return{value:e[0]?e[1]:void 0,done:!0}}([e,A])}}}function t(A,e,t){if(t||2===arguments.length)for(var r,B=0,n=e.length;B<n;B++)!r&&B in e||((r=r||Array.prototype.slice.call(e,0,B))[B]=e[B]);return A.concat(r||e)}var d=(B.prototype.add=function(A,e,t,r){return new B(this.left+A,this.top+e,this.width+t,this.height+r)},B.fromClientRect=function(A,e){return new B(e.left+A.windowBounds.left,e.top+A.windowBounds.top,e.width,e.height)},B.fromDOMRectList=function(A,e){e=Array.from(e).find(function(A){return 0!==A.width});return e?new B(e.left+A.windowBounds.left,e.top+A.windowBounds.top,e.width,e.height):B.EMPTY},B.EMPTY=new B(0,0,0,0),B);function B(A,e,t,r){this.left=A,this.top=e,this.width=t,this.height=r}for(var f=function(A,e){return d.fromClientRect(A,e.getBoundingClientRect())},Q=function(A){for(var e=[],t=0,r=A.length;t<r;){var B,n=A.charCodeAt(t++);55296<=n&&n<=56319&&t<r?56320==(64512&(B=A.charCodeAt(t++)))?e.push(((1023&n)<<10)+(1023&B)+65536):(e.push(n),t--):e.push(n)}return e},g=function(){for(var A=[],e=0;e<arguments.length;e++)A[e]=arguments[e];if(String.fromCodePoint)return String.fromCodePoint.apply(String,A);var t=A.length;if(!t)return"";for(var r=[],B=-1,n="";++B<t;){var s=A[B];s<=65535?r.push(s):(s-=65536,r.push(55296+(s>>10),s%1024+56320)),(B+1===t||16384<r.length)&&(n+=String.fromCharCode.apply(String,r),r.length=0)}return n},e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",n="undefined"==typeof Uint8Array?[]:new Uint8Array(256),s=0;s<e.length;s++)n[e.charCodeAt(s)]=s;for(var o="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",c="undefined"==typeof Uint8Array?[]:new Uint8Array(256),i=0;i<o.length;i++)c[o.charCodeAt(i)]=i;function w(A,e,t){return A.slice?A.slice(e,t):new Uint16Array(Array.prototype.slice.call(A,e,t))}var U=(l.prototype.get=function(A){var e;if(0<=A){if(A<55296||56319<A&&A<=65535)return e=this.index[A>>5],this.data[e=(e<<2)+(31&A)];if(A<=65535)return e=this.index[2048+(A-55296>>5)],this.data[e=(e<<2)+(31&A)];if(A<this.highStart)return e=this.index[e=2080+(A>>11)],e=this.index[e+=A>>5&63],this.data[e=(e<<2)+(31&A)];if(A<=1114111)return this.data[this.highValueIndex]}return this.errorValue},l);function l(A,e,t,r,B,n){this.initialValue=A,this.errorValue=e,this.highStart=t,this.highValueIndex=r,this.index=B,this.data=n}for(var C="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",u="undefined"==typeof Uint8Array?[]:new Uint8Array(256),F=0;F<C.length;F++)u[C.charCodeAt(F)]=F;function p(A,e,t,r){var B=r[t];if(Array.isArray(A)?-1!==A.indexOf(B):A===B)for(var n=t;n<=r.length;){if((o=r[++n])===e)return 1;if(o!==D)break}if(B===D)for(n=t;0<n;){var s=r[--n];if(Array.isArray(A)?-1!==A.indexOf(s):A===s)for(var o,i=t;i<=r.length;){if((o=r[++i])===e)return 1;if(o!==D)break}if(s!==D)break}}function E(A,e){for(var t=A;0<=t;){var r=e[t];if(r!==D)return r;t--}return 0}function I(t,A){var e=(B=function(A,r){void 0===r&&(r="strict");var B=[],n=[],sLine truncated
|
|
||||||
Reference in new issue
Block a user