Files
simple-nanoshare/templates/side/views/webpage/links/composer.htm
T
daniel156161 ed62837f6d
Build and Push Docker Container / build-and-push (push) Successful in 1m36s
feat: add self-contained link composer
- Add structured URL codec with compact alphabets and dictionary support.
- Add public decode, redirect, and QR endpoints for composed links.
- Add authenticated encode API and link composer UI.
- Generate PNG QR codes via qrcode with Pillow support.
- Register the new blueprint and navigation entry.
- Cover public decode, auth gating, redirect URL parsing, and QR output.
- Bump NanoShare to 1.27.0 and lock new dependencies.
2026-08-14 08:56:42 +02:00

219 lines
8.1 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.htm" %}
{% block title %}NanoShare - Link Composer{% endblock %}
{% block meta %}
<meta name="description" content="Encode, decode and QR-share self-contained NanoShare links.">
<meta name="robots" content="noindex, nofollow" />
{% endblock %}
{% block content %}
<main class="wrap link-composer">
<section class="card hero-card">
<span class="hero-badge"><i class="fa-solid fa-link"></i> Link Composer</span>
<h1 class="title">Encode, decode and QR links</h1>
<p class="subtitle">Decode is public. Log in to create compressed links and QR codes.</p>
</section>
<section class="composer-grid {% if not can_encode %}is-single{% endif %}">
{% if can_encode %}
<section class="card composer-card">
<div class="card-heading">
<h2>Encode</h2>
</div>
<label for="compose-url">URL</label>
<textarea id="compose-url" placeholder="https://example.com/docs?page=1"></textarea>
<label class="switch-row">
<input id="qr-mode" type="checkbox">
QR alphabet mode
</label>
<div class="actions-row">
<button id="encode-btn" class="btn" type="button">Encode URL</button>
<button id="clear-btn" class="btn btn-ghost" type="button">Clear</button>
</div>
<div id="encode-error" class="form-error"></div>
</section>
{% endif %}
<section class="card composer-card">
<div class="card-heading">
<h2>Decode</h2>
</div>
<label for="compose-code">Code or redirect link</label>
<textarea id="compose-code" placeholder="u... or https://example.com/l/u..."></textarea>
{% if can_encode %}
<div class="switch-row switch-row-spacer" aria-hidden="true">&nbsp;</div>
{% endif %}
<div class="actions-row">
<button id="decode-btn" class="btn" type="button">Decode Code</button>
{% if not can_encode %}
<a class="btn btn-ghost" href="{{ url_for('auth_login.login') }}">Login to encode</a>
{% endif %}
</div>
<div id="decode-error" class="form-error"></div>
</section>
</section>
<section id="result-panel" class="card result-panel">
<div class="card-heading">
<h2>Result</h2>
<span id="mode-badge" class="badge">structured</span>
</div>
<div class="result-grid">
<div class="result-main">
<div class="output-block">
<span class="output-label">Compressed code</span>
<div class="copy-field">
<code id="out-code"></code>
<button class="btn btn-ghost mini" type="button" data-copy="out-code">Copy</button>
</div>
</div>
<div class="output-block">
<span class="output-label">Redirect link</span>
<div class="copy-field">
<code id="out-redirect"></code>
<button class="btn btn-ghost mini" type="button" data-copy="out-redirect">Copy</button>
</div>
</div>
<div class="output-block">
<span class="output-label">Decoded URL</span>
<div class="copy-field">
<code id="out-url"></code>
<button class="btn btn-ghost mini" type="button" data-copy="out-url">Copy</button>
</div>
</div>
<div class="stats-grid">
<div class="stat-card"><strong id="stat-original"></strong><span>Original</span></div>
<div class="stat-card"><strong id="stat-code"></strong><span>Code</span></div>
<div class="stat-card"><strong id="stat-ratio"></strong><span>Ratio</span></div>
<div class="stat-card"><strong id="stat-bits"></strong><span>Bits</span></div>
</div>
</div>
<div class="qr-card">
<img id="qr-image" alt="Generated QR code">
<div class="actions-row centered">
<a id="qr-open" class="btn btn-ghost" target="_blank" rel="noreferrer">Open PNG</a>
<a id="qr-download" class="btn" download="nanoshare-link-qr.png">Download PNG</a>
</div>
</div>
</div>
</section>
</main>
{% endblock %}
{% block script %}
<script>
const canEncode = {{ 'true' if can_encode else 'false' }};
const encodeApiUrl = {{ encode_api_url | tojson }};
const decodeApiUrl = {{ decode_api_url | tojson }};
const redirectPrefix = {{ redirect_prefix | tojson }};
const qrPrefix = {{ qr_prefix | tojson }};
const $ = (id) => document.getElementById(id);
function setError(id, message) {
const el = $(id);
el.textContent = message || '';
el.classList.toggle('visible', Boolean(message));
}
function setText(id, value) {
$(id).textContent = value || '';
}
async function postJson(path, body) {
const response = await fetch(path, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
});
const data = await response.json();
if (!response.ok || data.ok === false) throw new Error(data.error || data.detail || 'Request failed');
return data;
}
function showResult(data) {
setText('out-code', data.code || $('compose-code').value.trim());
setText('out-url', data.url);
setText('out-redirect', data.redirect_url || `${location.origin}${redirectPrefix}${encodeURIComponent(data.code || $('compose-code').value.trim())}`);
setText('stat-original', data.original_length ?? '');
setText('stat-code', data.code_length ?? ((data.code || '').length || ''));
setText('stat-ratio', data.ratio ?? '');
setText('stat-bits', data.bit_length ?? '');
setText('mode-badge', data.mode || 'decoded');
const code = data.code || $('compose-code').value.trim();
if (code) {
const qrUrl = data.qr_url || `${location.origin}${qrPrefix}${encodeURIComponent(code)}`;
$('qr-image').src = qrUrl;
$('qr-open').href = qrUrl;
$('qr-download').href = qrUrl;
}
$('result-panel').classList.add('visible');
}
$('encode-btn')?.addEventListener('click', async () => {
if (!canEncode) return;
setError('encode-error', '');
try {
const data = await postJson(encodeApiUrl, { url: $('compose-url').value, qr: $('qr-mode').checked });
$('compose-code').value = data.code;
showResult(data);
} catch (error) {
setError('encode-error', error.message);
}
});
function extractCode(value) {
const trimmed = value.trim();
try {
const url = new URL(trimmed);
const fullPath = url.pathname + url.search + url.hash;
if (fullPath.startsWith(redirectPrefix)) return decodeURIComponent(fullPath.slice(redirectPrefix.length));
if (fullPath.startsWith(qrPrefix)) return decodeURIComponent(fullPath.slice(qrPrefix.length));
} catch (_) {}
return trimmed;
}
$('decode-btn').addEventListener('click', async () => {
setError('decode-error', '');
try {
const input = $('compose-code').value.trim();
const code = extractCode(input);
const data = await postJson(decodeApiUrl, { code: input });
showResult({
...data,
code,
redirect_url: `${location.origin}${redirectPrefix}${encodeURIComponent(code)}`,
qr_url: `${location.origin}${qrPrefix}${encodeURIComponent(code)}`,
mode: 'decoded',
});
} catch (error) {
setError('decode-error', error.message);
}
});
$('clear-btn')?.addEventListener('click', () => {
$('compose-url').value = '';
$('compose-code').value = '';
$('result-panel').classList.remove('visible');
setError('encode-error', '');
setError('decode-error', '');
});
document.addEventListener('click', async (event) => {
const button = event.target.closest('[data-copy]');
if (!button) return;
await navigator.clipboard.writeText($(button.dataset.copy).textContent);
const old = button.textContent;
button.textContent = 'Copied';
setTimeout(() => button.textContent = old, 900);
});
</script>
{% endblock %}