From 387885aaf028c1ec292dddb9a92bf9d082770edf Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 8 Jun 2026 09:20:17 +0000 Subject: [PATCH 1/2] Add GitHub Actions release workflow for NameScrub+ Desktop - .github/workflows/release.yml: matrix build (Windows/macOS/Linux) on v*.*.* tags via PyInstaller; caches spaCy de_core_news_lg per OS to avoid repeated 560 MB downloads; release job attaches all three binaries to a GitHub Release using GITHUB_TOKEN - NameScrub+ modal: replace single download button with three platform-specific buttons (Windows .exe / macOS / Linux) linking directly to release asset URLs - workflow_dispatch input for manual test builds https://claude.ai/code/session_01EVuoocp6FUTUt4jX9njQrK --- .github/workflows/release.yml | 37 +++++++++++++-- .../{main-B7XnHbPp.js => main-DwCg2qVh.js} | 0 .../{main-DEb9rLK_.css => main-p6bvg9LT.css} | 2 +- dist/index.html | 46 +++++++++++++------ index.html | 42 ++++++++++++----- src/style.css | 12 +++++ 6 files changed, 110 insertions(+), 29 deletions(-) rename dist/assets/{main-B7XnHbPp.js => main-DwCg2qVh.js} (100%) rename dist/assets/{main-DEb9rLK_.css => main-p6bvg9LT.css} (92%) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bfecb4a..8ae950a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,7 @@ name: Build & Release NameScrub+ on: push: tags: - - 'v*' + - 'v*.*.*' workflow_dispatch: inputs: tag_name: @@ -11,9 +11,6 @@ on: required: true default: 'v1.0.0' -permissions: - contents: write - jobs: build: name: Build on ${{ matrix.os }} @@ -42,6 +39,35 @@ jobs: - name: Install dependencies run: pip install spacy pyinstaller Pillow + - name: Cache spaCy model + id: cache-spacy + uses: actions/cache@v4 + with: + path: | + ${{ runner.tool_cache }}/spacy-models + key: spacy-de_core_news_lg-${{ runner.os }} + + - name: Download spaCy model + if: steps.cache-spacy.outputs.cache-hit != 'true' + run: python -m spacy download de_core_news_lg + + - name: Restore spaCy model from cache + if: steps.cache-spacy.outputs.cache-hit == 'true' + shell: bash + run: | + MODEL_CACHE="${{ runner.tool_cache }}/spacy-models" + if [ -d "$MODEL_CACHE" ]; then + pip install --no-index --find-links="$MODEL_CACHE" de_core_news_lg || true + fi + + - name: Save spaCy model to cache + if: steps.cache-spacy.outputs.cache-hit != 'true' + shell: bash + run: | + MODEL_CACHE="${{ runner.tool_cache }}/spacy-models" + mkdir -p "$MODEL_CACHE" + pip download de_core_news_lg --no-deps -d "$MODEL_CACHE" || true + - name: Build executable run: python cli/build_exe.py env: @@ -66,6 +92,8 @@ jobs: name: Create GitHub Release needs: build runs-on: ubuntu-latest + permissions: + contents: write steps: - uses: actions/checkout@v4 @@ -122,3 +150,4 @@ jobs: artifacts/NameScrub-macos/NameScrub-macos draft: false prerelease: false + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/dist/assets/main-B7XnHbPp.js b/dist/assets/main-DwCg2qVh.js similarity index 100% rename from dist/assets/main-B7XnHbPp.js rename to dist/assets/main-DwCg2qVh.js diff --git a/dist/assets/main-DEb9rLK_.css b/dist/assets/main-p6bvg9LT.css similarity index 92% rename from dist/assets/main-DEb9rLK_.css rename to dist/assets/main-p6bvg9LT.css index fe6d072..da652ac 100644 --- a/dist/assets/main-DEb9rLK_.css +++ b/dist/assets/main-p6bvg9LT.css @@ -1 +1 @@ -:root{--nz-green: #00FF9C;--nz-green-strong:#00E88D;--nz-green-soft: #B7FFE0;--nz-paper: #FFFEE5;--nz-paper-alt: #FAF8D4;--nz-ink: #000000;--nz-ink-70: rgba(0,0,0,.72);--nz-ink-20: rgba(0,0,0,.18);--nz-radius: 0;--nz-stroke: 2px solid #000;--nz-shadow: 6px 6px 0 0 #000;--nz-shadow-sm: 3px 3px 0 0 #000;--nz-dur: .16s cubic-bezier(.2,0,0,1);--font-display: "Zilla Slab", Georgia, serif;--font-body: "Inter", Arial, sans-serif;--font-mono: "Space Mono", "Courier New", monospace}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html{font-size:16px}body{background:var(--nz-paper);color:var(--nz-ink);font-family:var(--font-body);min-height:100vh;display:flex;flex-direction:column}header{background:var(--nz-paper);display:flex;align-items:center;justify-content:space-between;padding:0 2rem}.header-right{flex-shrink:0;display:flex;align-items:center}.btn-plus{font-family:var(--font-mono);font-weight:700;font-size:.7rem;text-transform:uppercase;letter-spacing:.1em;border:var(--nz-stroke);padding:.55rem 1rem;cursor:pointer;background:var(--nz-ink);color:var(--nz-green);box-shadow:var(--nz-shadow-sm);transition:transform var(--nz-dur),box-shadow var(--nz-dur);white-space:nowrap;display:inline-flex;align-items:center;gap:.2em}.btn-plus:hover{background:#222}.btn-plus:active{transform:translate(3px,3px);box-shadow:none}.plus-badge{color:var(--nz-green);font-size:1.1em;line-height:1}.header-inner,.header-left{max-width:1400px;width:100%;margin:0 auto;padding:1.25rem 2rem;display:flex;flex-direction:row;align-items:center;gap:1.5rem}.header-logo{height:64px;width:auto;display:block;flex-shrink:0}.header-sub{font-family:var(--font-body);font-size:.95rem;font-weight:400;color:var(--nz-ink)}.header-status,.status-hidden{display:none}main{flex:1;display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;max-width:1400px;width:100%;margin:0 auto;padding:2rem}.panel{border:var(--nz-stroke);box-shadow:var(--nz-shadow);display:flex;flex-direction:column;background:#fff}.panel-label{background:var(--nz-ink);color:var(--nz-paper);font-family:var(--font-mono);font-size:.65rem;font-weight:700;text-transform:uppercase;letter-spacing:.12em;padding:.35rem .75rem}@keyframes cursor-blink{0%,49%{opacity:1}50%,to{opacity:0}}.input-hint{font-family:var(--font-display);font-size:1.3rem;font-weight:700;color:var(--nz-ink);background:var(--nz-green);padding:.4rem .75rem;grid-column:1;grid-row:1;align-self:start;display:inline-flex;align-items:center}.input-hint:after{content:"";display:inline-block;width:2px;height:1.1em;background:var(--nz-ink);margin-left:4px;animation:cursor-blink 1s step-start infinite}.input-panel{grid-column:1;grid-row:2}.output-panel{grid-column:2;grid-row:2}#input{flex:1;border:none;border-top:var(--nz-stroke);padding:1rem;font-family:var(--font-mono);font-size:.9rem;line-height:1.65;resize:none;min-height:400px;outline:none;background:#fff;color:var(--nz-ink)}#input:focus{background:#fff}#input::placeholder{color:var(--nz-ink-70)}#output{flex:1;border-top:var(--nz-stroke);padding:1rem;font-family:var(--font-mono);font-size:.9rem;line-height:2;min-height:400px;overflow-y:auto;white-space:pre-wrap;word-break:break-word;background:#fff}.token.name{background:var(--nz-green);border:2px solid var(--nz-ink);padding:0 3px;cursor:pointer;font-weight:700;transition:background var(--nz-dur);display:inline;position:relative}.token.name:hover{background:var(--nz-green-strong);outline:2px solid var(--nz-ink)}.token.honorific{background:var(--nz-green);border-color:var(--nz-ink);box-shadow:var(--nz-shadow-sm)}.token.replaced{background:var(--nz-ink);color:var(--nz-green);font-family:var(--font-mono);font-weight:700;font-size:.82em;padding:1px 5px;letter-spacing:.04em;display:inline;cursor:default}.token.email{background:#a8daff;border:2px solid var(--nz-ink);padding:0 3px;cursor:pointer;font-weight:700;transition:background var(--nz-dur);display:inline;position:relative}.token.email:hover{background:#7ec8ff;outline:2px solid var(--nz-ink)}.token.phone{background:#e0aaff;border:2px solid var(--nz-ink);padding:0 3px;cursor:pointer;font-weight:700;transition:background var(--nz-dur);display:inline;position:relative}.token.phone:hover{background:#c77dff;outline:2px solid var(--nz-ink)}.token.date{background:#ffd6a5;border:2px solid var(--nz-ink);padding:0 3px;cursor:pointer;font-weight:700;transition:background var(--nz-dur);display:inline;position:relative}.token.date:hover{background:#ffba6b;outline:2px solid var(--nz-ink)}.token.address{background:#b9fbc0;border:2px solid var(--nz-ink);padding:0 3px;cursor:pointer;font-weight:700;transition:background var(--nz-dur);display:inline;position:relative}.token.address:hover{background:#82ef90;outline:2px solid var(--nz-ink)}.name-popup{position:absolute;top:calc(100% + 4px);left:0;z-index:100;display:flex;background:var(--nz-paper);border:var(--nz-stroke);box-shadow:var(--nz-shadow);white-space:nowrap}.popup-btn{font-family:var(--font-mono);font-weight:700;font-size:.7rem;text-transform:uppercase;letter-spacing:.1em;border:none;padding:.45rem .9rem;cursor:pointer;transition:background var(--nz-dur)}.popup-btn.delete{background:var(--nz-ink);color:var(--nz-paper);border-right:var(--nz-stroke)}.popup-btn.delete:hover{background:#333}.popup-btn.false-positive{background:var(--nz-paper);color:var(--nz-ink);border-right:var(--nz-stroke)}.popup-btn.false-positive:hover{background:var(--nz-paper-alt)}.popup-btn.replace{background:var(--nz-green);color:var(--nz-ink)}.popup-btn.replace:hover{background:var(--nz-green-strong)}.controls{grid-column:1 / 3;grid-row:3;display:flex;gap:1rem;align-items:center;flex-wrap:wrap}.opt-label{font-family:var(--font-mono);font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;cursor:pointer;display:flex;align-items:center;gap:.4rem;color:var(--nz-ink);-webkit-user-select:none;user-select:none}.opt-label input[type=checkbox]{width:1rem;height:1rem;accent-color:var(--nz-green);cursor:pointer}.prefix-details{font-family:var(--font-mono);font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em}.prefix-summary{cursor:pointer;-webkit-user-select:none;user-select:none;color:var(--nz-ink);padding:.3rem 0;list-style:none;display:inline-flex;align-items:center;gap:.4rem}.prefix-summary::-webkit-details-marker{display:none}.prefix-summary:before{content:"▶";font-size:.55rem;transition:transform var(--nz-dur)}details[open] .prefix-summary:before{transform:rotate(90deg)}.prefix-grid{display:grid;grid-template-columns:auto 1fr;gap:.35rem .6rem;align-items:center;margin-top:.5rem;padding:.6rem .75rem;border:2px solid var(--nz-ink);background:var(--nz-paper)}.prefix-label{font-family:var(--font-mono);font-size:.65rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--nz-ink);white-space:nowrap;cursor:default}.prefix-input{font-family:var(--font-mono);font-size:.72rem;border:2px solid var(--nz-ink);background:var(--nz-paper);color:var(--nz-ink);padding:.2rem .4rem;width:8rem;outline:none;transition:box-shadow var(--nz-dur)}.prefix-input:focus{box-shadow:2px 2px 0 var(--nz-ink)}.prefix-input::placeholder{color:var(--nz-ink-70)}button{font-family:var(--font-mono);font-weight:700;font-size:.75rem;text-transform:uppercase;letter-spacing:.1em;border:var(--nz-stroke);padding:.75rem 1.4rem;cursor:pointer;box-shadow:var(--nz-shadow-sm);transition:transform var(--nz-dur),box-shadow var(--nz-dur);white-space:nowrap;border-radius:var(--nz-radius)}button:active:not(:disabled){transform:translate(3px,3px);box-shadow:none}button:disabled{opacity:.35;cursor:not-allowed;box-shadow:none}#btn-analyse{background:var(--nz-green);color:var(--nz-ink)}#btn-analyse:hover:not(:disabled){background:var(--nz-green-strong)}#btn-purge{background:var(--nz-paper);color:var(--nz-ink)}#btn-purge:hover:not(:disabled){background:var(--nz-paper-alt)}#btn-copy{background:var(--nz-ink);color:var(--nz-paper);margin-left:auto}#btn-copy:hover:not(:disabled){background:#222}.status-hidden{display:none}.legend{grid-column:1 / 3;grid-row:4;display:flex;gap:1.5rem;align-items:center;font-family:var(--font-mono);font-size:.65rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;flex-wrap:wrap;color:var(--nz-ink-70)}#stats{color:var(--nz-ink)}.legend-item{display:flex;align-items:center;gap:.4rem}.legend-swatch{width:1.1rem;height:1.1rem;border:2px solid var(--nz-ink);display:inline-block}.legend-swatch.name{background:var(--nz-green)}.legend-swatch.honorific{background:var(--nz-green);box-shadow:var(--nz-shadow-sm)}.legend-swatch.email{background:#a8daff}.legend-swatch.phone{background:#e0aaff}.legend-swatch.date{background:#ffd6a5}.legend-swatch.address{background:#b9fbc0}.legend-swatch.replaced{background:var(--nz-ink)}.status.loading,.status.ready,.status.error,.status.warn{display:none}footer{margin-top:4rem;padding:3rem 2rem 2.5rem;font-family:var(--font-mono);font-size:.8rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1.25rem;color:var(--nz-ink-70);background:var(--nz-paper);position:relative}.footer-links{display:flex;align-items:center;justify-content:center;gap:2rem}.footer-links a{color:var(--nz-ink);text-decoration:none;display:flex;align-items:center;gap:.35rem}.footer-github{letter-spacing:0}.footer-github svg{flex-shrink:0}.footer-brand{font-size:.58rem;color:var(--nz-ink-70);text-decoration:none;letter-spacing:.06em;text-transform:none}.footer-version{position:absolute;bottom:1rem;right:2rem;font-family:var(--font-mono);font-size:.55rem;color:var(--nz-ink-20);letter-spacing:0;text-transform:none;-webkit-user-select:none;user-select:none}@media (max-width: 800px){main{grid-template-columns:1fr;padding:1rem}.input-hint{grid-column:1;grid-row:auto;font-size:1rem}.input-panel,.output-panel,.controls,.legend{grid-column:1;grid-row:auto}#input,#output{min-height:250px}.header-sub{font-size:.8rem}footer{margin-top:2rem;padding:2rem 1rem}.btn-plus{font-size:.62rem;padding:.45rem .7rem}}.plus-modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1000;display:flex;align-items:center;justify-content:center}.plus-modal[hidden]{display:none}.plus-modal-backdrop{position:absolute;top:0;right:0;bottom:0;left:0;background:#000000b8;cursor:pointer}.plus-modal-box{position:relative;background:var(--nz-paper);border:var(--nz-stroke);box-shadow:10px 10px #000;max-width:680px;width:calc(100% - 2rem);max-height:92vh;overflow-y:auto;display:flex;flex-direction:column}.plus-modal-close{position:absolute;top:.75rem;right:.75rem;background:none;border:none;font-size:1.1rem;cursor:pointer;padding:.25rem .5rem;box-shadow:none;color:var(--nz-ink);line-height:1}.plus-modal-close:hover{background:var(--nz-ink);color:var(--nz-paper)}.plus-modal-close:active{transform:none}.plus-modal-header{background:var(--nz-ink);padding:1.5rem 2rem 1.25rem}.plus-modal-logo{font-family:var(--font-display);font-size:2rem;font-weight:700;color:var(--nz-green);display:block;margin-bottom:.4rem}.plus-modal-tagline{font-family:var(--font-body);font-size:.95rem;color:#ffffffd9;margin:0}.plus-modal-body{padding:1.75rem 2rem;display:flex;flex-direction:column;gap:1.5rem}.plus-section p{font-family:var(--font-body);font-size:.9rem;line-height:1.7;color:var(--nz-ink);margin-bottom:.6rem}.plus-section-title{font-family:var(--font-mono);font-size:.65rem;font-weight:700;text-transform:uppercase;letter-spacing:.12em;background:var(--nz-ink);color:var(--nz-paper);display:inline-block;padding:.2rem .6rem;margin-bottom:.75rem}.plus-compare{display:grid;grid-template-columns:1fr 1fr;gap:1rem;border:var(--nz-stroke)}.plus-compare-col{padding:1rem 1.1rem}.plus-compare-col--plus{background:var(--nz-green);border-left:var(--nz-stroke)}.plus-compare-label{font-family:var(--font-mono);font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;margin-bottom:.75rem;display:flex;align-items:center;gap:.4rem}.plus-compare-tag{font-size:.6rem;background:var(--nz-ink-20);padding:.1rem .4rem}.plus-compare-tag--plus{background:var(--nz-ink);color:var(--nz-green)}.plus-list{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:.4rem}.plus-list li{font-family:var(--font-body);font-size:.85rem;line-height:1.4;padding-left:1.1em;position:relative}.plus-list li:before{content:"—";position:absolute;left:0;color:var(--nz-ink-70);font-size:.75em}.plus-code{font-family:var(--font-mono);font-size:.8rem;background:var(--nz-ink);color:var(--nz-green);padding:.85rem 1rem;margin-top:.75rem;white-space:pre;overflow-x:auto;border:none}.plus-modal-footer{border-top:var(--nz-stroke);padding:1.25rem 2rem;display:flex;align-items:center;gap:1.5rem;flex-wrap:wrap}.btn-plus-download{font-family:var(--font-mono);font-weight:700;font-size:.75rem;text-transform:uppercase;letter-spacing:.1em;background:var(--nz-green);color:var(--nz-ink);border:var(--nz-stroke);padding:.75rem 1.4rem;cursor:pointer;box-shadow:var(--nz-shadow-sm);text-decoration:none;display:inline-flex;align-items:center;gap:.5rem;transition:transform var(--nz-dur),box-shadow var(--nz-dur),background var(--nz-dur)}.btn-plus-download:hover{background:var(--nz-green-strong)}.btn-plus-download:active{transform:translate(3px,3px);box-shadow:none}.plus-github-link{font-family:var(--font-mono);font-size:.72rem;font-weight:700;color:var(--nz-ink);text-decoration:none;text-transform:uppercase;letter-spacing:.08em}.plus-github-link:hover{text-decoration:underline}@media (max-width: 600px){.plus-modal-box{max-height:100vh;border-left:none;border-right:none}.plus-compare{grid-template-columns:1fr}.plus-compare-col--plus{border-left:none;border-top:var(--nz-stroke)}.plus-modal-body,.plus-modal-header{padding:1.25rem 1rem}.plus-modal-footer{padding:1rem}}#btn-undo{background:var(--nz-paper);color:var(--nz-ink)}#btn-undo:hover:not(:disabled){background:var(--nz-paper-alt)}#btn-file-open{background:var(--nz-paper);color:var(--nz-ink)}#btn-file-open:hover:not(:disabled){background:var(--nz-paper-alt)}.input-panel.drag-over{outline:3px dashed var(--nz-ink);outline-offset:-3px}#btn-download{background:var(--nz-paper);color:var(--nz-ink)}#btn-download:hover:not(:disabled){background:var(--nz-paper-alt)} +:root{--nz-green: #00FF9C;--nz-green-strong:#00E88D;--nz-green-soft: #B7FFE0;--nz-paper: #FFFEE5;--nz-paper-alt: #FAF8D4;--nz-ink: #000000;--nz-ink-70: rgba(0,0,0,.72);--nz-ink-20: rgba(0,0,0,.18);--nz-radius: 0;--nz-stroke: 2px solid #000;--nz-shadow: 6px 6px 0 0 #000;--nz-shadow-sm: 3px 3px 0 0 #000;--nz-dur: .16s cubic-bezier(.2,0,0,1);--font-display: "Zilla Slab", Georgia, serif;--font-body: "Inter", Arial, sans-serif;--font-mono: "Space Mono", "Courier New", monospace}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html{font-size:16px}body{background:var(--nz-paper);color:var(--nz-ink);font-family:var(--font-body);min-height:100vh;display:flex;flex-direction:column}header{background:var(--nz-paper);display:flex;align-items:center;justify-content:space-between;padding:0 2rem}.header-right{flex-shrink:0;display:flex;align-items:center}.btn-plus{font-family:var(--font-mono);font-weight:700;font-size:.7rem;text-transform:uppercase;letter-spacing:.1em;border:var(--nz-stroke);padding:.55rem 1rem;cursor:pointer;background:var(--nz-ink);color:var(--nz-green);box-shadow:var(--nz-shadow-sm);transition:transform var(--nz-dur),box-shadow var(--nz-dur);white-space:nowrap;display:inline-flex;align-items:center;gap:.2em}.btn-plus:hover{background:#222}.btn-plus:active{transform:translate(3px,3px);box-shadow:none}.plus-badge{color:var(--nz-green);font-size:1.1em;line-height:1}.header-inner,.header-left{max-width:1400px;width:100%;margin:0 auto;padding:1.25rem 2rem;display:flex;flex-direction:row;align-items:center;gap:1.5rem}.header-logo{height:64px;width:auto;display:block;flex-shrink:0}.header-sub{font-family:var(--font-body);font-size:.95rem;font-weight:400;color:var(--nz-ink)}.header-status,.status-hidden{display:none}main{flex:1;display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;max-width:1400px;width:100%;margin:0 auto;padding:2rem}.panel{border:var(--nz-stroke);box-shadow:var(--nz-shadow);display:flex;flex-direction:column;background:#fff}.panel-label{background:var(--nz-ink);color:var(--nz-paper);font-family:var(--font-mono);font-size:.65rem;font-weight:700;text-transform:uppercase;letter-spacing:.12em;padding:.35rem .75rem}@keyframes cursor-blink{0%,49%{opacity:1}50%,to{opacity:0}}.input-hint{font-family:var(--font-display);font-size:1.3rem;font-weight:700;color:var(--nz-ink);background:var(--nz-green);padding:.4rem .75rem;grid-column:1;grid-row:1;align-self:start;display:inline-flex;align-items:center}.input-hint:after{content:"";display:inline-block;width:2px;height:1.1em;background:var(--nz-ink);margin-left:4px;animation:cursor-blink 1s step-start infinite}.input-panel{grid-column:1;grid-row:2}.output-panel{grid-column:2;grid-row:2}#input{flex:1;border:none;border-top:var(--nz-stroke);padding:1rem;font-family:var(--font-mono);font-size:.9rem;line-height:1.65;resize:none;min-height:400px;outline:none;background:#fff;color:var(--nz-ink)}#input:focus{background:#fff}#input::placeholder{color:var(--nz-ink-70)}#output{flex:1;border-top:var(--nz-stroke);padding:1rem;font-family:var(--font-mono);font-size:.9rem;line-height:2;min-height:400px;overflow-y:auto;white-space:pre-wrap;word-break:break-word;background:#fff}.token.name{background:var(--nz-green);border:2px solid var(--nz-ink);padding:0 3px;cursor:pointer;font-weight:700;transition:background var(--nz-dur);display:inline;position:relative}.token.name:hover{background:var(--nz-green-strong);outline:2px solid var(--nz-ink)}.token.honorific{background:var(--nz-green);border-color:var(--nz-ink);box-shadow:var(--nz-shadow-sm)}.token.replaced{background:var(--nz-ink);color:var(--nz-green);font-family:var(--font-mono);font-weight:700;font-size:.82em;padding:1px 5px;letter-spacing:.04em;display:inline;cursor:default}.token.email{background:#a8daff;border:2px solid var(--nz-ink);padding:0 3px;cursor:pointer;font-weight:700;transition:background var(--nz-dur);display:inline;position:relative}.token.email:hover{background:#7ec8ff;outline:2px solid var(--nz-ink)}.token.phone{background:#e0aaff;border:2px solid var(--nz-ink);padding:0 3px;cursor:pointer;font-weight:700;transition:background var(--nz-dur);display:inline;position:relative}.token.phone:hover{background:#c77dff;outline:2px solid var(--nz-ink)}.token.date{background:#ffd6a5;border:2px solid var(--nz-ink);padding:0 3px;cursor:pointer;font-weight:700;transition:background var(--nz-dur);display:inline;position:relative}.token.date:hover{background:#ffba6b;outline:2px solid var(--nz-ink)}.token.address{background:#b9fbc0;border:2px solid var(--nz-ink);padding:0 3px;cursor:pointer;font-weight:700;transition:background var(--nz-dur);display:inline;position:relative}.token.address:hover{background:#82ef90;outline:2px solid var(--nz-ink)}.name-popup{position:absolute;top:calc(100% + 4px);left:0;z-index:100;display:flex;background:var(--nz-paper);border:var(--nz-stroke);box-shadow:var(--nz-shadow);white-space:nowrap}.popup-btn{font-family:var(--font-mono);font-weight:700;font-size:.7rem;text-transform:uppercase;letter-spacing:.1em;border:none;padding:.45rem .9rem;cursor:pointer;transition:background var(--nz-dur)}.popup-btn.delete{background:var(--nz-ink);color:var(--nz-paper);border-right:var(--nz-stroke)}.popup-btn.delete:hover{background:#333}.popup-btn.false-positive{background:var(--nz-paper);color:var(--nz-ink);border-right:var(--nz-stroke)}.popup-btn.false-positive:hover{background:var(--nz-paper-alt)}.popup-btn.replace{background:var(--nz-green);color:var(--nz-ink)}.popup-btn.replace:hover{background:var(--nz-green-strong)}.controls{grid-column:1 / 3;grid-row:3;display:flex;gap:1rem;align-items:center;flex-wrap:wrap}.opt-label{font-family:var(--font-mono);font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;cursor:pointer;display:flex;align-items:center;gap:.4rem;color:var(--nz-ink);-webkit-user-select:none;user-select:none}.opt-label input[type=checkbox]{width:1rem;height:1rem;accent-color:var(--nz-green);cursor:pointer}.prefix-details{font-family:var(--font-mono);font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em}.prefix-summary{cursor:pointer;-webkit-user-select:none;user-select:none;color:var(--nz-ink);padding:.3rem 0;list-style:none;display:inline-flex;align-items:center;gap:.4rem}.prefix-summary::-webkit-details-marker{display:none}.prefix-summary:before{content:"▶";font-size:.55rem;transition:transform var(--nz-dur)}details[open] .prefix-summary:before{transform:rotate(90deg)}.prefix-grid{display:grid;grid-template-columns:auto 1fr;gap:.35rem .6rem;align-items:center;margin-top:.5rem;padding:.6rem .75rem;border:2px solid var(--nz-ink);background:var(--nz-paper)}.prefix-label{font-family:var(--font-mono);font-size:.65rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--nz-ink);white-space:nowrap;cursor:default}.prefix-input{font-family:var(--font-mono);font-size:.72rem;border:2px solid var(--nz-ink);background:var(--nz-paper);color:var(--nz-ink);padding:.2rem .4rem;width:8rem;outline:none;transition:box-shadow var(--nz-dur)}.prefix-input:focus{box-shadow:2px 2px 0 var(--nz-ink)}.prefix-input::placeholder{color:var(--nz-ink-70)}button{font-family:var(--font-mono);font-weight:700;font-size:.75rem;text-transform:uppercase;letter-spacing:.1em;border:var(--nz-stroke);padding:.75rem 1.4rem;cursor:pointer;box-shadow:var(--nz-shadow-sm);transition:transform var(--nz-dur),box-shadow var(--nz-dur);white-space:nowrap;border-radius:var(--nz-radius)}button:active:not(:disabled){transform:translate(3px,3px);box-shadow:none}button:disabled{opacity:.35;cursor:not-allowed;box-shadow:none}#btn-analyse{background:var(--nz-green);color:var(--nz-ink)}#btn-analyse:hover:not(:disabled){background:var(--nz-green-strong)}#btn-purge{background:var(--nz-paper);color:var(--nz-ink)}#btn-purge:hover:not(:disabled){background:var(--nz-paper-alt)}#btn-copy{background:var(--nz-ink);color:var(--nz-paper);margin-left:auto}#btn-copy:hover:not(:disabled){background:#222}.status-hidden{display:none}.legend{grid-column:1 / 3;grid-row:4;display:flex;gap:1.5rem;align-items:center;font-family:var(--font-mono);font-size:.65rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;flex-wrap:wrap;color:var(--nz-ink-70)}#stats{color:var(--nz-ink)}.legend-item{display:flex;align-items:center;gap:.4rem}.legend-swatch{width:1.1rem;height:1.1rem;border:2px solid var(--nz-ink);display:inline-block}.legend-swatch.name{background:var(--nz-green)}.legend-swatch.honorific{background:var(--nz-green);box-shadow:var(--nz-shadow-sm)}.legend-swatch.email{background:#a8daff}.legend-swatch.phone{background:#e0aaff}.legend-swatch.date{background:#ffd6a5}.legend-swatch.address{background:#b9fbc0}.legend-swatch.replaced{background:var(--nz-ink)}.status.loading,.status.ready,.status.error,.status.warn{display:none}footer{margin-top:4rem;padding:3rem 2rem 2.5rem;font-family:var(--font-mono);font-size:.8rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1.25rem;color:var(--nz-ink-70);background:var(--nz-paper);position:relative}.footer-links{display:flex;align-items:center;justify-content:center;gap:2rem}.footer-links a{color:var(--nz-ink);text-decoration:none;display:flex;align-items:center;gap:.35rem}.footer-github{letter-spacing:0}.footer-github svg{flex-shrink:0}.footer-brand{font-size:.58rem;color:var(--nz-ink-70);text-decoration:none;letter-spacing:.06em;text-transform:none}.footer-version{position:absolute;bottom:1rem;right:2rem;font-family:var(--font-mono);font-size:.55rem;color:var(--nz-ink-20);letter-spacing:0;text-transform:none;-webkit-user-select:none;user-select:none}@media (max-width: 800px){main{grid-template-columns:1fr;padding:1rem}.input-hint{grid-column:1;grid-row:auto;font-size:1rem}.input-panel,.output-panel,.controls,.legend{grid-column:1;grid-row:auto}#input,#output{min-height:250px}.header-sub{font-size:.8rem}footer{margin-top:2rem;padding:2rem 1rem}.btn-plus{font-size:.62rem;padding:.45rem .7rem}}.plus-modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1000;display:flex;align-items:center;justify-content:center}.plus-modal[hidden]{display:none}.plus-modal-backdrop{position:absolute;top:0;right:0;bottom:0;left:0;background:#000000b8;cursor:pointer}.plus-modal-box{position:relative;background:var(--nz-paper);border:var(--nz-stroke);box-shadow:10px 10px #000;max-width:680px;width:calc(100% - 2rem);max-height:92vh;overflow-y:auto;display:flex;flex-direction:column}.plus-modal-close{position:absolute;top:.75rem;right:.75rem;background:none;border:none;font-size:1.1rem;cursor:pointer;padding:.25rem .5rem;box-shadow:none;color:var(--nz-ink);line-height:1}.plus-modal-close:hover{background:var(--nz-ink);color:var(--nz-paper)}.plus-modal-close:active{transform:none}.plus-modal-header{background:var(--nz-ink);padding:1.5rem 2rem 1.25rem}.plus-modal-logo{font-family:var(--font-display);font-size:2rem;font-weight:700;color:var(--nz-green);display:block;margin-bottom:.4rem}.plus-modal-tagline{font-family:var(--font-body);font-size:.95rem;color:#ffffffd9;margin:0}.plus-modal-body{padding:1.75rem 2rem;display:flex;flex-direction:column;gap:1.5rem}.plus-section p{font-family:var(--font-body);font-size:.9rem;line-height:1.7;color:var(--nz-ink);margin-bottom:.6rem}.plus-section-title{font-family:var(--font-mono);font-size:.65rem;font-weight:700;text-transform:uppercase;letter-spacing:.12em;background:var(--nz-ink);color:var(--nz-paper);display:inline-block;padding:.2rem .6rem;margin-bottom:.75rem}.plus-compare{display:grid;grid-template-columns:1fr 1fr;gap:1rem;border:var(--nz-stroke)}.plus-compare-col{padding:1rem 1.1rem}.plus-compare-col--plus{background:var(--nz-green);border-left:var(--nz-stroke)}.plus-compare-label{font-family:var(--font-mono);font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;margin-bottom:.75rem;display:flex;align-items:center;gap:.4rem}.plus-compare-tag{font-size:.6rem;background:var(--nz-ink-20);padding:.1rem .4rem}.plus-compare-tag--plus{background:var(--nz-ink);color:var(--nz-green)}.plus-list{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:.4rem}.plus-list li{font-family:var(--font-body);font-size:.85rem;line-height:1.4;padding-left:1.1em;position:relative}.plus-list li:before{content:"—";position:absolute;left:0;color:var(--nz-ink-70);font-size:.75em}.plus-code{font-family:var(--font-mono);font-size:.8rem;background:var(--nz-ink);color:var(--nz-green);padding:.85rem 1rem;margin-top:.75rem;white-space:pre;overflow-x:auto;border:none}.plus-modal-footer{border-top:var(--nz-stroke);padding:1.25rem 2rem;display:flex;align-items:center;gap:1.5rem;flex-wrap:wrap}.btn-plus-download{font-family:var(--font-mono);font-weight:700;font-size:.75rem;text-transform:uppercase;letter-spacing:.1em;background:var(--nz-green);color:var(--nz-ink);border:var(--nz-stroke);padding:.75rem 1.4rem;cursor:pointer;box-shadow:var(--nz-shadow-sm);text-decoration:none;display:inline-flex;align-items:center;gap:.5rem;transition:transform var(--nz-dur),box-shadow var(--nz-dur),background var(--nz-dur)}.btn-plus-download:hover{background:var(--nz-green-strong)}.btn-plus-download:active{transform:translate(3px,3px);box-shadow:none}.plus-download-group{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.btn-plus-download--platform{font-size:.7rem;padding:.6rem 1rem}.plus-github-link{font-family:var(--font-mono);font-size:.72rem;font-weight:700;color:var(--nz-ink);text-decoration:none;text-transform:uppercase;letter-spacing:.08em}.plus-github-link:hover{text-decoration:underline}@media (max-width: 600px){.plus-modal-box{max-height:100vh;border-left:none;border-right:none}.plus-compare{grid-template-columns:1fr}.plus-compare-col--plus{border-left:none;border-top:var(--nz-stroke)}.plus-modal-body,.plus-modal-header{padding:1.25rem 1rem}.plus-modal-footer{padding:1rem}}#btn-undo{background:var(--nz-paper);color:var(--nz-ink)}#btn-undo:hover:not(:disabled){background:var(--nz-paper-alt)}#btn-file-open{background:var(--nz-paper);color:var(--nz-ink)}#btn-file-open:hover:not(:disabled){background:var(--nz-paper-alt)}.input-panel.drag-over{outline:3px dashed var(--nz-ink);outline-offset:-3px}#btn-download{background:var(--nz-paper);color:var(--nz-ink)}#btn-download:hover:not(:disabled){background:var(--nz-paper-alt)} diff --git a/dist/index.html b/dist/index.html index 262b799..4b390f6 100644 --- a/dist/index.html +++ b/dist/index.html @@ -15,8 +15,8 @@ - - + + @@ -85,17 +85,37 @@

