+170
-67
@@ -1,5 +0,11 @@ | ||
| // Zoom pro Subdomain - Hintergrundlogik. | ||
| // Setzt den Zoom bewusst mit scope "per-tab", damit der Browser ihn NICHT in seiner | ||
| // eigenen (host-weiten) Zoom-Tabelle persistiert. Genau dadurch faerbt der Zoom nicht | ||
| // auf andere Subdomains ab. | ||
| // Zoom pro Adresse - Hintergrundlogik. | ||
| // Der Zoom wird mit scope "per-tab" gesetzt, damit der Browser ihn NICHT in seiner | ||
| // eigenen host-weiten Zoom-Tabelle ablegt. Welcher Faktor gilt, entscheidet diese | ||
| // Erweiterung bei jeder Navigation selbst anhand des Adressmusters. | ||
| // | ||
| // Musterschreibweise (Vergleich ohne Schema, ohne #fragment, Kleinschreibung): | ||
| // app.example.com nur dieser Host, jede Seite darunter | ||
| // *.example.com der Host selbst und alles darunter | ||
| // example.com/index.php?kat=sub genau diese eine Adresse | ||
| // example.com/admin/* alles unterhalb von /admin/ | ||
@@ -13,22 +19,47 @@ const api = globalThis.browser ?? globalThis.chrome; | ||
| // tabId -> Faktor, den wir selbst zuletzt gesetzt haben (um eigene onZoomChange-Events zu ignorieren) | ||
| const selfSet = new Map(); | ||
| // Tabs, deren Zoom-Scope wir bereits auf per-tab umgestellt haben | ||
| const scopedTabs = new Set(); | ||
| function ready() { | ||
| return (readyPromise ??= loadState()); | ||
| return (readyPromise ??= laden()); | ||
| } | ||
| async function loadState() { | ||
| async function laden() { | ||
| const stored = await api.storage.local.get(DEFAULTS); | ||
| state = { ...DEFAULTS, ...stored }; | ||
| const migriert = migriere(state.rules); | ||
| if (migriert) { | ||
| state.rules = migriert; | ||
| await api.storage.local.set({ rules: migriert }); | ||
| } | ||
| return state; | ||
| } | ||
| function hostOf(url) { | ||
| // Regeln aus Version 1.0 kannten nur host + includeSubdomains. | ||
| function migriere(rules) { | ||
| if (!Array.isArray(rules)) return null; | ||
| let noetig = false; | ||
| const neu = rules.map((r) => { | ||
| if (typeof r.pattern === "string" && r.pattern) return r; | ||
| noetig = true; | ||
| const host = String(r.host || "").toLowerCase(); | ||
| return { | ||
| pattern: r.includeSubdomains === true ? "*." + host : host, | ||
| zoom: r.zoom, | ||
| enabled: r.enabled !== false, | ||
| }; | ||
| }); | ||
| return noetig ? neu : null; | ||
| } | ||
| // --- Adressen und Muster -------------------------------------------------- | ||
| function adresseVon(url) { | ||
| try { | ||
| const u = new URL(url); | ||
| if (u.protocol !== "http:" && u.protocol !== "https:") return null; | ||
| return u.hostname.toLowerCase(); | ||
| return { | ||
| host: u.hostname.toLowerCase(), | ||
| voll: (u.hostname + u.pathname + u.search).toLowerCase(), | ||
| }; | ||
| } catch { | ||
@@ -39,33 +70,93 @@ return null; | ||
| // Spezifischste passende Regel gewinnt: exakter Treffer schlaegt Subdomain-Wildcard, | ||
| // bei mehreren Wildcards die mit dem laengsten Host. | ||
| function matchRule(host) { | ||
| if (!host) return null; | ||
| let best = null; | ||
| for (const rule of state.rules) { | ||
| if (rule.enabled === false) continue; | ||
| const h = String(rule.host || "").toLowerCase(); | ||
| if (!h) continue; | ||
| const exact = host === h; | ||
| const sub = rule.includeSubdomains === true && host.endsWith("." + h); | ||
| if (!exact && !sub) continue; | ||
| const score = h.length + (exact ? 1000 : 0); | ||
| if (!best || score > best.score) best = { rule, score }; | ||
| function musterNormalisieren(roh) { | ||
| let s = String(roh || "").trim().toLowerCase(); | ||
| if (!s) return ""; | ||
| const schema = s.indexOf("://"); | ||
| if (schema > 0 && schema < 12) s = s.slice(schema + 3); | ||
| const raute = s.indexOf("#"); | ||
| if (raute >= 0) s = s.slice(0, raute); | ||
| // Port abschneiden - zugeordnet wird ohne Port, sonst liefe localhost:5173/app ins Leere | ||
| let grenze = s.length; | ||
| for (const zeichen of ["/", "?"]) { | ||
| const i = s.indexOf(zeichen); | ||
| if (i >= 0 && i < grenze) grenze = i; | ||
| } | ||
| return best ? best.rule : null; | ||
| const hostTeil = s.slice(0, grenze); | ||
| const doppel = hostTeil.lastIndexOf(":"); | ||
| if (doppel > 0) { | ||
| const port = hostTeil.slice(doppel + 1); | ||
| if (port.length && [...port].every((z) => z >= "0" && z <= "9")) { | ||
| s = hostTeil.slice(0, doppel) + s.slice(grenze); | ||
| } | ||
| } | ||
| return s; | ||
| } | ||
| function faktorFuer(host) { | ||
| const rule = matchRule(host); | ||
| return rule ? rule.zoom : 1; | ||
| function istHostMuster(muster) { | ||
| return !muster.includes("/") && !muster.includes("?"); | ||
| } | ||
| async function applyToTab(tabId, url) { | ||
| const host = hostOf(url); | ||
| if (!host) return; | ||
| const faktor = faktorFuer(host); | ||
| // Platzhalter * steht fuer beliebig viele Zeichen. | ||
| function globPasst(muster, text) { | ||
| const teile = muster.split("*"); | ||
| if (teile.length === 1) return text === muster; | ||
| if (!text.startsWith(teile[0])) return false; | ||
| const letzt = teile[teile.length - 1]; | ||
| let pos = teile[0].length; | ||
| for (let i = 1; i < teile.length - 1; i++) { | ||
| const t = teile[i]; | ||
| if (!t) continue; | ||
| const idx = text.indexOf(t, pos); | ||
| if (idx < 0) return false; | ||
| pos = idx + t.length; | ||
| } | ||
| if (!letzt) return true; | ||
| return text.length - letzt.length >= pos && text.endsWith(letzt); | ||
| } | ||
| function passt(muster, adr) { | ||
| if (!muster || !adr) return false; | ||
| if (istHostMuster(muster)) { | ||
| if (muster.startsWith("*.")) { | ||
| const basis = muster.slice(2); | ||
| return adr.host === basis || adr.host.endsWith("." + basis); | ||
| } | ||
| if (muster.includes("*")) return globPasst(muster, adr.host); | ||
| return adr.host === muster; | ||
| } | ||
| return globPasst(muster, adr.voll); | ||
| } | ||
| // Je genauer das Muster, desto hoeher das Gewicht. | ||
| function gewicht(muster) { | ||
| const konkret = muster.split("*").join("").length; | ||
| let punkte = konkret; | ||
| if (!istHostMuster(muster)) punkte += 10000; // Adressmuster schlaegt Hostmuster | ||
| if (!muster.includes("*")) punkte += 1000; // ohne Platzhalter schlaegt mit | ||
| return punkte; | ||
| } | ||
| function regelFuer(adr) { | ||
| let beste = null; | ||
| for (const r of state.rules) { | ||
| if (r.enabled === false) continue; | ||
| const muster = musterNormalisieren(r.pattern); | ||
| if (!passt(muster, adr)) continue; | ||
| const punkte = gewicht(muster); | ||
| if (!beste || punkte > beste.punkte) beste = { regel: r, punkte }; | ||
| } | ||
| return beste ? beste.regel : null; | ||
| } | ||
| // --- Anwenden ------------------------------------------------------------- | ||
| async function aufTabAnwenden(tabId, url) { | ||
| const adr = adresseVon(url); | ||
| if (!adr) return; | ||
| const regel = regelFuer(adr); | ||
| const faktor = regel ? regel.zoom : 1; | ||
| try { | ||
| if (!scopedTabs.has(tabId)) { | ||
| // per-tab => der Browser merkt sich den Wert nicht global fuer den Host | ||
| await api.tabs.setZoomSettings(tabId, { scope: "per-tab", mode: "automatic" }); | ||
@@ -80,10 +171,10 @@ scopedTabs.add(tabId); | ||
| } catch { | ||
| // Tab geschlossen oder privilegierte Seite (about:, chrome://, Store-Seiten) - ignorieren | ||
| // Tab geschlossen oder privilegierte Seite - ignorieren | ||
| } | ||
| } | ||
| async function applyToAllTabs() { | ||
| async function aufAlleTabsAnwenden() { | ||
| try { | ||
| const tabs = await api.tabs.query({}); | ||
| await Promise.all(tabs.map((t) => (t.url ? applyToTab(t.id, t.url) : null))); | ||
| await Promise.all(tabs.map((t) => (t.url ? aufTabAnwenden(t.id, t.url) : null))); | ||
| } catch { | ||
@@ -94,3 +185,3 @@ /* ignorieren */ | ||
| async function saveRules(rules) { | ||
| async function regelnSpeichern(rules) { | ||
| state.rules = rules; | ||
@@ -100,40 +191,51 @@ await api.storage.local.set({ rules }); | ||
| // Manuelles Strg+/- als Regel fuer genau diesen Host uebernehmen. | ||
| // Pro Host existiert hoechstens eine Regel; includeSubdomains ist eine Eigenschaft davon. | ||
| async function upsertRule(host, zoom) { | ||
| // Manuelles Strg+/- uebernehmen: greift bereits eine Regel, wird deren Wert | ||
| // angepasst - sonst entsteht eine neue Regel fuer den Host der Seite. | ||
| async function uebernehmen(adr, zoom) { | ||
| const rules = state.rules.slice(); | ||
| const i = rules.findIndex((r) => String(r.host).toLowerCase() === host); | ||
| const treffer = regelFuer(adr); | ||
| const istStandard = Math.abs(zoom - 1) < 0.001; | ||
| if (istStandard) { | ||
| if (i < 0) { | ||
| // 100 % waehrend eine *.-Regel greift: als ausdrueckliche Ausnahme festhalten | ||
| if (!matchRule(host)) return; | ||
| rules.push({ host, zoom: 1, enabled: true, includeSubdomains: false }); | ||
| } else if (rules[i].includeSubdomains === true) { | ||
| rules[i] = { ...rules[i], zoom: 1, enabled: true }; // Wildcard-Regel nicht wegwerfen | ||
| if (treffer) { | ||
| const i = rules.indexOf(treffer); | ||
| const muster = musterNormalisieren(treffer.pattern); | ||
| if (Math.abs(treffer.zoom - zoom) < 0.001) return; | ||
| if (istStandard && muster === adr.host) { | ||
| rules.splice(i, 1); // schlichte Hostregel auf 100 % = ueberfluessig | ||
| } else if (istStandard) { | ||
| // spezielleres Muster: Ausnahme fuer genau diese Seite festhalten | ||
| rules.push({ pattern: adr.host, zoom: 1, enabled: true }); | ||
| } else { | ||
| rules.splice(i, 1); | ||
| rules[i] = { ...treffer, zoom, enabled: true }; | ||
| } | ||
| } else if (i >= 0) { | ||
| if (Math.abs(rules[i].zoom - zoom) < 0.001 && rules[i].enabled !== false) return; | ||
| rules[i] = { ...rules[i], zoom, enabled: true }; | ||
| } else { | ||
| rules.push({ host, zoom, enabled: true, includeSubdomains: false }); | ||
| if (istStandard) return; | ||
| rules.push({ pattern: adr.host, zoom, enabled: true }); | ||
| } | ||
| await saveRules(rules); | ||
| await regelnSpeichern(rules); | ||
| } | ||
| // --- Events --------------------------------------------------------------- | ||
| // --- Ereignisse ----------------------------------------------------------- | ||
| api.webNavigation.onCommitted.addListener(async (details) => { | ||
| async function beiNavigation(details) { | ||
| if (details.frameId !== 0) return; | ||
| await ready(); | ||
| await applyToTab(details.tabId, details.url); | ||
| }); | ||
| await aufTabAnwenden(details.tabId, details.url); | ||
| } | ||
| api.webNavigation.onCommitted.addListener(beiNavigation); | ||
| // Seiten, die die Adresse per JavaScript wechseln, loesen kein onCommitted aus. | ||
| if (api.webNavigation.onHistoryStateUpdated) { | ||
| api.webNavigation.onHistoryStateUpdated.addListener(beiNavigation); | ||
| } | ||
| if (api.webNavigation.onReferenceFragmentUpdated) { | ||
| api.webNavigation.onReferenceFragmentUpdated.addListener(beiNavigation); | ||
| } | ||
| api.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { | ||
| if (!changeInfo.url && changeInfo.status !== "complete") return; | ||
| await ready(); | ||
| await applyToTab(tabId, changeInfo.url || tab.url); | ||
| await aufTabAnwenden(tabId, changeInfo.url || tab.url); | ||
| }); | ||
@@ -153,10 +255,9 @@ | ||
| const eigen = selfSet.get(tabId); | ||
| // war unsere eigene Aenderung -> nicht als manuelle Anpassung werten | ||
| if (eigen !== undefined && Math.abs(eigen - newZoomFactor) < 0.001) return; | ||
| const tab = await api.tabs.get(tabId).catch(() => null); | ||
| const host = hostOf(tab && tab.url); | ||
| if (!host) return; | ||
| const adr = adresseVon(tab && tab.url); | ||
| if (!adr) return; | ||
| await upsertRule(host, newZoomFactor); | ||
| await uebernehmen(adr, newZoomFactor); | ||
| selfSet.set(tabId, newZoomFactor); | ||
@@ -168,5 +269,7 @@ }); | ||
| if (area !== "local") return; | ||
| await loadState(); | ||
| const stored = await api.storage.local.get(DEFAULTS); | ||
| state = { ...DEFAULTS, ...stored }; | ||
| state.rules = migriere(state.rules) || state.rules; // Altbestand im Arbeitsspeicher angleichen | ||
| readyPromise = Promise.resolve(state); | ||
| await applyToAllTabs(); | ||
| await aufAlleTabsAnwenden(); | ||
| }); | ||
@@ -176,3 +279,3 @@ | ||
| await ready(); | ||
| await applyToAllTabs(); | ||
| await aufAlleTabsAnwenden(); | ||
| }); | ||
@@ -183,4 +286,4 @@ | ||
| await ready(); | ||
| await applyToAllTabs(); | ||
| await aufAlleTabsAnwenden(); | ||
| }); | ||
| } |
+4
-4
| { | ||
| "manifest_version": 3, | ||
| "name": "Zoom pro Subdomain", | ||
| "version": "1.0.0", | ||
| "description": "Eigener Zoom-Faktor pro exakter Subdomain - ohne andere Subdomains derselben Domain zu beeinflussen.", | ||
| "name": "Zoom pro Adresse", | ||
| "version": "1.1.0", | ||
| "description": "Eigener Zoom pro Adresse - fuer einen Host, eine Domain samt allem darunter oder genau eine einzelne Seite.", | ||
| "permissions": [ | ||
@@ -17,3 +17,3 @@ "tabs", | ||
| "action": { | ||
| "default_title": "Zoom pro Subdomain", | ||
| "default_title": "Zoom pro Adresse", | ||
| "default_popup": "popup.html", | ||
@@ -20,0 +20,0 @@ "default_icon": { |
+90
-0
@@ -549,1 +549,91 @@ /* Zoom pro Subdomain - Messinstrument-Anmutung: Graphit, ein Bernstein-Akzent, | ||
| } | ||
| /* --- Version 1.1: Adresse, Geltungsbereich, Muster ----------------------- */ | ||
| .pfad { | ||
| margin-top: 2px; | ||
| font-family: var(--mono); | ||
| font-size: 11px; | ||
| color: var(--muted); | ||
| word-break: break-all; | ||
| } | ||
| .feldlabel { | ||
| margin-top: 13px; | ||
| font-family: var(--mono); | ||
| font-size: 9.5px; | ||
| letter-spacing: 0.1em; | ||
| text-transform: uppercase; | ||
| color: var(--muted); | ||
| } | ||
| .segmente { | ||
| display: grid; | ||
| grid-template-columns: repeat(3, 1fr); | ||
| gap: 3px; | ||
| margin-top: 6px; | ||
| padding: 3px; | ||
| background: var(--bg); | ||
| border: 1px solid var(--line); | ||
| border-radius: 6px; | ||
| } | ||
| .segmente button { | ||
| padding: 5px 0; | ||
| font: inherit; | ||
| font-size: 11px; | ||
| color: var(--muted); | ||
| background: transparent; | ||
| border: 0; | ||
| border-radius: 4px; | ||
| cursor: pointer; | ||
| transition: background 0.12s, color 0.12s; | ||
| } | ||
| .segmente button:hover { | ||
| color: var(--fg); | ||
| } | ||
| .segmente button[aria-pressed="true"] { | ||
| color: var(--accent-ink); | ||
| background: var(--accent); | ||
| font-weight: 600; | ||
| } | ||
| .muster { | ||
| width: 100%; | ||
| margin-top: 6px; | ||
| padding: 6px 8px; | ||
| font: 11.5px var(--mono); | ||
| color: var(--fg); | ||
| background: var(--bg); | ||
| border: 1px solid var(--line); | ||
| border-radius: 5px; | ||
| } | ||
| .muster:focus { | ||
| outline: none; | ||
| border-color: var(--accent); | ||
| box-shadow: 0 0 0 3px var(--soft); | ||
| } | ||
| .stage .readout { | ||
| margin-top: 14px; | ||
| } | ||
| .hilfe { | ||
| margin: 6px 0 0; | ||
| font-size: 10.5px; | ||
| line-height: 1.5; | ||
| color: var(--muted); | ||
| } | ||
| .hilfe code { | ||
| font-family: var(--mono); | ||
| font-size: 10px; | ||
| color: var(--fg); | ||
| } | ||
| .rules li .rhost { | ||
| font-size: 11px; | ||
| } |
+30
-14
@@ -5,3 +5,3 @@ <!doctype html> | ||
| <meta charset="utf-8" /> | ||
| <title>Zoom pro Subdomain</title> | ||
| <title>Zoom pro Adresse</title> | ||
| <link rel="stylesheet" href="popup.css" /> | ||
@@ -11,3 +11,3 @@ </head> | ||
| <header class="bar"> | ||
| <span class="eyebrow">Zoom pro Subdomain</span> | ||
| <span class="eyebrow">Zoom pro Adresse</span> | ||
| <span id="count" class="eyebrow dim"></span> | ||
@@ -22,2 +22,23 @@ </header> | ||
| <div id="host" class="host">–</div> | ||
| <div id="pfad" class="pfad"></div> | ||
| <div class="feldlabel">Regel gilt für</div> | ||
| <div class="segmente" role="group" aria-label="Geltungsbereich"> | ||
| <button type="button" data-bereich="seite" title="Nur genau diese Adresse mit Pfad und Parametern"> | ||
| Diese Adresse | ||
| </button> | ||
| <button type="button" data-bereich="host" title="Alle Seiten dieser Website">Website</button> | ||
| <button type="button" data-bereich="sub" title="Diese Website und alles unterhalb der Domain"> | ||
| Unterdomains | ||
| </button> | ||
| </div> | ||
| <input | ||
| id="pattern" | ||
| class="muster" | ||
| type="text" | ||
| spellcheck="false" | ||
| autocomplete="off" | ||
| autocapitalize="off" | ||
| aria-label="Adressmuster, Sternchen als Platzhalter" | ||
| /> | ||
| <p id="inherit" class="note" hidden></p> | ||
@@ -49,7 +70,2 @@ | ||
| </div> | ||
| <label class="check"> | ||
| <input type="checkbox" id="subs" /> | ||
| <span>Auch für Subdomains von <b id="basehost"></b></span> | ||
| </label> | ||
| </section> | ||
@@ -61,3 +77,3 @@ | ||
| <button type="button" id="addToggle" class="ghost" aria-expanded="false"> | ||
| Domain hinzufügen | ||
| Adresse hinzufügen | ||
| </button> | ||
@@ -70,3 +86,3 @@ </div> | ||
| type="text" | ||
| placeholder="app.beispiel.de" | ||
| placeholder="test.test.com/index.php?kategorie=sub" | ||
| spellcheck="false" | ||
@@ -76,2 +92,6 @@ autocomplete="off" | ||
| /> | ||
| <p class="hilfe"> | ||
| Ganze Website: <code>test.test.com</code> · mit Platzhalter: | ||
| <code>test.test.com/index.php?kategorie=*</code> | ||
| </p> | ||
| <div class="formrow"> | ||
@@ -81,6 +101,2 @@ <span class="figure small"> | ||
| </span> | ||
| <label class="check inline"> | ||
| <input type="checkbox" id="newSubs" /> | ||
| <span>inkl. Subdomains</span> | ||
| </label> | ||
| </div> | ||
@@ -93,3 +109,3 @@ <p id="addError" class="note error" hidden></p> | ||
| <p id="empty" class="note"> | ||
| Noch keine Regeln. Stell oben einen Wert ein oder füge eine Domain hinzu. | ||
| Noch keine Regeln. Stell oben einen Wert ein oder füge eine Adresse hinzu. | ||
| </p> | ||
@@ -96,0 +112,0 @@ </section> |
+216
-88
@@ -1,2 +0,4 @@ | ||
| // Popup: Zoom fuer den aktiven Tab live stellen und Regeln pro Host verwalten. | ||
| // Popup: Zoom fuer die aktuelle Adresse stellen und Regeln verwalten. | ||
| // Eine Regel ist ein Adressmuster - Host, Host mit Unterdomains oder eine | ||
| // vollstaendige Adresse, wahlweise mit * als Platzhalter. | ||
@@ -11,4 +13,5 @@ const api = globalThis.browser ?? globalThis.chrome; | ||
| let tab = null; | ||
| let host = null; | ||
| let adresse = null; // { host, pfad, voll } | ||
| let rules = []; | ||
| let regelMuster = null; // Muster der Regel, die dieses Popup gerade verwaltet | ||
| let saveTimer = null; | ||
@@ -18,7 +21,11 @@ | ||
| function hostOf(url) { | ||
| function adresseVon(url) { | ||
| try { | ||
| const u = new URL(url); | ||
| if (u.protocol !== "http:" && u.protocol !== "https:") return null; | ||
| return u.hostname.toLowerCase(); | ||
| return { | ||
| host: u.hostname.toLowerCase(), | ||
| pfad: (u.pathname + u.search).toLowerCase(), | ||
| voll: (u.hostname + u.pathname + u.search).toLowerCase(), | ||
| }; | ||
| } catch { | ||
@@ -29,36 +36,110 @@ return null; | ||
| // Akzeptiert "app.beispiel.de" genauso wie eine komplette URL aus der Adresszeile. | ||
| function normalizeHost(raw) { | ||
| let s = String(raw || "").trim().toLowerCase(); | ||
| if (!s) return null; | ||
| if (!s.includes("://")) s = "https://" + s; | ||
| try { | ||
| const h = new URL(s).hostname; | ||
| if (!h) return null; | ||
| if (!h.includes(".") && h !== "localhost") return null; | ||
| return h; | ||
| } catch { | ||
| return null; | ||
| function musterNormalisieren(roh) { | ||
| let s = String(roh || "").trim().toLowerCase(); | ||
| if (!s) return ""; | ||
| const schema = s.indexOf("://"); | ||
| if (schema > 0 && schema < 12) s = s.slice(schema + 3); | ||
| const raute = s.indexOf("#"); | ||
| if (raute >= 0) s = s.slice(0, raute); | ||
| let grenze = s.length; | ||
| for (const zeichen of ["/", "?"]) { | ||
| const i = s.indexOf(zeichen); | ||
| if (i >= 0 && i < grenze) grenze = i; | ||
| } | ||
| const hostTeil = s.slice(0, grenze); | ||
| const doppel = hostTeil.lastIndexOf(":"); | ||
| if (doppel > 0) { | ||
| const port = hostTeil.slice(doppel + 1); | ||
| if (port.length && [...port].every((z) => z >= "0" && z <= "9")) { | ||
| s = hostTeil.slice(0, doppel) + s.slice(grenze); | ||
| } | ||
| } | ||
| return s; | ||
| } | ||
| function indexOfHost(h) { | ||
| return rules.findIndex((r) => String(r.host).toLowerCase() === h); | ||
| const istHostMuster = (m) => !m.includes("/") && !m.includes("?"); | ||
| function globPasst(muster, text) { | ||
| const teile = muster.split("*"); | ||
| if (teile.length === 1) return text === muster; | ||
| if (!text.startsWith(teile[0])) return false; | ||
| const letzt = teile[teile.length - 1]; | ||
| let pos = teile[0].length; | ||
| for (let i = 1; i < teile.length - 1; i++) { | ||
| const t = teile[i]; | ||
| if (!t) continue; | ||
| const idx = text.indexOf(t, pos); | ||
| if (idx < 0) return false; | ||
| pos = idx + t.length; | ||
| } | ||
| if (!letzt) return true; | ||
| return text.length - letzt.length >= pos && text.endsWith(letzt); | ||
| } | ||
| // gleiche Rangfolge wie im Background: exakter Treffer schlaegt Wildcard | ||
| function findEffective(h) { | ||
| let best = null; | ||
| function passt(muster, adr) { | ||
| if (!muster || !adr) return false; | ||
| if (istHostMuster(muster)) { | ||
| if (muster.startsWith("*.")) { | ||
| const basis = muster.slice(2); | ||
| return adr.host === basis || adr.host.endsWith("." + basis); | ||
| } | ||
| if (muster.includes("*")) return globPasst(muster, adr.host); | ||
| return adr.host === muster; | ||
| } | ||
| return globPasst(muster, adr.voll); | ||
| } | ||
| function gewicht(muster) { | ||
| let punkte = muster.split("*").join("").length; | ||
| if (!istHostMuster(muster)) punkte += 10000; | ||
| if (!muster.includes("*")) punkte += 1000; | ||
| return punkte; | ||
| } | ||
| function regelFuer(adr) { | ||
| let beste = null; | ||
| for (const r of rules) { | ||
| if (r.enabled === false) continue; | ||
| const rh = String(r.host).toLowerCase(); | ||
| const exact = h === rh; | ||
| const sub = r.includeSubdomains === true && h.endsWith("." + rh); | ||
| if (!exact && !sub) continue; | ||
| const score = rh.length + (exact ? 1000 : 0); | ||
| if (!best || score > best.score) best = { rule: r, score }; | ||
| const m = musterNormalisieren(r.pattern); | ||
| if (!passt(m, adr)) continue; | ||
| const p = gewicht(m); | ||
| if (!beste || p > beste.p) beste = { regel: r, p }; | ||
| } | ||
| return best ? best.rule : null; | ||
| return beste ? beste.regel : null; | ||
| } | ||
| // --- Muster und Bereichsknoepfe ------------------------------------------- | ||
| function musterFuerBereich(bereich) { | ||
| if (!adresse) return ""; | ||
| if (bereich === "seite") return adresse.voll; | ||
| if (bereich === "sub") return "*." + adresse.host; | ||
| return adresse.host; | ||
| } | ||
| function bereichVonMuster(muster) { | ||
| if (!adresse) return null; | ||
| if (muster === adresse.host) return "host"; | ||
| if (muster === "*." + adresse.host) return "sub"; | ||
| if (muster === adresse.voll) return "seite"; | ||
| return null; // eigenes Muster | ||
| } | ||
| function knoepfeAktualisieren(muster) { | ||
| const aktiv = bereichVonMuster(muster); | ||
| for (const b of document.querySelectorAll(".segmente button")) { | ||
| b.setAttribute("aria-pressed", String(b.dataset.bereich === aktiv)); | ||
| } | ||
| } | ||
| function musterAnzeigen(muster) { | ||
| $("pattern").value = muster; | ||
| knoepfeAktualisieren(muster); | ||
| } | ||
| const aktuellesMuster = () => musterNormalisieren($("pattern").value); | ||
| // --- Regeln schreiben ----------------------------------------------------- | ||
| function persist() { | ||
@@ -68,3 +149,6 @@ return api.storage.local.set({ rules }); | ||
| // Fuellung laeuft von der 100-%-Marke zum aktuellen Wert: sichtbar wird die Abweichung. | ||
| function indexVon(muster) { | ||
| return rules.findIndex((r) => musterNormalisieren(r.pattern) === muster); | ||
| } | ||
| function paintScale(pct) { | ||
@@ -96,13 +180,18 @@ const pos = (v) => ((clampPct(v) - MIN) / (MAX - MIN)) * 100; | ||
| function applyRule(pct) { | ||
| if (!host) return; | ||
| const subs = $("subs").checked; | ||
| const i = indexOfHost(host); | ||
| const muster = aktuellesMuster(); | ||
| if (!muster) return; | ||
| if (pct === 100 && !subs) { | ||
| const i = indexVon(muster); | ||
| const schlicht = adresse && muster === adresse.host; | ||
| if (pct === 100 && schlicht) { | ||
| if (i < 0) return; | ||
| rules.splice(i, 1); // 100 % ohne Subdomain-Regel = kein Sonderfall mehr | ||
| rules.splice(i, 1); // Hostregel auf 100 % ist ueberfluessig | ||
| regelMuster = null; | ||
| } else if (i >= 0) { | ||
| rules[i] = { ...rules[i], zoom: pct / 100, includeSubdomains: subs, enabled: true }; | ||
| rules[i] = { ...rules[i], zoom: pct / 100, enabled: true }; | ||
| regelMuster = muster; | ||
| } else { | ||
| rules.push({ host, zoom: pct / 100, includeSubdomains: subs, enabled: true }); | ||
| rules.push({ pattern: muster, zoom: pct / 100, enabled: true }); | ||
| regelMuster = muster; | ||
| } | ||
@@ -114,6 +203,43 @@ persist(); | ||
| // Bereich gewechselt: die verwaltete Regel umschreiben statt eine zweite anzulegen | ||
| function musterWechseln(neu) { | ||
| const muster = musterNormalisieren(neu); | ||
| if (!muster) return; | ||
| const alt = regelMuster; | ||
| musterAnzeigen(muster); | ||
| if (alt && alt !== muster) { | ||
| const i = indexVon(alt); | ||
| if (i >= 0) { | ||
| const doppelt = indexVon(muster); | ||
| const wert = rules[i].zoom; | ||
| if (doppelt >= 0 && doppelt !== i) { | ||
| rules.splice(i, 1); | ||
| rules[indexVon(muster)] = { ...rules[indexVon(muster)], zoom: wert, enabled: true }; | ||
| } else { | ||
| rules[i] = { ...rules[i], pattern: muster }; | ||
| } | ||
| regelMuster = muster; | ||
| persist(); | ||
| renderList(); | ||
| return; | ||
| } | ||
| } | ||
| const vorhanden = indexVon(muster); | ||
| if (vorhanden >= 0) { | ||
| regelMuster = muster; | ||
| showValue(clampPct(Math.round(rules[vorhanden].zoom * 100))); | ||
| if (tab) api.tabs.setZoom(tab.id, rules[vorhanden].zoom).catch(() => {}); | ||
| } else { | ||
| regelMuster = null; | ||
| } | ||
| } | ||
| function renderList() { | ||
| const list = $("list"); | ||
| list.textContent = ""; | ||
| const sorted = rules.slice().sort((a, b) => String(a.host).localeCompare(String(b.host))); | ||
| const sorted = rules | ||
| .slice() | ||
| .sort((a, b) => String(a.pattern).localeCompare(String(b.pattern))); | ||
@@ -126,5 +252,6 @@ $("empty").hidden = sorted.length > 0; | ||
| for (const r of sorted) { | ||
| const istAktuell = Boolean(host) && String(r.host).toLowerCase() === host; | ||
| const muster = musterNormalisieren(r.pattern); | ||
| const greift = adresse && passt(muster, adresse); | ||
| const li = document.createElement("li"); | ||
| if (istAktuell) li.classList.add("current"); | ||
| if (greift) li.classList.add("current"); | ||
| if (r.enabled === false) li.classList.add("off"); | ||
@@ -145,4 +272,4 @@ | ||
| name.className = "rhost"; | ||
| name.textContent = (r.includeSubdomains === true ? "*." : "") + r.host; | ||
| name.title = name.textContent; | ||
| name.textContent = r.pattern; | ||
| name.title = r.pattern; | ||
@@ -156,3 +283,3 @@ const val = document.createElement("input"); | ||
| val.value = Math.round(r.zoom * 100); | ||
| val.setAttribute("aria-label", "Zoomfaktor für " + r.host); | ||
| val.setAttribute("aria-label", "Zoomfaktor für " + r.pattern); | ||
| val.addEventListener("change", () => { | ||
@@ -166,5 +293,9 @@ const v = clampPct(val.value); | ||
| persist(); | ||
| if (istAktuell) showValue(v); | ||
| if (muster === aktuellesMuster()) showValue(v); | ||
| }); | ||
| const unit = document.createElement("span"); | ||
| unit.className = "runit"; | ||
| unit.textContent = "%"; | ||
| const del = document.createElement("button"); | ||
@@ -177,14 +308,8 @@ del.type = "button"; | ||
| rules = rules.filter((x) => x !== r); | ||
| if (regelMuster === muster) regelMuster = null; | ||
| persist(); | ||
| renderList(); | ||
| if (istAktuell) { | ||
| $("subs").checked = false; | ||
| setPct(100, false); | ||
| } | ||
| if (muster === aktuellesMuster()) setPct(100, false); | ||
| }); | ||
| const unit = document.createElement("span"); | ||
| unit.className = "runit"; | ||
| unit.textContent = "%"; | ||
| li.append(toggle, name, val, unit, del); | ||
@@ -195,4 +320,5 @@ list.appendChild(li); | ||
| // Zoom dieses Tabs darf nicht host-weit persistiert werden | ||
| async function ensurePerTabScope() { | ||
| // --- Aufbau --------------------------------------------------------------- | ||
| async function perTabScopeSichern() { | ||
| try { | ||
@@ -214,3 +340,3 @@ const s = await api.tabs.getZoomSettings(tab.id); | ||
| const n = Number(e.target.value); | ||
| if (!e.target.value || !Number.isFinite(n) || n < MIN || n > MAX) return; // beim Tippen nicht dazwischenfunken | ||
| if (!e.target.value || !Number.isFinite(n) || n < MIN || n > MAX) return; | ||
| setPct(n); | ||
@@ -227,3 +353,7 @@ }); | ||
| $("subs").addEventListener("change", () => applyRule(clampPct($("slider").value))); | ||
| for (const b of document.querySelectorAll(".segmente button")) { | ||
| b.addEventListener("click", () => musterWechseln(musterFuerBereich(b.dataset.bereich))); | ||
| } | ||
| $("pattern").addEventListener("change", () => musterWechseln($("pattern").value)); | ||
| } | ||
@@ -235,3 +365,3 @@ | ||
| $("addToggle").setAttribute("aria-expanded", "false"); | ||
| $("addToggle").textContent = "Domain hinzufügen"; | ||
| $("addToggle").textContent = "Adresse hinzufügen"; | ||
| } | ||
@@ -256,5 +386,8 @@ | ||
| e.preventDefault(); | ||
| const h = normalizeHost($("newHost").value); | ||
| if (!h) { | ||
| $("addError").textContent = "Das ist kein gültiger Hostname. Beispiel: app.beispiel.de"; | ||
| const muster = musterNormalisieren($("newHost").value); | ||
| const hostTeil = muster.split("/")[0].split("?")[0].split("*").join(""); | ||
| if (!muster || (!hostTeil.includes(".") && hostTeil !== "localhost")) { | ||
| $("addError").textContent = | ||
| "Das ergibt keine gültige Adresse. Beispiel: test.test.com/index.php?kategorie=sub"; | ||
| $("addError").hidden = false; | ||
@@ -265,13 +398,10 @@ return; | ||
| const pct = clampPct($("newPct").value); | ||
| const subs = $("newSubs").checked; | ||
| const i = indexOfHost(h); | ||
| if (i >= 0) { | ||
| rules[i] = { ...rules[i], zoom: pct / 100, includeSubdomains: subs, enabled: true }; | ||
| } else { | ||
| rules.push({ host: h, zoom: pct / 100, includeSubdomains: subs, enabled: true }); | ||
| } | ||
| const i = indexVon(muster); | ||
| if (i >= 0) rules[i] = { ...rules[i], zoom: pct / 100, enabled: true }; | ||
| else rules.push({ pattern: muster, zoom: pct / 100, enabled: true }); | ||
| persist(); | ||
| if (h === host) { | ||
| $("subs").checked = subs; | ||
| if (adresse && passt(muster, adresse)) { | ||
| musterAnzeigen(muster); | ||
| regelMuster = muster; | ||
| showValue(pct); | ||
@@ -283,3 +413,2 @@ if (tab) api.tabs.setZoom(tab.id, pct / 100).catch(() => {}); | ||
| $("newHost").value = ""; | ||
| $("newSubs").checked = false; | ||
| closeForm(); | ||
@@ -303,5 +432,5 @@ renderList(); | ||
| wireForm(); | ||
| host = hostOf(tab && tab.url); | ||
| adresse = adresseVon(tab && tab.url); | ||
| if (!host) { | ||
| if (!adresse) { | ||
| $("control").hidden = true; | ||
@@ -313,24 +442,23 @@ $("unsupported").hidden = false; | ||
| $("host").textContent = host; | ||
| $("basehost").textContent = host; | ||
| await ensurePerTabScope(); | ||
| $("host").textContent = adresse.host; | ||
| $("pfad").textContent = adresse.pfad === "/" ? "" : adresse.pfad; | ||
| await perTabScopeSichern(); | ||
| const i = indexOfHost(host); | ||
| const exact = i >= 0 ? rules[i] : null; | ||
| const treffer = regelFuer(adresse); | ||
| let pct; | ||
| if (exact) { | ||
| pct = Math.round(exact.zoom * 100); | ||
| $("subs").checked = exact.includeSubdomains === true; | ||
| } else { | ||
| const eff = findEffective(host); | ||
| if (eff) { | ||
| pct = Math.round(eff.zoom * 100); | ||
| $("inherit").textContent = | ||
| "Gilt über die Regel *." + eff.host + ". Eine Änderung legt eine eigene Regel für diesen Host an."; | ||
| if (treffer) { | ||
| const muster = musterNormalisieren(treffer.pattern); | ||
| musterAnzeigen(muster); | ||
| regelMuster = muster; | ||
| pct = Math.round(treffer.zoom * 100); | ||
| if (!bereichVonMuster(muster)) { | ||
| $("inherit").textContent = "Diese Seite folgt dem Muster " + muster + "."; | ||
| $("inherit").hidden = false; | ||
| } else { | ||
| const z = await api.tabs.getZoom(tab.id).catch(() => 1); | ||
| pct = Math.round(z * 100); | ||
| } | ||
| } else { | ||
| musterAnzeigen(adresse.host); | ||
| regelMuster = null; | ||
| const z = await api.tabs.getZoom(tab.id).catch(() => 1); | ||
| pct = Math.round(z * 100); | ||
| } | ||
@@ -337,0 +465,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet