@twinsearth/dsh-2xray 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,195 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>DSH-2Xray · 二维码导入</title>
7
+ <style>
8
+ :root { color-scheme: dark; }
9
+ body { margin:0; font:15px/1.6 system-ui, -apple-system, "Segoe UI", sans-serif; background:#0f172a; color:#e2e8f0; }
10
+ main { max-width: 760px; margin: 0 auto; padding: 32px 20px 48px; }
11
+ h1 { font-size: 20px; margin: 0 0 4px; }
12
+ p.hint { color:#94a3b8; margin:0 0 24px; font-size:13px; }
13
+ section { background:#1e293b; border:1px solid #334155; border-radius:12px; padding:16px; margin-bottom:16px; }
14
+ h2 { font-size:15px; margin:0 0 10px; color:#f8fafc; }
15
+ textarea { width:100%; box-sizing:border-box; min-height:84px; resize:vertical; background:#0b1220; color:#e2e8f0; border:1px solid #334155; border-radius:8px; padding:10px; font:13px/1.5 ui-monospace, Consolas, monospace; }
16
+ .row { display:flex; gap:10px; flex-wrap:wrap; align-items:center; }
17
+ button { appearance:none; border:1px solid #334155; background:#334155; color:#e2e8f0; border-radius:8px; padding:8px 14px; font-size:14px; cursor:pointer; }
18
+ button.primary { background:#2563eb; border-color:#2563eb; }
19
+ button:disabled { opacity:.5; cursor:not-allowed; }
20
+ input[type=file] { color:#cbd5e1; }
21
+ video { width:100%; max-height:320px; border-radius:8px; background:#000; margin-top:12px; display:none; }
22
+ #status { font-size:13px; color:#94a3b8; margin-top:10px; min-height:20px; }
23
+ #status.error { color:#f87171; }
24
+ #status.ok { color:#4ade80; }
25
+ .badge { font-size:11px; color:#94a3b8; border:1px solid #334155; border-radius:999px; padding:1px 8px; margin-left:8px; }
26
+ </style>
27
+ </head>
28
+ <body>
29
+ <main>
30
+ <h1>DSH-2Xray · 二维码导入</h1>
31
+ <p class="hint">把二维码里的 <code>vless://</code> 文本取出来,粘贴到 DSH 插件设置页的「分享链接」字段。</p>
32
+
33
+ <section>
34
+ <h2>1. 导入分享链接</h2>
35
+ <textarea id="link" placeholder="vless://uuid@host:443?security=reality&sni=...&pbk=...#备注"></textarea>
36
+ <div class="row" style="margin-top:10px">
37
+ <button class="primary" id="copy">复制链接</button>
38
+ <span id="copyState" class="hint" style="margin:0"></span>
39
+ </div>
40
+ </section>
41
+
42
+ <section>
43
+ <h2>2. 二维码图片 <span class="badge" id="imgEngine">检测引擎…</span></h2>
44
+ <input type="file" id="file" accept="image/*">
45
+ </section>
46
+
47
+ <section>
48
+ <h2>3. 扫描屏幕上的二维码</h2>
49
+ <div class="row">
50
+ <button class="primary" id="screen">开始扫描屏幕</button>
51
+ <button id="stop" disabled>停止</button>
52
+ </div>
53
+ <video id="video" playsinline muted></video>
54
+ <div id="status"></div>
55
+ </section>
56
+ </main>
57
+
58
+ <script src="./jsQR.js"></script>
59
+ <script>
60
+ (() => {
61
+ const link = document.getElementById('link')
62
+ const status = document.getElementById('status')
63
+ const copyState = document.getElementById('copyState')
64
+ const imgEngine = document.getElementById('imgEngine')
65
+ const video = document.getElementById('video')
66
+ const fileInput = document.getElementById('file')
67
+ const screenBtn = document.getElementById('screen')
68
+ const stopBtn = document.getElementById('stop')
69
+ let stream = null
70
+ let timer = null
71
+
72
+ const say = (text, kind = '') => { status.textContent = text; status.className = kind }
73
+
74
+ const detector = 'BarcodeDetector' in window
75
+ ? new BarcodeDetector({ formats: ['qr_code'] })
76
+ : null
77
+ imgEngine.textContent = detector ? 'BarcodeDetector' : (window.jsQR ? 'jsQR 兜底' : '不可用')
78
+
79
+ function accept(text) {
80
+ const value = String(text || '').trim()
81
+ if (!value) return false
82
+ link.value = value
83
+ say('已识别二维码:' + value.slice(0, 60) + (value.length > 60 ? '…' : ''), 'ok')
84
+ link.focus()
85
+ link.select()
86
+ return true
87
+ }
88
+
89
+ async function decodeCanvas(canvas) {
90
+ if (detector) {
91
+ const codes = await detector.detect(canvas)
92
+ if (codes && codes.length > 0) return codes[0].rawValue
93
+ }
94
+ if (window.jsQR) {
95
+ const ctx = canvas.getContext('2d', { willReadFrequently: true })
96
+ const data = ctx.getImageData(0, 0, canvas.width, canvas.height)
97
+ const code = window.jsQR(data.data, data.width, data.height, { inversionAttempts: 'dontInvert' })
98
+ if (code && code.data) return code.data
99
+ }
100
+ return null
101
+ }
102
+
103
+ async function decodeImage(file) {
104
+ const bitmap = await createImageBitmap(file)
105
+ const scale = Math.min(1, 1600 / Math.max(bitmap.width, bitmap.height))
106
+ const canvas = document.createElement('canvas')
107
+ canvas.width = Math.max(1, Math.round(bitmap.width * scale))
108
+ canvas.height = Math.max(1, Math.round(bitmap.height * scale))
109
+ canvas.getContext('2d').drawImage(bitmap, 0, 0, canvas.width, canvas.height)
110
+ return decodeCanvas(canvas)
111
+ }
112
+
113
+ fileInput.addEventListener('change', async () => {
114
+ const file = fileInput.files && fileInput.files[0]
115
+ if (!file) return
116
+ say('正在解析图片…')
117
+ try {
118
+ const text = await decodeImage(file)
119
+ if (!text) say('图片里没有识别到二维码,换一张更清晰/更大的图片。', 'error')
120
+ else accept(text)
121
+ } catch (error) {
122
+ say('解析失败:' + error.message, 'error')
123
+ }
124
+ })
125
+
126
+ async function tick() {
127
+ if (!stream) return
128
+ if (video.readyState < 2) return
129
+ const canvas = document.createElement('canvas')
130
+ canvas.width = video.videoWidth
131
+ canvas.height = video.videoHeight
132
+ if (!canvas.width || !canvas.height) return
133
+ canvas.getContext('2d').drawImage(video, 0, 0)
134
+ try {
135
+ const text = await decodeCanvas(canvas)
136
+ if (text) {
137
+ accept(text)
138
+ await stop()
139
+ }
140
+ } catch {
141
+ // keep scanning; a frame that fails to decode is normal
142
+ }
143
+ }
144
+
145
+ async function stop() {
146
+ if (timer) { clearInterval(timer); timer = null }
147
+ if (stream) {
148
+ for (const track of stream.getTracks()) track.stop()
149
+ stream = null
150
+ }
151
+ video.srcObject = null
152
+ video.style.display = 'none'
153
+ screenBtn.disabled = false
154
+ stopBtn.disabled = true
155
+ say('已停止屏幕扫描')
156
+ }
157
+
158
+ screenBtn.addEventListener('click', async () => {
159
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getDisplayMedia) {
160
+ say('当前浏览器不支持屏幕捕获;请改用「二维码图片」。', 'error')
161
+ return
162
+ }
163
+ try {
164
+ stream = await navigator.mediaDevices.getDisplayMedia({ video: { frameRate: 10 }, audio: false })
165
+ video.srcObject = stream
166
+ video.style.display = 'block'
167
+ await video.play()
168
+ screenBtn.disabled = true
169
+ stopBtn.disabled = false
170
+ say('请把二维码放到共享屏幕上,正在识别…(选择“共享整个屏幕”或包含二维码的窗口)')
171
+ timer = setInterval(tick, 350)
172
+ stream.getVideoTracks()[0].addEventListener('ended', stop)
173
+ } catch (error) {
174
+ say('屏幕共享未开始:' + error.message, 'error')
175
+ }
176
+ })
177
+
178
+ stopBtn.addEventListener('click', stop)
179
+
180
+ document.getElementById('copy').addEventListener('click', async () => {
181
+ const value = link.value.trim()
182
+ if (!value) { say('还没有链接可复制。', 'error'); return }
183
+ try {
184
+ await navigator.clipboard.writeText(value)
185
+ copyState.textContent = '已复制'
186
+ setTimeout(() => { copyState.textContent = '' }, 1500)
187
+ } catch {
188
+ link.focus(); link.select()
189
+ copyState.textContent = '请按 Ctrl+C 复制'
190
+ }
191
+ })
192
+ })()
193
+ </script>
194
+ </body>
195
+ </html>