mirror of
https://github.com/sartoopjj/thefeed.git
synced 2026-05-18 07:44:45 +03:00
refactor: update scoreboard layout to use flexbox for improved responsiveness and clarity
This commit is contained in:
@@ -1594,9 +1594,57 @@
|
||||
background: var(--card-bg)
|
||||
}
|
||||
|
||||
/* Bank-row action buttons (+ / ✕). Sized 32×32 with a real
|
||||
border so they're easy to tap on touch screens; spaced apart
|
||||
so a stray tap on "+" doesn't hit "✕". */
|
||||
/* Bank scoreboard rows — vertical stack of cards, two columns:
|
||||
a "main" block with address + a wrapped stats line, and a
|
||||
fixed-width actions column on the trailing edge. The action
|
||||
buttons stay visible on phone widths because the main column
|
||||
flexes/wraps instead of pushing the actions off-screen. */
|
||||
.rb-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px
|
||||
}
|
||||
|
||||
.rb-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: var(--card-bg)
|
||||
}
|
||||
|
||||
.rb-row-main {
|
||||
flex: 1;
|
||||
min-width: 0
|
||||
}
|
||||
|
||||
.rb-row-addr {
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap
|
||||
}
|
||||
|
||||
.rb-row-stats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 3px;
|
||||
font-size: 11px;
|
||||
color: var(--text-dim)
|
||||
}
|
||||
|
||||
.rb-row-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0
|
||||
}
|
||||
|
||||
.rb-row-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -1613,10 +1661,6 @@
|
||||
transition: background 120ms, border-color 120ms, color 120ms
|
||||
}
|
||||
|
||||
.rb-row-btn + .rb-row-btn {
|
||||
margin-inline-start: 8px
|
||||
}
|
||||
|
||||
.rb-row-add {
|
||||
color: var(--accent)
|
||||
}
|
||||
@@ -2376,8 +2420,6 @@
|
||||
border-radius: 2px
|
||||
}
|
||||
</style>
|
||||
<!-- Lazy-loadable; only used when the share modal asks for QR. -->
|
||||
<script src="/static/qrcode.js" defer></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -2656,13 +2698,8 @@
|
||||
<label data-i18n="share_uri">Share URI</label>
|
||||
<textarea id="shareModalUri" readonly rows="3" style="width:100%;font-family:monospace;font-size:12px;resize:vertical;box-sizing:border-box"></textarea>
|
||||
</div>
|
||||
<div id="shareModalQrWrap" style="display:none;text-align:center;margin-top:8px">
|
||||
<div id="shareModalQr" style="display:inline-block;background:#fff;padding:10px;border-radius:8px"></div>
|
||||
<div style="font-size:11px;color:var(--text-dim);margin-top:6px" data-i18n="share_qr_hint">Scan with another device to import</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-flat" onclick="closeShareModal()" data-i18n="close">Close</button>
|
||||
<button class="btn btn-outline" id="shareModalQrBtn" onclick="toggleShareModalQr()" data-i18n="show_qr">Show QR</button>
|
||||
<button class="btn btn-primary" onclick="copyShareModalUri()" data-i18n="copy">Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2992,9 +3029,6 @@
|
||||
share_source: 'منبع',
|
||||
profile_name: 'نام پروفایل',
|
||||
share_uri: 'پیوند اشتراک',
|
||||
show_qr: 'نمایش QR',
|
||||
hide_qr: 'پنهان کردن QR',
|
||||
share_qr_hint: 'برای ایمپورت با دستگاه دیگر اسکن کن',
|
||||
paste: 'چسباندن',
|
||||
clipboard_blocked: 'دسترسی به کلیپبورد ممکن نیست',
|
||||
resolver_list_switch_failed: 'تغییر لیست ممکن نشد',
|
||||
@@ -3205,9 +3239,6 @@
|
||||
share_source: 'Source',
|
||||
profile_name: 'Profile name',
|
||||
share_uri: 'Share URI',
|
||||
show_qr: 'Show QR',
|
||||
hide_qr: 'Hide QR',
|
||||
share_qr_hint: 'Scan with another device to import',
|
||||
paste: 'Paste',
|
||||
clipboard_blocked: 'Clipboard access blocked',
|
||||
resolver_list_switch_failed: 'Could not switch list',
|
||||
@@ -4073,6 +4104,9 @@
|
||||
var menu = document.getElementById('resolverTabMenu');
|
||||
if (!menu) return;
|
||||
if (!menu.hidden) { hideResolverTabMenu(); return; }
|
||||
// Defensive: drop any leftover outside-click listener from a
|
||||
// previous open before installing a new one.
|
||||
document.removeEventListener('click', _resolverTabMenuOutside);
|
||||
// Use viewport coordinates from getBoundingClientRect — pairs
|
||||
// with position: fixed in CSS. Anchor the menu's top edge just
|
||||
// below the ⋮ button, and align horizontally so the menu hangs
|
||||
@@ -4123,6 +4157,12 @@
|
||||
var bankAddPickerAddr = '';
|
||||
|
||||
function openBankAddPicker(anchor, addr) {
|
||||
// Strip any pending outside-click listener from a previous
|
||||
// open. Without this, clicking + on a second row while the
|
||||
// menu is still open lets the stale listener fire on the new
|
||||
// click and snap-close the freshly-opened menu — looks like
|
||||
// "+ doesn't work" from the user's seat.
|
||||
hideBankAddMenu();
|
||||
bankAddPickerAddr = addr;
|
||||
var menu = document.getElementById('bankAddMenu');
|
||||
if (!menu) return;
|
||||
@@ -4520,9 +4560,6 @@
|
||||
var p = (profiles && profiles.profiles || []).find(function (x) { return x.id === id });
|
||||
if (!p) return;
|
||||
document.getElementById('shareModalProfileName').textContent = p.nickname || p.config.domain;
|
||||
document.getElementById('shareModalQrWrap').style.display = 'none';
|
||||
var qrBtn = document.getElementById('shareModalQrBtn');
|
||||
if (qrBtn) qrBtn.textContent = t('show_qr') || 'Show QR';
|
||||
// Make sure rlState is loaded — the user may not have opened
|
||||
// the Bank modal yet, in which case rlState is its empty
|
||||
// default and the source dropdown would only have "Bank".
|
||||
@@ -4601,10 +4638,6 @@
|
||||
var uri = getShareModalUri();
|
||||
var ta = document.getElementById('shareModalUri');
|
||||
if (ta) ta.value = uri || (t('no_config') || '');
|
||||
// If QR is currently visible, keep it in sync with the URI.
|
||||
if (document.getElementById('shareModalQrWrap').style.display === 'block') {
|
||||
renderShareModalQr(uri);
|
||||
}
|
||||
}
|
||||
|
||||
function copyShareModalUri() {
|
||||
@@ -4617,34 +4650,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
function toggleShareModalQr() {
|
||||
var wrap = document.getElementById('shareModalQrWrap');
|
||||
var btn = document.getElementById('shareModalQrBtn');
|
||||
if (!wrap) return;
|
||||
if (wrap.style.display === 'block') {
|
||||
wrap.style.display = 'none';
|
||||
if (btn) btn.textContent = t('show_qr') || 'Show QR';
|
||||
} else {
|
||||
wrap.style.display = 'block';
|
||||
if (btn) btn.textContent = t('hide_qr') || 'Hide QR';
|
||||
renderShareModalQr(getShareModalUri());
|
||||
}
|
||||
}
|
||||
|
||||
function renderShareModalQr(uri) {
|
||||
var box = document.getElementById('shareModalQr');
|
||||
if (!box) return;
|
||||
if (!uri) { box.innerHTML = ''; return }
|
||||
try {
|
||||
if (typeof qrEncodeSvg === 'function') {
|
||||
box.innerHTML = qrEncodeSvg(uri, 6);
|
||||
} else {
|
||||
box.innerHTML = '<span style="color:#000;font-size:12px;padding:8px">QR not available</span>';
|
||||
}
|
||||
} catch (e) {
|
||||
box.innerHTML = '<span style="color:#a00;font-size:12px;padding:8px">' + esc(e.message || 'QR error') + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
async function activateProfile(id) {
|
||||
if (id === activeProfileId) { closeProfiles(); return }
|
||||
@@ -7463,43 +7468,39 @@
|
||||
var resolversRefreshTimer = null;
|
||||
function _buildScoreboardTable(board, showRemove, removeFromBank) {
|
||||
if (!board.length) return '<div style="color:var(--text-dim)">' + t('no_active_resolvers') + '</div>';
|
||||
var h = '<table style="width:100%;border-collapse:collapse;font-size:12px">';
|
||||
h += '<thead><tr style="border-bottom:2px solid var(--border);text-align:left">';
|
||||
h += '<th style="padding:6px 8px">Resolver</th>';
|
||||
h += '<th style="padding:6px 8px;text-align:right">' + t('resolver_speed') + '</th>';
|
||||
h += '<th style="padding:6px 8px;text-align:right">' + t('resolver_score') + '</th>';
|
||||
h += '<th style="padding:6px 8px;text-align:center">\u2705</th>';
|
||||
h += '<th style="padding:6px 8px;text-align:center">\u274C</th>';
|
||||
if (showRemove) h += '<th style="padding:6px 8px"></th>';
|
||||
h += '</tr></thead><tbody>';
|
||||
// Row-card layout — left holds resolver address + a stats line
|
||||
// (speed · score · ✅ · ❌), right holds the action buttons.
|
||||
// Replaces the old 6-column table that overflowed off the right
|
||||
// edge on phone widths and hid the × button.
|
||||
var h = '<div class="rb-rows">';
|
||||
for (var i = 0; i < board.length; i++) {
|
||||
var b = board[i];
|
||||
var scoreColor = b.score >= 0.5 ? 'var(--success)' : b.score >= 0.15 ? 'var(--text)' : 'var(--error)';
|
||||
h += '<tr style="border-bottom:1px solid var(--border)">';
|
||||
h += '<td style="padding:5px 8px;font-family:monospace">' + esc(b.addr);
|
||||
if (b.active !== undefined && b.active) h += ' <span style="color:var(--success);font-size:10px">\u25CF</span>';
|
||||
h += '</td>';
|
||||
h += '<td style="padding:5px 8px;text-align:right">' + (b.avgMs > 0 ? Math.round(b.avgMs) + 'ms' : '-') + '</td>';
|
||||
h += '<td style="padding:5px 8px;text-align:right;color:' + scoreColor + ';font-weight:600">' + b.score.toFixed(2) + '</td>';
|
||||
h += '<td style="padding:5px 8px;text-align:center;color:var(--success)">' + b.success + '</td>';
|
||||
h += '<td style="padding:5px 8px;text-align:center;color:var(--error)">' + b.failure + '</td>';
|
||||
var dot = (b.active !== undefined && b.active)
|
||||
? ' <span style="color:var(--success);font-size:10px">●</span>' : '';
|
||||
h += '<div class="rb-row">';
|
||||
h += '<div class="rb-row-main">';
|
||||
h += '<div class="rb-row-addr">' + esc(b.addr) + dot + '</div>';
|
||||
h += '<div class="rb-row-stats">';
|
||||
h += '<span>' + (b.avgMs > 0 ? Math.round(b.avgMs) + 'ms' : '-') + '</span>';
|
||||
h += '<span style="color:' + scoreColor + ';font-weight:600">' + b.score.toFixed(2) + '</span>';
|
||||
h += '<span style="color:var(--success)">✅ ' + b.success + '</span>';
|
||||
h += '<span style="color:var(--error)">❌ ' + b.failure + '</span>';
|
||||
h += '</div></div>';
|
||||
if (showRemove) {
|
||||
var fn = removeFromBank ? 'removeResolverFromBank' : 'removeResolver';
|
||||
h += '<td style="padding:5px 8px;text-align:center;white-space:nowrap">';
|
||||
// Bank tab gets the add-to-list "+" alongside the remove
|
||||
// "✕". Both buttons sized for touch: 32×32, 8px gap, full
|
||||
// border so they read as tap targets rather than glyphs.
|
||||
h += '<div class="rb-row-actions">';
|
||||
if (removeFromBank) {
|
||||
h += '<button class="rb-row-btn rb-row-add" onclick="openBankAddPicker(this,\'' + esc(b.addr) + '\')" '
|
||||
+ 'data-i18n-title="add_to_list" title="Add to list" aria-label="Add to list">+</button>';
|
||||
}
|
||||
h += '<button class="rb-row-btn rb-row-del" onclick="' + fn + '(\'' + esc(b.addr) + '\')" '
|
||||
+ 'title="Remove" aria-label="Remove">×</button>';
|
||||
h += '</td>';
|
||||
h += '</div>';
|
||||
}
|
||||
h += '</tr>';
|
||||
h += '</div>';
|
||||
}
|
||||
h += '</tbody></table>';
|
||||
h += '</div>';
|
||||
return h;
|
||||
}
|
||||
async function _fetchActiveBoard() {
|
||||
@@ -7624,11 +7625,16 @@
|
||||
}
|
||||
function copyResolversList() {
|
||||
var panelId = currentResolverTab === 'active' ? 'resolverPanelActive' : 'resolverBankListEl';
|
||||
var rows = document.querySelectorAll('#' + panelId + ' tbody tr');
|
||||
// New row layout uses .rb-row-addr divs in place of <td>.
|
||||
var addrs = document.querySelectorAll('#' + panelId + ' .rb-row-addr');
|
||||
var lines = [];
|
||||
rows.forEach(function (tr) {
|
||||
var cells = tr.querySelectorAll('td');
|
||||
if (cells.length > 0) lines.push(cells[0].textContent.trim());
|
||||
addrs.forEach(function (el) {
|
||||
// Strip the trailing active-dot span if present.
|
||||
var clone = el.cloneNode(true);
|
||||
var dot = clone.querySelector('span');
|
||||
if (dot) dot.remove();
|
||||
var t = clone.textContent.trim();
|
||||
if (t) lines.push(t);
|
||||
});
|
||||
if (!lines.length) { showToast(t('no_active_resolvers')); return }
|
||||
navigator.clipboard.writeText(lines.join('\n')).then(function () { showToast(t('copied')) });
|
||||
|
||||
@@ -1,394 +0,0 @@
|
||||
// Minimal QR encoder: byte mode + L (low) error correction, versions
|
||||
// 1–10. Renders to SVG. Used by the share modal so the offline app
|
||||
// can produce a scannable code without a CDN dependency.
|
||||
//
|
||||
// Spec: ISO/IEC 18004:2015, simplified for our subset.
|
||||
// - Byte mode only (UTF-8 input).
|
||||
// - Low-level error correction (~7%).
|
||||
// - Versions 1–10 (max ~230 ASCII chars).
|
||||
// - Mask 0 only (skip the 8-mask penalty search).
|
||||
//
|
||||
// Exposes window.qrEncodeSvg(text, scale=4) -> string (SVG markup).
|
||||
|
||||
(function (global) {
|
||||
// ===== Galois field GF(256) tables =====
|
||||
var EXP = new Array(256);
|
||||
var LOG = new Array(256);
|
||||
(function init() {
|
||||
var x = 1;
|
||||
for (var i = 0; i < 255; i++) {
|
||||
EXP[i] = x;
|
||||
LOG[x] = i;
|
||||
x <<= 1;
|
||||
if (x & 0x100) x ^= 0x11d;
|
||||
}
|
||||
EXP[255] = EXP[0];
|
||||
})();
|
||||
|
||||
function gfMul(a, b) {
|
||||
if (a === 0 || b === 0) return 0;
|
||||
return EXP[(LOG[a] + LOG[b]) % 255];
|
||||
}
|
||||
|
||||
function rsPoly(degree) {
|
||||
var poly = [1];
|
||||
for (var i = 0; i < degree; i++) {
|
||||
var next = new Array(poly.length + 1);
|
||||
for (var k = 0; k < next.length; k++) next[k] = 0;
|
||||
for (var j = 0; j < poly.length; j++) {
|
||||
next[j] ^= poly[j];
|
||||
next[j + 1] ^= gfMul(poly[j], EXP[i]);
|
||||
}
|
||||
poly = next;
|
||||
}
|
||||
return poly;
|
||||
}
|
||||
|
||||
function rsEncode(data, ecCount) {
|
||||
var poly = rsPoly(ecCount);
|
||||
var buf = data.slice();
|
||||
for (var i = 0; i < ecCount; i++) buf.push(0);
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var coef = buf[i];
|
||||
if (coef !== 0) {
|
||||
for (var j = 0; j < poly.length; j++) {
|
||||
buf[i + j] ^= gfMul(poly[j], coef);
|
||||
}
|
||||
}
|
||||
}
|
||||
return buf.slice(data.length);
|
||||
}
|
||||
|
||||
// ===== Version capacity tables (L correction, byte mode) =====
|
||||
// {size, ecPerBlock, groups: [[blockCount, dataBytesPerBlock], ...]}
|
||||
var VER = [
|
||||
{ size: 21, ec: 7, groups: [[1, 19]] },
|
||||
{ size: 25, ec: 10, groups: [[1, 34]] },
|
||||
{ size: 29, ec: 15, groups: [[1, 55]] },
|
||||
{ size: 33, ec: 20, groups: [[1, 80]] },
|
||||
{ size: 37, ec: 26, groups: [[1, 108]] },
|
||||
{ size: 41, ec: 18, groups: [[2, 68]] },
|
||||
{ size: 45, ec: 20, groups: [[2, 78]] },
|
||||
{ size: 49, ec: 24, groups: [[2, 97]] },
|
||||
{ size: 53, ec: 30, groups: [[2, 116]] },
|
||||
{ size: 57, ec: 18, groups: [[2, 68], [2, 69]] }
|
||||
];
|
||||
|
||||
// Alignment-pattern centre coordinates for each version.
|
||||
var ALIGN = [
|
||||
[], [6, 18], [6, 22], [6, 26], [6, 30],
|
||||
[6, 34], [6, 22, 38], [6, 24, 42], [6, 26, 46], [6, 28, 50]
|
||||
];
|
||||
|
||||
function totalDataBytes(info) {
|
||||
var t = 0;
|
||||
for (var i = 0; i < info.groups.length; i++) {
|
||||
t += info.groups[i][0] * info.groups[i][1];
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
function pickVersion(byteLen) {
|
||||
for (var v = 0; v < VER.length; v++) {
|
||||
var info = VER[v];
|
||||
var ccBits = (v + 1) <= 9 ? 8 : 16;
|
||||
var maxBytes = totalDataBytes(info) - 2 - Math.ceil(ccBits / 8);
|
||||
if (byteLen <= maxBytes) return v + 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function utf8Bytes(s) {
|
||||
var bytes = [];
|
||||
for (var i = 0; i < s.length; i++) {
|
||||
var c = s.charCodeAt(i);
|
||||
if (c < 0x80) {
|
||||
bytes.push(c);
|
||||
} else if (c < 0x800) {
|
||||
bytes.push(0xc0 | (c >> 6));
|
||||
bytes.push(0x80 | (c & 0x3f));
|
||||
} else if (c < 0xd800 || c >= 0xe000) {
|
||||
bytes.push(0xe0 | (c >> 12));
|
||||
bytes.push(0x80 | ((c >> 6) & 0x3f));
|
||||
bytes.push(0x80 | (c & 0x3f));
|
||||
} else {
|
||||
var c2 = s.charCodeAt(++i);
|
||||
var cp = 0x10000 + (((c & 0x3ff) << 10) | (c2 & 0x3ff));
|
||||
bytes.push(0xf0 | (cp >> 18));
|
||||
bytes.push(0x80 | ((cp >> 12) & 0x3f));
|
||||
bytes.push(0x80 | ((cp >> 6) & 0x3f));
|
||||
bytes.push(0x80 | (cp & 0x3f));
|
||||
}
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function buildBitstream(bytes, version, info) {
|
||||
var ccBits = version <= 9 ? 8 : 16;
|
||||
var totalBytes = totalDataBytes(info);
|
||||
var totalBits = totalBytes * 8;
|
||||
|
||||
var bits = [];
|
||||
function pushBits(val, n) {
|
||||
for (var i = n - 1; i >= 0; i--) bits.push((val >> i) & 1);
|
||||
}
|
||||
pushBits(0x4, 4); // byte mode
|
||||
pushBits(bytes.length, ccBits);
|
||||
for (var i = 0; i < bytes.length; i++) pushBits(bytes[i], 8);
|
||||
|
||||
var term = Math.min(4, totalBits - bits.length);
|
||||
for (var i = 0; i < term; i++) bits.push(0);
|
||||
while (bits.length % 8 !== 0) bits.push(0);
|
||||
|
||||
var out = [];
|
||||
for (var i = 0; i < bits.length; i += 8) {
|
||||
var b = 0;
|
||||
for (var j = 0; j < 8; j++) b = (b << 1) | bits[i + j];
|
||||
out.push(b);
|
||||
}
|
||||
var pad = [0xec, 0x11];
|
||||
while (out.length < totalBytes) out.push(pad[out.length % 2]);
|
||||
return out;
|
||||
}
|
||||
|
||||
function buildBlocks(dataBytes, info) {
|
||||
var data = [];
|
||||
var ec = [];
|
||||
var idx = 0;
|
||||
for (var g = 0; g < info.groups.length; g++) {
|
||||
var count = info.groups[g][0];
|
||||
var perBlock = info.groups[g][1];
|
||||
for (var i = 0; i < count; i++) {
|
||||
var slice = dataBytes.slice(idx, idx + perBlock);
|
||||
idx += perBlock;
|
||||
data.push(slice);
|
||||
ec.push(rsEncode(slice, info.ec));
|
||||
}
|
||||
}
|
||||
return { data: data, ec: ec };
|
||||
}
|
||||
|
||||
function interleave(blocks) {
|
||||
var maxData = 0;
|
||||
for (var i = 0; i < blocks.data.length; i++) {
|
||||
if (blocks.data[i].length > maxData) maxData = blocks.data[i].length;
|
||||
}
|
||||
var out = [];
|
||||
for (var i = 0; i < maxData; i++) {
|
||||
for (var j = 0; j < blocks.data.length; j++) {
|
||||
if (i < blocks.data[j].length) out.push(blocks.data[j][i]);
|
||||
}
|
||||
}
|
||||
var ecLen = blocks.ec[0].length;
|
||||
for (var i = 0; i < ecLen; i++) {
|
||||
for (var j = 0; j < blocks.ec.length; j++) out.push(blocks.ec[j][i]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function makeGrid(size) {
|
||||
var m = new Array(size);
|
||||
var f = new Array(size);
|
||||
for (var i = 0; i < size; i++) {
|
||||
m[i] = new Array(size);
|
||||
f[i] = new Array(size);
|
||||
for (var j = 0; j < size; j++) { m[i][j] = 0; f[i][j] = false; }
|
||||
}
|
||||
return { m: m, f: f, size: size };
|
||||
}
|
||||
|
||||
function placeFinder(g, r, c) {
|
||||
for (var dr = -1; dr <= 7; dr++) {
|
||||
for (var dc = -1; dc <= 7; dc++) {
|
||||
var rr = r + dr, cc = c + dc;
|
||||
if (rr < 0 || cc < 0 || rr >= g.size || cc >= g.size) continue;
|
||||
var v = 0;
|
||||
if (dr >= 0 && dr <= 6 && dc >= 0 && dc <= 6) {
|
||||
if (dr === 0 || dr === 6 || dc === 0 || dc === 6) v = 1;
|
||||
else if (dr >= 2 && dr <= 4 && dc >= 2 && dc <= 4) v = 1;
|
||||
}
|
||||
g.m[rr][cc] = v;
|
||||
g.f[rr][cc] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function placeAlignment(g, r, c) {
|
||||
for (var dr = -2; dr <= 2; dr++) {
|
||||
for (var dc = -2; dc <= 2; dc++) {
|
||||
var rr = r + dr, cc = c + dc;
|
||||
if (rr < 0 || cc < 0 || rr >= g.size || cc >= g.size) continue;
|
||||
var v = 0;
|
||||
if (dr === -2 || dr === 2 || dc === -2 || dc === 2) v = 1;
|
||||
else if (dr === 0 && dc === 0) v = 1;
|
||||
g.m[rr][cc] = v;
|
||||
g.f[rr][cc] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function placeFunctionPatterns(g, version) {
|
||||
var size = g.size;
|
||||
placeFinder(g, 0, 0);
|
||||
placeFinder(g, 0, size - 7);
|
||||
placeFinder(g, size - 7, 0);
|
||||
// Timing
|
||||
for (var i = 8; i < size - 8; i++) {
|
||||
g.m[6][i] = (i % 2 === 0) ? 1 : 0;
|
||||
g.m[i][6] = (i % 2 === 0) ? 1 : 0;
|
||||
g.f[6][i] = true;
|
||||
g.f[i][6] = true;
|
||||
}
|
||||
// Alignment
|
||||
var pos = ALIGN[version - 1];
|
||||
for (var i = 0; i < pos.length; i++) {
|
||||
for (var j = 0; j < pos.length; j++) {
|
||||
var r = pos[i], c = pos[j];
|
||||
if (r === 6 && c === 6) continue;
|
||||
if (r === 6 && c === size - 7) continue;
|
||||
if (r === size - 7 && c === 6) continue;
|
||||
placeAlignment(g, r, c);
|
||||
}
|
||||
}
|
||||
// Reserve format info (filled later)
|
||||
for (var i = 0; i < 9; i++) {
|
||||
g.f[8][i] = true;
|
||||
g.f[i][8] = true;
|
||||
}
|
||||
for (var i = 0; i < 8; i++) {
|
||||
g.f[size - 1 - i][8] = true;
|
||||
g.f[8][size - 1 - i] = true;
|
||||
}
|
||||
// Dark module
|
||||
g.m[size - 8][8] = 1;
|
||||
g.f[size - 8][8] = true;
|
||||
// Reserve version info (v ≥ 7)
|
||||
if (version >= 7) {
|
||||
for (var i = 0; i < 6; i++) {
|
||||
for (var j = size - 11; j <= size - 9; j++) {
|
||||
g.f[i][j] = true;
|
||||
g.f[j][i] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mask 0: (i + j) % 2 === 0
|
||||
function maskBit(r, c) { return ((r + c) % 2) === 0 ? 1 : 0; }
|
||||
|
||||
function placeData(g, bits) {
|
||||
var size = g.size;
|
||||
var bitIdx = 0;
|
||||
var dir = -1;
|
||||
var col = size - 1;
|
||||
while (col > 0) {
|
||||
if (col === 6) col--;
|
||||
var row = (dir === -1) ? size - 1 : 0;
|
||||
for (var k = 0; k < size; k++) {
|
||||
for (var x = 0; x < 2; x++) {
|
||||
var c = col - x;
|
||||
if (!g.f[row][c]) {
|
||||
var bit = (bitIdx < bits.length) ? bits[bitIdx] : 0;
|
||||
g.m[row][c] = bit ^ maskBit(row, c);
|
||||
bitIdx++;
|
||||
}
|
||||
}
|
||||
row += dir;
|
||||
}
|
||||
col -= 2;
|
||||
dir = -dir;
|
||||
}
|
||||
}
|
||||
|
||||
// Format info: ECC level L = 01, mask = 000 → 5 bits = 0b01000 → 0x08.
|
||||
// Append BCH(15,5) parity, mask with 0x5412.
|
||||
function formatInfoBits() {
|
||||
var data = 0x08; // L + mask 0
|
||||
var rem = data << 10;
|
||||
var poly = 0x537;
|
||||
for (var i = 14; i >= 10; i--) {
|
||||
if ((rem >> i) & 1) rem ^= poly << (i - 10);
|
||||
}
|
||||
var bits = ((data << 10) | rem) ^ 0x5412;
|
||||
return bits & 0x7fff;
|
||||
}
|
||||
|
||||
function placeFormatInfo(g) {
|
||||
var bits = formatInfoBits();
|
||||
var size = g.size;
|
||||
function get(i) { return (bits >> i) & 1; }
|
||||
// Top-left horizontal
|
||||
for (var i = 0; i < 6; i++) g.m[8][i] = get(i);
|
||||
g.m[8][7] = get(6);
|
||||
g.m[8][8] = get(7);
|
||||
g.m[7][8] = get(8);
|
||||
for (var i = 9; i < 15; i++) g.m[14 - i][8] = get(i);
|
||||
// Bottom + right
|
||||
for (var i = 0; i < 7; i++) g.m[size - 1 - i][8] = get(i);
|
||||
for (var i = 7; i < 15; i++) g.m[8][size - 15 + i] = get(i);
|
||||
}
|
||||
|
||||
// Version info BCH(18,6) for v ≥ 7.
|
||||
function versionInfoBits(version) {
|
||||
var rem = version << 12;
|
||||
var poly = 0x1f25;
|
||||
for (var i = 17; i >= 12; i--) {
|
||||
if ((rem >> i) & 1) rem ^= poly << (i - 12);
|
||||
}
|
||||
return (version << 12) | rem;
|
||||
}
|
||||
|
||||
function placeVersionInfo(g, version) {
|
||||
if (version < 7) return;
|
||||
var bits = versionInfoBits(version);
|
||||
var size = g.size;
|
||||
for (var i = 0; i < 18; i++) {
|
||||
var bit = (bits >> i) & 1;
|
||||
var a = Math.floor(i / 3);
|
||||
var b = (i % 3) + size - 11;
|
||||
g.m[a][b] = bit;
|
||||
g.m[b][a] = bit;
|
||||
}
|
||||
}
|
||||
|
||||
function renderSvg(matrix, scale) {
|
||||
var size = matrix.length;
|
||||
var quiet = 4;
|
||||
var dim = (size + quiet * 2) * scale;
|
||||
var parts = [];
|
||||
parts.push('<svg xmlns="http://www.w3.org/2000/svg" width="' + dim + '" height="' + dim + '" viewBox="0 0 ' + (size + quiet * 2) + ' ' + (size + quiet * 2) + '" shape-rendering="crispEdges">');
|
||||
parts.push('<rect width="100%" height="100%" fill="#fff"/>');
|
||||
var path = '';
|
||||
for (var r = 0; r < size; r++) {
|
||||
for (var c = 0; c < size; c++) {
|
||||
if (matrix[r][c]) {
|
||||
path += 'M' + (c + quiet) + ',' + (r + quiet) + 'h1v1h-1z';
|
||||
}
|
||||
}
|
||||
}
|
||||
parts.push('<path d="' + path + '" fill="#000"/>');
|
||||
parts.push('</svg>');
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
global.qrEncodeSvg = function (text, scale) {
|
||||
scale = scale || 4;
|
||||
var bytes = utf8Bytes(text || '');
|
||||
var version = pickVersion(bytes.length);
|
||||
if (version < 0) throw new Error('text too long for QR (max ~230 chars)');
|
||||
var info = VER[version - 1];
|
||||
var dataBytes = buildBitstream(bytes, version, info);
|
||||
var blocks = buildBlocks(dataBytes, info);
|
||||
var bytestream = interleave(blocks);
|
||||
var bits = [];
|
||||
for (var i = 0; i < bytestream.length; i++) {
|
||||
for (var j = 7; j >= 0; j--) bits.push((bytestream[i] >> j) & 1);
|
||||
}
|
||||
var g = makeGrid(info.size);
|
||||
placeFunctionPatterns(g, version);
|
||||
placeData(g, bits);
|
||||
placeFormatInfo(g);
|
||||
placeVersionInfo(g, version);
|
||||
return renderSvg(g.m, scale);
|
||||
};
|
||||
})(window);
|
||||
Reference in New Issue
Block a user