Files
filament-short-url/resources/views/table/qr-code-modal.blade.php

262 lines
15 KiB
PHP

@php
$shortUrl = $record->getShortUrl();
$qrTargetUrl = $shortUrl . '?source=qr';
$destHost = parse_url($record->destination_url, PHP_URL_HOST) ?? '';
$urlKey = $record->url_key;
$eid = 'fsu_' . substr(md5($shortUrl), 0, 8);
$qrHelperText = __('filament-short-url::default.qr_modal_helper');
$downloadSvgText = __('filament-short-url::default.qr_download_svg');
$downloadPngText = __('filament-short-url::default.qr_download_png');
$closeButtonText = __('filament-short-url::default.close_button');
$copyLinkText = __('filament-short-url::default.action_copy');
$openLinkText = __('filament-short-url::default.open_link');
$qrDefaults = $record->getQrOptions();
$isGrad = ($qrDefaults['gradient_enabled'] ?? false) || (($qrDefaults['color_mode'] ?? '') === 'gradient');
$dotStyle = $qrDefaults['dot_style'] ?? 'square';
$fgColor = $qrDefaults['foreground_color'] ?? '#000000';
$bgColor = ($qrDefaults['bg_transparent'] ?? false) ? 'rgba(0,0,0,0)' : ($qrDefaults['background_color'] ?? '#ffffff');
$dotsOptions = $isGrad ? [
'type' => $dotStyle,
'gradient' => [
'type' => $qrDefaults['gradient_type'] ?? 'linear',
'colorStops' => [
['offset' => 0, 'color' => $qrDefaults['gradient_from'] ?? '#4f46e5'],
['offset' => 1, 'color' => $qrDefaults['gradient_to'] ?? '#06b6d4'],
],
],
] : [
'type' => $dotStyle,
'color' => $fgColor,
];
$mainColor = $isGrad ? ($qrDefaults['gradient_from'] ?? '#4f46e5') : $fgColor;
$eyeConfigEnabled = $qrDefaults['eye_config_enabled'] ?? false;
$eyeSquareStyle = $qrDefaults['eye_square_style'] ?? ($dotStyle === 'dots' ? 'dot' : 'square');
$eyeDotStyle = $qrDefaults['eye_dot_style'] ?? ($dotStyle === 'dots' ? 'dot' : 'square');
$eyeColor = $qrDefaults['eye_color'] ?? $mainColor;
$cornersSquareOptions = $eyeConfigEnabled ? [
'type' => $eyeSquareStyle,
'color' => $eyeColor,
] : [
'type' => $dotStyle === 'dots' ? 'dot' : 'square',
'color' => $mainColor,
];
$cornersDotOptions = $eyeConfigEnabled ? [
'type' => $eyeDotStyle,
'color' => $eyeColor,
] : [
'type' => $dotStyle === 'dots' ? 'dot' : 'square',
'color' => $mainColor,
];
$logo = $qrDefaults['logo'] ?? null;
$logoSize = $qrDefaults['logo_size'] ?? 0.3;
$logoMargin = $qrDefaults['logo_margin'] ?? 9;
$logoHideBackground = $qrDefaults['logo_hide_background'] ?? true;
$logoShape = $qrDefaults['logo_shape'] ?? 'square';
$qrOptionsJson = json_encode([
'type' => 'svg',
'width' => 200,
'height' => 200,
'margin' => $qrDefaults['margin'] ?? 1,
'dotsOptions' => $dotsOptions,
'backgroundOptions' => ['color' => $bgColor],
'cornersSquareOptions' => $cornersSquareOptions,
'cornersDotOptions' => $cornersDotOptions,
'image' => $logo ?: null,
'imageOptions' => [
'crossOrigin' => 'anonymous',
'hideBackgroundDots' => $logoHideBackground,
'imageSize' => $logoSize,
'margin' => $logoMargin,
'logoShape' => $logoShape,
],
'qrOptions' => ['errorCorrectionLevel' => $logo ? 'H' : 'M'],
]);
@endphp
<div data-qr-options="{{ $qrOptionsJson }}" x-data="{
init() {
const el = this.$el;
const eid = '{{ $eid }}';
const qrTargetUrl = '{{ $qrTargetUrl }}';
this.$nextTick(() => {
this.loadScript().then(() => {
const canvas = document.getElementById(eid + '_qr_canvas');
if (canvas && !canvas.innerHTML) {
const opts = JSON.parse(el.getAttribute('data-qr-options'));
opts.data = qrTargetUrl;
const fixSvg = () => {
const svg = canvas.querySelector('svg');
if (svg) {
const w = svg.getAttribute('width') || opts.width || 200;
const h = svg.getAttribute('height') || opts.height || 200;
svg.setAttribute('viewBox', '0 0 ' + w + ' ' + h);
svg.style.width = '100%';
svg.style.height = '100%';
}
};
if (opts.image && opts.imageOptions) {
const img = new Image();
img.onload = () => {
const cv = document.createElement('canvas');
cv.width = 1200;
cv.height = 1200;
const cx = cv.getContext('2d');
cx.imageSmoothingEnabled = true;
cx.imageSmoothingQuality = 'high';
const isCircle = opts.imageOptions.logoShape === 'circle';
const targetDim = 1200 - (parseFloat(opts.imageOptions.margin || 0) * 20);
cx.save();
if (isCircle) {
cx.beginPath();
cx.arc(600, 600, targetDim / 2, 0, 2 * Math.PI);
cx.clip();
const scale = Math.max(targetDim / img.width, targetDim / img.height);
const w = img.width * scale;
const h = img.height * scale;
const x = (1200 - w) / 2;
const y = (1200 - h) / 2;
cx.drawImage(img, x, y, w, h);
} else {
cx.beginPath();
const offset = (1200 - targetDim) / 2;
const radius = 144 * (targetDim / 1200);
if (typeof cx.roundRect === 'function') {
cx.roundRect(offset, offset, targetDim, targetDim, radius);
} else {
cx.moveTo(offset + radius, offset);
cx.lineTo(offset + targetDim - radius, offset);
cx.quadraticCurveTo(offset + targetDim, offset, offset + targetDim, offset + radius);
cx.lineTo(offset + targetDim, offset + targetDim - radius);
cx.quadraticCurveTo(offset + targetDim, offset + targetDim, offset + targetDim - radius, offset + targetDim);
cx.lineTo(offset + radius, offset + targetDim);
cx.quadraticCurveTo(offset, offset + targetDim, offset, offset + targetDim - radius);
cx.lineTo(offset, offset + radius);
cx.quadraticCurveTo(offset, offset, offset + radius, offset);
cx.closePath();
}
cx.clip();
const scale = Math.max(targetDim / img.width, targetDim / img.height);
const w = img.width * scale;
const h = img.height * scale;
const x = (1200 - w) / 2;
const y = (1200 - h) / 2;
cx.drawImage(img, x, y, w, h);
}
cx.restore();
opts.image = cv.toDataURL('image/png');
opts.imageOptions.margin = 0;
window['qr_' + eid] = new window.QRCodeStyling(opts);
window['qr_' + eid].append(canvas);
fixSvg();
};
img.onerror = () => {
opts.image = null;
window['qr_' + eid] = new window.QRCodeStyling(opts);
window['qr_' + eid].append(canvas);
fixSvg();
};
img.src = opts.image;
} else {
window['qr_' + eid] = new window.QRCodeStyling(opts);
window['qr_' + eid].append(canvas);
fixSvg();
}
}
});
});
},
loadScript() {
return new Promise((resolve) => {
if (window.QRCodeStyling) return resolve();
const s = document.createElement('script');
s.src = 'https://unpkg.com/qr-code-styling@1.6.0-rc.1/lib/qr-code-styling.js';
s.onload = () => resolve();
s.onerror = () => {
const s2 = document.createElement('script');
s2.src = 'https://cdn.jsdelivr.net/npm/qr-code-styling@1.6.0-rc.1/lib/qr-code-styling.js';
s2.onload = () => resolve();
document.head.appendChild(s2);
};
document.head.appendChild(s);
});
}
}">
<!-- Close Button (x) -->
<button type="button" x-on:click="event.preventDefault(); event.stopPropagation(); const f=$el.closest('.fi-modal-window'); const m=f?(f.getAttribute('x-on:keydown.window.escape')||'').match(/'(fi-[^']*)'/):null; if(m){$dispatch('close-modal',{id:m[1]})}else{$wire.unmountAction()}"
style="position:absolute;top:16px;right:16px;z-index:50;width:28px;height:28px;border-radius:50%;border:none;background:#f3f4f6;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#6b7280;transition:color .15s,background-color .15s"
onmouseover="this.style.color='#374151';this.style.backgroundColor='#e5e7eb'"
onmouseout="this.style.color='#6b7280';this.style.backgroundColor='#f3f4f6'"
title="{{ e($closeButtonText) }}">
<svg style="width:16px;height:16px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<div style="text-align:center;padding:4px 0 8px">
<p style="font-size:13px;color:#9ca3af;margin:0">{{ $qrHelperText }}</p>
</div>
<!-- URL pill -->
<div style="display:flex;align-items:center;gap:10px;background:#EFF6FF;border-radius:999px;padding:10px 14px;margin:18px 0 0">
<div style="width:30px;height:30px;border-radius:50%;background:#fff;display:flex;align-items:center;justify-content:center;flex-shrink:0;border:1px solid #e5e7eb">
<img src="https://icons.duckduckgo.com/ip2/{{ e($destHost) }}.ico" style="width:16px;height:16px;object-fit:contain" onerror="this.style.display='none'">
</div>
<span style="flex:1;font-size:14px;font-weight:600;color:#1d4ed8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ $shortUrl }}</span>
<div style="display:flex;align-items:center;gap:4px;flex-shrink:0">
<button id="{{ e($eid) }}_copy" type="button"
onclick="
event.preventDefault();
event.stopPropagation();
const u='{{ e($shortUrl) }}';
if(navigator.clipboard&&window.isSecureContext){navigator.clipboard.writeText(u);}
else{const t=document.createElement('textarea');t.value=u;t.style.cssText='position:fixed;left:-9999px';document.body.appendChild(t);t.select();document.execCommand('copy');t.remove();}
const b=document.getElementById('{{ e($eid) }}_copy');
const prev=b.innerHTML;
b.innerHTML='<svg style=\'width:16px;height:16px;color:#16a34a\' fill=\'none\' viewBox=\'0 0 24 24\' stroke=\'currentColor\' stroke-width=\'2\'><path stroke-linecap=\'round\' stroke-linejoin=\'round\' d=\'M5 13l4 4L19 7\'/></svg>';
setTimeout(()=>b.innerHTML=prev,1800);
"
style="width:30px;height:30px;border-radius:7px;border:1px solid #e5e7eb;background:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .15s"
onmouseover="this.style.background='#f3f4f6'" onmouseout="this.style.background='#fff'" title="{{ e($copyLinkText) }}">
<svg style="width:16px;height:16px;color:#6b7280" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path stroke-linecap="round" stroke-linejoin="round" d="M5 15H4a2 2 0 01-2-2V6a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</button>
<a href="{{ $shortUrl }}" target="_blank" rel="noopener noreferrer"
style="width:30px;height:30px;border-radius:7px;border:1px solid #e5e7eb;background:#fff;display:flex;align-items:center;justify-content:center;text-decoration:none;transition:background .15s"
onmouseover="this.style.background='#f3f4f6'" onmouseout="this.style.background='#fff'" title="{{ e($openLinkText) }}">
<svg style="width:15px;height:15px;color:#6b7280" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
</a>
</div>
</div>
<!-- QR Code Container (Visible immediately) -->
<div id="{{ e($eid) }}_qr_container" style="display:block;margin:16px 0 0;text-align:center;background:#f9fafb;border:1px solid #e5e7eb;border-radius:12px;padding:16px">
<div style="display:flex;justify-content:center;margin-bottom:12px">
<div id="{{ e($eid) }}_qr_canvas" style="background:#fff;padding:8px;border-radius:8px;box-shadow:0 1px 3px rgba(0,0,0,0.05)"></div>
</div>
<div style="display:flex;justify-content:center;gap:8px">
<button type="button" onclick="event.preventDefault();event.stopPropagation();window['qr_{{ e($eid) }}']?.download({ name: '{{ e($urlKey) }}-qr', extension: 'svg' })" style="font-size:12px;font-weight:600;color:#374151;border:1.5px solid #e5e7eb;background:#fff;padding:6px 12px;border-radius:8px;cursor:pointer;transition:background .15s" onmouseover="this.style.background='#f3f4f6'" onmouseout="this.style.background='#fff'">{{ $downloadSvgText }}</button>
<button type="button" onclick="event.preventDefault();event.stopPropagation();window['qr_{{ e($eid) }}']?.download({ name: '{{ e($urlKey) }}-qr', extension: 'png' })" style="font-size:12px;font-weight:600;color:#374151;border:1.5px solid #e5e7eb;background:#fff;padding:6px 12px;border-radius:8px;cursor:pointer;transition:background .15s" onmouseover="this.style.background='#f3f4f6'" onmouseout="this.style.background='#fff'">{{ $downloadPngText }}</button>
</div>
</div>