krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

remove-splash-highlighter: HutchSafariExtension/Resources/content.js · raw

  1const hutchSupportedHosts = new Set([
  2  "git.sr.ht",
  3  "hg.sr.ht",
  4  "todo.sr.ht",
  5  "builds.sr.ht",
  6  "lists.sr.ht",
  7  "meta.sr.ht",
  8  "sr.ht",
  9]);
 10
 11const hutchBrowserAPI = globalThis.browser ?? globalThis.chrome;
 12const bannerStorageKey = "showOpenInHutchBanner";
 13const dismissedStorageKey = `dismissedOpenInHutchBanner:${location.hostname}`;
 14
 15function hutchIsOwnerRootPath(path) {
 16  const components = path.split("/").filter(Boolean);
 17  return components.length === 1 && components[0].startsWith("~");
 18}
 19
 20function hutchNormalizedPath(pathname) {
 21  return pathname.split("/").filter(Boolean).join("/");
 22}
 23
 24function hutchDeepLinkPath(hostname, path) {
 25  const components = path.split("/").filter(Boolean);
 26  if (
 27    hostname === "sr.ht" &&
 28    components.length === 2 &&
 29    components[0] === "projects" &&
 30    components[1].startsWith("~")
 31  ) {
 32    return components[1];
 33  }
 34
 35  return path;
 36}
 37
 38function hutchDeepLinkService(hostname, path) {
 39  if (hutchIsOwnerRootPath(path)) {
 40    return "lookup";
 41  }
 42
 43  switch (hostname) {
 44    case "git.sr.ht":
 45      return "git";
 46    case "hg.sr.ht":
 47      return "hg";
 48    case "todo.sr.ht":
 49      return "todo";
 50    case "builds.sr.ht":
 51      return "builds";
 52    case "lists.sr.ht":
 53      return "lists";
 54    default:
 55      return "lookup";
 56  }
 57}
 58
 59function hutchDeepLinkForLocation() {
 60  if (location.protocol !== "https:" || !hutchSupportedHosts.has(location.hostname)) {
 61    return null;
 62  }
 63
 64  const path = hutchDeepLinkPath(location.hostname, hutchNormalizedPath(location.pathname));
 65  const service = hutchDeepLinkService(location.hostname, path);
 66  const pathSegment = path ? `/${path}` : "";
 67  return `hutch://${service}${pathSegment}${location.search}${location.hash}`;
 68}
 69
 70function storageGet(defaults) {
 71  return new Promise((resolve) => {
 72    hutchBrowserAPI.storage.local.get(defaults, resolve);
 73  });
 74}
 75
 76function storageSet(values) {
 77  return new Promise((resolve) => {
 78    hutchBrowserAPI.storage.local.set(values, resolve);
 79  });
 80}
 81
 82function showNotice(message) {
 83  const existing = document.getElementById("hutch-open-notice");
 84  existing?.remove();
 85
 86  const notice = document.createElement("div");
 87  notice.id = "hutch-open-notice";
 88  notice.textContent = message;
 89  notice.style.cssText = [
 90    "position:fixed",
 91    "left:50%",
 92    "bottom:18px",
 93    "z-index:2147483647",
 94    "transform:translateX(-50%)",
 95    "background:#1f2937",
 96    "color:white",
 97    "font:13px -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif",
 98    "padding:8px 12px",
 99    "border-radius:8px",
100    "box-shadow:0 8px 24px rgba(0,0,0,.24)",
101  ].join(";");
102
103  document.documentElement.appendChild(notice);
104  setTimeout(() => notice.remove(), 3500);
105}
106
107async function injectBannerIfEnabled() {
108  const hutchURL = hutchDeepLinkForLocation();
109  if (!hutchURL || document.getElementById("hutch-open-banner")) {
110    return;
111  }
112
113  const settings = await storageGet({
114    [bannerStorageKey]: false,
115    [dismissedStorageKey]: false,
116  });
117  if (!settings[bannerStorageKey] || settings[dismissedStorageKey]) {
118    return;
119  }
120
121  const banner = document.createElement("div");
122  banner.id = "hutch-open-banner";
123  banner.style.cssText = [
124    "position:fixed",
125    "right:14px",
126    "bottom:14px",
127    "z-index:2147483647",
128    "display:flex",
129    "align-items:center",
130    "gap:8px",
131    "background:#111827",
132    "color:white",
133    "font:13px -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif",
134    "padding:8px 10px",
135    "border-radius:8px",
136    "box-shadow:0 8px 24px rgba(0,0,0,.22)",
137  ].join(";");
138
139  const button = document.createElement("button");
140  button.type = "button";
141  button.textContent = "Open in Hutch";
142  button.style.cssText = [
143    "appearance:none",
144    "border:0",
145    "background:transparent",
146    "color:inherit",
147    "font:inherit",
148    "font-weight:600",
149    "padding:0",
150  ].join(";");
151  button.addEventListener("click", () => {
152    console.debug("[Hutch] Banner opening deep link", { sourceURL: location.href, hutchURL });
153    location.href = hutchURL;
154  });
155
156  const dismiss = document.createElement("button");
157  dismiss.type = "button";
158  dismiss.textContent = "Close";
159  dismiss.setAttribute("aria-label", "Dismiss Open in Hutch banner");
160  dismiss.style.cssText = [
161    "appearance:none",
162    "border:0",
163    "background:rgba(255,255,255,.14)",
164    "color:inherit",
165    "font:12px -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif",
166    "padding:3px 6px",
167    "border-radius:6px",
168  ].join(";");
169  dismiss.addEventListener("click", async () => {
170    await storageSet({ [dismissedStorageKey]: true });
171    banner.remove();
172  });
173
174  banner.append(button, dismiss);
175  document.documentElement.appendChild(banner);
176}
177
178hutchBrowserAPI.runtime.onMessage.addListener((message) => {
179  if (message?.type === "hutchUnsupportedPage") {
180    showNotice(message.message);
181  }
182});
183
184injectBannerIfEnabled();