Voraussetzungen

diff --git a/index.html b/index.html index 2b09b35..348bb35 100644 --- a/index.html +++ b/index.html @@ -84,17 +84,37 @@

Voraussetzungen

diff --git a/src/style.css b/src/style.css index 74be88e..364236b 100644 --- a/src/style.css +++ b/src/style.css @@ -824,6 +824,18 @@ footer { .btn-plus-download:hover { background: var(--nz-green-strong); } .btn-plus-download:active { transform: translate(3px,3px); box-shadow: none; } +.plus-download-group { + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; +} + +.btn-plus-download--platform { + font-size: 0.7rem; + padding: 0.6rem 1rem; +} + .plus-github-link { font-family: var(--font-mono); font-size: 0.72rem; From bbadcd2d21b93cbedfb221db02efff28cd37c141 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 8 Jun 2026 09:22:56 +0000 Subject: [PATCH 2/2] Add progress bar and batch folder processing to Desktop GUI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ttk.Progressbar below the text area: indeterminate mode during single- file analysis, determinate mode (per-file steps) during batch runs - "Ordner verarbeiten…" button opens a folder picker, finds all .txt files recursively, confirms with the user (count shown), then processes each file via anonymise() in a background thread writing _anon.txt next to each original; already-anonymised files (_anon suffix) are skipped automatically - UI buttons locked during batch run and restored on completion; status label shows "Verarbeite: filename.txt (N/total)" per step - Both features use the existing threading pattern; no CLI changes https://claude.ai/code/session_01EVuoocp6FUTUt4jX9njQrK --- cli/namescrub_gui.py | 126 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/cli/namescrub_gui.py b/cli/namescrub_gui.py index 98df6b5..6d3f87a 100644 --- a/cli/namescrub_gui.py +++ b/cli/namescrub_gui.py @@ -181,6 +181,14 @@ def _build_ui(self): self.btn_save.pack(side=tk.LEFT, padx=(0, 8)) self.btn_save.config(state=tk.DISABLED) + self.btn_batch = tk.Button( + btns, text="Ordner verarbeiten…", + bg=BG, fg=INK, activebackground="#F0EFC0", + command=self._batch_folder, **btn_cfg, + ) + self.btn_batch.pack(side=tk.LEFT, padx=(0, 8)) + self.btn_batch.config(state=tk.DISABLED) + self.btn_copy = tk.Button( btns, text="In Zwischenablage", bg=INK, fg=GREEN, activebackground="#222222", @@ -189,6 +197,15 @@ def _build_ui(self): self.btn_copy.pack(side=tk.RIGHT) self.btn_copy.config(state=tk.DISABLED) + # ── Fortschrittsbalken ──────────────────────────────────────────────── + progress_frame = tk.Frame(self.root, bg=BG, padx=14, pady=0) + progress_frame.pack(fill=tk.X) + + self.progressbar = ttk.Progressbar( + progress_frame, mode="indeterminate", length=0, + ) + self.progressbar.pack(fill=tk.X, ipady=4) + # ── Statusleiste ────────────────────────────────────────────────────── self.status_var = tk.StringVar(value="Modell wird geladen…") status_bar = tk.Label( @@ -216,6 +233,7 @@ def _load_model(self): def _on_model_ready(self): self.model_label.config(text=f"✓ {self.model_name}", fg=GREEN) self.btn_analyse.config(state=tk.NORMAL) + self.btn_batch.config(state=tk.NORMAL) self.status_var.set("Bereit. Text eingeben und Analyse starten.") def _on_spacy_missing(self): @@ -248,6 +266,7 @@ def _analyse(self): self.btn_analyse.config(state=tk.DISABLED, text="Analyse läuft…") self.status_var.set("Analyse läuft…") + self._progress_start(indeterminate=True) entity_types = self._get_entity_types() def run(): @@ -258,6 +277,7 @@ def run(): threading.Thread(target=run, daemon=True).start() def _show_result(self, result: str, mapping: dict): + self._progress_stop() self.output_text.config(state=tk.NORMAL) self.output_text.delete("1.0", tk.END) @@ -290,6 +310,112 @@ def _show_result(self, result: str, mapping: dict): self.btn_save.config(state=tk.NORMAL) self.btn_copy.config(state=tk.NORMAL) + # ── Fortschrittsbalken-Hilfsmethoden ────────────────────────────────────── + + def _progress_start(self, indeterminate: bool = True, maximum: int = 100): + """Startet den Fortschrittsbalken. indeterminate=True für unbekannte Dauer.""" + if indeterminate: + self.progressbar.config(mode="indeterminate") + self.progressbar.start(15) + else: + self.progressbar.config(mode="determinate", maximum=maximum, value=0) + + def _progress_stop(self): + """Stoppt den Fortschrittsbalken und setzt ihn zurück.""" + self.progressbar.stop() + self.progressbar.config(mode="determinate", value=0) + + def _set_progress(self, value: int): + """Setzt den Fortschrittswert des Balkens (nur im determinate-Modus).""" + self.progressbar["value"] = value + + # ── Batch-Verarbeitung ──────────────────────────────────────────────────── + + def _batch_folder(self): + if not self.nlp: + return + + folder = filedialog.askdirectory(title="Ordner mit .txt-Dateien auswählen") + if not folder: + return + + txt_files = sorted(Path(folder).rglob("*.txt")) + + if not txt_files: + messagebox.showwarning( + "Keine Dateien", + f"Im gewählten Ordner wurden keine .txt-Dateien gefunden:\n{folder}", + ) + return + + n_files = len(txt_files) + confirmed = messagebox.askyesno( + "Batch-Verarbeitung", + f"{n_files} .txt-Datei{'en' if n_files != 1 else ''} gefunden.\n\n" + "Alle anonymisieren?\n\n" + "Ausgabe: _anon.txt (im gleichen Ordner)", + ) + if not confirmed: + return + + entity_types = self._get_entity_types() + + # UI sperren während der Verarbeitung + self.btn_analyse.config(state=tk.DISABLED) + self.btn_batch.config(state=tk.DISABLED) + self.btn_open.config(state=tk.DISABLED) + self._progress_start(indeterminate=False, maximum=n_files) + self.status_var.set(f"Batch-Verarbeitung gestartet… (0/{n_files})") + + def run(): + done = 0 + errors = 0 + for i, txt_path in enumerate(txt_files, 1): + # Überspringe bereits anonymisierte Dateien + if txt_path.stem.endswith("_anon"): + self.root.after(0, lambda p=txt_path, idx=i: self.status_var.set( + f"Übersprungen: {p.name} ({idx}/{n_files})" + )) + self.root.after(0, lambda v=i: self._set_progress(v)) + continue + + self.root.after(0, lambda p=txt_path, idx=i: self.status_var.set( + f"Verarbeite: {p.name} ({idx}/{n_files})" + )) + + try: + text = txt_path.read_text(encoding="utf-8") + if text.strip(): + result, _ = anonymise(text, self.nlp, entity_types) + out_path = txt_path.with_stem(txt_path.stem + "_anon") + out_path.write_text(result, encoding="utf-8") + done += 1 + except Exception: + errors += 1 + + self.root.after(0, lambda v=i: self._set_progress(v)) + + self.root.after(0, lambda: self._on_batch_done(done, errors, n_files)) + + threading.Thread(target=run, daemon=True).start() + + def _on_batch_done(self, done: int, errors: int, total: int): + """Wird im Haupt-Thread nach Abschluss der Batch-Verarbeitung aufgerufen.""" + self._progress_stop() + self.btn_analyse.config(state=tk.NORMAL) + self.btn_batch.config(state=tk.NORMAL) + self.btn_open.config(state=tk.NORMAL) + + msg = f"{done} Datei{'en' if done != 1 else ''} anonymisiert." + if errors: + msg += ( + f"\n{errors} Datei{'en' if errors != 1 else ''} " + f"konnte{'n' if errors != 1 else ''} nicht verarbeitet werden." + ) + + self.status_var.set(f"Batch abgeschlossen: {done}/{total} Dateien anonymisiert.") + messagebox.showinfo("Batch abgeschlossen", msg) + # ── Datei-Operationen ────────────────────────────────────────────────────── def _clear_placeholder(self, _event):