PageSourceSearch

https://grigsi.com/js/grigsi-media-permission.js?v=1

js grigsi.com collected 2026-10-02 01:33:29 UTC 8,135 bytes, 195 lines download raw bytes

1/**
2 * GriGsi — confirm before camera/microphone access when permission is not granted.
3 */
4(function (global) {
5  "use strict";
6
7  const QUESTION = "Искате ли да го разрешите?";
8  const DENIED_HINT =
9    "Достъпът е блокиран в настройките на браузъра — разрешете от иконата на катинара в адресната лента.";
10  const BTN_ALLOW = "Разреши";
11  const BTN_DENY = "Не сега";
12
13  let overlayEl = null;
14  let pendingResolve = null;
15
16  function injectStyles() {
17    if (document.getElementById("grigsi-media-perm-style")) return;
18    const style = document.createElement("style");
19    style.id = "grigsi-media-perm-style";
20    style.textContent =
21      ".grigsi-media-perm-overlay{position:fixed;inset:0;z-index:2147483000;display:flex;align-items:center;justify-content:center;padding:1rem;background:rgba(12,8,24,.62);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}" +
22      ".grigsi-media-perm-panel{background:#fff;border-radius:14px;max-width:22rem;width:100%;padding:1.35rem 1.4rem 1.15rem;box-shadow:0 14px 40px rgba(0,0,0,.28);font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif}" +
23      ".grigsi-media-perm-q{margin:0 0 .55rem;font-size:1.05rem;font-weight:600;color:#1a1228;line-height:1.35}" +
24      ".grigsi-media-perm-detail{margin:0 0 .65rem;font-size:.92rem;color:#4a3d5c;line-height:1.4}" +
25      ".grigsi-media-perm-hint{margin:0 0 1rem;font-size:.82rem;color:#7a2e2e;line-height:1.35}" +
26      ".grigsi-media-perm-actions{display:flex;gap:.55rem;justify-content:flex-end;flex-wrap:wrap}" +
27      ".grigsi-media-perm-btn{border:0;border-radius:9px;padding:.55rem .95rem;font-size:.92rem;font-weight:600;cursor:pointer}" +
28      ".grigsi-media-perm-btn--allow{background:#3d2860;color:#fff}" +
29      ".grigsi-media-perm-btn--allow:hover{background:#4e3478}" +
30      ".grigsi-media-perm-btn--deny{background:#ece8f2;color:#3d2860}" +
31      ".grigsi-media-perm-btn--deny:hover{background:#e0d8ec}" +
32      ".grigsi-media-perm-overlay.hidden{display:none!important;pointer-events:none!important;visibility:hidden!important}";
33    document.head.appendChild(style);
34  }
35
36  function detailLine(wantVideo, wantAudio) {
37    if (wantVideo && wantAudio) return "Камера и микрофон";
38    if (wantVideo) return "Камера";
39    if (wantAudio) return "Микрофон";
40    return "";
41  }
42
43  function ensureOverlay() {
44    injectStyles();
45    if (overlayEl) return overlayEl;
46    overlayEl = document.createElement("div");
47    overlayEl.className = "grigsi-media-perm-overlay hidden";
48    overlayEl.setAttribute("role", "dialog");
49    overlayEl.setAttribute("aria-modal", "true");
50    overlayEl.setAttribute("aria-labelledby", "grigsi-media-perm-q");
51    overlayEl.innerHTML =
52      '<div class="grigsi-media-perm-panel">' +
53      '<p class="grigsi-media-perm-q" id="grigsi-media-perm-q"></p>' +
54      '<p class="grigsi-media-perm-detail" id="grigsi-media-perm-detail"></p>' +
55      '<p class="grigsi-media-perm-hint hidden" id="grigsi-media-perm-hint"></p>' +
56      '<div class="grigsi-media-perm-actions">' +
57      '<button type="button" class="grigsi-media-perm-btn grigsi-media-perm-btn--deny" id="grigsi-media-perm-deny"></button>' +
58      '<button type="button" class="grigsi-media-perm-btn grigsi-media-perm-btn--allow" id="grigsi-media-perm-allow"></button>' +
59      "</div></div>";
60    document.body.appendChild(overlayEl);
61    overlayEl.querySelector("#grigsi-media-perm-deny").textContent = BTN_DENY;
62    overlayEl.querySelector("#grigsi-media-perm-allow").textContent = BTN_ALLOW;
63    overlayEl.querySelector("#grigsi-media-perm-deny").addEventListener("click", () => finishDialog(false));
64    overlayEl.querySelector("#grigsi-media-perm-allow").addEventListener("click", () => finishDialog(true));
65    overlayEl.addEventListener("click", (e) => {
66      if (e.target === overlayEl) finishDialog(false);
67    });
68    return overlayEl;
69  }
70
71  function finishDialog(allowed) {
72    if (!overlayEl) return;
73    overlayEl.classList.add("hidden");
74    overlayEl.setAttribute("aria-hidden", "true");
75    const resolve = pendingResolve;
76    pendingResolve = null;
77    if (resolve) resolve(!!allowed);
78  }
79
80  function showDialog(opts) {
81    const wantVideo = !!opts.wantVideo;
82    const wantAudio = !!opts.wantAudio;
83    const anyDenied = !!opts.anyDenied;
84    const el = ensureOverlay();
85    el.querySelector("#grigsi-media-perm-q").textContent = QUESTION;
86    const detail = el.querySelector("#grigsi-media-perm-detail");
87    const hint = el.querySelector("#grigsi-media-perm-hint");
88    const line = detailLine(wantVideo, wantAudio);
89    if (detail) {
90      detail.textContent = line;
91      detail.classList.toggle("hidden", !line);
92    }
93    if (hint) {
94      hint.textContent = DENIED_HINT;
95      hint.classList.toggle("hidden", !anyDenied);
96    }
97    el.classList.remove("hidden");
98    el.setAttribute("aria-hidden", "false");
99    return new Promise((resolve) => {
100      pendingResolve = resolve;
101    });
102  }
103
104  async function queryPermissionName(name) {
105    if (!navigator.permissions || typeof navigator.permissions.query !== "function") {
106      return "unknown";
107    }
108    try {
109      const r = await navigator.permissions.query({ name: name });
110      return String(r && r.state ? r.state : "unknown");
111    } catch (_) {
112      return "unknown";
113    }
114  }
115
116  function sessionGranted(kind) {
117    try {
118      return sessionStorage.getItem("grigsi_media_ok_" + kind) === "1";
119    } catch (_) {
120      return false;
121    }
122  }
123
124  function markSessionGranted(opts) {
125    try {
126      if (opts && opts.wantVideo) sessionStorage.setItem("grigsi_media_ok_camera", "1");
127      if (opts && opts.wantAudio) sessionStorage.setItem("grigsi_media_ok_mic", "1");
128    } catch (_) {}
129  }
130
131  function normalizeState(state, kind) {
132    const s = String(state || "unknown");
133    if (s === "granted" || s === "denied" || s === "prompt") return s;
134    return sessionGranted(kind) ? "granted" : "unknown";
135  }
136
137  async function permissionStates(opts) {
138    const wantVideo = !!opts.wantVideo;
139    const wantAudio = !!opts.wantAudio;
140    const video = wantVideo ? normalizeState(await queryPermissionName("camera"), "camera") : "granted";
141    const audio = wantAudio ? normalizeState(await queryPermissionName("microphone"), "microphone") : "granted";
142    const allGranted =
143      (!wantVideo || video === "granted") && (!wantAudio || audio === "granted");
144    const anyDenied = (wantVideo && video === "denied") || (wantAudio && audio === "denied");
145    const needsPrompt =
146      !allGranted &&
147      ((wantVideo && video !== "granted") || (wantAudio && audio !== "granted"));
148    return { video, audio, allGranted, anyDenied, needsPrompt };
149  }
150
151  async function needsPrompt(opts) {
152    if (!opts || (!opts.wantVideo && !opts.wantAudio)) return false;
153    const st = await permissionStates(opts);
154    if (st.allGranted) return false;
155    return true;
156  }
157
158  /**
159   * When camera/mic not granted: show confirm dialog. Resolves true if user allows.
160   * If already granted, resolves true immediately (same user-gesture turn as caller).
161   */
162  async function ensureAllowed(opts) {
163    const wantVideo = !!(opts && opts.wantVideo);
164    const wantAudio = !!(opts && opts.wantAudio);
165    if (!wantVideo && !wantAudio) return true;
166    const st = await permissionStates({ wantVideo, wantAudio });
167    if (st.allGranted) return true;
168    return showDialog({ wantVideo, wantAudio, anyDenied: st.anyDenied });
169  }
170
171  /**
172   * onAllow runs after user confirms (or immediately when already granted).
173   */
174  async function runIfAllowed(opts) {
175    const onAllow = opts && opts.onAllow;
176    const onDeny = opts && opts.onDeny;
177    const ok = await ensureAllowed(opts || {});
178    if (ok) {
179      if (onAllow) onAllow();
180      return true;
181    }
182    if (onDeny) onDeny();
183    return false;
184  }
185
186  global.GriGsiMediaPermission = {
187    QUESTION,
188    BTN_ALLOW,
189    BTN_DENY,
190    needsPrompt,
191    ensureAllowed,
192    runIfAllowed,
193    markSessionGranted,
194  };
195})(typeof window !== "undefined" ? window : global);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.