-
Notifications
You must be signed in to change notification settings - Fork 0
Add static "My Spy Code" site, update 客戶.html and README, and add GitHub Pages workflow
#1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| name: Deploy static site to GitHub Pages | ||
|
|
||
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
| - master | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
| pages: write | ||
| id-token: write | ||
|
|
||
| concurrency: | ||
| group: pages | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| deploy: | ||
| environment: | ||
| name: github-pages | ||
| url: ${{ steps.deployment.outputs.page_url }} | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Configure Pages | ||
| uses: actions/configure-pages@v5 | ||
|
|
||
| - name: Upload artifact | ||
| uses: actions/upload-pages-artifact@v3 | ||
| with: | ||
| path: . | ||
|
|
||
| - name: Deploy to GitHub Pages | ||
| id: deployment | ||
| uses: actions/deploy-pages@v4 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,15 @@ | ||
| # - | ||
| # My Spy Code | ||
|
|
||
| 這個專案是可直接部署到 GitHub Pages 的靜態網站。 | ||
|
|
||
| ## 預設頁面 | ||
| - `index.html`(由 `客戶.html` 同步而來) | ||
|
|
||
| ## 啟用 GitHub Pages | ||
| 1. 將此 repo push 到 GitHub(建議 repo 名稱:`ferzzzzzz-tools`)。 | ||
| 2. 到 **Settings → Pages**,Source 選 **GitHub Actions**。 | ||
| 3. push 到 `main` 或 `master` 後,`.github/workflows/pages.yml` 會自動部署。 | ||
|
|
||
| ## 預期網址 | ||
| - 若帳號為 `<你的 GitHub 帳號>` 且 repo 名稱為 `ferzzzzzz-tools`,網址會是: | ||
| - `https://<你的 GitHub 帳號>.github.io/ferzzzzzz-tools/` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,332 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="zh-Hant"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>My Spy Code|專屬加密摩斯</title> | ||
| <style> | ||
| :root { | ||
| --bg: #0f172a; | ||
| --panel: #111827; | ||
| --panel-2: #1f2937; | ||
| --text: #e5e7eb; | ||
| --muted: #9ca3af; | ||
| --accent: #22d3ee; | ||
| --good: #34d399; | ||
| --warn: #fbbf24; | ||
| } | ||
| * { box-sizing: border-box; } | ||
| body { | ||
| margin: 0; | ||
| font-family: "Noto Sans TC", "Segoe UI Emoji", sans-serif; | ||
| background: radial-gradient(circle at top, #1e293b, var(--bg)); | ||
| color: var(--text); | ||
| min-height: 100vh; | ||
| padding: 24px; | ||
| } | ||
| .wrap { | ||
| max-width: 960px; | ||
| margin: 0 auto; | ||
| display: grid; | ||
| gap: 16px; | ||
| } | ||
| .card { | ||
| background: linear-gradient(160deg, var(--panel), var(--panel-2)); | ||
| border: 1px solid #334155; | ||
| border-radius: 14px; | ||
| padding: 18px; | ||
| box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35); | ||
| } | ||
| h1 { margin: 0 0 8px; font-size: 1.5rem; } | ||
| p { margin: 6px 0; color: var(--muted); } | ||
| textarea { | ||
| width: 100%; | ||
| min-height: 120px; | ||
| border-radius: 10px; | ||
| border: 1px solid #475569; | ||
| background: #0b1220; | ||
| color: var(--text); | ||
| padding: 12px; | ||
| resize: vertical; | ||
| font-size: 1rem; | ||
| } | ||
| .row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; } | ||
| button { | ||
| border: none; | ||
| border-radius: 10px; | ||
| padding: 10px 14px; | ||
| font-size: 0.95rem; | ||
| color: #04121a; | ||
| background: var(--accent); | ||
| font-weight: 700; | ||
| cursor: pointer; | ||
| } | ||
| button.secondary { background: #c4b5fd; } | ||
| button.good { background: var(--good); } | ||
| button.warn { background: var(--warn); } | ||
| .output { | ||
| margin-top: 12px; | ||
| background: #020617; | ||
| border: 1px solid #334155; | ||
| border-radius: 10px; | ||
| padding: 12px; | ||
| word-break: break-word; | ||
| line-height: 1.7; | ||
| white-space: pre-wrap; | ||
| } | ||
| .small { font-size: 0.85rem; color: var(--muted); } | ||
| .pill { | ||
| display: inline-block; | ||
| padding: 3px 8px; | ||
| border: 1px solid #64748b; | ||
| border-radius: 99px; | ||
| margin-right: 6px; | ||
| margin-top: 6px; | ||
| font-size: 0.8rem; | ||
| color: #cbd5e1; | ||
| } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <main class="wrap"> | ||
| <section class="card"> | ||
| <h1>🕵️ My Spy Code(你的專屬加密摩斯)</h1> | ||
| <p>不是國際通用摩斯,而是自訂「My Spy Code」。可加密:中文、英文、數字、特殊符號、表情符號。</p> | ||
| <p class="small">規則:先把文字轉成 UTF-8(可處理所有字元),每個 16 進位位元再轉成自訂摩斯碼;以 <code>/</code> 分隔每個位元組。</p> | ||
| <div> | ||
| <span class="pill">範例:# = ....--</span> | ||
| <span class="pill">支援 emoji 名稱前處理</span> | ||
| </div> | ||
| </section> | ||
|
|
||
| <section class="card"> | ||
| <h2>1) 輸入原文</h2> | ||
| <textarea id="plainText" placeholder="輸入你想加密的內容,例如:秘密任務#1 😎 GO!"></textarea> | ||
| <div class="row"> | ||
| <button id="encryptBtn">加密成 My Spy Code</button> | ||
| <button class="secondary" id="clearBtn">清空</button> | ||
| </div> | ||
| <div class="output" id="encryptedOutput" aria-live="polite"></div> | ||
| <div class="row"> | ||
| <button class="good" id="copyCipherBtn">複製加密結果</button> | ||
| <button class="warn" id="shareBtn">分享(產生可解密連結)</button> | ||
| </div> | ||
| <div class="output" id="shareOutput"></div> | ||
| </section> | ||
|
|
||
| <section class="card"> | ||
| <h2>2) 解密</h2> | ||
| <textarea id="cipherText" placeholder="貼上 My Spy Code(例如 ....-- .....- / -...-- ..---."></textarea> | ||
| <div class="row"> | ||
| <button id="decryptBtn">解密內容</button> | ||
| </div> | ||
| <div class="output" id="decryptedOutput"></div> | ||
| </section> | ||
|
|
||
| <section class="card"> | ||
| <h2>自訂摩斯表(與國際摩斯不同)</h2> | ||
| <div class="output" id="codebookView"></div> | ||
| </section> | ||
| </main> | ||
|
|
||
| <script> | ||
| const CODEBOOK = { | ||
| '0': '-----.', | ||
| '1': '.----.', | ||
| '2': '..---.', | ||
| '3': '...--.', | ||
| '4': '....-.', | ||
| '5': '.....-', | ||
| '6': '-....-', | ||
| '7': '--...-', | ||
| '8': '---..-', | ||
| '9': '----.-', | ||
| 'a': '.-.-.-', | ||
| 'b': '-...--', | ||
| 'c': '-.-..-', | ||
| 'd': '-..-.-', | ||
| 'e': '..-.-.', | ||
| 'f': '.--.-.' | ||
| }; | ||
|
|
||
| // 使用者要求示例:# = ....--(優先映射) | ||
| const SPECIAL_CHAR_DIRECT = { | ||
| '#': '....--' | ||
| }; | ||
|
|
||
| const REVERSE = Object.fromEntries(Object.entries(CODEBOOK).map(([k, v]) => [v, k])); | ||
|
|
||
| const EMOJI_NAME_MAP = { | ||
| '😀': 'grinning face', | ||
| '😁': 'beaming face', | ||
| '😂': 'face with tears of joy', | ||
| '🤣': 'rolling on the floor laughing', | ||
| '😊': 'smiling face with smiling eyes', | ||
| '😎': 'smiling face with sunglasses', | ||
| '❤️': 'red heart', | ||
| '👍': 'thumbs up', | ||
| '🙏': 'folded hands', | ||
| '🔥': 'fire', | ||
| '🎉': 'party popper', | ||
| '😭': 'loudly crying face', | ||
| '🥺': 'pleading face', | ||
| '🤔': 'thinking face', | ||
| '😡': 'pouting face', | ||
| '👀': 'eyes' | ||
| }; | ||
|
|
||
| function isLikelyEmoji(char) { | ||
| return /\p{Extended_Pictographic}/u.test(char); | ||
| } | ||
|
|
||
| function emojiToName(char) { | ||
| if (EMOJI_NAME_MAP[char]) return EMOJI_NAME_MAP[char]; | ||
| const cps = [...char].map(c => c.codePointAt(0).toString(16)).join('-'); | ||
| return `emoji-${cps}`; | ||
| } | ||
|
|
||
| function preprocessEmojiToEnglishName(text) { | ||
| const graphemes = [...text]; | ||
| return graphemes.map(ch => isLikelyEmoji(ch) ? `:${emojiToName(ch)}:` : ch).join(''); | ||
| } | ||
|
|
||
| function encodeTextToSpyCode(input) { | ||
| const prepared = preprocessEmojiToEnglishName(input); | ||
| const out = []; | ||
|
|
||
| for (const char of [...prepared]) { | ||
| if (SPECIAL_CHAR_DIRECT[char]) { | ||
| out.push(SPECIAL_CHAR_DIRECT[char]); | ||
| continue; | ||
| } | ||
|
|
||
| const bytes = new TextEncoder().encode(char); | ||
| const encodedBytes = []; | ||
|
|
||
| for (const b of bytes) { | ||
| const hex = b.toString(16).padStart(2, '0'); | ||
| const left = CODEBOOK[hex[0]]; | ||
| const right = CODEBOOK[hex[1]]; | ||
| encodedBytes.push(`${left} ${right}`); | ||
| } | ||
| out.push(encodedBytes.join(' / ')); | ||
| } | ||
| return out.join(' | '); | ||
| } | ||
|
|
||
| function decodeSpyCodeToText(cipher) { | ||
| const segments = cipher.split('|').map(s => s.trim()).filter(Boolean); | ||
| const chars = []; | ||
|
|
||
| for (const seg of segments) { | ||
| if (seg === SPECIAL_CHAR_DIRECT['#']) { | ||
| chars.push('#'); | ||
| continue; | ||
| } | ||
|
|
||
| const byteChunks = seg.split('/').map(s => s.trim()).filter(Boolean); | ||
| const bytes = []; | ||
|
|
||
| for (const chunk of byteChunks) { | ||
| const pair = chunk.split(/\s+/).filter(Boolean); | ||
| if (pair.length !== 2) throw new Error(`區塊格式錯誤:${chunk}`); | ||
| const h1 = REVERSE[pair[0]]; | ||
| const h2 = REVERSE[pair[1]]; | ||
| if (h1 === undefined || h2 === undefined) { | ||
| throw new Error(`找不到代碼:${chunk}`); | ||
| } | ||
| bytes.push(parseInt(h1 + h2, 16)); | ||
| } | ||
|
|
||
| const text = new TextDecoder().decode(new Uint8Array(bytes)); | ||
| chars.push(text); | ||
| } | ||
|
|
||
| let decoded = chars.join(''); | ||
| decoded = decoded.replace(/:([a-z0-9\- ]+):/gi, '[$1]'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The decoder currently applies Useful? React with 👍 / 👎. |
||
| return decoded; | ||
| } | ||
|
|
||
| function renderCodebook() { | ||
| const lines = Object.entries(CODEBOOK) | ||
| .map(([hex, code]) => `${hex.toUpperCase()} => ${code}`) | ||
| .join('\n'); | ||
| document.getElementById('codebookView').textContent = `${lines}\n# => ${SPECIAL_CHAR_DIRECT['#']}`; | ||
| } | ||
|
|
||
| function setText(id, text) { | ||
| document.getElementById(id).textContent = text || '(尚無內容)'; | ||
| } | ||
|
|
||
| document.getElementById('encryptBtn').addEventListener('click', () => { | ||
| const plain = document.getElementById('plainText').value; | ||
| if (!plain.trim()) { | ||
| setText('encryptedOutput', '請先輸入文字'); | ||
| return; | ||
| } | ||
| const cipher = encodeTextToSpyCode(plain); | ||
| setText('encryptedOutput', cipher); | ||
| document.getElementById('cipherText').value = cipher; | ||
| }); | ||
|
|
||
| document.getElementById('decryptBtn').addEventListener('click', () => { | ||
| const cipher = document.getElementById('cipherText').value.trim(); | ||
| if (!cipher) { | ||
| setText('decryptedOutput', '請先輸入加密碼'); | ||
| return; | ||
| } | ||
| try { | ||
| const text = decodeSpyCodeToText(cipher); | ||
| setText('decryptedOutput', text); | ||
| } catch (err) { | ||
| setText('decryptedOutput', `解密失敗:${err.message}`); | ||
| } | ||
| }); | ||
|
|
||
| document.getElementById('copyCipherBtn').addEventListener('click', async () => { | ||
| const cipher = document.getElementById('encryptedOutput').textContent; | ||
| if (!cipher || cipher.includes('尚無內容')) return; | ||
| await navigator.clipboard.writeText(cipher); | ||
| alert('已複製加密內容'); | ||
| }); | ||
|
|
||
| document.getElementById('shareBtn').addEventListener('click', async () => { | ||
| const cipher = document.getElementById('encryptedOutput').textContent; | ||
| if (!cipher || cipher.includes('尚無內容')) { | ||
| setText('shareOutput', '請先加密後再分享'); | ||
| return; | ||
| } | ||
| const url = `${location.origin}${location.pathname}?c=${encodeURIComponent(cipher)}`; | ||
| setText('shareOutput', url); | ||
| await navigator.clipboard.writeText(url); | ||
| }); | ||
|
|
||
| document.getElementById('clearBtn').addEventListener('click', () => { | ||
| document.getElementById('plainText').value = ''; | ||
| document.getElementById('cipherText').value = ''; | ||
| setText('encryptedOutput', ''); | ||
| setText('decryptedOutput', ''); | ||
| setText('shareOutput', ''); | ||
| }); | ||
|
|
||
| function decodeFromQueryIfExists() { | ||
| const c = new URLSearchParams(location.search).get('c'); | ||
| if (!c) return; | ||
| document.getElementById('cipherText').value = c; | ||
| setText('encryptedOutput', c); | ||
| try { | ||
| const decoded = decodeSpyCodeToText(c); | ||
| setText('decryptedOutput', `${decoded}\n\n(此內容由分享連結自動解密)`); | ||
| } catch (err) { | ||
| setText('decryptedOutput', `自動解密失敗:${err.message}`); | ||
| } | ||
| } | ||
|
|
||
| setText('encryptedOutput', ''); | ||
| setText('decryptedOutput', ''); | ||
| setText('shareOutput', ''); | ||
| renderCodebook(); | ||
| decodeFromQueryIfExists(); | ||
| </script> | ||
| </body> | ||
| </html> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
preprocessEmojiToEnglishNameuses[...text], which iterates Unicode code points rather than full grapheme clusters, so multi-codepoint emoji (for example❤️) are split and no longer matchEMOJI_NAME_MAP. This produces inconsistent placeholders and leaves artifacts (e.g., a trailing variation selector) in decrypted output, breaking the advertised emoji handling for common emoji sequences.Useful? React with 👍 / 👎.