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();