Unfollow tool for Bluesky

add dark theme

Changed files
+180 -53
src
+29 -19
index.html
··· 1 - <!DOCTYPE html> 1 + <!doctype html> 2 2 <html lang="en"> 3 - 4 - <head> 5 - <meta charset="utf-8" /> 6 - <meta name="viewport" content="width=device-width, initial-scale=1" /> 7 - <meta name="theme-color" content="#000000" /> 8 - <meta property="og:title" content="cleanfollow-bsky" /> 9 - <meta property="og:type" content="website" /> 10 - <meta property="og:url" content="https://cleanfollow-bsky.pages.dev" /> 11 - <meta property="og:description" content="Unfollow blocked, deleted, suspended, and deactivated Bluesky accounts" /> 12 - <link rel="shortcut icon" type="image/ico" href="/src/assets/favicon.ico" /> 13 - <title>cleanfollow-bsky</title> 14 - </head> 15 - 16 - <body> 17 - <noscript>You need to enable JavaScript to run this app.</noscript> 18 - <div id="root"></div> 3 + <head> 4 + <meta charset="utf-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1" /> 6 + <meta name="theme-color" content="#000000" /> 7 + <meta property="og:title" content="cleanfollow-bsky" /> 8 + <meta property="og:type" content="website" /> 9 + <meta property="og:url" content="https://cleanfollow-bsky.pages.dev" /> 10 + <meta 11 + property="og:description" 12 + content="Unfollow blocked, deleted, suspended, and deactivated Bluesky accounts" 13 + /> 14 + <link rel="shortcut icon" type="image/ico" href="/src/assets/favicon.ico" /> 15 + <title>cleanfollow-bsky</title> 16 + <script> 17 + if ( 18 + localStorage.theme === "dark" || 19 + (!("theme" in localStorage) && 20 + window.matchMedia("(prefers-color-scheme: dark)").matches) 21 + ) 22 + document.documentElement.classList.add("dark"); 23 + else document.documentElement.classList.remove("dark"); 24 + </script> 25 + </head> 19 26 20 - <script src="/src/index.tsx" type="module"></script> 21 - </body> 27 + <body id="root" class="dark:bg-dark-500 bg-slate-100"> 28 + <noscript>You need to enable JavaScript to run this app.</noscript> 29 + <div id="root"></div> 22 30 31 + <script src="/src/index.tsx" type="module"></script> 32 + </body> 23 33 </html>
+60 -34
src/App.tsx
··· 24 24 resolveFromIdentity, 25 25 type Session, 26 26 } from "@atcute/oauth-browser-client"; 27 + import { AiFillGithub, Bluesky, TbMoonStar, TbSun } from "./svg"; 27 28 28 29 configureOAuth({ 29 30 metadata: { ··· 148 149 return ( 149 150 <div class="flex flex-col items-center"> 150 151 <Show when={!loginState() && !notice().includes("Loading")}> 151 - <form 152 - class="flex flex-col items-center" 153 - onsubmit={(e) => e.preventDefault()} 154 - > 155 - <label for="handle">Handle:</label> 152 + <form class="flex flex-col" onsubmit={(e) => e.preventDefault()}> 153 + <label for="handle" class="ml-0.5"> 154 + Handle 155 + </label> 156 156 <input 157 157 type="text" 158 158 id="handle" 159 159 placeholder="user.bsky.social" 160 - class="mb-3 mt-1 rounded-md border border-black px-2 py-1" 160 + class="dark:bg-dark-100 mb-2 rounded-lg border border-gray-400 px-2 py-1 focus:outline-none focus:ring-1 focus:ring-gray-300" 161 161 onInput={(e) => setLoginInput(e.currentTarget.value)} 162 162 /> 163 163 <button 164 164 onclick={() => loginBsky(loginInput())} 165 - class="rounded bg-blue-500 px-2 py-2 font-bold text-white hover:bg-blue-700" 165 + class="rounded bg-blue-600 py-1.5 font-bold text-slate-100 hover:bg-blue-700" 166 166 > 167 167 Login 168 168 </button> ··· 170 170 </Show> 171 171 <Show when={loginState() && handle()}> 172 172 <div class="mb-4"> 173 - Logged in as @{handle()} ( 174 - <a href="" class="text-red-600" onclick={() => logoutBsky()}> 173 + Logged in as @{handle()} 174 + <a href="" class="ml-2 text-red-500" onclick={() => logoutBsky()}> 175 175 Logout 176 176 </a> 177 - ) 178 177 </div> 179 178 </Show> 180 179 <Show when={notice()}> ··· 315 314 <button 316 315 type="button" 317 316 onclick={() => fetchHiddenAccounts()} 318 - class="rounded bg-blue-500 px-2 py-2 font-bold text-white hover:bg-blue-700" 317 + class="rounded bg-blue-600 px-2 py-2 font-bold text-slate-100 hover:bg-blue-700" 319 318 > 320 319 Preview 321 320 </button> ··· 324 323 <button 325 324 type="button" 326 325 onclick={() => unfollow()} 327 - class="rounded bg-green-600 px-2 py-2 font-bold text-white hover:bg-green-700" 326 + class="rounded bg-green-600 px-2 py-2 font-bold text-slate-100 hover:bg-green-700" 328 327 > 329 328 Confirm 330 329 </button> ··· 371 370 372 371 return ( 373 372 <div class="mt-6 flex flex-col sm:w-full sm:flex-row sm:justify-center"> 374 - <div class="sticky top-0 mb-3 mr-5 flex w-full flex-wrap justify-around border-b border-b-gray-400 bg-white pb-3 sm:top-3 sm:mb-0 sm:w-auto sm:flex-col sm:self-start sm:border-none"> 373 + <div class="dark:bg-dark-500 sticky top-0 mb-3 mr-5 flex w-full flex-wrap justify-around border-b border-b-gray-400 bg-slate-100 pb-3 sm:top-3 sm:mb-0 sm:w-auto sm:flex-col sm:self-start sm:border-none"> 375 374 <For each={options}> 376 375 {(option, index) => ( 377 376 <div ··· 396 395 } 397 396 /> 398 397 <span class="peer relative h-5 w-9 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-4 after:w-4 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rtl:peer-checked:after:-translate-x-full dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800"></span> 399 - <span class="ms-3 select-none dark:text-gray-300"> 400 - {option.label} 401 - </span> 398 + <span class="ms-3 select-none">{option.label}</span> 402 399 </label> 403 400 </div> 404 401 <div class="flex items-center"> ··· 434 431 <div 435 432 classList={{ 436 433 "mb-1 flex items-center border-b py-1": true, 437 - "bg-red-400": record.toDelete, 434 + "bg-red-300 dark:bg-red-800": record.toDelete, 438 435 }} 439 436 > 440 437 <div class="mx-2"> ··· 471 468 }; 472 469 473 470 const App: Component = () => { 471 + const [theme, setTheme] = createSignal( 472 + ( 473 + localStorage.theme === "dark" || 474 + (!("theme" in localStorage) && 475 + globalThis.matchMedia("(prefers-color-scheme: dark)").matches) 476 + ) ? 477 + "dark" 478 + : "light", 479 + ); 480 + 474 481 return ( 475 - <div class="m-5 flex flex-col items-center"> 476 - <h1 class="mb-2 text-xl font-bold">cleanfollow-bsky</h1> 477 - <div class="mb-2 text-center"> 478 - <p>Select then unfollow inactive or blocked accounts</p> 479 - <div> 480 - <a 481 - class="text-blue-600 hover:underline" 482 - href="https://github.com/notjuliet/cleanfollow-bsky" 482 + <div class="m-5 flex flex-col items-center text-slate-900 dark:text-slate-100"> 483 + <div class="mb-2 flex w-[20rem] items-center"> 484 + <div class="basis-1/3"> 485 + <div 486 + class="w-fit cursor-pointer" 487 + onclick={() => { 488 + setTheme(theme() === "light" ? "dark" : "light"); 489 + if (theme() === "dark") 490 + document.documentElement.classList.add("dark"); 491 + else document.documentElement.classList.remove("dark"); 492 + localStorage.theme = theme(); 493 + }} 483 494 > 484 - Source Code 485 - </a> 486 - <span> | </span> 495 + {theme() === "dark" ? 496 + <TbMoonStar class="size-6" /> 497 + : <TbSun class="size-6" />} 498 + </div> 499 + </div> 500 + <div class="basis-1/3 text-center text-xl font-bold"> 501 + <a href="">cleanfollow</a> 502 + </div> 503 + <div class="justify-right flex basis-1/3 gap-x-2"> 487 504 <a 488 - class="text-blue-600 hover:underline" 489 505 href="https://bsky.app/profile/did:plc:b3pn34agqqchkaf75v7h43dk" 506 + target="_blank" 490 507 > 491 - Bluesky 508 + <Bluesky class="size-6" /> 492 509 </a> 493 - <span> | </span> 494 510 <a 495 - class="text-blue-600 hover:underline" 496 - href="https://mary-ext.codeberg.page/bluesky-quiet-posters/" 511 + href="https://github.com/notjuliet/cleanfollow-bsky" 512 + target="_blank" 497 513 > 498 - Quiet Posters 514 + <AiFillGithub class="size-6" /> 499 515 </a> 500 516 </div> 517 + </div> 518 + <div class="mb-2 text-center"> 519 + <p>Select then unfollow inactive or blocked accounts</p> 520 + <a 521 + class="text-blue-500 hover:underline" 522 + target="_blank" 523 + href="https://mary-ext.codeberg.page/bluesky-quiet-posters/" 524 + > 525 + Quiet Posters 526 + </a> 501 527 </div> 502 528 <Login /> 503 529 <Show when={loginState()}>
+91
src/svg.tsx
··· 1 + import { Component } from "solid-js"; 2 + 3 + const AiFillGithub: Component<{ class?: string }> = (props) => { 4 + return ( 5 + <div class={props.class}> 6 + <svg 7 + class="size-full" 8 + fill="currentColor" 9 + stroke-width="0" 10 + xmlns="http://www.w3.org/2000/svg" 11 + viewBox="0 0 16 16" 12 + height="1em" 13 + width="1em" 14 + style="overflow: visible; color: currentcolor;" 15 + > 16 + <path 17 + fill="currentColor" 18 + d="M8 .198a8 8 0 0 0-2.529 15.591c.4.074.547-.174.547-.385 0-.191-.008-.821-.011-1.489-2.226.484-2.695-.944-2.695-.944-.364-.925-.888-1.171-.888-1.171-.726-.497.055-.486.055-.486.803.056 1.226.824 1.226.824.714 1.223 1.872.869 2.328.665.072-.517.279-.87.508-1.07-1.777-.202-3.645-.888-3.645-3.954 0-.873.313-1.587.824-2.147-.083-.202-.357-1.015.077-2.117 0 0 .672-.215 2.201.82A7.672 7.672 0 0 1 8 4.066c.68.003 1.365.092 2.004.269 1.527-1.035 2.198-.82 2.198-.82.435 1.102.162 1.916.079 2.117.513.56.823 1.274.823 2.147 0 3.073-1.872 3.749-3.653 3.947.287.248.543.735.543 1.481 0 1.07-.009 1.932-.009 2.195 0 .213.144.462.55.384A8 8 0 0 0 8.001.196z" 19 + ></path> 20 + </svg> 21 + </div> 22 + ); 23 + }; 24 + 25 + const Bluesky: Component<{ class?: string }> = (props) => { 26 + return ( 27 + <div class={props.class}> 28 + <svg 29 + class="size-full" 30 + width="1em" 31 + height="1em" 32 + viewBox="0 0 568 501" 33 + fill="currentColor" 34 + xmlns="http://www.w3.org/2000/svg" 35 + > 36 + <path d="M123.121 33.6637C188.241 82.5526 258.281 181.681 284 234.873C309.719 181.681 379.759 82.5526 444.879 33.6637C491.866 -1.61183 568 -28.9064 568 57.9464C568 75.2916 558.055 203.659 552.222 224.501C531.947 296.954 458.067 315.434 392.347 304.249C507.222 323.8 536.444 388.56 473.333 453.32C353.473 576.312 301.061 422.461 287.631 383.039C285.169 375.812 284.017 372.431 284 375.306C283.983 372.431 282.831 375.812 280.369 383.039C266.939 422.461 214.527 576.312 94.6667 453.32C31.5556 388.56 60.7778 323.8 175.653 304.249C109.933 315.434 36.0535 296.954 15.7778 224.501C9.94525 203.659 0 75.2916 0 57.9464C0 -28.9064 76.1345 -1.61183 123.121 33.6637Z" /> 37 + </svg> 38 + </div> 39 + ); 40 + }; 41 + 42 + const TbMoonStar: Component<{ class?: string }> = (props) => { 43 + return ( 44 + <div class={props.class}> 45 + <svg 46 + class="size-full" 47 + fill="none" 48 + stroke-width="2" 49 + xmlns="http://www.w3.org/2000/svg" 50 + width="1em" 51 + height="1em" 52 + viewBox="0 0 24 24" 53 + stroke="currentColor" 54 + stroke-linecap="round" 55 + stroke-linejoin="round" 56 + style="overflow: visible; color: currentcolor;" 57 + > 58 + <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> 59 + <path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z"></path> 60 + <path d="M17 4a2 2 0 0 0 2 2a2 2 0 0 0 -2 2a2 2 0 0 0 -2 -2a2 2 0 0 0 2 -2"></path> 61 + <path d="M19 11h2m-1 -1v2"></path> 62 + </svg> 63 + </div> 64 + ); 65 + }; 66 + 67 + const TbSun: Component<{ class?: string }> = (props) => { 68 + return ( 69 + <div class={props.class}> 70 + <svg 71 + class="size-full" 72 + fill="none" 73 + stroke-width="2" 74 + xmlns="http://www.w3.org/2000/svg" 75 + width="1em" 76 + height="1em" 77 + viewBox="0 0 24 24" 78 + stroke="currentColor" 79 + stroke-linecap="round" 80 + stroke-linejoin="round" 81 + style="overflow: visible; color: currentcolor;" 82 + > 83 + <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> 84 + <path d="M12 12m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0"></path> 85 + <path d="M3 12h1m8 -9v1m8 8h1m-9 8v1m-6.4 -15.4l.7 .7m12.1 -.7l-.7 .7m0 11.4l.7 .7m-12.1 -.7l-.7 .7"></path> 86 + </svg> 87 + </div> 88 + ); 89 + }; 90 + 91 + export { AiFillGithub, Bluesky, TbMoonStar, TbSun };