krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
v3.7.0: 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 return `hutch://${service}${path ? `/${path}` : ""}${location.search}${location.hash}`;
67}
68
69function storageGet(defaults) {
70 return new Promise((resolve) => {
71 hutchBrowserAPI.storage.local.get(defaults, resolve);
72 });
73}
74
75function storageSet(values) {
76 return new Promise((resolve) => {
77 hutchBrowserAPI.storage.local.set(values, resolve);
78 });
79}
80
81function showNotice(message) {
82 const existing = document.getElementById("hutch-open-notice");
83 existing?.remove();
84
85 const notice = document.createElement("div");
86 notice.id = "hutch-open-notice";
87 notice.textContent = message;
88 notice.style.cssText = [
89 "position:fixed",
90 "left:50%",
91 "bottom:18px",
92 "z-index:2147483647",
93 "transform:translateX(-50%)",
94 "background:#1f2937",
95 "color:white",
96 "font:13px -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif",
97 "padding:8px 12px",
98 "border-radius:8px",
99 "box-shadow:0 8px 24px rgba(0,0,0,.24)",
100 ].join(";");
101
102 document.documentElement.appendChild(notice);
103 setTimeout(() => notice.remove(), 3500);
104}
105
106async function injectBannerIfEnabled() {
107 const hutchURL = hutchDeepLinkForLocation();
108 if (!hutchURL || document.getElementById("hutch-open-banner")) {
109 return;
110 }
111
112 const settings = await storageGet({
113 [bannerStorageKey]: false,
114 [dismissedStorageKey]: false,
115 });
116 if (!settings[bannerStorageKey] || settings[dismissedStorageKey]) {
117 return;
118 }
119
120 const banner = document.createElement("div");
121 banner.id = "hutch-open-banner";
122 banner.style.cssText = [
123 "position:fixed",
124 "right:14px",
125 "bottom:14px",
126 "z-index:2147483647",
127 "display:flex",
128 "align-items:center",
129 "gap:8px",
130 "background:#111827",
131 "color:white",
132 "font:13px -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif",
133 "padding:8px 10px",
134 "border-radius:8px",
135 "box-shadow:0 8px 24px rgba(0,0,0,.22)",
136 ].join(";");
137
138 const button = document.createElement("button");
139 button.type = "button";
140 button.textContent = "Open in Hutch";
141 button.style.cssText = [
142 "appearance:none",
143 "border:0",
144 "background:transparent",
145 "color:inherit",
146 "font:inherit",
147 "font-weight:600",
148 "padding:0",
149 ].join(";");
150 button.addEventListener("click", () => {
151 console.debug("[Hutch] Banner opening deep link", { sourceURL: location.href, hutchURL });
152 location.href = hutchURL;
153 });
154
155 const dismiss = document.createElement("button");
156 dismiss.type = "button";
157 dismiss.textContent = "Close";
158 dismiss.setAttribute("aria-label", "Dismiss Open in Hutch banner");
159 dismiss.style.cssText = [
160 "appearance:none",
161 "border:0",
162 "background:rgba(255,255,255,.14)",
163 "color:inherit",
164 "font:12px -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif",
165 "padding:3px 6px",
166 "border-radius:6px",
167 ].join(";");
168 dismiss.addEventListener("click", async () => {
169 await storageSet({ [dismissedStorageKey]: true });
170 banner.remove();
171 });
172
173 banner.append(button, dismiss);
174 document.documentElement.appendChild(banner);
175}
176
177hutchBrowserAPI.runtime.onMessage.addListener((message) => {
178 if (message?.type === "hutchUnsupportedPage") {
179 showNotice(message.message);
180 }
181});
182
183injectBannerIfEnabled();