generator share links; secure send copy message

This commit is contained in:
2026-08-28 11:46:56 -04:00
parent 8e02235a71
commit e60945e56e
5 changed files with 418 additions and 2 deletions

View File

@@ -2,7 +2,25 @@
<div class="code-generator">
<div class="page-header no-print">
<h1>Barcode / QR Generator</h1>
<router-link to="/tools" class="btn btn-secondary">Back to Tech Tools</router-link>
<div class="header-actions">
<!-- Sends somebody THIS code, already set up. The content rides in the
fragment, so it never reaches the server or its logs. -->
<button class="btn btn-secondary" :disabled="!shareable" @click="copyShareLink">
{{ sharecopied ? 'Link copied' : 'Copy share link' }}
</button>
<router-link to="/tools" class="btn btn-secondary">Back to Tech Tools</router-link>
</div>
</div>
<div v-if="sharelink" class="share-row no-print">
<input class="form-control mono" :value="sharelink" readonly
@focus="$event.target.select()">
<p class="share-note">
Opens this page with the content and layout already set. The part after
the <code>#</code> never reaches the server, so it stays out of the logs
- but anyone holding the link can read it, so send a password with
Secure Send instead.
</p>
</div>
<div class="workspace">
@@ -343,6 +361,8 @@ import { LABEL_PRESETS, qrModuleCount, qrFit, qrSvgDataUri, barcodeSvgDataUri }
from '@/utils/codes'
import { parseCsv, buildPages, MAX_LABELS } from '../labelPages'
import { labelVars, LABEL_VAR_NAMES } from '../labelVars'
import { buildFragment, readFragment, coerce } from '../sharelink'
import { withBase } from '@/utils/basePath'
const PRESETS = LABEL_PRESETS
@@ -396,6 +416,11 @@ const images = ref([])
const testMode = ref(false)
const overlong = ref([])
// The values a first-time visitor starts with, captured before localStorage or
// a shared link overwrites anything. A share link then carries only what the
// sender actually changed.
const DEFAULT_SETTINGS = {}
const SETTING_KEYS = [
'codetype', 'errorcorrection', 'preset', 'labelwidth', 'labelheight', 'padding',
'quiet', 'gap', 'codesize', 'barheight', 'labelfont', 'layout', 'align', 'valign',
@@ -410,6 +435,7 @@ const settingRefs = {
picturesize, pictureposition, pictureside, pictureopacity,
backenabled, backsource, backcodetype, backtext, backlayout, backorder,
}
for (const key of SETTING_KEYS) DEFAULT_SETTINGS[key] = settingRefs[key].value
// --- back side --------------------------------------------------------------
@@ -725,6 +751,52 @@ function saveSettings() {
try { localStorage.setItem('toolsCodeGenerator', JSON.stringify(state)) } catch { /* private mode */ }
}
// ---------------------------------------------------------------- sharing ---
//
// A link that opens this page with the work already in it. Everything rides in
// the fragment, which is never sent to the server - see ../sharelink.js.
const sharelink = ref('')
const sharecopied = ref(false)
// What travels: the typed content and every layout value. The picture does not
// - a data URL runs to megabytes and no chat client would carry the link.
function shareState () {
const state = {
source: source.value,
content: singleContent.value,
label: singleLabel.value,
copies: singleCopies.value,
back: singleBack.value,
csv: source.value === 'csv' ? csvText.value : '',
}
for (const key of SETTING_KEYS) state[key] = settingRefs[key].value
return state
}
const shareable = computed(() =>
Boolean(source.value === 'csv' ? csvText.value.trim() : singleContent.value.trim()))
function makeShareLink () {
// Defaults are what a first-time visitor starts with, so only the values
// somebody actually changed end up in the link.
const fragment = buildFragment(shareState(), DEFAULT_SETTINGS)
const mount = withBase('/').replace(/\/+$/, '')
sharelink.value = `${window.location.origin}${mount}/tools/codes${fragment}`
}
async function copyShareLink () {
makeShareLink()
try {
await navigator.clipboard.writeText(sharelink.value)
sharecopied.value = true
setTimeout(() => { sharecopied.value = false }, 2000)
} catch {
// Clipboard refused (http, or a locked-down browser). The box below still
// holds the link to copy by hand, which is why it is rendered at all.
}
}
onMounted(() => {
try {
const saved = JSON.parse(localStorage.getItem('toolsCodeGenerator') || '{}')
@@ -732,6 +804,24 @@ onMounted(() => {
if (saved[key] !== undefined) settingRefs[key].value = saved[key]
}
} catch { /* ignore a corrupt entry */ }
// A shared link wins over this browser's saved settings: somebody following
// one is asking to see THAT code, not their own last one.
const shared = readFragment(window.location.hash)
if (Object.keys(shared).length) {
if (shared.source) source.value = shared.source === 'csv' ? 'csv' : 'single'
if (shared.content !== undefined) singleContent.value = shared.content
if (shared.label !== undefined) singleLabel.value = shared.label
if (shared.copies !== undefined) singleCopies.value = coerce(shared.copies, singleCopies.value)
if (shared.back !== undefined) singleBack.value = shared.back
if (shared.csv) { csvText.value = shared.csv; source.value = 'csv' }
for (const key of SETTING_KEYS) {
if (shared[key] !== undefined) {
settingRefs[key].value = coerce(shared[key], settingRefs[key].value)
}
}
}
reconcileBackLayout()
applyVars()
renderAll()
@@ -754,6 +844,22 @@ watch([pages, codetype, backcodetype, errorcorrection], renderAll)
.code-generator { width: 100%; }
.header-actions { display: flex; gap: 0.5rem; align-items: center; }
/* Full width and monospace: the link is long, and somebody copying it by hand
needs to see where it ends. */
.share-row { margin: 0 0 1rem; }
.share-row .mono {
font-family: Consolas, monospace;
font-size: 0.85rem;
width: 100%;
}
.share-note {
margin: 0.35rem 0 0;
font-size: 0.85rem;
color: var(--text-light);
}
/* Settings on the left, the labels themselves on the right, where they stay in
view while the settings that change them are being worked. The preview
column scrolls on its own so a 500-label run cannot push the controls off