├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── background-worker.js ├── build.sh ├── chrome-store.webp ├── chrome_manifest_v3.json ├── filter-by-title.mp4 ├── filter-by-username.mp4 ├── firefox-store.webp ├── firefox_manifest_v3.json ├── flake.lock ├── flake.nix ├── icons ├── icon128.png ├── icon16.png ├── icon32.png └── icon48.png ├── rustfmt.toml ├── src ├── dom_manipulation.rs ├── getters.rs ├── lib.rs ├── pure.rs ├── setters.rs ├── sidebar.html ├── style_for_sidebar.css └── util.rs ├── support-on-liberapay.png └── support-on-patreon.png /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /pkg 3 | 4 | 5 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.20" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "autocfg" 16 | version = "1.1.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 19 | 20 | [[package]] 21 | name = "base64" 22 | version = "0.21.5" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" 25 | 26 | [[package]] 27 | name = "bincode" 28 | version = "1.3.3" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" 31 | dependencies = [ 32 | "serde", 33 | ] 34 | 35 | [[package]] 36 | name = "bumpalo" 37 | version = "3.11.1" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" 40 | 41 | [[package]] 42 | name = "bytes" 43 | version = "1.5.0" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" 46 | 47 | [[package]] 48 | name = "cfg-if" 49 | version = "1.0.0" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 52 | 53 | [[package]] 54 | name = "console_error_panic_hook" 55 | version = "0.1.7" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" 58 | dependencies = [ 59 | "cfg-if", 60 | "wasm-bindgen", 61 | ] 62 | 63 | [[package]] 64 | name = "equivalent" 65 | version = "1.0.1" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 68 | 69 | [[package]] 70 | name = "fnv" 71 | version = "1.0.7" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 74 | 75 | [[package]] 76 | name = "form_urlencoded" 77 | version = "1.1.0" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" 80 | dependencies = [ 81 | "percent-encoding", 82 | ] 83 | 84 | [[package]] 85 | name = "futures" 86 | version = "0.3.29" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" 89 | dependencies = [ 90 | "futures-channel", 91 | "futures-core", 92 | "futures-io", 93 | "futures-sink", 94 | "futures-task", 95 | "futures-util", 96 | ] 97 | 98 | [[package]] 99 | name = "futures-channel" 100 | version = "0.3.29" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" 103 | dependencies = [ 104 | "futures-core", 105 | "futures-sink", 106 | ] 107 | 108 | [[package]] 109 | name = "futures-core" 110 | version = "0.3.29" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" 113 | 114 | [[package]] 115 | name = "futures-io" 116 | version = "0.3.29" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" 119 | 120 | [[package]] 121 | name = "futures-macro" 122 | version = "0.3.29" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" 125 | dependencies = [ 126 | "proc-macro2", 127 | "quote", 128 | "syn 2.0.41", 129 | ] 130 | 131 | [[package]] 132 | name = "futures-sink" 133 | version = "0.3.29" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" 136 | 137 | [[package]] 138 | name = "futures-task" 139 | version = "0.3.29" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" 142 | 143 | [[package]] 144 | name = "futures-util" 145 | version = "0.3.29" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" 148 | dependencies = [ 149 | "futures-channel", 150 | "futures-core", 151 | "futures-io", 152 | "futures-macro", 153 | "futures-sink", 154 | "futures-task", 155 | "memchr", 156 | "pin-project-lite", 157 | "pin-utils", 158 | "slab", 159 | ] 160 | 161 | [[package]] 162 | name = "getrandom" 163 | version = "0.2.11" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" 166 | dependencies = [ 167 | "cfg-if", 168 | "js-sys", 169 | "libc", 170 | "wasi", 171 | "wasm-bindgen", 172 | ] 173 | 174 | [[package]] 175 | name = "gloo" 176 | version = "0.11.0" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "d15282ece24eaf4bd338d73ef580c6714c8615155c4190c781290ee3fa0fd372" 179 | dependencies = [ 180 | "gloo-console", 181 | "gloo-dialogs", 182 | "gloo-events", 183 | "gloo-file", 184 | "gloo-history", 185 | "gloo-net", 186 | "gloo-render", 187 | "gloo-storage", 188 | "gloo-timers", 189 | "gloo-utils", 190 | "gloo-worker", 191 | ] 192 | 193 | [[package]] 194 | name = "gloo-console" 195 | version = "0.3.0" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "2a17868f56b4a24f677b17c8cb69958385102fa879418052d60b50bc1727e261" 198 | dependencies = [ 199 | "gloo-utils", 200 | "js-sys", 201 | "serde", 202 | "wasm-bindgen", 203 | "web-sys", 204 | ] 205 | 206 | [[package]] 207 | name = "gloo-dialogs" 208 | version = "0.2.0" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "bf4748e10122b01435750ff530095b1217cf6546173459448b83913ebe7815df" 211 | dependencies = [ 212 | "wasm-bindgen", 213 | "web-sys", 214 | ] 215 | 216 | [[package]] 217 | name = "gloo-events" 218 | version = "0.2.0" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "27c26fb45f7c385ba980f5fa87ac677e363949e065a083722697ef1b2cc91e41" 221 | dependencies = [ 222 | "wasm-bindgen", 223 | "web-sys", 224 | ] 225 | 226 | [[package]] 227 | name = "gloo-file" 228 | version = "0.3.0" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "97563d71863fb2824b2e974e754a81d19c4a7ec47b09ced8a0e6656b6d54bd1f" 231 | dependencies = [ 232 | "gloo-events", 233 | "js-sys", 234 | "wasm-bindgen", 235 | "web-sys", 236 | ] 237 | 238 | [[package]] 239 | name = "gloo-history" 240 | version = "0.2.2" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "903f432be5ba34427eac5e16048ef65604a82061fe93789f2212afc73d8617d6" 243 | dependencies = [ 244 | "getrandom", 245 | "gloo-events", 246 | "gloo-utils", 247 | "serde", 248 | "serde-wasm-bindgen", 249 | "serde_urlencoded", 250 | "thiserror", 251 | "wasm-bindgen", 252 | "web-sys", 253 | ] 254 | 255 | [[package]] 256 | name = "gloo-net" 257 | version = "0.5.0" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "43aaa242d1239a8822c15c645f02166398da4f8b5c4bae795c1f5b44e9eee173" 260 | dependencies = [ 261 | "futures-channel", 262 | "futures-core", 263 | "futures-sink", 264 | "gloo-utils", 265 | "http", 266 | "js-sys", 267 | "pin-project", 268 | "serde", 269 | "serde_json", 270 | "thiserror", 271 | "wasm-bindgen", 272 | "wasm-bindgen-futures", 273 | "web-sys", 274 | ] 275 | 276 | [[package]] 277 | name = "gloo-render" 278 | version = "0.2.0" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "56008b6744713a8e8d98ac3dcb7d06543d5662358c9c805b4ce2167ad4649833" 281 | dependencies = [ 282 | "wasm-bindgen", 283 | "web-sys", 284 | ] 285 | 286 | [[package]] 287 | name = "gloo-storage" 288 | version = "0.3.0" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a" 291 | dependencies = [ 292 | "gloo-utils", 293 | "js-sys", 294 | "serde", 295 | "serde_json", 296 | "thiserror", 297 | "wasm-bindgen", 298 | "web-sys", 299 | ] 300 | 301 | [[package]] 302 | name = "gloo-timers" 303 | version = "0.3.0" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" 306 | dependencies = [ 307 | "js-sys", 308 | "wasm-bindgen", 309 | ] 310 | 311 | [[package]] 312 | name = "gloo-utils" 313 | version = "0.2.0" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" 316 | dependencies = [ 317 | "js-sys", 318 | "serde", 319 | "serde_json", 320 | "wasm-bindgen", 321 | "web-sys", 322 | ] 323 | 324 | [[package]] 325 | name = "gloo-worker" 326 | version = "0.5.0" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "085f262d7604911c8150162529cefab3782e91adb20202e8658f7275d2aefe5d" 329 | dependencies = [ 330 | "bincode", 331 | "futures", 332 | "gloo-utils", 333 | "gloo-worker-macros", 334 | "js-sys", 335 | "pinned", 336 | "serde", 337 | "thiserror", 338 | "wasm-bindgen", 339 | "wasm-bindgen-futures", 340 | "web-sys", 341 | ] 342 | 343 | [[package]] 344 | name = "gloo-worker-macros" 345 | version = "0.1.0" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "956caa58d4857bc9941749d55e4bd3000032d8212762586fa5705632967140e7" 348 | dependencies = [ 349 | "proc-macro-crate", 350 | "proc-macro2", 351 | "quote", 352 | "syn 2.0.41", 353 | ] 354 | 355 | [[package]] 356 | name = "hackernews-userscript" 357 | version = "0.1.0" 358 | dependencies = [ 359 | "base64", 360 | "console_error_panic_hook", 361 | "gloo", 362 | "gloo-console", 363 | "indexmap", 364 | "js-sys", 365 | "regex", 366 | "serde_json", 367 | "wasm-bindgen", 368 | "wasm-bindgen-futures", 369 | "web-sys", 370 | ] 371 | 372 | [[package]] 373 | name = "hashbrown" 374 | version = "0.14.3" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 377 | 378 | [[package]] 379 | name = "http" 380 | version = "0.2.11" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" 383 | dependencies = [ 384 | "bytes", 385 | "fnv", 386 | "itoa", 387 | ] 388 | 389 | [[package]] 390 | name = "indexmap" 391 | version = "2.1.0" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" 394 | dependencies = [ 395 | "equivalent", 396 | "hashbrown", 397 | ] 398 | 399 | [[package]] 400 | name = "itoa" 401 | version = "1.0.5" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" 404 | 405 | [[package]] 406 | name = "js-sys" 407 | version = "0.3.60" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" 410 | dependencies = [ 411 | "wasm-bindgen", 412 | ] 413 | 414 | [[package]] 415 | name = "libc" 416 | version = "0.2.151" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" 419 | 420 | [[package]] 421 | name = "log" 422 | version = "0.4.17" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 425 | dependencies = [ 426 | "cfg-if", 427 | ] 428 | 429 | [[package]] 430 | name = "memchr" 431 | version = "2.5.0" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 434 | 435 | [[package]] 436 | name = "once_cell" 437 | version = "1.17.0" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" 440 | 441 | [[package]] 442 | name = "percent-encoding" 443 | version = "2.2.0" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" 446 | 447 | [[package]] 448 | name = "pin-project" 449 | version = "1.0.12" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" 452 | dependencies = [ 453 | "pin-project-internal", 454 | ] 455 | 456 | [[package]] 457 | name = "pin-project-internal" 458 | version = "1.0.12" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" 461 | dependencies = [ 462 | "proc-macro2", 463 | "quote", 464 | "syn 1.0.107", 465 | ] 466 | 467 | [[package]] 468 | name = "pin-project-lite" 469 | version = "0.2.13" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" 472 | 473 | [[package]] 474 | name = "pin-utils" 475 | version = "0.1.0" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 478 | 479 | [[package]] 480 | name = "pinned" 481 | version = "0.1.0" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "a829027bd95e54cfe13e3e258a1ae7b645960553fb82b75ff852c29688ee595b" 484 | dependencies = [ 485 | "futures", 486 | "rustversion", 487 | "thiserror", 488 | ] 489 | 490 | [[package]] 491 | name = "proc-macro-crate" 492 | version = "1.3.1" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" 495 | dependencies = [ 496 | "once_cell", 497 | "toml_edit", 498 | ] 499 | 500 | [[package]] 501 | name = "proc-macro2" 502 | version = "1.0.70" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" 505 | dependencies = [ 506 | "unicode-ident", 507 | ] 508 | 509 | [[package]] 510 | name = "quote" 511 | version = "1.0.33" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 514 | dependencies = [ 515 | "proc-macro2", 516 | ] 517 | 518 | [[package]] 519 | name = "regex" 520 | version = "1.7.1" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" 523 | dependencies = [ 524 | "aho-corasick", 525 | "memchr", 526 | "regex-syntax", 527 | ] 528 | 529 | [[package]] 530 | name = "regex-syntax" 531 | version = "0.6.28" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" 534 | 535 | [[package]] 536 | name = "rustversion" 537 | version = "1.0.14" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" 540 | 541 | [[package]] 542 | name = "ryu" 543 | version = "1.0.12" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" 546 | 547 | [[package]] 548 | name = "serde" 549 | version = "1.0.193" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" 552 | dependencies = [ 553 | "serde_derive", 554 | ] 555 | 556 | [[package]] 557 | name = "serde-wasm-bindgen" 558 | version = "0.6.3" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "b9b713f70513ae1f8d92665bbbbda5c295c2cf1da5542881ae5eefe20c9af132" 561 | dependencies = [ 562 | "js-sys", 563 | "serde", 564 | "wasm-bindgen", 565 | ] 566 | 567 | [[package]] 568 | name = "serde_derive" 569 | version = "1.0.193" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" 572 | dependencies = [ 573 | "proc-macro2", 574 | "quote", 575 | "syn 2.0.41", 576 | ] 577 | 578 | [[package]] 579 | name = "serde_json" 580 | version = "1.0.91" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" 583 | dependencies = [ 584 | "itoa", 585 | "ryu", 586 | "serde", 587 | ] 588 | 589 | [[package]] 590 | name = "serde_urlencoded" 591 | version = "0.7.1" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 594 | dependencies = [ 595 | "form_urlencoded", 596 | "itoa", 597 | "ryu", 598 | "serde", 599 | ] 600 | 601 | [[package]] 602 | name = "slab" 603 | version = "0.4.9" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 606 | dependencies = [ 607 | "autocfg", 608 | ] 609 | 610 | [[package]] 611 | name = "syn" 612 | version = "1.0.107" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" 615 | dependencies = [ 616 | "proc-macro2", 617 | "quote", 618 | "unicode-ident", 619 | ] 620 | 621 | [[package]] 622 | name = "syn" 623 | version = "2.0.41" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" 626 | dependencies = [ 627 | "proc-macro2", 628 | "quote", 629 | "unicode-ident", 630 | ] 631 | 632 | [[package]] 633 | name = "thiserror" 634 | version = "1.0.38" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" 637 | dependencies = [ 638 | "thiserror-impl", 639 | ] 640 | 641 | [[package]] 642 | name = "thiserror-impl" 643 | version = "1.0.38" 644 | source = "registry+https://github.com/rust-lang/crates.io-index" 645 | checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" 646 | dependencies = [ 647 | "proc-macro2", 648 | "quote", 649 | "syn 1.0.107", 650 | ] 651 | 652 | [[package]] 653 | name = "toml_datetime" 654 | version = "0.6.5" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" 657 | 658 | [[package]] 659 | name = "toml_edit" 660 | version = "0.19.15" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" 663 | dependencies = [ 664 | "indexmap", 665 | "toml_datetime", 666 | "winnow", 667 | ] 668 | 669 | [[package]] 670 | name = "unicode-ident" 671 | version = "1.0.6" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" 674 | 675 | [[package]] 676 | name = "wasi" 677 | version = "0.11.0+wasi-snapshot-preview1" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 680 | 681 | [[package]] 682 | name = "wasm-bindgen" 683 | version = "0.2.89" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" 686 | dependencies = [ 687 | "cfg-if", 688 | "wasm-bindgen-macro", 689 | ] 690 | 691 | [[package]] 692 | name = "wasm-bindgen-backend" 693 | version = "0.2.89" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" 696 | dependencies = [ 697 | "bumpalo", 698 | "log", 699 | "once_cell", 700 | "proc-macro2", 701 | "quote", 702 | "syn 2.0.41", 703 | "wasm-bindgen-shared", 704 | ] 705 | 706 | [[package]] 707 | name = "wasm-bindgen-futures" 708 | version = "0.4.33" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" 711 | dependencies = [ 712 | "cfg-if", 713 | "js-sys", 714 | "wasm-bindgen", 715 | "web-sys", 716 | ] 717 | 718 | [[package]] 719 | name = "wasm-bindgen-macro" 720 | version = "0.2.89" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" 723 | dependencies = [ 724 | "quote", 725 | "wasm-bindgen-macro-support", 726 | ] 727 | 728 | [[package]] 729 | name = "wasm-bindgen-macro-support" 730 | version = "0.2.89" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" 733 | dependencies = [ 734 | "proc-macro2", 735 | "quote", 736 | "syn 2.0.41", 737 | "wasm-bindgen-backend", 738 | "wasm-bindgen-shared", 739 | ] 740 | 741 | [[package]] 742 | name = "wasm-bindgen-shared" 743 | version = "0.2.89" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" 746 | 747 | [[package]] 748 | name = "web-sys" 749 | version = "0.3.60" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" 752 | dependencies = [ 753 | "js-sys", 754 | "wasm-bindgen", 755 | ] 756 | 757 | [[package]] 758 | name = "winnow" 759 | version = "0.5.28" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "6c830786f7720c2fd27a1a0e27a709dbd3c4d009b56d098fc742d4f4eab91fe2" 762 | dependencies = [ 763 | "memchr", 764 | ] 765 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = 'hackernews-userscript' 3 | version = '0.1.0' 4 | authors = ['user'] 5 | edition = '2018' 6 | 7 | [lib] 8 | crate-type = ['cdylib', 'rlib'] 9 | 10 | [dependencies] 11 | wasm-bindgen = '0.2' 12 | js-sys = '0.3' 13 | wasm-bindgen-futures = '0.4' 14 | console_error_panic_hook = '0.1.6' 15 | gloo = '0.11' 16 | gloo-console = '0.3' 17 | regex = '1.7' 18 | serde_json = '1.0' 19 | indexmap = '2.1' 20 | base64 = '0.21' 21 | 22 | [dependencies.web-sys] 23 | version = '0.3' 24 | features = [ 25 | 'Document', 26 | 'Text', 27 | 'Element', 28 | 'HtmlCollection', 29 | 'HtmlElement', 30 | 'Node', 31 | 'NodeList', 32 | 'Window', 33 | 'console', 34 | 'HtmlButtonElement', 35 | 'KeyboardEvent', 36 | 'HtmlInputElement', 37 | 'HtmlImageElement', 38 | ] 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 drakerossman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Available for Firefox and Chrome: 2 | 3 | [![Firefox Add-Onn](./firefox-store.webp)](https://addons.mozilla.org/en-US/firefox/addon/hackernews-userscript) [![Chrome Extension](./chrome-store.webp)](https://chromewebstore.google.com/detail/hackernews-userscript/ifhpcnanpbmjddhbmdjigdmkcliehdeo?hl=en&authuser=0) 4 | 5 | # Description 6 | Hacker News Userscript - a Browser Extension to Play Hide and Seek! 7 | 8 | The Usescript is written in Rust with Web Assembly compile target. Based on the [Mubelotix's Wasm Extension Template](https://github.com/Mubelotix/wasm-extension-template). 9 | 10 | The extension provides features, which allow users to filter submissions based on a text or a regular expression: 11 | - Filter submissions by text in title or link 12 | - Filter comments by text 13 | - Hide submissions and comments from a specific user 14 | - Get visual feedback on how many items were hidden by a particular filter 15 | - Toggle hide of all the children for a hidden parent comment 16 | - Show `[hidden]` placeholder in place of hidden items 17 | - Hide an individual submission, without auto-loading the next one. You can hide everything on the page, and it will stay blank! 18 | 19 | Uses [Rust's Regex](https://github.com/rust-lang/regex) to enable expression matching. Regardless of regex usage, matches by text as a substring in an item 20 | 21 | All the filtering data and settings are stored in the localstorage. 22 | 23 | See the videos below for the features showcase! 24 | 25 | # Features Showcase 26 | https://github.com/drakerossman/hackernews-userscript/assets/97120319/0cbbf697-d617-49f7-a157-35f0fb3e7eba 27 | 28 | https://github.com/drakerossman/hackernews-userscript/assets/97120319/fc6d9e68-b897-4f78-b2e0-172dce9f8e6b 29 | 30 | https://github.com/drakerossman/hackernews-userscript/assets/97120319/0a18c470-61aa-43ca-bdd5-e9079af97375 31 | 32 | # Local Development and Installation 33 | For the local development, a convenient `flake.nix` with devshell is provided. 34 | 35 | 1. Clone the repo: 36 | ```sh 37 | git clone https://github.com/drakerossman/hackernews-userscript 38 | ``` 39 | 40 | 2. If not already, install the nix package manager via Determinate Systems' nix installer: 41 | ```sh 42 | curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install 43 | ``` 44 | It will walk you through the installation process via easy-to-follow prompts. You will also be prompted for sudo. 45 | 46 | 3. Navigate to the cloned repo's directory and invoke the devshell: 47 | ```sh 48 | cd hackernews-userscript 49 | nix --extra-experimental-features 'nix-command flakes' develop 50 | ``` 51 | 52 | After fetching the files, all the tools required for compilation and development should be made ready-available in the new shell. 53 | 54 | 4. Build the extension for chrome and firefox: 55 | ```sh 56 | ./build.sh 57 | ``` 58 | 59 | To load extension in Chrome: 60 | - navigate to [chrome://extensions/](chrome://extensions/) 61 | - enable Developer Mode by clicking the toggle switch next to Developer mode. 62 | - use Load unpacked and select the extension directory - pkg/chrome 63 | 64 | To load extension in Firefox: 65 | - navigate to [about:debugging](about:debugging) 66 | - click "This Firefox" 67 | - use Load Temporary Add-on, navigate to the extension directory, and select any file from the root of the directory - pkg/firefox/background-worker.js 68 | 69 | # For Contributors 70 | PRs are always welcome, especially if: 71 | - you can make the interface prettier and more erogonomic 72 | - you know how to optimize regex so it won't spend a full second on parsing one 73 | - you are eager to refactor the codebase 74 | 75 | Please mind, that I have been developing this extension on NixOS via the provided devShell in `flake.nix`. 76 | 77 | For whatever reason, should you also use NixOS, and should the path to the folder of this project contain spaces, the compilation via `build.sh` would fail with the following error: 78 | ```shell 79 | error: linking with `cc` failed: exit status: 1 80 | ``` 81 | 82 | # Coming Soon 83 | - [ ] Highlighting by topic 84 | - [ ] Endless scrolling 85 | - [ ] Thread auto-update 86 | - [ ] Background thread watcher 87 | - [ ] Codebase refactoring 88 | - [ ] Replies, subscriptions 89 | - [ ] Group items by topics 90 | - [ ] Chronological comments with reply links 91 | - [ ] Easy point-and-click to filter an item 92 | - [ ] Collapsible sidebar 93 | - [ ] Better UI 94 | - [ ] Favorites functionality 95 | - [ ] Automatically mark already-read submissions and comments 96 | - [ ] Export and import filters and settings 97 | - [ ] Infotips on hover 98 | 99 | # In the Future 100 | - [ ] Safari support, mobile support 101 | - [ ] Local-first cloud sync between devices 102 | - [ ] Support for other websites 103 | - [ ] Social collaborative filtering 104 | 105 | # Sponsor This Project 106 | You can sponsor this work, as well as other projects of mine via Liberapay and Patreon: 107 | 108 | Donate using Liberapay Donate using Patreon 109 | -------------------------------------------------------------------------------- /background-worker.js: -------------------------------------------------------------------------------- 1 | chrome.runtime.onStartup.addListener 2 | ( 3 | () => 4 | { 5 | initializeExceptionLogic() 6 | 7 | }, 8 | ) 9 | 10 | chrome.tabs.onCreated.addListener 11 | ( 12 | ( 13 | _tab, 14 | ) => 15 | { 16 | initializeExceptionLogic() 17 | }, 18 | ) 19 | 20 | const initializeExceptionLogic = () => 21 | { 22 | chrome.declarativeNetRequest.updateSessionRules 23 | ( 24 | { 25 | addRules: [ 26 | dropXFrameAndCsp(), 27 | ], 28 | }, 29 | () => 30 | { 31 | if (chrome.runtime.lastError) 32 | { 33 | console.log( 34 | chrome.runtime.lastError.message, 35 | ) 36 | } 37 | }, 38 | ) 39 | } 40 | 41 | const dropXFrameAndCsp = () => 42 | { 43 | return { 44 | id: 1, 45 | priority: 1, 46 | action: { 47 | type: 'modifyHeaders', 48 | responseHeaders: [ 49 | { 50 | header: 'x-frame-options', 51 | operation: 'remove', 52 | }, 53 | { 54 | header: 'content-security-policy', 55 | operation: 'remove', 56 | }, 57 | ], 58 | }, 59 | condition: { 60 | urlFilter: `*${'https://news.ycombinator.com/*'}*`, 61 | resourceTypes: [ 62 | 'main_frame', 63 | 'sub_frame', 64 | ], 65 | }, 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | rm -rf pkg 4 | wasm-pack build --target=no-modules --mode no-install || exit 1 5 | 6 | mkdir tmp-target 7 | mkdir tmp-target/firefox 8 | mkdir tmp-target/chrome 9 | 10 | cp pkg/* tmp-target/firefox 11 | mv pkg/* tmp-target/chrome 12 | mv tmp-target/* pkg 13 | rm -rf tmp-target 14 | 15 | cp firefox_manifest_v3.json pkg/firefox/manifest.json 16 | cp chrome_manifest_v3.json pkg/chrome/manifest.json 17 | 18 | cp background-worker.js pkg/firefox/background-worker.js 19 | cp background-worker.js pkg/chrome/background-worker.js 20 | 21 | cp icons/* pkg/firefox/ 22 | cp icons/* pkg/chrome/ 23 | 24 | printf " 25 | const runtime = chrome.runtime || browser.runtime; 26 | async function run() { 27 | await wasm_bindgen(runtime.getURL('hackernews_userscript_bg.wasm')); 28 | } 29 | run(); 30 | " >> pkg/firefox/run_wasm.js 31 | 32 | printf " 33 | const runtime = chrome.runtime || browser.runtime; 34 | async function run() { 35 | await wasm_bindgen(runtime.getURL('hackernews_userscript_bg.wasm')); 36 | } 37 | run(); 38 | " >> pkg/chrome/run_wasm.js 39 | -------------------------------------------------------------------------------- /chrome-store.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakerossman/hackernews-userscript/67c58d7e44cf0a49bdcb4895b2ba4347a497b8c6/chrome-store.webp -------------------------------------------------------------------------------- /chrome_manifest_v3.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Hackernews Userscript", 4 | "description": "Play Hide and Seek", 5 | "version": "1.0.0", 6 | "icons": { 7 | "16": "icon16.png", 8 | "32": "icon32.png", 9 | "48": "icon48.png", 10 | "128": "icon128.png" 11 | }, 12 | "background": { 13 | "service_worker": "background-worker.js" 14 | }, 15 | "permissions": [ 16 | "declarativeNetRequest" 17 | ], 18 | "host_permissions": [ 19 | "*://news.ycombinator.com/*" 20 | ], 21 | "content_scripts": [ 22 | { 23 | "matches": [ 24 | "*://news.ycombinator.com/*" 25 | ], 26 | "js": [ 27 | "hackernews_userscript.js", 28 | "run_wasm.js" 29 | ] 30 | } 31 | ], 32 | "web_accessible_resources": [ 33 | { 34 | "resources": [ 35 | "hackernews_userscript_bg.wasm" 36 | ], 37 | "matches": [ 38 | "*://news.ycombinator.com/*" 39 | ] 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /filter-by-title.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakerossman/hackernews-userscript/67c58d7e44cf0a49bdcb4895b2ba4347a497b8c6/filter-by-title.mp4 -------------------------------------------------------------------------------- /filter-by-username.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakerossman/hackernews-userscript/67c58d7e44cf0a49bdcb4895b2ba4347a497b8c6/filter-by-username.mp4 -------------------------------------------------------------------------------- /firefox-store.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakerossman/hackernews-userscript/67c58d7e44cf0a49bdcb4895b2ba4347a497b8c6/firefox-store.webp -------------------------------------------------------------------------------- /firefox_manifest_v3.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Hackernews Userscript", 4 | "description": "Play Hide and Seek", 5 | "version": "1.0.0", 6 | "icons": { 7 | "16": "icon16.png", 8 | "32": "icon32.png", 9 | "48": "icon48.png", 10 | "128": "icon128.png" 11 | }, 12 | "background": { 13 | "scripts": ["background-worker.js"] 14 | }, 15 | "permissions": [ 16 | "declarativeNetRequest" 17 | ], 18 | "host_permissions": [ 19 | "*://news.ycombinator.com/*" 20 | ], 21 | "content_scripts": [ 22 | { 23 | "matches": [ 24 | "*://news.ycombinator.com/*" 25 | ], 26 | "js": [ 27 | "hackernews_userscript.js", 28 | "run_wasm.js" 29 | ] 30 | } 31 | ], 32 | "web_accessible_resources": [ 33 | { 34 | "resources": [ 35 | "hackernews_userscript_bg.wasm" 36 | ], 37 | "matches": [ 38 | "*://news.ycombinator.com/*" 39 | ] 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "devenv": { 4 | "inputs": { 5 | "flake-compat": "flake-compat", 6 | "nix": "nix", 7 | "nixpkgs": "nixpkgs", 8 | "pre-commit-hooks": "pre-commit-hooks" 9 | }, 10 | "locked": { 11 | "lastModified": 1671716968, 12 | "narHash": "sha256-LThNtwAXH/0KVMgyTFBwl93ktuWpWTZhCh6NBlydBbQ=", 13 | "owner": "cachix", 14 | "repo": "devenv", 15 | "rev": "ba6818f4c39fd95aebdb6dc441401f2b60484652", 16 | "type": "github" 17 | }, 18 | "original": { 19 | "owner": "cachix", 20 | "ref": "v0.5", 21 | "repo": "devenv", 22 | "type": "github" 23 | } 24 | }, 25 | "flake-compat": { 26 | "flake": false, 27 | "locked": { 28 | "lastModified": 1668681692, 29 | "narHash": "sha256-Ht91NGdewz8IQLtWZ9LCeNXMSXHUss+9COoqu6JLmXU=", 30 | "owner": "edolstra", 31 | "repo": "flake-compat", 32 | "rev": "009399224d5e398d03b22badca40a37ac85412a1", 33 | "type": "github" 34 | }, 35 | "original": { 36 | "owner": "edolstra", 37 | "repo": "flake-compat", 38 | "type": "github" 39 | } 40 | }, 41 | "flake-utils": { 42 | "locked": { 43 | "lastModified": 1667395993, 44 | "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", 45 | "owner": "numtide", 46 | "repo": "flake-utils", 47 | "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", 48 | "type": "github" 49 | }, 50 | "original": { 51 | "owner": "numtide", 52 | "repo": "flake-utils", 53 | "type": "github" 54 | } 55 | }, 56 | "flake-utils_2": { 57 | "inputs": { 58 | "systems": "systems" 59 | }, 60 | "locked": { 61 | "lastModified": 1701680307, 62 | "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", 63 | "owner": "numtide", 64 | "repo": "flake-utils", 65 | "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", 66 | "type": "github" 67 | }, 68 | "original": { 69 | "owner": "numtide", 70 | "repo": "flake-utils", 71 | "type": "github" 72 | } 73 | }, 74 | "flake-utils_3": { 75 | "inputs": { 76 | "systems": "systems_2" 77 | }, 78 | "locked": { 79 | "lastModified": 1681202837, 80 | "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", 81 | "owner": "numtide", 82 | "repo": "flake-utils", 83 | "rev": "cfacdce06f30d2b68473a46042957675eebb3401", 84 | "type": "github" 85 | }, 86 | "original": { 87 | "owner": "numtide", 88 | "repo": "flake-utils", 89 | "type": "github" 90 | } 91 | }, 92 | "gitignore": { 93 | "inputs": { 94 | "nixpkgs": [ 95 | "devenv", 96 | "pre-commit-hooks", 97 | "nixpkgs" 98 | ] 99 | }, 100 | "locked": { 101 | "lastModified": 1660459072, 102 | "narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=", 103 | "owner": "hercules-ci", 104 | "repo": "gitignore.nix", 105 | "rev": "a20de23b925fd8264fd7fad6454652e142fd7f73", 106 | "type": "github" 107 | }, 108 | "original": { 109 | "owner": "hercules-ci", 110 | "repo": "gitignore.nix", 111 | "type": "github" 112 | } 113 | }, 114 | "lowdown-src": { 115 | "flake": false, 116 | "locked": { 117 | "lastModified": 1633514407, 118 | "narHash": "sha256-Dw32tiMjdK9t3ETl5fzGrutQTzh2rufgZV4A/BbxuD4=", 119 | "owner": "kristapsdz", 120 | "repo": "lowdown", 121 | "rev": "d2c2b44ff6c27b936ec27358a2653caaef8f73b8", 122 | "type": "github" 123 | }, 124 | "original": { 125 | "owner": "kristapsdz", 126 | "repo": "lowdown", 127 | "type": "github" 128 | } 129 | }, 130 | "nix": { 131 | "inputs": { 132 | "lowdown-src": "lowdown-src", 133 | "nixpkgs": [ 134 | "devenv", 135 | "nixpkgs" 136 | ], 137 | "nixpkgs-regression": "nixpkgs-regression" 138 | }, 139 | "locked": { 140 | "lastModified": 1671638174, 141 | "narHash": "sha256-FeEmVix8l/HglWtRgeHOfjqEm2etvp+MLYd1C/raq3Y=", 142 | "owner": "domenkozar", 143 | "repo": "nix", 144 | "rev": "51b770e985f9e1b84fb5e03a983ef1e19f18c3e9", 145 | "type": "github" 146 | }, 147 | "original": { 148 | "owner": "domenkozar", 149 | "ref": "relaxed-flakes", 150 | "repo": "nix", 151 | "type": "github" 152 | } 153 | }, 154 | "nixpkgs": { 155 | "locked": { 156 | "lastModified": 1671458120, 157 | "narHash": "sha256-2+k/OONN4OF21TeoNjKB5sXVZv6Zvm/uEyQIW9OYCg8=", 158 | "owner": "NixOS", 159 | "repo": "nixpkgs", 160 | "rev": "e37ef84b478fa8da0ced96522adfd956fde9047a", 161 | "type": "github" 162 | }, 163 | "original": { 164 | "owner": "NixOS", 165 | "ref": "nixpkgs-unstable", 166 | "repo": "nixpkgs", 167 | "type": "github" 168 | } 169 | }, 170 | "nixpkgs-regression": { 171 | "locked": { 172 | "lastModified": 1643052045, 173 | "narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=", 174 | "owner": "NixOS", 175 | "repo": "nixpkgs", 176 | "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", 177 | "type": "github" 178 | }, 179 | "original": { 180 | "owner": "NixOS", 181 | "repo": "nixpkgs", 182 | "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", 183 | "type": "github" 184 | } 185 | }, 186 | "nixpkgs-stable": { 187 | "locked": { 188 | "lastModified": 1671271954, 189 | "narHash": "sha256-cSvu+bnvN08sOlTBWbBrKaBHQZq8mvk8bgpt0ZJ2Snc=", 190 | "owner": "NixOS", 191 | "repo": "nixpkgs", 192 | "rev": "d513b448cc2a6da2c8803e3c197c9fc7e67b19e3", 193 | "type": "github" 194 | }, 195 | "original": { 196 | "owner": "NixOS", 197 | "ref": "nixos-22.05", 198 | "repo": "nixpkgs", 199 | "type": "github" 200 | } 201 | }, 202 | "nixpkgs_2": { 203 | "locked": { 204 | "lastModified": 1703255338, 205 | "narHash": "sha256-Z6wfYJQKmDN9xciTwU3cOiOk+NElxdZwy/FiHctCzjU=", 206 | "owner": "NixOS", 207 | "repo": "nixpkgs", 208 | "rev": "6df37dc6a77654682fe9f071c62b4242b5342e04", 209 | "type": "github" 210 | }, 211 | "original": { 212 | "owner": "NixOS", 213 | "ref": "nixos-unstable", 214 | "repo": "nixpkgs", 215 | "type": "github" 216 | } 217 | }, 218 | "nixpkgs_3": { 219 | "locked": { 220 | "lastModified": 1681358109, 221 | "narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=", 222 | "owner": "NixOS", 223 | "repo": "nixpkgs", 224 | "rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9", 225 | "type": "github" 226 | }, 227 | "original": { 228 | "owner": "NixOS", 229 | "ref": "nixpkgs-unstable", 230 | "repo": "nixpkgs", 231 | "type": "github" 232 | } 233 | }, 234 | "pre-commit-hooks": { 235 | "inputs": { 236 | "flake-compat": [ 237 | "devenv", 238 | "flake-compat" 239 | ], 240 | "flake-utils": "flake-utils", 241 | "gitignore": "gitignore", 242 | "nixpkgs": [ 243 | "devenv", 244 | "nixpkgs" 245 | ], 246 | "nixpkgs-stable": "nixpkgs-stable" 247 | }, 248 | "locked": { 249 | "lastModified": 1671452357, 250 | "narHash": "sha256-HqzXiQEegpRQ4VEl9pEPgHSIxhJrNJ27HfN1wOc7w2E=", 251 | "owner": "cachix", 252 | "repo": "pre-commit-hooks.nix", 253 | "rev": "200790e9c77064c53eaf95805b013d96615ecc27", 254 | "type": "github" 255 | }, 256 | "original": { 257 | "owner": "cachix", 258 | "repo": "pre-commit-hooks.nix", 259 | "type": "github" 260 | } 261 | }, 262 | "root": { 263 | "inputs": { 264 | "devenv": "devenv", 265 | "flake-utils": "flake-utils_2", 266 | "nixpkgs": "nixpkgs_2", 267 | "rust-overlay": "rust-overlay" 268 | } 269 | }, 270 | "rust-overlay": { 271 | "inputs": { 272 | "flake-utils": "flake-utils_3", 273 | "nixpkgs": "nixpkgs_3" 274 | }, 275 | "locked": { 276 | "lastModified": 1703384182, 277 | "narHash": "sha256-g5K8bFBCIQ3x/j/MFTpkZo4It5SGWPwhBp/lASiy+pA=", 278 | "owner": "oxalica", 279 | "repo": "rust-overlay", 280 | "rev": "cb6395cb3c2f69ad028914c90bce833e51d339c9", 281 | "type": "github" 282 | }, 283 | "original": { 284 | "owner": "oxalica", 285 | "repo": "rust-overlay", 286 | "type": "github" 287 | } 288 | }, 289 | "systems": { 290 | "locked": { 291 | "lastModified": 1681028828, 292 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 293 | "owner": "nix-systems", 294 | "repo": "default", 295 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 296 | "type": "github" 297 | }, 298 | "original": { 299 | "owner": "nix-systems", 300 | "repo": "default", 301 | "type": "github" 302 | } 303 | }, 304 | "systems_2": { 305 | "locked": { 306 | "lastModified": 1681028828, 307 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 308 | "owner": "nix-systems", 309 | "repo": "default", 310 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 311 | "type": "github" 312 | }, 313 | "original": { 314 | "owner": "nix-systems", 315 | "repo": "default", 316 | "type": "github" 317 | } 318 | } 319 | }, 320 | "root": "root", 321 | "version": 7 322 | } 323 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Rust development shell"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | rust-overlay.url = "github:oxalica/rust-overlay"; 8 | devenv.url = "github:cachix/devenv/v0.5"; 9 | }; 10 | 11 | outputs = { 12 | self, 13 | nixpkgs, 14 | flake-utils, 15 | rust-overlay, 16 | devenv, 17 | ... 18 | }: 19 | flake-utils.lib.eachDefaultSystem ( system: 20 | let 21 | overlays = [ ( import rust-overlay ) ]; 22 | pkgs = import nixpkgs { 23 | inherit system overlays; 24 | }; 25 | in with pkgs; { 26 | 27 | devShells.default = mkShell { 28 | nativeBuildInputs = [ 29 | pkg-config 30 | ]; 31 | 32 | buildInputs = [ 33 | pkg-config 34 | ( 35 | pkgs.rust-bin.selectLatestNightlyWith ( 36 | toolchain: toolchain.default.override { 37 | extensions = [ 38 | "rust-src" 39 | "rust-analyzer" 40 | ]; 41 | targets = [ "wasm32-unknown-unknown" ]; 42 | } 43 | ) 44 | ) 45 | pkgs.wasm-pack 46 | pkgs.cargo-generate 47 | pkgs.openssl 48 | pkgs.perl 49 | pkgs.wasm-bindgen-cli 50 | ]; 51 | }; 52 | } 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakerossman/hackernews-userscript/67c58d7e44cf0a49bdcb4895b2ba4347a497b8c6/icons/icon128.png -------------------------------------------------------------------------------- /icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakerossman/hackernews-userscript/67c58d7e44cf0a49bdcb4895b2ba4347a497b8c6/icons/icon16.png -------------------------------------------------------------------------------- /icons/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakerossman/hackernews-userscript/67c58d7e44cf0a49bdcb4895b2ba4347a497b8c6/icons/icon32.png -------------------------------------------------------------------------------- /icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakerossman/hackernews-userscript/67c58d7e44cf0a49bdcb4895b2ba4347a497b8c6/icons/icon48.png -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2018" 2 | 3 | brace_style = "AlwaysNextLine" 4 | chain_width = 1 5 | control_brace_style = "AlwaysNextLine" 6 | fn_args_layout = "Vertical" 7 | imports_layout = "Vertical" 8 | match_arm_leading_pipes = "Always" 9 | match_block_trailing_comma = true 10 | merge_derives = false 11 | imports_granularity = "Module" 12 | reorder_impl_items = true 13 | reorder_imports = false 14 | group_imports = "StdExternalCrate" 15 | reorder_modules = false 16 | struct_lit_single_line = false 17 | trailing_comma = "Vertical" 18 | -------------------------------------------------------------------------------- /src/dom_manipulation.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | use web_sys::{ 3 | Document, 4 | Element, 5 | Event, 6 | HtmlInputElement, 7 | }; 8 | use regex::Regex; 9 | use wasm_bindgen::JsCast; 10 | use gloo::events::EventListener; 11 | 12 | use crate::getters::{ 13 | obtain_document, 14 | obtain_filter_summary_text_to_filter_target_text_from_local_storage, 15 | obtain_list_of_titleline_spans_as_rust_list_of_links_as_rust_list_of_hnusers_as_rust, 16 | obtain_athing_id_to_submission_title_from_local_storage, 17 | }; 18 | 19 | use crate::setters::{ 20 | write_athing_id_to_submission_title_as_stringified_json_to_local_storage, 21 | }; 22 | 23 | use gloo_console::log as gloo_log; 24 | 25 | #[wasm_bindgen] 26 | pub fn replace_center_tag_with_div() 27 | { 28 | let document = obtain_document(); 29 | 30 | let center_element = document 31 | .get_elements_by_tag_name("center") 32 | .item(0) 33 | .expect("no center element found"); 34 | 35 | let div_element = document 36 | .create_element("div") 37 | .expect("couldn't create div element"); 38 | 39 | let inner_html_of_center = center_element.inner_html(); 40 | 41 | div_element.set_inner_html(&inner_html_of_center); 42 | 43 | let parent = center_element 44 | .parent_node() 45 | .ok_or("kek") 46 | .unwrap(); 47 | 48 | parent 49 | .remove_child(¢er_element) 50 | .unwrap(); 51 | parent 52 | .append_child(&div_element) 53 | .unwrap(); 54 | } 55 | 56 | #[wasm_bindgen] 57 | pub fn remove_all_children_of_filter_items_container() 58 | { 59 | let document = obtain_document(); 60 | 61 | let filter_items_container = document 62 | .get_element_by_id("filter-items-container") 63 | .unwrap(); 64 | 65 | let filter_items_container: Element = filter_items_container 66 | .dyn_into() 67 | .unwrap(); 68 | 69 | let mut child = filter_items_container.first_child(); 70 | 71 | while let Some(child_element) = child 72 | { 73 | filter_items_container 74 | .remove_child(&child_element) 75 | .unwrap(); 76 | 77 | child = filter_items_container.first_child(); 78 | } 79 | } 80 | 81 | pub fn select_submission_elements_and_set_their_style( 82 | titleline_span_or_link_or_hnuser_or_comment: &Element, 83 | switch: &str, 84 | value_of_style_attribute: &str, 85 | ) 86 | { 87 | let document = obtain_document(); 88 | 89 | let enable_filters_checkbox = document 90 | .get_element_by_id("enable-filters-checkbox") 91 | .unwrap(); 92 | 93 | let enable_filters_checkbox_as_el: Element = enable_filters_checkbox 94 | .dyn_into() 95 | .unwrap(); 96 | 97 | let local_storage = web_sys::window() 98 | .unwrap() 99 | .local_storage() 100 | .unwrap() 101 | .unwrap(); 102 | 103 | let enable_filters_checkbox_state = local_storage 104 | .get_item("enable_filters") 105 | .unwrap() 106 | .unwrap_or_else( 107 | || { 108 | "true".to_owned() 109 | }, 110 | ); 111 | 112 | let enable_filters_checkbox_state_as_bool = enable_filters_checkbox_state 113 | .parse::() 114 | .unwrap(); 115 | 116 | let value_of_style_attribute = 117 | if !enable_filters_checkbox_state_as_bool { "" } 118 | else { value_of_style_attribute }; 119 | 120 | 121 | let submission_placeholder_tr = match switch 122 | { 123 | | "titleline_span" => 124 | { 125 | let title_td = titleline_span_or_link_or_hnuser_or_comment 126 | .parent_node() 127 | .unwrap(); 128 | 129 | let title_td_as_el: Element = title_td 130 | .dyn_into() 131 | .unwrap(); 132 | 133 | let submission_placeholder_tr = title_td_as_el 134 | .parent_node() 135 | .unwrap(); 136 | 137 | submission_placeholder_tr 138 | }, 139 | | "hnuser" => 140 | { 141 | let hn_user = titleline_span_or_link_or_hnuser_or_comment 142 | .parent_node() 143 | .unwrap(); 144 | 145 | let hn_user_as_el: Element = hn_user 146 | .dyn_into() 147 | .unwrap(); 148 | 149 | let subline_span = hn_user_as_el 150 | .parent_node() 151 | .unwrap(); 152 | 153 | let subline_span_as_el: Element = subline_span 154 | .dyn_into() 155 | .unwrap(); 156 | 157 | let subtext_td = subline_span_as_el 158 | .parent_node() 159 | .unwrap(); 160 | 161 | let subtext_td_as_el: Element = subtext_td 162 | .dyn_into() 163 | .unwrap(); 164 | 165 | let submission_placeholder_tr = subtext_td_as_el 166 | .previous_sibling() 167 | .unwrap(); 168 | 169 | submission_placeholder_tr 170 | }, 171 | | "link" => 172 | { 173 | let link = titleline_span_or_link_or_hnuser_or_comment 174 | .parent_node() 175 | .unwrap(); 176 | 177 | let link_as_el: Element = link 178 | .dyn_into() 179 | .unwrap(); 180 | 181 | let titleline_span = link_as_el 182 | .parent_node() 183 | .unwrap(); 184 | 185 | let titleline_span_as_el: Element = titleline_span 186 | .dyn_into() 187 | .unwrap(); 188 | let title_td = link_as_el 189 | .parent_node() 190 | .unwrap(); 191 | 192 | let title_td_as_el: Element = title_td 193 | .dyn_into() 194 | .unwrap(); 195 | 196 | let submission_placeholder_tr = title_td_as_el 197 | .parent_node() 198 | .unwrap(); 199 | 200 | submission_placeholder_tr 201 | }, 202 | | "comment" => 203 | { 204 | let commtext = titleline_span_or_link_or_hnuser_or_comment 205 | .parent_node() 206 | .unwrap(); 207 | 208 | let commtext_as_el: Element = commtext 209 | .dyn_into() 210 | .unwrap(); 211 | 212 | let comment = commtext_as_el 213 | .parent_node() 214 | .unwrap(); 215 | 216 | let comment_as_el: Element = comment 217 | .dyn_into() 218 | .unwrap(); 219 | 220 | let comment_td = comment_as_el 221 | .parent_node() 222 | .unwrap(); 223 | 224 | let comment_td_as_el: Element = comment_td 225 | .dyn_into() 226 | .unwrap(); 227 | 228 | let comment_tr = comment_as_el 229 | .parent_node() 230 | .unwrap(); 231 | 232 | let comment_tr_as_el: Element = comment_tr 233 | .dyn_into() 234 | .unwrap(); 235 | 236 | let comment_tbody = comment_as_el 237 | .parent_node() 238 | .unwrap(); 239 | 240 | let comment_tbody_as_el: Element = comment_tbody 241 | .dyn_into() 242 | .unwrap(); 243 | 244 | let comment_table = comment_as_el 245 | .parent_node() 246 | .unwrap(); 247 | 248 | let comment_table_as_el: Element = comment_table 249 | .dyn_into() 250 | .unwrap(); 251 | 252 | let comment_table_td = comment_as_el 253 | .parent_node() 254 | .unwrap(); 255 | 256 | let comment_table_td_as_el: Element = comment_table_td 257 | .dyn_into() 258 | .unwrap(); 259 | 260 | let comment_itself = comment_as_el 261 | .parent_node() 262 | .unwrap(); 263 | 264 | let submission_placeholder_tr = comment_itself; 265 | 266 | submission_placeholder_tr 267 | }, 268 | | &_ => 269 | { 270 | panic!("switch must be either titleline_span or hnuser"); 271 | }, 272 | }; 273 | 274 | let submission_placeholder_tr_as_el: Element = submission_placeholder_tr 275 | .dyn_into() 276 | .unwrap(); 277 | 278 | submission_placeholder_tr_as_el 279 | .set_attribute("style", value_of_style_attribute) 280 | .unwrap(); 281 | 282 | let subtext_tr = submission_placeholder_tr_as_el 283 | .next_element_sibling(); 284 | 285 | let document = obtain_document(); 286 | 287 | let substitute_placeholder_checkbox = document 288 | .get_element_by_id("substitute-placeholder-checkbox") 289 | .unwrap(); 290 | 291 | let substitute_placeholder_checkbox_as_el: Element = substitute_placeholder_checkbox 292 | .dyn_into() 293 | .unwrap(); 294 | 295 | let local_storage = web_sys::window() 296 | .unwrap() 297 | .local_storage() 298 | .unwrap() 299 | .unwrap(); 300 | 301 | let substitute_placeholder_checkbox_state = local_storage 302 | .get_item("substitute_placeholder") 303 | .unwrap() 304 | .unwrap_or_else( 305 | || { 306 | "false".to_owned() 307 | }, 308 | ); 309 | 310 | let substitute_placeholder_checkbox_state_as_bool = substitute_placeholder_checkbox_state 311 | .parse::() 312 | .unwrap(); 313 | 314 | 315 | if value_of_style_attribute == "display: none;" && substitute_placeholder_checkbox_state_as_bool 316 | { 317 | let document = obtain_document(); 318 | 319 | let substitute_placeholder_of_hidden_entry = document 320 | .create_element("tr") 321 | .unwrap(); 322 | 323 | substitute_placeholder_of_hidden_entry 324 | .set_attribute("class", "substitute-placeholder-of-hidden-entry") 325 | .unwrap(); 326 | 327 | let inner_html_of_substitute_placeholder_of_hidden_entry = "[hidden]"; 328 | 329 | substitute_placeholder_of_hidden_entry 330 | .set_inner_html(inner_html_of_substitute_placeholder_of_hidden_entry); 331 | 332 | submission_placeholder_tr_as_el.insert_adjacent_element("afterend", &substitute_placeholder_of_hidden_entry).unwrap(); 333 | 334 | } 335 | 336 | match subtext_tr 337 | { 338 | | Some(subtext_tr) => 339 | { 340 | subtext_tr 341 | .set_attribute("style", &value_of_style_attribute) 342 | .unwrap(); 343 | let spacer_tr = subtext_tr 344 | .next_element_sibling(); 345 | 346 | match spacer_tr 347 | { 348 | | Some(spacer_tr) => 349 | { 350 | if !substitute_placeholder_checkbox_state_as_bool 351 | { 352 | let value_of_style_attribute_for_spacer 353 | = if value_of_style_attribute == "display: none;" 354 | { 355 | value_of_style_attribute 356 | } 357 | else 358 | { 359 | "height:5px" 360 | }; 361 | 362 | spacer_tr 363 | .set_attribute("style", value_of_style_attribute_for_spacer) 364 | .unwrap(); 365 | } 366 | 367 | }, 368 | | None => {}, 369 | } 370 | }, 371 | | None => {}, 372 | } 373 | 374 | let document = obtain_document(); 375 | 376 | let hide_children_comments_checkbox_checkbox = document 377 | .get_element_by_id("hide-children-comments-checkbox") 378 | .unwrap(); 379 | 380 | let hide_children_comments_checkbox_checkbox_as_el: Element = hide_children_comments_checkbox_checkbox 381 | .dyn_into() 382 | .unwrap(); 383 | 384 | let local_storage = web_sys::window() 385 | .unwrap() 386 | .local_storage() 387 | .unwrap() 388 | .unwrap(); 389 | 390 | let hide_children_comments_checkbox_checkbox_state = local_storage 391 | .get_item("hide_children_comments") 392 | .unwrap() 393 | .unwrap_or_else( 394 | || { 395 | "false".to_owned() 396 | }, 397 | ); 398 | 399 | let hide_children_comments_checkbox_checkbox_state_as_bool = hide_children_comments_checkbox_checkbox_state 400 | .parse::() 401 | .unwrap(); 402 | 403 | 404 | let value_of_style_attribute_for_children_comments 405 | = if hide_children_comments_checkbox_checkbox_state_as_bool && value_of_style_attribute == "display: none;" 406 | { 407 | "display: none;" 408 | } 409 | else 410 | { 411 | "" 412 | }; 413 | 414 | if ( 415 | true 416 | ) { 417 | let comment_tr = &submission_placeholder_tr_as_el 418 | .parent_node() 419 | .unwrap(); 420 | 421 | let comment_tr_as_el: Element = comment_tr 422 | .clone() 423 | .dyn_into() 424 | .unwrap(); 425 | 426 | let comment_tbody = comment_tr_as_el 427 | .parent_node() 428 | .unwrap(); 429 | 430 | let comment_tbody_as_el: Element = comment_tbody 431 | .dyn_into() 432 | .unwrap(); 433 | 434 | let comment_table = comment_tbody_as_el 435 | .parent_node() 436 | .unwrap(); 437 | 438 | let comment_table_as_el: Element = comment_table 439 | .dyn_into() 440 | .unwrap(); 441 | 442 | let comment_table_td = 443 | if switch == "hnuser" { 444 | let more_in_between = comment_table_as_el 445 | .parent_node() 446 | .unwrap(); 447 | 448 | let more_in_between_as_el: Element = more_in_between 449 | .dyn_into() 450 | .unwrap(); 451 | 452 | more_in_between_as_el 453 | .parent_node() 454 | .unwrap() 455 | } 456 | else { 457 | comment_table_as_el 458 | .parent_node() 459 | .unwrap() 460 | }; 461 | 462 | let comment_table_td_as_el: Element = comment_table_td 463 | .dyn_into() 464 | .unwrap(); 465 | 466 | 467 | { 468 | let mut next_sibling_comment_as_el = comment_table_td_as_el 469 | .next_element_sibling(); 470 | 471 | while ( 472 | next_sibling_comment_as_el.clone().is_some() 473 | ) { 474 | 475 | let next_sibling_comment_as_el_unwrapped = next_sibling_comment_as_el 476 | .unwrap(); 477 | 478 | let this_comment_as_el: Element = next_sibling_comment_as_el_unwrapped; 479 | 480 | this_comment_as_el 481 | .set_attribute("style", &value_of_style_attribute_for_children_comments) 482 | .unwrap(); 483 | 484 | next_sibling_comment_as_el = this_comment_as_el 485 | .next_element_sibling(); 486 | } 487 | } 488 | 489 | } 490 | } 491 | 492 | #[wasm_bindgen] 493 | pub fn remove_elements_with_class( 494 | class_name: &str 495 | ) -> Result<(), JsValue> 496 | { 497 | let document = obtain_document(); 498 | 499 | let selector = format!(".{}", class_name); 500 | let elements = document.query_selector_all(&selector)?; 501 | 502 | for i in 0..elements.length() 503 | { 504 | if let Some(element) = elements.item(i) { 505 | let element: Element = element.dyn_into().unwrap(); 506 | element.remove(); 507 | } 508 | } 509 | 510 | Ok(()) 511 | } 512 | 513 | 514 | pub fn hide_specific_item_of_a_specific_category( 515 | jaywalked_string_with_categories: &String, 516 | text_as_source_of_truth_against_which_to_check: &String, 517 | element_whose_text_is_to_be_checked: &Element, 518 | category: &str, 519 | mut count_of_filtered_entries: &mut (u32, u32, u32, u32), 520 | ) 521 | { 522 | if jaywalked_string_with_categories.contains(category) 523 | { 524 | let text_as_source_of_truth_against_which_to_check_as_regex = 525 | Regex::new(&text_as_source_of_truth_against_which_to_check); 526 | 527 | let text_as_source_of_truth_against_which_to_check_as_regex = 528 | match text_as_source_of_truth_against_which_to_check_as_regex 529 | { 530 | | Ok(regex) => regex, 531 | | Err(_) => 532 | { 533 | Regex::new("invalid_regex").unwrap() 534 | }, 535 | }; 536 | 537 | let text_to_be_checked_if_its_element_should_be_filtered = match category 538 | { 539 | | "Title" => 540 | { 541 | let text_to_be_checked_if_its_element_should_be_filtered = 542 | element_whose_text_is_to_be_checked 543 | .text_content() 544 | .unwrap(); 545 | 546 | text_to_be_checked_if_its_element_should_be_filtered 547 | }, 548 | | "Link" => 549 | { 550 | let text_to_be_checked_if_its_element_should_be_filtered = 551 | element_whose_text_is_to_be_checked 552 | .get_attribute("href") 553 | .unwrap(); 554 | 555 | text_to_be_checked_if_its_element_should_be_filtered 556 | }, 557 | | "Username" => 558 | { 559 | let text_to_be_checked_if_its_element_should_be_filtered = 560 | element_whose_text_is_to_be_checked 561 | .text_content() 562 | .unwrap(); 563 | 564 | text_to_be_checked_if_its_element_should_be_filtered 565 | }, 566 | | "Comment" => 567 | { 568 | let text_to_be_checked_if_its_element_should_be_filtered = 569 | element_whose_text_is_to_be_checked 570 | .text_content() 571 | .unwrap(); 572 | 573 | text_to_be_checked_if_its_element_should_be_filtered 574 | }, 575 | | &_ => 576 | { 577 | panic!("Not supported"); 578 | }, 579 | }; 580 | 581 | let text_as_source_of_truth_against_which_to_check_as_js_value = JsValue::from_str(&text_as_source_of_truth_against_which_to_check); 582 | 583 | if text_as_source_of_truth_against_which_to_check_as_regex 584 | .is_match(&text_to_be_checked_if_its_element_should_be_filtered) 585 | || text_to_be_checked_if_its_element_should_be_filtered.contains(&text_as_source_of_truth_against_which_to_check.to_owned()) 586 | { 587 | match category 588 | { 589 | | "Title" => 590 | { 591 | select_submission_elements_and_set_their_style( 592 | element_whose_text_is_to_be_checked, 593 | "titleline_span", 594 | "display: none;", 595 | ); 596 | 597 | count_of_filtered_entries.0 += 1; 598 | }, 599 | | "Link" => 600 | { 601 | select_submission_elements_and_set_their_style( 602 | element_whose_text_is_to_be_checked, 603 | "link", 604 | "display: none;", 605 | ); 606 | 607 | count_of_filtered_entries.1 += 1; 608 | }, 609 | | "Username" => 610 | { 611 | select_submission_elements_and_set_their_style( 612 | element_whose_text_is_to_be_checked, 613 | "hnuser", 614 | "display: none;", 615 | ); 616 | 617 | count_of_filtered_entries.2 += 1; 618 | }, 619 | | "Comment" => 620 | { 621 | select_submission_elements_and_set_their_style( 622 | element_whose_text_is_to_be_checked, 623 | "comment", 624 | "display: none;", 625 | ); 626 | 627 | count_of_filtered_entries.3 += 1; 628 | }, 629 | | &_ => 630 | { 631 | panic!("Not supported"); 632 | }, 633 | } 634 | } 635 | 636 | } 637 | } 638 | 639 | #[wasm_bindgen] 640 | pub fn parse_string_with_sidebar_and_insert_into_body() -> Result<(), JsValue> 641 | { 642 | let html_string = include_str!("sidebar.html"); 643 | 644 | let document = obtain_document(); 645 | 646 | let sidebar = document.create_element("div")?; 647 | sidebar.set_attribute("id", "sidebar")?; 648 | sidebar.set_class_name("visible"); 649 | sidebar.set_inner_html(html_string); 650 | 651 | let body = document 652 | .get_elements_by_tag_name("body") 653 | .get_with_index(0) 654 | .unwrap(); 655 | 656 | let body: Element = body.dyn_into()?; 657 | 658 | body.set_attribute("style", "display: grid; grid-template-columns: 3fr 1fr;") 659 | .unwrap(); 660 | 661 | body.append_child(&sidebar)?; 662 | 663 | let hnmain = document 664 | .get_elements_by_tag_name("table") 665 | .get_with_index(0) 666 | .unwrap(); 667 | 668 | hnmain.set_attribute("width", "100%")?; 669 | hnmain.set_attribute("margin", "0 auto")?; 670 | 671 | Ok(()) 672 | } 673 | 674 | #[wasm_bindgen] 675 | pub fn parse_string_with_css_and_insert_into_header() -> Result<(), JsValue> 676 | { 677 | let html_string = include_str!("style_for_sidebar.css"); 678 | 679 | let document = obtain_document(); 680 | 681 | let style_element = document 682 | .create_element("style") 683 | .expect("couldn't create style element"); 684 | 685 | style_element.set_inner_html(html_string); 686 | 687 | let head = document 688 | .get_elements_by_tag_name("head") 689 | .get_with_index(0) 690 | .unwrap(); 691 | 692 | head.append_child(&style_element)?; 693 | 694 | Ok(()) 695 | } 696 | 697 | pub fn load_items_from_locastorage_populate_sidebar_and_apply_filters() -> Result<(), JsValue> 698 | { 699 | 700 | 701 | let document = obtain_document(); 702 | 703 | remove_elements_with_class("substitute-placeholder-of-hidden-entry").unwrap(); 704 | 705 | let filter_summary_text_to_filter_target_text = 706 | obtain_filter_summary_text_to_filter_target_text_from_local_storage(); 707 | 708 | let body = document 709 | .get_elements_by_tag_name("body") 710 | .get_with_index(0) 711 | .unwrap(); 712 | 713 | let filter_summary_texts = filter_summary_text_to_filter_target_text 714 | .keys() 715 | .cloned() 716 | .collect::>(); 717 | 718 | let mut iteration = 0; 719 | 720 | let result = 721 | obtain_list_of_titleline_spans_as_rust_list_of_links_as_rust_list_of_hnusers_as_rust(); 722 | 723 | let ( 724 | list_of_titleline_spans_as_rust, 725 | list_of_links_as_rust, 726 | list_of_comments_as_rust, 727 | list_of_hnusers_as_rust, 728 | ) = 729 | match result 730 | { 731 | | Ok(value) => value, 732 | | Err(err) => return Err(err), 733 | }; 734 | 735 | let document = obtain_document(); 736 | 737 | let athing_id_to_submission_title = 738 | obtain_athing_id_to_submission_title_from_local_storage(); 739 | 740 | let body = document 741 | .get_elements_by_tag_name("body") 742 | .get_with_index(0) 743 | .unwrap(); 744 | 745 | let list_of_titlelines_to_softhide = athing_id_to_submission_title 746 | .values() 747 | .cloned() 748 | .collect::>(); 749 | 750 | let list_of_athing_ids_to_softhide = athing_id_to_submission_title 751 | .keys() 752 | .cloned() 753 | .collect::>(); 754 | 755 | let softhidden_entries_container = document 756 | .get_element_by_id("softhidden-entries-container") 757 | .unwrap(); 758 | 759 | let mut index_of_athing = 0; 760 | 761 | for text_of_sidebar_item in list_of_titlelines_to_softhide 762 | { 763 | let mut count_of_filtered_entries: &mut (u32, u32, u32, u32) = &mut (0, 0, 0, 0); 764 | 765 | for titleline_span in &list_of_titleline_spans_as_rust 766 | { 767 | hide_specific_item_of_a_specific_category( 768 | &"Title".to_owned(), 769 | &text_of_sidebar_item, 770 | &titleline_span, 771 | "Title", 772 | count_of_filtered_entries, 773 | ); 774 | } 775 | 776 | let document = obtain_document(); 777 | 778 | let softhidden_entry_in_sidebar_div = document 779 | .create_element("div") 780 | .unwrap(); 781 | 782 | let softhidden_entry_in_sidebar_div: Element = softhidden_entry_in_sidebar_div 783 | .dyn_into() 784 | .unwrap(); 785 | 786 | softhidden_entry_in_sidebar_div 787 | .set_attribute("class", "softhidden-entry") 788 | .unwrap(); 789 | 790 | softhidden_entry_in_sidebar_div 791 | .set_attribute("style", "position: relative; clear: both; float: left; box-sizing: border-box; width: 100%; border-radius: 4px; background-color: #f6f6ef; padding: 3px; margin-bottom: 6px; font-size:0.6rem" 792 | ); 793 | 794 | let athing_id = list_of_athing_ids_to_softhide[index_of_athing].clone(); 795 | 796 | softhidden_entry_in_sidebar_div.set_inner_html( 797 | &format!( 798 | r###" 799 |
800 |
{text_of_sidebar_item}
801 | "### 802 | ) 803 | ); 804 | 805 | softhidden_entry_in_sidebar_div.set_attribute("id", &format!("container-{athing_id}")); 806 | 807 | &softhidden_entries_container 808 | .append_child(&softhidden_entry_in_sidebar_div) 809 | .unwrap(); 810 | 811 | let softhidden_entries_container_clone = softhidden_entries_container.clone(); 812 | 813 | let exclude_softhidden_button = &document 814 | .get_element_by_id(&format!("softhidden-{athing_id}")) 815 | .unwrap(); 816 | 817 | let exclude_softhidden_button: Element = exclude_softhidden_button 818 | .clone() 819 | .dyn_into() 820 | .unwrap(); 821 | 822 | let listener = EventListener::new(&exclude_softhidden_button, "click", move |event: &Event| { 823 | 824 | let local_storage = web_sys::window() 825 | .unwrap() 826 | .local_storage() 827 | .unwrap() 828 | .unwrap(); 829 | 830 | let athing_id_to_submission_title_as_string = local_storage 831 | .get_item("softhidden_things") 832 | .unwrap() 833 | .unwrap_or_else( 834 | || { 835 | "{}".to_owned() 836 | }, 837 | ); 838 | 839 | let mut athing_id_to_submission_title: serde_json::Value = 840 | serde_json::from_str(&athing_id_to_submission_title_as_string).unwrap(); 841 | 842 | if let Some(obj) = athing_id_to_submission_title.as_object_mut() { 843 | obj.remove(&athing_id.to_string()); // This line modifies `value` in place 844 | } 845 | 846 | let amended_athing_id_to_submission_title_as_string = serde_json::to_string(&athing_id_to_submission_title); 847 | 848 | 849 | match amended_athing_id_to_submission_title_as_string { 850 | Ok(stringified_json) => { 851 | write_athing_id_to_submission_title_as_stringified_json_to_local_storage(&stringified_json); 852 | } 853 | Err(e) => { } 854 | } 855 | 856 | if let Some(submission_to_unhide_tr) = 857 | &document.get_element_by_id(&athing_id) 858 | { 859 | submission_to_unhide_tr.set_attribute("style", ""); 860 | 861 | if let Some(subtext_tr) = submission_to_unhide_tr.next_element_sibling() 862 | { 863 | subtext_tr.set_attribute("style", ""); 864 | 865 | 866 | if let Some(spacer_tr) = subtext_tr.next_element_sibling() 867 | { 868 | spacer_tr.set_attribute("style", ""); 869 | } 870 | } 871 | } 872 | 873 | let document_clone = obtain_document(); 874 | 875 | let softhidden_entries_container_here = document_clone 876 | .get_element_by_id("softhidden-entries-container") 877 | .unwrap(); 878 | 879 | 880 | 881 | let softhidden_entry_in_sidebar_div = document_clone 882 | .get_element_by_id(&format!("container-{athing_id}")) 883 | .unwrap(); 884 | 885 | 886 | softhidden_entries_container_here.remove_child(&softhidden_entry_in_sidebar_div); 887 | 888 | }); 889 | 890 | listener.forget(); 891 | 892 | 893 | index_of_athing += 1; 894 | 895 | } 896 | 897 | for text_of_sidebar_item in filter_summary_texts 898 | { 899 | iteration += 1; 900 | 901 | let document = obtain_document(); 902 | 903 | let filter_container_div = document 904 | .create_element("div") 905 | .unwrap(); 906 | 907 | let filter_container_div_as_el: Element = filter_container_div 908 | .dyn_into() 909 | .unwrap(); 910 | 911 | filter_container_div_as_el 912 | .set_attribute( 913 | "class", 914 | &format!("filter-item-wrapper"), 915 | ) 916 | .unwrap(); 917 | 918 | let filter_summary_div = document 919 | .create_element("div") 920 | .unwrap(); 921 | 922 | let filter_summary_div: Element = filter_summary_div 923 | .dyn_into() 924 | .unwrap(); 925 | 926 | filter_summary_div 927 | .set_attribute("class", "filter-item-keyword-wrapper") 928 | .unwrap(); 929 | 930 | filter_summary_div.set_inner_html( 931 | &format!( 932 | r###" 933 | 934 | 938 | "### 939 | ) 940 | ); 941 | 942 | filter_container_div_as_el 943 | .append_child(&filter_summary_div) 944 | .unwrap(); 945 | 946 | let radiobuttons_holder = format!( 947 | r###" 948 |
949 |
950 | 951 | 958 |
959 | 969 |
970 | 971 | 978 |
979 |
980 | 981 | 985 |
986 | "###); 987 | 988 | let inner_html_of_fitler_container_div_as_el = filter_container_div_as_el.inner_html(); 989 | 990 | filter_container_div_as_el.set_inner_html(&format!( 991 | "{inner_html_of_fitler_container_div_as_el} {radiobuttons_holder}" 992 | )); 993 | 994 | let sidebar_items_container = document 995 | .get_element_by_id("filter-items-container") 996 | .unwrap(); 997 | 998 | sidebar_items_container 999 | .append_child(&filter_container_div_as_el) 1000 | .unwrap(); 1001 | 1002 | let filter_target_text = filter_summary_text_to_filter_target_text 1003 | .get(&text_of_sidebar_item) 1004 | .unwrap(); 1005 | 1006 | // ---------------------------- 1007 | // ---------------------------- 1008 | // ---------------------------- 1009 | 1010 | let mut count_of_filtered_entries: &mut (u32, u32, u32, u32) = &mut (0, 0, 0, 0); 1011 | 1012 | for titleline_span in &list_of_titleline_spans_as_rust 1013 | { 1014 | hide_specific_item_of_a_specific_category( 1015 | filter_target_text, 1016 | &text_of_sidebar_item, 1017 | &titleline_span, 1018 | "Title", 1019 | count_of_filtered_entries, 1020 | ); 1021 | } 1022 | 1023 | // ---------------------------- 1024 | // ---------------------------- 1025 | // ---------------------------- 1026 | 1027 | for link in &list_of_links_as_rust 1028 | { 1029 | hide_specific_item_of_a_specific_category( 1030 | filter_target_text, 1031 | &text_of_sidebar_item, 1032 | &link, 1033 | "Link", 1034 | count_of_filtered_entries, 1035 | ); 1036 | } 1037 | 1038 | // ---------------------------- 1039 | // ---------------------------- 1040 | // ---------------------------- 1041 | 1042 | for username in &list_of_hnusers_as_rust 1043 | { 1044 | hide_specific_item_of_a_specific_category( 1045 | filter_target_text, 1046 | &text_of_sidebar_item, 1047 | &username, 1048 | "Username", 1049 | count_of_filtered_entries, 1050 | ); 1051 | } 1052 | 1053 | // ---------------------------- 1054 | // ---------------------------- 1055 | // ---------------------------- 1056 | 1057 | for comment in &list_of_comments_as_rust 1058 | { 1059 | hide_specific_item_of_a_specific_category( 1060 | filter_target_text, 1061 | &text_of_sidebar_item, 1062 | &comment, 1063 | "Comment", 1064 | count_of_filtered_entries, 1065 | ); 1066 | } 1067 | 1068 | let ( 1069 | count_of_filtered_entries_0, 1070 | count_of_filtered_entries_1, 1071 | count_of_filtered_entries_2, 1072 | count_of_filtered_entries_3, 1073 | ) = count_of_filtered_entries; 1074 | 1075 | let radiobuttons_holder_2 = format!( 1076 | r#" 1077 |
{count_of_filtered_entries_0}/{count_of_filtered_entries_1}/{count_of_filtered_entries_2}/{count_of_filtered_entries_3}
1078 | "#); 1079 | 1080 | let filter_items_container = document 1081 | .get_element_by_id("filter-items-container") 1082 | .unwrap(); 1083 | 1084 | let filter_container_div_as_el_with_proper_radiobutton_set = filter_items_container 1085 | .last_child() 1086 | .unwrap(); 1087 | 1088 | let filter_container_div_as_el_with_proper_radiobutton_set_as_el: Element = filter_container_div_as_el_with_proper_radiobutton_set 1089 | .dyn_into() 1090 | .unwrap(); 1091 | 1092 | let inner_html_of_fitler_container_div_as_el = filter_container_div_as_el_with_proper_radiobutton_set_as_el.inner_html(); 1093 | 1094 | filter_container_div_as_el_with_proper_radiobutton_set_as_el.set_inner_html(&format!( 1095 | "{inner_html_of_fitler_container_div_as_el} {radiobuttons_holder_2}" 1096 | )); 1097 | 1098 | 1099 | if filter_target_text.contains("Title") 1100 | { 1101 | set_specific_radiobutton_for_category_as_checked( 1102 | &filter_container_div_as_el_with_proper_radiobutton_set_as_el, 1103 | "title", 1104 | iteration, 1105 | ); 1106 | } 1107 | if filter_target_text.contains("Link") 1108 | { 1109 | set_specific_radiobutton_for_category_as_checked( 1110 | &filter_container_div_as_el_with_proper_radiobutton_set_as_el, 1111 | "link", 1112 | iteration, 1113 | ); 1114 | } 1115 | 1116 | if filter_target_text.contains("Username") 1117 | { 1118 | set_specific_radiobutton_for_category_as_checked( 1119 | &filter_container_div_as_el_with_proper_radiobutton_set_as_el, 1120 | "username", 1121 | iteration, 1122 | ); 1123 | } 1124 | 1125 | if filter_target_text.contains("Comment") 1126 | { 1127 | set_specific_radiobutton_for_category_as_checked( 1128 | &filter_container_div_as_el_with_proper_radiobutton_set_as_el, 1129 | "comment", 1130 | iteration, 1131 | ); 1132 | } 1133 | } 1134 | 1135 | Ok(()) 1136 | } 1137 | 1138 | pub fn set_specific_radiobutton_for_category_as_checked( 1139 | filter_container_div_as_el: &Element, 1140 | category_str: &str, 1141 | iteration: usize, 1142 | ) 1143 | { 1144 | let radiobutton_as_rust = filter_container_div_as_el 1145 | .query_selector(&format!("#cb-{category_str}-keywords-{iteration}")) 1146 | .unwrap(); 1147 | 1148 | let radiobutton_as_el: Element = radiobutton_as_rust 1149 | .unwrap() 1150 | .dyn_into() 1151 | .unwrap(); 1152 | 1153 | let radiobutton_as_radiobutton: HtmlInputElement = radiobutton_as_el 1154 | .dyn_into() 1155 | .unwrap(); 1156 | 1157 | radiobutton_as_radiobutton.set_checked(true); 1158 | } 1159 | -------------------------------------------------------------------------------- /src/getters.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | use web_sys::{ 3 | Document, 4 | Element, 5 | Event, 6 | *, 7 | }; 8 | use indexmap::IndexMap; 9 | use wasm_bindgen::JsCast; 10 | 11 | use gloo_console::log as gloo_log; 12 | 13 | #[wasm_bindgen] 14 | pub fn obtain_document() -> Document 15 | { 16 | let window = web_sys::window().expect("no global `window` exists"); 17 | 18 | window 19 | .document() 20 | .expect("should have a document on window") 21 | } 22 | 23 | pub fn obtain_filter_summary_text_to_filter_target_text_from_local_storage( 24 | ) -> IndexMap 25 | { 26 | let local_storage = web_sys::window() 27 | .unwrap() 28 | .local_storage() 29 | .unwrap() 30 | .unwrap(); 31 | 32 | let hackernew_userscript_data_as_string_option = local_storage 33 | .get_item("hackernews_userscript") 34 | .unwrap() 35 | .unwrap_or_else( 36 | || { 37 | let stringified_json_with_single_key = "{\"sidebar_items\": []}"; 38 | 39 | local_storage 40 | .set_item("hackernews_userscript", &stringified_json_with_single_key) 41 | .unwrap(); 42 | 43 | stringified_json_with_single_key.to_owned() 44 | }, 45 | ); 46 | 47 | let hackernew_userscript_data_as_string = hackernew_userscript_data_as_string_option; 48 | 49 | let hackernew_userscript_data_as_json: serde_json::Value = 50 | serde_json::from_str(&hackernew_userscript_data_as_string).unwrap(); 51 | 52 | let sidebar_items_as_json = hackernew_userscript_data_as_json 53 | .as_object() 54 | .unwrap() 55 | .get("sidebar_items") 56 | .unwrap(); 57 | 58 | let sidebar_items_as_vec_of_tuples_with_two_strings: Vec<(String, String)> = 59 | serde_json::from_value(sidebar_items_as_json.clone()).unwrap(); 60 | 61 | let sidebar_items_as_indexmap: IndexMap = 62 | sidebar_items_as_vec_of_tuples_with_two_strings 63 | .into_iter() 64 | .collect(); 65 | 66 | sidebar_items_as_indexmap 67 | } 68 | 69 | pub fn obtain_settings_from_local_storage( 70 | ) -> serde_json::Value 71 | { 72 | let local_storage = web_sys::window() 73 | .unwrap() 74 | .local_storage() 75 | .unwrap() 76 | .unwrap(); 77 | 78 | let settings_as_string_option = local_storage 79 | .get_item("settings") 80 | .unwrap() 81 | .unwrap_or_else( 82 | || { 83 | let stringified_json_with_single_key = "{}"; 84 | 85 | local_storage 86 | .set_item("settings", &stringified_json_with_single_key) 87 | .unwrap(); 88 | 89 | stringified_json_with_single_key.to_owned() 90 | }, 91 | ); 92 | 93 | let settings_as_string = settings_as_string_option; 94 | 95 | let settings_as_json: serde_json::Value = 96 | serde_json::from_str(&settings_as_string).unwrap(); 97 | 98 | settings_as_json 99 | } 100 | 101 | pub fn obtain_list_of_titleline_spans_as_rust_list_of_links_as_rust_list_of_hnusers_as_rust( 102 | ) -> Result<(Vec, Vec, Vec, Vec), JsValue> 103 | { 104 | let document = obtain_document(); 105 | 106 | let body = document 107 | .get_elements_by_tag_name("body") 108 | .get_with_index(0) 109 | .ok_or_else(|| JsValue::from_str("No body element found"))?; 110 | 111 | let list_of_titleline_spans_as_js = body.query_selector_all(".athing .titleline")?; 112 | 113 | let list_of_titleline_spans_as_rust: Vec = (0..list_of_titleline_spans_as_js.length()) 114 | .map(|index| { 115 | list_of_titleline_spans_as_js 116 | .item(index) 117 | .ok_or_else(|| JsValue::from_str("Missing element at index")) 118 | .and_then(|item| { 119 | item.dyn_into::() 120 | .map_err(|_| JsValue::from_str("Failed to cast to Element")) 121 | }) 122 | }) 123 | .collect::>()?; 124 | 125 | let list_of_links_as_rust = list_of_titleline_spans_as_rust 126 | .iter() 127 | .map(|titleline_span| { 128 | titleline_span 129 | .query_selector("a") 130 | .map_err(|_| JsValue::from_str("Failed to query_selector for 'a'"))? 131 | .ok_or_else(|| JsValue::from_str("No 'a' element found")) 132 | .and_then(|link| { 133 | link.dyn_into::() 134 | .map_err(|_| JsValue::from_str("Failed to cast to Element")) 135 | }) 136 | }) 137 | .collect::>()?; 138 | 139 | let list_of_hnusers_as_js = body.query_selector_all(".hnuser")?; 140 | 141 | let list_of_hnusers_as_rust: Vec = (0..list_of_hnusers_as_js.length()) 142 | .map(|index| { 143 | list_of_hnusers_as_js 144 | .item(index) 145 | .ok_or_else(|| JsValue::from_str("Missing hnuser at index")) 146 | .and_then(|item| { 147 | item.dyn_into::() 148 | .map_err(|_| JsValue::from_str("Failed to cast to Element")) 149 | }) 150 | }) 151 | .collect::>()?; 152 | 153 | let list_of_comments_as_js = body.query_selector_all(".commtext")?; 154 | 155 | let list_of_comments_as_rust: Vec = (0..list_of_comments_as_js.length()) 156 | .map(|index| { 157 | list_of_comments_as_js 158 | .item(index) 159 | .ok_or_else(|| JsValue::from_str("Missing comment at index")) 160 | .and_then(|item| { 161 | item.dyn_into::() 162 | .map_err(|_| JsValue::from_str("Failed to cast to Element")) 163 | }) 164 | }) 165 | .collect::>()?; 166 | 167 | Ok( 168 | ( 169 | list_of_titleline_spans_as_rust, 170 | list_of_links_as_rust, 171 | list_of_comments_as_rust, 172 | list_of_hnusers_as_rust, 173 | ) 174 | ) 175 | } 176 | 177 | fn value_to_indexmap(value: &serde_json::Value) -> Option> { 178 | let mut map = IndexMap::new(); 179 | 180 | if let serde_json::Value::Object(obj) = value { 181 | for (key, val) in obj { 182 | let key_str = key.to_string(); 183 | let val_str = val.as_str().unwrap_or("").to_string(); 184 | map.insert(key_str, val_str); 185 | } 186 | Some(map) 187 | } else { 188 | None 189 | } 190 | } 191 | 192 | 193 | pub fn obtain_athing_id_to_submission_title_from_local_storage( 194 | ) -> IndexMap 195 | { 196 | let local_storage = web_sys::window() 197 | .unwrap() 198 | .local_storage() 199 | .unwrap() 200 | .unwrap(); 201 | 202 | let athing_id_to_submission_title_as_string_option = local_storage 203 | .get_item("softhidden_things") 204 | .unwrap() 205 | .unwrap_or_else( 206 | || { 207 | let stringified_json_with_single_key = "{}"; 208 | 209 | local_storage 210 | .set_item("softhidden_things", &stringified_json_with_single_key) 211 | .unwrap(); 212 | 213 | stringified_json_with_single_key.to_owned() 214 | }, 215 | ); 216 | 217 | let athing_id_to_submission_title_as_string = athing_id_to_submission_title_as_string_option; 218 | 219 | let athing_id_to_submission_title_as_json: serde_json::Value = 220 | serde_json::from_str(&athing_id_to_submission_title_as_string).unwrap(); 221 | 222 | let athing_id_to_submission_title_indexmap: IndexMap = 223 | value_to_indexmap(&athing_id_to_submission_title_as_json).unwrap(); 224 | 225 | athing_id_to_submission_title_indexmap 226 | } 227 | 228 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use base64; 2 | use wasm_bindgen::prelude::*; 3 | use web_sys::*; 4 | #[macro_use] 5 | mod util; 6 | use std::iter::Iterator; 7 | use std::collections::HashMap; 8 | 9 | use indexmap::IndexMap; 10 | use serde_json::{json, Value, to_string}; 11 | use regex::Regex; 12 | use wasm_bindgen::JsCast; 13 | use gloo_console::log as gloo_log; 14 | use gloo::events::EventListener; 15 | use web_sys::{ 16 | Document, 17 | Element, 18 | Event, 19 | HtmlButtonElement, 20 | KeyboardEvent, 21 | HtmlInputElement, 22 | HtmlImageElement, 23 | }; 24 | 25 | mod getters; 26 | use getters::{ 27 | obtain_document, 28 | obtain_filter_summary_text_to_filter_target_text_from_local_storage, 29 | obtain_list_of_titleline_spans_as_rust_list_of_links_as_rust_list_of_hnusers_as_rust, 30 | }; 31 | 32 | mod setters; 33 | use setters::{ 34 | write_sidebar_items_as_stringified_json_to_local_storage, 35 | write_athing_id_to_submission_title_as_stringified_json_to_local_storage, 36 | }; 37 | 38 | mod dom_manipulation; 39 | use dom_manipulation::{ 40 | hide_specific_item_of_a_specific_category, 41 | load_items_from_locastorage_populate_sidebar_and_apply_filters, 42 | parse_string_with_css_and_insert_into_header, 43 | parse_string_with_sidebar_and_insert_into_body, 44 | remove_all_children_of_filter_items_container, 45 | replace_center_tag_with_div, 46 | select_submission_elements_and_set_their_style, 47 | set_specific_radiobutton_for_category_as_checked, 48 | }; 49 | 50 | mod pure; 51 | use pure::stringify_filter_summary_text_to_filter_target_text_from_map; 52 | 53 | #[wasm_bindgen] 54 | pub fn register_sidebar_item_event_listener() -> Result<(), JsValue> 55 | { 56 | let document = obtain_document(); 57 | 58 | let body = document 59 | .get_elements_by_tag_name("body") 60 | .get_with_index(0) 61 | .unwrap(); 62 | 63 | let result = 64 | obtain_list_of_titleline_spans_as_rust_list_of_links_as_rust_list_of_hnusers_as_rust(); 65 | let ( 66 | list_of_titleline_spans_as_rust, 67 | list_of_links_as_rust, 68 | list_of_comments_as_rust, 69 | list_of_hnusers_as_rust, 70 | ) = 71 | match result 72 | { 73 | | Ok(value) => value, 74 | | Err(err) => return Err(err), 75 | }; 76 | 77 | let sidebar_container = &document 78 | .get_element_by_id("filter-items-container") 79 | .unwrap(); 80 | 81 | let sidebar_container: Element = sidebar_container 82 | .clone() 83 | .dyn_into()?; 84 | 85 | let document_ref = document; 86 | 87 | let list_of_titleline_spans_as_rust_ref = list_of_titleline_spans_as_rust; 88 | 89 | let listener = EventListener::new(&sidebar_container, "click", move |event: &Event| { 90 | let ic_remove_or_filter_selector_wrapper = event 91 | .target() 92 | .unwrap(); 93 | 94 | if let Ok(ic_remove_or_filter_selector_wrapper) = 95 | ic_remove_or_filter_selector_wrapper.dyn_into::() 96 | { 97 | match ic_remove_or_filter_selector_wrapper 98 | .class_name() 99 | .as_str() 100 | { 101 | | "ic-remove" => 102 | { 103 | let filter_item_wrapper = ic_remove_or_filter_selector_wrapper 104 | .parent_node() 105 | .unwrap(); 106 | 107 | let filter_item_keyword_wrapper = filter_item_wrapper 108 | .first_child() 109 | .unwrap(); 110 | 111 | let filter_item_keyword_wrapper_as_el = filter_item_keyword_wrapper 112 | .dyn_into::() 113 | .unwrap(); 114 | 115 | let filter_item_keyword = filter_item_keyword_wrapper_as_el 116 | .query_selector(".filter-item-keyword") 117 | .unwrap(); 118 | 119 | let filter_item_keyword_as_el: Element = filter_item_keyword 120 | .unwrap() 121 | .dyn_into() 122 | .unwrap(); 123 | 124 | let text_of_filter_item_keyword = filter_item_keyword_as_el 125 | .text_content() 126 | .unwrap(); 127 | 128 | let text_of_filter_item_keyword = text_of_filter_item_keyword 129 | .trim() 130 | .to_owned(); 131 | 132 | let mut filter_summary_text_to_filter_target = 133 | obtain_filter_summary_text_to_filter_target_text_from_local_storage(); 134 | 135 | filter_summary_text_to_filter_target.remove(&text_of_filter_item_keyword); 136 | 137 | let filter_summary_text_to_filter_target_as_stringified_json = 138 | stringify_filter_summary_text_to_filter_target_text_from_map( 139 | &filter_summary_text_to_filter_target, 140 | ); 141 | 142 | write_sidebar_items_as_stringified_json_to_local_storage( 143 | &filter_summary_text_to_filter_target_as_stringified_json, 144 | ); 145 | }, 146 | 147 | | "filter-selector-wrapper" => 148 | { 149 | let filter_item_wrapper_as_node = ic_remove_or_filter_selector_wrapper 150 | .parent_node() 151 | .unwrap(); 152 | 153 | let filter_item_keyword_wrapper = filter_item_wrapper_as_node 154 | .first_child() 155 | .unwrap(); 156 | 157 | let filter_item_keyword_wrapper_as_el = filter_item_keyword_wrapper 158 | .dyn_into::() 159 | .unwrap(); 160 | 161 | let filter_item_keyword = filter_item_keyword_wrapper_as_el 162 | .query_selector(".filter-item-keyword") 163 | .unwrap(); 164 | 165 | let filter_item_keyword_as_el: Element = filter_item_keyword 166 | .unwrap() 167 | .dyn_into() 168 | .unwrap(); 169 | 170 | let text_of_filter_item_keyword = filter_item_keyword_as_el 171 | .text_content() 172 | .unwrap(); 173 | 174 | let text_of_filter_item_keyword = text_of_filter_item_keyword 175 | .trim() 176 | .to_owned(); 177 | 178 | let filter_item_wrapper: Element = filter_item_wrapper_as_node 179 | .clone() 180 | .dyn_into() 181 | .unwrap(); 182 | 183 | let checkbox_container_div_children = filter_item_wrapper 184 | .query_selector_all("input[type='checkbox']") 185 | .unwrap(); 186 | 187 | let checkbox_container_div_children_length = 188 | checkbox_container_div_children.length(); 189 | 190 | let checkbox_container_div_children_as_vec_of_els: Vec = (1 191 | ..checkbox_container_div_children_length) 192 | .map(|index| { 193 | checkbox_container_div_children 194 | .item(index) 195 | .unwrap() 196 | }) 197 | .map(|item| { 198 | item.dyn_into::() 199 | .unwrap() 200 | }) 201 | .collect(); 202 | 203 | let checkbox_container_div_children_as_vec_of_els_length = 204 | checkbox_container_div_children_as_vec_of_els.len(); 205 | 206 | let checkbox_id_to_checkbox_toggle_state = (0 207 | ..checkbox_container_div_children_as_vec_of_els_length) 208 | .map(|index| { 209 | let checkbox_id = checkbox_container_div_children_as_vec_of_els 210 | .get(index) 211 | .unwrap() 212 | .get_attribute("id") 213 | .unwrap(); 214 | 215 | let checkbox_toggle_state = 216 | checkbox_container_div_children_as_vec_of_els 217 | .get(index) 218 | .unwrap() 219 | .clone() 220 | .dyn_into::() 221 | .unwrap() 222 | .checked(); 223 | 224 | (checkbox_id, checkbox_toggle_state) 225 | }) 226 | .collect::>(); 227 | 228 | let checkbox_id_to_checkbox_toggle_state_as_stringified_json = 229 | serde_json::to_string(&checkbox_id_to_checkbox_toggle_state).unwrap(); 230 | 231 | let some_checkboxes_are_true = checkbox_id_to_checkbox_toggle_state 232 | .values() 233 | .any(|checkbox_toggle_state| *checkbox_toggle_state); 234 | 235 | let vector_of_all_checkbox_ids_that_are_true = 236 | checkbox_id_to_checkbox_toggle_state 237 | .iter() 238 | .filter(|(_checkbox_id, checkbox_toggle_state)| **checkbox_toggle_state) 239 | .map(|(checkbox_id, _checkbox_toggle_state)| checkbox_id) 240 | .collect::>(); 241 | 242 | let vector_of_all_checkbox_ids_that_are_true = 243 | vector_of_all_checkbox_ids_that_are_true 244 | .iter() 245 | .map(|s| { 246 | let mut s = s.clone(); 247 | let mut s = s 248 | .split("-") 249 | .collect::>(); 250 | 251 | let mut s = s 252 | .iter_mut() 253 | .map(|s| { 254 | let mut s = s 255 | .chars() 256 | .collect::>(); 257 | s[0] = s[0].to_ascii_uppercase(); 258 | s.iter() 259 | .collect::() 260 | }) 261 | .collect::>(); 262 | 263 | let kek = s 264 | .get(1) 265 | .unwrap() 266 | .to_string(); 267 | kek 268 | 269 | }) 270 | .collect::>(); 271 | 272 | let vector_of_all_checkbox_ids_that_are_true_as_joined_string = 273 | vector_of_all_checkbox_ids_that_are_true 274 | .iter() 275 | .map(|s| s.to_string()) 276 | .collect::>() 277 | .join(","); 278 | 279 | let mut filter_summary_text_to_filter_target_text_ = 280 | obtain_filter_summary_text_to_filter_target_text_from_local_storage(); 281 | 282 | filter_summary_text_to_filter_target_text_.insert( 283 | text_of_filter_item_keyword, 284 | vector_of_all_checkbox_ids_that_are_true_as_joined_string, 285 | ); 286 | 287 | let filter_summary_text_to_filter_target_text_as_stringified_json = 288 | stringify_filter_summary_text_to_filter_target_text_from_map( 289 | &filter_summary_text_to_filter_target_text_, 290 | ); 291 | 292 | write_sidebar_items_as_stringified_json_to_local_storage( 293 | &filter_summary_text_to_filter_target_text_as_stringified_json, 294 | ); 295 | 296 | event.stop_propagation(); 297 | }, 298 | 299 | | _ => 300 | { 301 | let mut parent = ic_remove_or_filter_selector_wrapper.parent_element(); 302 | 303 | let mut found = false; 304 | 305 | let mut parent_clone = parent.clone(); 306 | 307 | while let Some(element) = parent 308 | { 309 | if let Some(class_string) = element.get_attribute("class") 310 | { 311 | let class_list: Vec<&str> = class_string 312 | .split_whitespace() 313 | .collect(); 314 | 315 | if class_list.contains(&"filter-selector-wrapper") 316 | { 317 | found = true; 318 | 319 | event.stop_propagation(); 320 | 321 | break; 322 | } 323 | } 324 | 325 | parent = element.parent_element(); 326 | 327 | parent_clone = parent.clone() 328 | } 329 | 330 | if found 331 | { 332 | let filter_item_wrapper_as_node = parent_clone 333 | .expect("no parent found") 334 | .parent_node() 335 | .unwrap(); 336 | 337 | let filter_item_wrapper: Element = filter_item_wrapper_as_node 338 | .clone() 339 | .dyn_into() 340 | .unwrap(); 341 | 342 | let filter_item_keyword_wrapper = filter_item_wrapper 343 | .first_child() 344 | .unwrap(); 345 | 346 | let filter_item_keyword_wrapper_as_el = filter_item_keyword_wrapper 347 | .dyn_into::() 348 | .unwrap(); 349 | 350 | let filter_item_keyword = filter_item_keyword_wrapper_as_el 351 | .query_selector(".filter-item-keyword") 352 | .unwrap(); 353 | 354 | let filter_item_keyword_as_el: Element = filter_item_keyword 355 | .unwrap() 356 | .dyn_into() 357 | .unwrap(); 358 | 359 | let text_of_filter_item_keyword = filter_item_keyword_as_el 360 | .text_content() 361 | .unwrap(); 362 | 363 | let text_of_filter_item_keyword = text_of_filter_item_keyword 364 | .trim() 365 | .to_owned(); 366 | 367 | let checkbox_container_div_children = filter_item_wrapper 368 | .query_selector_all("input[type='checkbox']") 369 | .unwrap(); 370 | 371 | let checkbox_container_div_children_length = 372 | checkbox_container_div_children.length(); 373 | 374 | let checkbox_container_div_children_as_vec_of_els: Vec = (1 375 | ..checkbox_container_div_children_length) 376 | .map(|index| { 377 | checkbox_container_div_children 378 | .item(index) 379 | .unwrap() 380 | }) 381 | .map(|item| { 382 | item.dyn_into::() 383 | .unwrap() 384 | }) 385 | .collect(); 386 | 387 | let checkbox_container_div_children_as_vec_of_els_length = 388 | checkbox_container_div_children_as_vec_of_els.len(); 389 | 390 | let checkbox_id_to_checkbox_toggle_state = (0 391 | ..checkbox_container_div_children_as_vec_of_els_length) 392 | .map(|index| { 393 | let checkbox_id = checkbox_container_div_children_as_vec_of_els 394 | .get(index) 395 | .unwrap() 396 | .get_attribute("id") 397 | .unwrap(); 398 | 399 | let checkbox_toggle_state = 400 | checkbox_container_div_children_as_vec_of_els 401 | .get(index) 402 | .unwrap() 403 | .clone() 404 | .dyn_into::() 405 | .unwrap() 406 | .checked(); 407 | 408 | (checkbox_id, checkbox_toggle_state) 409 | }) 410 | .collect::>(); 411 | 412 | let checkbox_id_to_checkbox_toggle_state_as_stringified_json = 413 | serde_json::to_string(&checkbox_id_to_checkbox_toggle_state).unwrap(); 414 | 415 | let some_checkboxes_are_true = checkbox_id_to_checkbox_toggle_state 416 | .values() 417 | .any(|checkbox_toggle_state| *checkbox_toggle_state); 418 | 419 | let vector_of_all_checkbox_ids_that_are_true = 420 | checkbox_id_to_checkbox_toggle_state 421 | .iter() 422 | .filter(|(_checkbox_id, checkbox_toggle_state)| { 423 | **checkbox_toggle_state 424 | }) 425 | .map(|(checkbox_id, _checkbox_toggle_state)| checkbox_id) 426 | .collect::>(); 427 | 428 | let vector_of_all_checkbox_ids_that_are_true = 429 | vector_of_all_checkbox_ids_that_are_true 430 | .iter() 431 | .map(|s| { 432 | let mut s = s.clone(); 433 | let mut s = s 434 | .split("-") 435 | .collect::>(); 436 | 437 | let mut s = s 438 | .iter_mut() 439 | .map(|s| { 440 | let mut s = s 441 | .chars() 442 | .collect::>(); 443 | s[0] = s[0].to_ascii_uppercase(); 444 | s.iter() 445 | .collect::() 446 | }) 447 | .collect::>(); 448 | 449 | let kek = s 450 | .get(1) 451 | .unwrap() 452 | .to_string(); 453 | kek 454 | }) 455 | .collect::>(); 456 | 457 | let vector_of_all_checkbox_ids_that_are_true_as_joined_string = 458 | vector_of_all_checkbox_ids_that_are_true 459 | .iter() 460 | .map(|s| s.to_string()) 461 | .collect::>() 462 | .join(","); 463 | 464 | let mut filter_summary_text_to_filter_target_text_ = 465 | obtain_filter_summary_text_to_filter_target_text_from_local_storage(); 466 | 467 | filter_summary_text_to_filter_target_text_.insert( 468 | text_of_filter_item_keyword, 469 | vector_of_all_checkbox_ids_that_are_true_as_joined_string, 470 | ); 471 | 472 | let filter_summary_text_to_filter_target_text_as_stringified_json = 473 | stringify_filter_summary_text_to_filter_target_text_from_map( 474 | &filter_summary_text_to_filter_target_text_, 475 | ); 476 | 477 | write_sidebar_items_as_stringified_json_to_local_storage( 478 | &filter_summary_text_to_filter_target_text_as_stringified_json, 479 | ); 480 | } 481 | }, 482 | } 483 | } 484 | 485 | match obtain_list_of_titleline_spans_as_rust_list_of_links_as_rust_list_of_hnusers_as_rust() 486 | { 487 | | Ok( 488 | ( 489 | list_of_titleline_spans_as_rust_, 490 | list_of_links_as_rust_, 491 | list_of_comments_as_rust_, 492 | list_of_hnusers_as_rust_, 493 | ) 494 | ) => 495 | { 496 | for titleline_span in &list_of_titleline_spans_as_rust_ 497 | { 498 | select_submission_elements_and_set_their_style( 499 | &titleline_span, 500 | "titleline_span", 501 | "", 502 | ); 503 | } 504 | 505 | for link in &list_of_links_as_rust_ 506 | { 507 | select_submission_elements_and_set_their_style(&link, "link", ""); 508 | } 509 | 510 | for comment in &list_of_comments_as_rust_ 511 | { 512 | select_submission_elements_and_set_their_style(&comment, "comment", ""); 513 | } 514 | 515 | for username in &list_of_hnusers_as_rust_ 516 | { 517 | select_submission_elements_and_set_their_style(&username, "hnuser", ""); 518 | } 519 | }, 520 | | Err(err) => 521 | { 522 | console::error_1(&format!("An error occurred: {:?}", err).into()); 523 | }, 524 | } 525 | 526 | let window = web_sys::window().expect("should have a Window"); 527 | 528 | let closure = Closure::once(move || { 529 | remove_all_children_of_filter_items_container(); 530 | load_items_from_locastorage_populate_sidebar_and_apply_filters(); 531 | }); 532 | 533 | window 534 | .request_animation_frame( 535 | closure 536 | .as_ref() 537 | .unchecked_ref(), 538 | ) 539 | .expect("should register `requestAnimationFrame` OK"); 540 | 541 | closure.forget(); 542 | }); 543 | 544 | listener.forget(); 545 | 546 | Ok(()) 547 | } 548 | 549 | use std::collections::HashSet; 550 | 551 | #[wasm_bindgen] 552 | pub fn register_filter_out_submissions_event_listener() -> Result<(), JsValue> 553 | { 554 | let document = obtain_document(); 555 | 556 | let filter_button = &document 557 | .get_element_by_id("filter-button") 558 | .unwrap(); 559 | 560 | let filter_button: Element = filter_button 561 | .clone() 562 | .dyn_into() 563 | .unwrap(); 564 | 565 | let result = 566 | obtain_list_of_titleline_spans_as_rust_list_of_links_as_rust_list_of_hnusers_as_rust(); 567 | 568 | let ( 569 | list_of_titleline_spans_as_rust, 570 | list_of_links_as_rust, 571 | list_of_comments_as_rust, 572 | list_of_hnusers_as_rust, 573 | ) = 574 | match result 575 | { 576 | | Ok(value) => value, 577 | | Err(err) => return Err(err), 578 | }; 579 | 580 | let document_ref = document; 581 | 582 | let listener = EventListener::new(&filter_button, "click", move |_event: &Event| { 583 | let document = document_ref.clone(); 584 | 585 | let input = document 586 | .get_element_by_id("sidebar-filter-input") 587 | .unwrap(); 588 | 589 | let input: Element = input 590 | .dyn_into() 591 | .unwrap(); 592 | 593 | let input_value = input 594 | .dyn_into::() 595 | .unwrap() 596 | .value(); 597 | 598 | let input_value_as_js = JsValue::from(&input_value); 599 | 600 | let object_str: String = input_value_as_js 601 | .as_string() 602 | .unwrap(); 603 | 604 | let object_str_as_regex = Regex::new(&object_str); 605 | 606 | let object_str_as_regex = match object_str_as_regex 607 | { 608 | | Ok(regex) => regex, 609 | | Err(_) => 610 | { 611 | Regex::new("invalid_regex").unwrap() 612 | }, 613 | }; 614 | 615 | let sidebar_items_container = &document 616 | .get_element_by_id("filter-items-container") 617 | .unwrap(); 618 | 619 | let sidebar_items_container: Element = sidebar_items_container 620 | .clone() 621 | .dyn_into() 622 | .unwrap(); 623 | 624 | let checkbox_container_div = document 625 | .get_element_by_id("checkbox-container") 626 | .unwrap(); 627 | 628 | let checkbox_container_div: Element = checkbox_container_div 629 | .dyn_into() 630 | .unwrap(); 631 | 632 | let checkbox_container_div_children = checkbox_container_div 633 | .query_selector_all("input[type='checkbox']") 634 | .unwrap(); 635 | 636 | let checkbox_container_div_children_length = checkbox_container_div_children.length(); 637 | 638 | let checkbox_container_div_children_as_vec_of_els: Vec = (0 639 | ..checkbox_container_div_children_length) 640 | .map(|index| { 641 | checkbox_container_div_children 642 | .item(index) 643 | .unwrap() 644 | }) 645 | .map(|item| { 646 | item.dyn_into::() 647 | .unwrap() 648 | }) 649 | .collect(); 650 | 651 | let checkbox_container_div_children_as_vec_of_els_as_string = 652 | checkbox_container_div_children_as_vec_of_els 653 | .iter() 654 | .map(|el| el.outer_html()) 655 | .collect::>() 656 | .join("\n"); 657 | 658 | let checkbox_container_div_children_as_vec_of_els_length = 659 | checkbox_container_div_children_as_vec_of_els.len(); 660 | 661 | let checkbox_id_to_checkbox_toggle_state = (0 662 | ..checkbox_container_div_children_as_vec_of_els_length) 663 | .map(|index| { 664 | let checkbox_id = checkbox_container_div_children_as_vec_of_els 665 | .get(index) 666 | .unwrap() 667 | .get_attribute("id") 668 | .unwrap(); 669 | 670 | let checkbox_toggle_state = checkbox_container_div_children_as_vec_of_els 671 | .get(index) 672 | .unwrap() 673 | .clone() 674 | .dyn_into::() 675 | .unwrap() 676 | .checked(); 677 | 678 | (checkbox_id, checkbox_toggle_state) 679 | }) 680 | .collect::>(); 681 | 682 | let checkbox_id_to_checkbox_toggle_state_as_stringified_json = 683 | serde_json::to_string(&checkbox_id_to_checkbox_toggle_state).unwrap(); 684 | 685 | let vector_of_all_checkbox_ids_that_are_true = checkbox_id_to_checkbox_toggle_state 686 | .iter() 687 | .filter(|(_checkbox_id, checkbox_toggle_state)| **checkbox_toggle_state) 688 | .map(|(checkbox_id, _checkbox_toggle_state)| checkbox_id) 689 | .collect::>(); 690 | 691 | let vector_of_all_checkbox_ids_that_are_true = vector_of_all_checkbox_ids_that_are_true 692 | .iter() 693 | .map(|s| { 694 | let mut s = s.clone(); 695 | let mut s = s 696 | .split("-") 697 | .collect::>(); 698 | 699 | let mut s = s 700 | .iter_mut() 701 | .map(|s| { 702 | let mut s = s 703 | .chars() 704 | .collect::>(); 705 | s[0] = s[0].to_ascii_uppercase(); 706 | s.iter() 707 | .collect::() 708 | }) 709 | .collect::>(); 710 | 711 | let kek = s 712 | .get(0) 713 | .unwrap() 714 | .to_string(); 715 | 716 | kek 717 | }) 718 | .collect::>(); 719 | 720 | let vector_of_all_checkbox_ids_that_are_true_as_joined_string = 721 | vector_of_all_checkbox_ids_that_are_true 722 | .iter() 723 | .map(|s| s.to_string()) 724 | .collect::>() 725 | .join(","); 726 | 727 | let mut object_str_to_checkbox_id = IndexMap::new(); 728 | 729 | object_str_to_checkbox_id.insert(&object_str, vector_of_all_checkbox_ids_that_are_true.clone()); 730 | 731 | let filter_summary_texts = object_str_to_checkbox_id 732 | .keys() 733 | .collect::>(); 734 | 735 | let filter_target_texts = object_str_to_checkbox_id 736 | .values() 737 | .collect::>(); 738 | 739 | let object_str_to_checkbox_id_as_vec_of_tuples = filter_summary_texts 740 | .iter() 741 | .zip(filter_target_texts.iter()) 742 | .collect::>(); 743 | 744 | let object_str_to_checkbox_id_as_json = json!( 745 | { 746 | "sidebar_items": object_str_to_checkbox_id_as_vec_of_tuples 747 | } 748 | ); 749 | 750 | let object_str_to_checkbox_id_as_stringified_json = 751 | object_str_to_checkbox_id_as_json.to_string(); 752 | 753 | let some_checkboxes_are_true = checkbox_id_to_checkbox_toggle_state 754 | .values() 755 | .any(|checkbox_toggle_state| *checkbox_toggle_state); 756 | 757 | if some_checkboxes_are_true 758 | { 759 | let mut filter_summary_text_to_filter_target_text_from_local_storage = 760 | obtain_filter_summary_text_to_filter_target_text_from_local_storage(); 761 | 762 | let object_str_key_is_already_present_in_filter_summary_text_to_filter_target_text_from_local_storage = 763 | filter_summary_text_to_filter_target_text_from_local_storage 764 | .contains_key(&object_str); 765 | 766 | if object_str_key_is_already_present_in_filter_summary_text_to_filter_target_text_from_local_storage 767 | { 768 | let string_of_targets = 769 | filter_summary_text_to_filter_target_text_from_local_storage 770 | .get(&object_str) 771 | .unwrap() 772 | .clone(); 773 | 774 | let vector_of_targets = string_of_targets 775 | .split(",") 776 | .collect::>(); 777 | 778 | let vector_of_all_checkbox_ids_that_are_true_: Vec<&str> = vector_of_all_checkbox_ids_that_are_true 779 | .iter() 780 | .map(|s| s.as_str()) 781 | .collect(); 782 | 783 | let concatenated_vec: Vec<&str> = vector_of_targets 784 | .iter() 785 | .chain(vector_of_all_checkbox_ids_that_are_true_.iter()) 786 | .cloned() 787 | .collect(); 788 | 789 | let hashset_of_targets = concatenated_vec 790 | .iter() 791 | .collect::>(); 792 | 793 | let joined_string_of_targets = hashset_of_targets 794 | .iter() 795 | .map(|s| s.to_string()) 796 | .collect::>() 797 | .join(","); 798 | 799 | filter_summary_text_to_filter_target_text_from_local_storage.insert( 800 | object_str, 801 | joined_string_of_targets, 802 | ); 803 | 804 | } 805 | else 806 | { 807 | 808 | filter_summary_text_to_filter_target_text_from_local_storage.insert( 809 | object_str, 810 | vector_of_all_checkbox_ids_that_are_true_as_joined_string, 811 | ); 812 | }; 813 | 814 | let filter_summary_text_to_filter_target_text_from_local_storage_as_stringified_json = 815 | stringify_filter_summary_text_to_filter_target_text_from_map( 816 | &filter_summary_text_to_filter_target_text_from_local_storage, 817 | ); 818 | 819 | write_sidebar_items_as_stringified_json_to_local_storage( 820 | &filter_summary_text_to_filter_target_text_from_local_storage_as_stringified_json, 821 | ); 822 | } 823 | 824 | remove_all_children_of_filter_items_container(); 825 | 826 | let input = document 827 | .get_element_by_id("sidebar-filter-input") 828 | .unwrap(); 829 | 830 | let input: Element = input 831 | .dyn_into() 832 | .unwrap(); 833 | 834 | let input_element = input.dyn_into::().unwrap(); 835 | 836 | if some_checkboxes_are_true { 837 | input_element.set_value(""); 838 | }; 839 | 840 | _ = load_items_from_locastorage_populate_sidebar_and_apply_filters(); 841 | }); 842 | 843 | listener.forget(); 844 | 845 | Ok(()) 846 | } 847 | 848 | #[wasm_bindgen] 849 | pub fn restore_active_tab_from_previous_session_and_register_filter_tab_switching_event_listener() -> Result<(), JsValue> 850 | { 851 | let document = obtain_document(); 852 | 853 | let list_of_tab_buttons = document 854 | .query_selector_all(".tab") 855 | .unwrap(); 856 | 857 | let list_of_tab_buttons_as_rust: Vec = (0..list_of_tab_buttons.length()) 858 | .map(|index| { 859 | list_of_tab_buttons 860 | .item(index) 861 | .ok_or_else(|| JsValue::from_str("Missing tab at index")) 862 | .and_then(|item| { 863 | item.dyn_into::() 864 | .map_err(|_| JsValue::from_str("Failed to cast to HtmlElement")) 865 | }) 866 | }) 867 | .collect::>() 868 | .unwrap(); 869 | 870 | let list_of_tab_contents = document 871 | .query_selector_all(".tab-content") 872 | .unwrap(); 873 | 874 | let list_of_tab_contents_as_rust: Vec = (0..list_of_tab_contents.length()) 875 | .map(|index| { 876 | list_of_tab_contents 877 | .item(index) 878 | .ok_or_else(|| JsValue::from_str("Missing tab content at index")) 879 | .and_then(|item| { 880 | item.dyn_into::() 881 | .map_err(|_| JsValue::from_str("Failed to cast to HtmlElement")) 882 | }) 883 | }) 884 | .collect::>() 885 | .unwrap(); 886 | 887 | let local_storage = web_sys::window() 888 | .unwrap() 889 | .local_storage() 890 | .unwrap() 891 | .unwrap(); 892 | 893 | let name_of_active_tab = local_storage 894 | .get_item("name_of_active_tab") 895 | .unwrap() 896 | .unwrap_or_else( 897 | || { 898 | "settings-tab".to_owned() 899 | }, 900 | ); 901 | 902 | let list_of_tab_contents_as_rust_clone_1 = list_of_tab_contents_as_rust.clone(); 903 | 904 | for tab_content in &list_of_tab_contents_as_rust_clone_1 { 905 | 906 | let tab_content_as_el = tab_content.clone(); 907 | 908 | let tab_content_class_name = tab_content_as_el.class_name(); 909 | 910 | if tab_content_class_name.contains(&name_of_active_tab) { 911 | 912 | tab_content_as_el.set_attribute("style", ""); 913 | 914 | } 915 | else { 916 | tab_content_as_el.set_attribute("style", "display: none"); 917 | 918 | } 919 | } 920 | 921 | for tab_button in &list_of_tab_buttons_as_rust { 922 | 923 | let tab_button_as_el = tab_button.clone(); 924 | 925 | let current_class_name = &tab_button_as_el.class_name(); 926 | 927 | if ( 928 | current_class_name.contains(&name_of_active_tab) 929 | ) 930 | { 931 | let class_name_with_active = format!("{} active", current_class_name); 932 | 933 | let class_name_with_active = class_name_with_active 934 | .split_whitespace() 935 | .collect::>() 936 | .join(" "); 937 | 938 | tab_button_as_el.set_class_name(&class_name_with_active); 939 | } 940 | 941 | 942 | 943 | let list_of_tab_buttons_as_rust_clone = list_of_tab_buttons_as_rust.clone(); 944 | let list_of_tab_contents_as_rust_clone = list_of_tab_contents_as_rust.clone(); 945 | 946 | let listener = EventListener::new(&tab_button, "click", move |event: &Event| { 947 | 948 | for tab_button in &list_of_tab_buttons_as_rust_clone { 949 | 950 | let tab_button_as_el = tab_button.clone(); 951 | 952 | let current_class_name = &tab_button_as_el.class_name(); 953 | 954 | let class_name_without_active = current_class_name 955 | .replace("active", ""); 956 | 957 | tab_button_as_el.set_class_name(&class_name_without_active); 958 | } 959 | 960 | let active_tab_button = event 961 | .target() 962 | .unwrap(); 963 | 964 | let active_tab_button_as_el = active_tab_button 965 | .dyn_into::() 966 | .unwrap(); 967 | 968 | let current_active_class_name = &active_tab_button_as_el.class_name(); 969 | 970 | let class_name_with_active = format!("{} active", current_active_class_name); 971 | 972 | let class_name_with_active = class_name_with_active 973 | .split_whitespace() 974 | .collect::>() 975 | .join(" "); 976 | 977 | active_tab_button_as_el.set_class_name(&class_name_with_active); 978 | 979 | let first_class = &class_name_with_active 980 | .split_whitespace() 981 | .collect::>() 982 | .get(0) 983 | .unwrap() 984 | .to_string(); 985 | 986 | let local_storage = web_sys::window() 987 | .unwrap() 988 | .local_storage() 989 | .unwrap() 990 | .unwrap(); 991 | 992 | local_storage 993 | .set_item("name_of_active_tab", &first_class) 994 | .unwrap(); 995 | 996 | let name_of_first_class_of_active_tab_button = &class_name_with_active 997 | .split_whitespace() 998 | .collect::>() 999 | .get(0) 1000 | .unwrap().to_string(); 1001 | 1002 | let name_of_first_class_of_active_tab_button_as_js = JsValue::from(name_of_first_class_of_active_tab_button.clone()); 1003 | 1004 | for tab_content in &list_of_tab_contents_as_rust_clone { 1005 | 1006 | let tab_content_as_el = tab_content.clone(); 1007 | 1008 | let tab_content_class_name = tab_content_as_el.class_name(); 1009 | 1010 | if tab_content_class_name.contains(&name_of_first_class_of_active_tab_button.as_str()) { 1011 | 1012 | let tab_content_that_matches_tab_button_name = tab_content_as_el.clone(); 1013 | 1014 | tab_content_as_el.set_attribute("style", ""); 1015 | } 1016 | else { 1017 | tab_content_as_el.set_attribute("style", "display: none"); 1018 | 1019 | } 1020 | } 1021 | }); 1022 | 1023 | listener.forget(); 1024 | } 1025 | 1026 | Ok(()) 1027 | } 1028 | 1029 | #[wasm_bindgen] 1030 | pub fn load_settings_from_local_storage() -> Result<(), JsValue> 1031 | { 1032 | 1033 | let document = obtain_document(); 1034 | 1035 | let substitute_placeholder_checkbox = document 1036 | .get_element_by_id("substitute-placeholder-checkbox") 1037 | .unwrap(); 1038 | 1039 | let substitute_placeholder_checkbox_as_el: Element = substitute_placeholder_checkbox 1040 | .dyn_into() 1041 | .unwrap(); 1042 | 1043 | let local_storage = web_sys::window() 1044 | .unwrap() 1045 | .local_storage() 1046 | .unwrap() 1047 | .unwrap(); 1048 | 1049 | let substitute_placeholder_checkbox_state = local_storage 1050 | .get_item("substitute_placeholder") 1051 | .unwrap() 1052 | .unwrap_or_else( 1053 | || { 1054 | "false".to_owned() 1055 | }, 1056 | ); 1057 | 1058 | let substitute_placeholder_checkbox_state_as_bool = substitute_placeholder_checkbox_state 1059 | .parse::() 1060 | .unwrap(); 1061 | 1062 | substitute_placeholder_checkbox_as_el 1063 | .dyn_into::() 1064 | .unwrap() 1065 | .set_checked(substitute_placeholder_checkbox_state_as_bool); 1066 | 1067 | let enable_filters_checkbox = document 1068 | .get_element_by_id("enable-filters-checkbox") 1069 | .unwrap(); 1070 | 1071 | let enable_filters_checkbox_as_el: Element = enable_filters_checkbox 1072 | .dyn_into() 1073 | .unwrap(); 1074 | 1075 | let local_storage = web_sys::window() 1076 | .unwrap() 1077 | .local_storage() 1078 | .unwrap() 1079 | .unwrap(); 1080 | 1081 | let enable_filters_checkbox_state = local_storage 1082 | .get_item("enable_filters") 1083 | .unwrap() 1084 | .unwrap_or_else( 1085 | || { 1086 | "true".to_owned() 1087 | }, 1088 | ); 1089 | 1090 | let enable_filters_checkbox_state_as_bool = enable_filters_checkbox_state 1091 | .parse::() 1092 | .unwrap(); 1093 | 1094 | enable_filters_checkbox_as_el 1095 | .dyn_into::() 1096 | .unwrap() 1097 | .set_checked(enable_filters_checkbox_state_as_bool); 1098 | 1099 | let hide_children_comments_checkbox = document 1100 | .get_element_by_id("hide-children-comments-checkbox") 1101 | .unwrap(); 1102 | 1103 | let hide_children_comments_checkbox_as_el: Element = hide_children_comments_checkbox 1104 | .dyn_into() 1105 | .unwrap(); 1106 | 1107 | let local_storage = web_sys::window() 1108 | .unwrap() 1109 | .local_storage() 1110 | .unwrap() 1111 | .unwrap(); 1112 | 1113 | let hide_children_comments_checkbox_state = local_storage 1114 | .get_item("hide_children_comments") 1115 | .unwrap() 1116 | .unwrap_or_else( 1117 | || { 1118 | "true".to_owned() 1119 | }, 1120 | ); 1121 | 1122 | let hide_children_comments_checkbox_state_as_bool = hide_children_comments_checkbox_state 1123 | .parse::() 1124 | .unwrap(); 1125 | 1126 | hide_children_comments_checkbox_as_el 1127 | .dyn_into::() 1128 | .unwrap() 1129 | .set_checked(hide_children_comments_checkbox_state_as_bool); 1130 | 1131 | Ok(()) 1132 | } 1133 | 1134 | #[wasm_bindgen] 1135 | pub fn register_settings_checkboxes_event_listener() -> Result<(), JsValue> 1136 | { 1137 | let document = obtain_document(); 1138 | 1139 | let substitute_placeholder_checkbox = document 1140 | .get_element_by_id("substitute-placeholder-checkbox") 1141 | .unwrap(); 1142 | 1143 | let substitute_placeholder_checkbox_as_el: Element = substitute_placeholder_checkbox 1144 | .dyn_into() 1145 | .unwrap(); 1146 | 1147 | let substitute_placeholder_checkbox_listener = EventListener::new(&substitute_placeholder_checkbox_as_el, "click", move |event: &Event| { 1148 | 1149 | let substitute_placeholder_checkbox_as_el = event 1150 | .target() 1151 | .unwrap(); 1152 | 1153 | let substitute_placeholder_checkbox_as_el = substitute_placeholder_checkbox_as_el 1154 | .dyn_into::() 1155 | .unwrap(); 1156 | 1157 | let substitute_placeholder_checkbox_state = substitute_placeholder_checkbox_as_el 1158 | .checked(); 1159 | 1160 | let substitute_placeholder_checkbox_state_as_string = substitute_placeholder_checkbox_state 1161 | .to_string(); 1162 | 1163 | let local_storage = window() 1164 | .unwrap() 1165 | .local_storage() 1166 | .unwrap() 1167 | .unwrap(); 1168 | 1169 | local_storage 1170 | .set_item( 1171 | "substitute_placeholder", 1172 | &substitute_placeholder_checkbox_state_as_string 1173 | ) 1174 | .unwrap(); 1175 | 1176 | remove_all_children_of_filter_items_container(); 1177 | 1178 | load_items_from_locastorage_populate_sidebar_and_apply_filters(); 1179 | }); 1180 | 1181 | substitute_placeholder_checkbox_listener.forget(); 1182 | 1183 | let enable_filters_checkbox = document 1184 | .get_element_by_id("enable-filters-checkbox") 1185 | .unwrap(); 1186 | 1187 | let enable_filters_checkbox_as_el: Element = enable_filters_checkbox 1188 | .dyn_into() 1189 | .unwrap(); 1190 | 1191 | let enable_filters_checkbox_listener = EventListener::new(&enable_filters_checkbox_as_el, "click", move |event: &Event| { 1192 | 1193 | let enable_filters_checkbox_as_el = event 1194 | .target() 1195 | .unwrap(); 1196 | 1197 | let enable_filters_checkbox_as_el = enable_filters_checkbox_as_el 1198 | .dyn_into::() 1199 | .unwrap(); 1200 | 1201 | let enable_filters_checkbox_state = enable_filters_checkbox_as_el 1202 | .checked(); 1203 | 1204 | let enable_filters_checkbox_state_as_string = enable_filters_checkbox_state 1205 | .to_string(); 1206 | 1207 | let local_storage = window() 1208 | .unwrap() 1209 | .local_storage() 1210 | .unwrap() 1211 | .unwrap(); 1212 | 1213 | local_storage 1214 | .set_item( 1215 | "enable_filters", 1216 | &enable_filters_checkbox_state_as_string 1217 | ) 1218 | .unwrap(); 1219 | 1220 | remove_all_children_of_filter_items_container(); 1221 | 1222 | load_items_from_locastorage_populate_sidebar_and_apply_filters(); 1223 | }); 1224 | 1225 | enable_filters_checkbox_listener.forget(); 1226 | 1227 | let hide_children_comments_checkbox = document 1228 | .get_element_by_id("hide-children-comments-checkbox") 1229 | .unwrap(); 1230 | 1231 | let hide_children_comments_checkbox_as_el: Element = hide_children_comments_checkbox 1232 | .dyn_into() 1233 | .unwrap(); 1234 | 1235 | let hide_children_comments_checkbox_listener = EventListener::new(&hide_children_comments_checkbox_as_el, "click", move |event: &Event| { 1236 | 1237 | let hide_children_comments_checkbox_as_el = event 1238 | .target() 1239 | .unwrap(); 1240 | 1241 | let hide_children_comments_checkbox_as_el = hide_children_comments_checkbox_as_el 1242 | .dyn_into::() 1243 | .unwrap(); 1244 | 1245 | let hide_children_comments_checkbox_state = hide_children_comments_checkbox_as_el 1246 | .checked(); 1247 | 1248 | let hide_children_comments_checkbox_state_as_string = hide_children_comments_checkbox_state 1249 | .to_string(); 1250 | 1251 | let local_storage = window() 1252 | .unwrap() 1253 | .local_storage() 1254 | .unwrap() 1255 | .unwrap(); 1256 | 1257 | local_storage 1258 | .set_item( 1259 | "hide_children_comments", 1260 | &hide_children_comments_checkbox_state_as_string 1261 | ) 1262 | .unwrap(); 1263 | 1264 | remove_all_children_of_filter_items_container(); 1265 | 1266 | load_items_from_locastorage_populate_sidebar_and_apply_filters(); 1267 | }); 1268 | 1269 | hide_children_comments_checkbox_listener.forget(); 1270 | 1271 | 1272 | 1273 | Ok(()) 1274 | 1275 | } 1276 | 1277 | #[wasm_bindgen] 1278 | pub fn add_donation_buttons_to_sidebar() -> Result<(), JsValue> { 1279 | let document = obtain_document(); 1280 | 1281 | let settings_tab_content = document 1282 | .get_element_by_id("settings-checkbox-container") 1283 | .unwrap(); 1284 | 1285 | let image_bytes_patreon = include_bytes!("../support-on-patreon.png"); 1286 | 1287 | let base64_string_patreon = base64::encode(image_bytes_patreon); 1288 | 1289 | let data_url_patreon = format!("data:image/png;base64,{}", base64_string_patreon); 1290 | 1291 | let container_with_images = document 1292 | .create_element("div") 1293 | .unwrap(); 1294 | 1295 | let container_with_images_as_el: Element = container_with_images 1296 | .dyn_into() 1297 | .unwrap(); 1298 | 1299 | container_with_images_as_el.set_inner_html( 1300 | r#" 1301 | Support my work: 1302 |
1303 | 1306 | 1307 | 1308 | 1309 | 1310 | 1311 | 1312 |
1313 | "# 1314 | ); 1315 | 1316 | settings_tab_content 1317 | .append_child(&container_with_images_as_el); 1318 | 1319 | let img_patreon = document 1320 | .get_element_by_id("patreon") 1321 | .unwrap() 1322 | .dyn_into::() 1323 | .expect("should be able to cast as img"); 1324 | 1325 | img_patreon.set_src(&data_url_patreon); 1326 | 1327 | let image_bytes_liberapay = include_bytes!("../support-on-liberapay.png"); 1328 | 1329 | let base64_string_liberapay = base64::encode(image_bytes_liberapay); 1330 | 1331 | let data_url_liberapay = format!("data:image/png;base64,{}", base64_string_liberapay); 1332 | 1333 | let img_liberapay = document 1334 | .get_element_by_id("liberapay") 1335 | .unwrap() 1336 | .dyn_into::() 1337 | .expect("should be able to cast as img"); 1338 | 1339 | img_liberapay.set_src(&data_url_liberapay); 1340 | 1341 | Ok(()) 1342 | } 1343 | 1344 | fn find_child_with_class(parent: &Element, class_name: &str) -> Result, JsValue> { 1345 | let selector = format!(".{}", class_name); 1346 | parent.query_selector(&selector).map_err(|e| e.into()) 1347 | } 1348 | 1349 | 1350 | #[wasm_bindgen] 1351 | pub fn insert_softhide_buttons() -> Result<(), JsValue> { 1352 | 1353 | let href = window().ok_or("No global `window` object")?.location().href()?; 1354 | 1355 | if href.contains("item?id") { return Ok(()); } 1356 | 1357 | let document = obtain_document(); 1358 | 1359 | let list_of_ranking_numbers = document. 1360 | get_elements_by_class_name("rank"); 1361 | 1362 | let length = list_of_ranking_numbers.length(); 1363 | 1364 | for index in 0..length 1365 | { 1366 | if let Some(ranking_number) = list_of_ranking_numbers.item(index) 1367 | { 1368 | let ranking_number_container = ranking_number.parent_element().unwrap(); 1369 | 1370 | let new_td = document.create_element("td")?; 1371 | 1372 | new_td.set_inner_html(" [–] "); 1373 | 1374 | new_td.set_attribute("style", "width: 10px; height: 10px; padding-left: 5px;"); 1375 | new_td.set_attribute("class", "softhide"); 1376 | 1377 | if let Some(athing_tr) = ranking_number_container.parent_element() 1378 | { 1379 | athing_tr.insert_before(&new_td, ranking_number_container.next_sibling().as_ref())?; 1380 | 1381 | let listener = EventListener::new(&new_td, "click", move |event: &Event| { 1382 | 1383 | let document = obtain_document(); 1384 | 1385 | let softhidden_entries_container = document 1386 | .get_element_by_id("softhidden-entries-container") 1387 | .unwrap(); 1388 | 1389 | let mut child = softhidden_entries_container.first_child(); 1390 | 1391 | while let Some(child_element) = child 1392 | { 1393 | softhidden_entries_container 1394 | .remove_child(&child_element) 1395 | .unwrap(); 1396 | 1397 | child = softhidden_entries_container.first_child(); 1398 | } 1399 | 1400 | let local_storage = web_sys::window() 1401 | .unwrap() 1402 | .local_storage() 1403 | .unwrap() 1404 | .unwrap(); 1405 | 1406 | let athing_id_to_submission_title_as_string = local_storage 1407 | .get_item("softhidden_things") 1408 | .unwrap() 1409 | .unwrap_or_else( 1410 | || { 1411 | "{}".to_owned() 1412 | }, 1413 | ); 1414 | 1415 | let mut athing_id_to_submission_title: serde_json::Value = 1416 | serde_json::from_str(&athing_id_to_submission_title_as_string).unwrap(); 1417 | 1418 | 1419 | let athing_id = athing_tr.id(); 1420 | 1421 | 1422 | 1423 | 1424 | match find_child_with_class(&athing_tr, "titleline") { 1425 | Ok(Some(title_td)) => { 1426 | 1427 | let a_href_with_title = title_td 1428 | .first_child() 1429 | .unwrap(); 1430 | 1431 | let submission_title = a_href_with_title.text_content(); 1432 | 1433 | if let Some(athing_id_to_submission_title) = 1434 | athing_id_to_submission_title.as_object_mut() 1435 | { 1436 | athing_id_to_submission_title.insert(athing_id.to_string(), Value::String(submission_title.unwrap_or_else(|| "".to_string()))); 1437 | } 1438 | 1439 | let amended_athing_id_to_submission_title_as_string = to_string(&athing_id_to_submission_title); 1440 | 1441 | match amended_athing_id_to_submission_title_as_string { 1442 | Ok(stringified_json) => { 1443 | write_athing_id_to_submission_title_as_stringified_json_to_local_storage(&stringified_json); 1444 | } 1445 | Err(e) => { } 1446 | } 1447 | 1448 | } 1449 | Ok(None) => { } 1450 | Err(e) => { } 1451 | } 1452 | 1453 | 1454 | remove_all_children_of_filter_items_container(); 1455 | 1456 | load_items_from_locastorage_populate_sidebar_and_apply_filters(); 1457 | }); 1458 | 1459 | listener.forget(); 1460 | 1461 | } 1462 | } 1463 | } 1464 | 1465 | let document = obtain_document(); 1466 | 1467 | let list_of_subtexts = document. 1468 | get_elements_by_class_name("subtext"); 1469 | 1470 | let length = list_of_subtexts.length(); 1471 | 1472 | for index in 0..length 1473 | { 1474 | if let Some(subtext) = list_of_subtexts.item(index) 1475 | { 1476 | let empty_td_to_preserve_layout = document.create_element("td")?; 1477 | 1478 | if let Some(parent) = subtext.parent_element() 1479 | { 1480 | let subtext_node = subtext.dyn_into::()?; 1481 | parent.insert_before(&empty_td_to_preserve_layout, Some(&subtext_node))?; 1482 | } 1483 | } 1484 | } 1485 | 1486 | 1487 | 1488 | 1489 | Ok(()) 1490 | } 1491 | 1492 | 1493 | #[wasm_bindgen] 1494 | pub fn register_enter_press_with_focused_input_event_listener() -> Result<(), JsValue> { 1495 | let document = obtain_document(); 1496 | 1497 | let filter_button = document 1498 | .get_element_by_id("filter-button") 1499 | .expect("Should have #filter-button on the page") 1500 | .dyn_into::() 1501 | .map_err(|_| JsValue::from_str("Could not cast to HtmlButtonElement"))?; 1502 | 1503 | let input = document 1504 | .get_element_by_id("sidebar-filter-input") 1505 | .expect("Should have #sidebar-filter-input on the page") 1506 | .dyn_into::() 1507 | .map_err(|_| JsValue::from_str("Could not cast to HtmlInputElement"))?; 1508 | 1509 | let closure = Closure::wrap(Box::new(move |event: web_sys::KeyboardEvent| { 1510 | if event.key() == "Enter" && document.active_element() == Some(input.clone().into()) { 1511 | filter_button.click(); 1512 | } 1513 | }) as Box); 1514 | 1515 | let document = obtain_document(); 1516 | document.add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref())?; 1517 | closure.forget(); 1518 | 1519 | Ok(()) 1520 | } 1521 | 1522 | #[wasm_bindgen(start)] 1523 | pub async fn main() -> Result<(), JsValue> 1524 | { 1525 | std::panic::set_hook(Box::new(console_error_panic_hook::hook)); 1526 | 1527 | replace_center_tag_with_div(); 1528 | 1529 | parse_string_with_sidebar_and_insert_into_body(); 1530 | 1531 | parse_string_with_css_and_insert_into_header(); 1532 | 1533 | load_items_from_locastorage_populate_sidebar_and_apply_filters(); 1534 | 1535 | register_filter_out_submissions_event_listener(); 1536 | 1537 | register_sidebar_item_event_listener(); 1538 | 1539 | restore_active_tab_from_previous_session_and_register_filter_tab_switching_event_listener(); 1540 | 1541 | load_settings_from_local_storage(); 1542 | 1543 | register_settings_checkboxes_event_listener(); 1544 | 1545 | register_enter_press_with_focused_input_event_listener(); 1546 | 1547 | insert_softhide_buttons(); 1548 | 1549 | add_donation_buttons_to_sidebar(); 1550 | 1551 | Ok(()) 1552 | } 1553 | -------------------------------------------------------------------------------- /src/pure.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexMap; 2 | use serde_json::json; 3 | 4 | pub fn stringify_filter_summary_text_to_filter_target_text_from_map( 5 | filter_summary_text_to_filter_target_text: &IndexMap 6 | ) -> String 7 | { 8 | let filter_summary_texts = filter_summary_text_to_filter_target_text 9 | .keys() 10 | .collect::>(); 11 | 12 | let filter_target_texts = filter_summary_text_to_filter_target_text 13 | .values() 14 | .collect::>(); 15 | 16 | let filter_summary_text_to_filter_target_text_as_vec_of_tuples = filter_summary_texts 17 | .iter() 18 | .zip(filter_target_texts.iter()) 19 | .collect::>(); 20 | 21 | let filter_summary_text_to_filter_target_text_as_json = json!( 22 | { 23 | "sidebar_items": filter_summary_text_to_filter_target_text_as_vec_of_tuples 24 | } 25 | ); 26 | 27 | let filter_summary_text_to_filter_target_text_as_stringified_json = 28 | filter_summary_text_to_filter_target_text_as_json.to_string(); 29 | 30 | filter_summary_text_to_filter_target_text_as_stringified_json 31 | } 32 | -------------------------------------------------------------------------------- /src/setters.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | use web_sys::{ 3 | window, 4 | Document, 5 | Element, 6 | Event, 7 | }; 8 | use wasm_bindgen::JsCast; 9 | 10 | fn set_item_in_local_storage( 11 | key: &str, 12 | value: &str 13 | ) 14 | { 15 | let local_storage = window() 16 | .unwrap() 17 | .local_storage() 18 | .unwrap() 19 | .unwrap(); 20 | 21 | local_storage 22 | .set_item(key, value) 23 | .unwrap(); 24 | } 25 | 26 | pub fn write_sidebar_items_as_stringified_json_to_local_storage( 27 | sidebar_items_as_stringified_json: &str 28 | ) 29 | { 30 | set_item_in_local_storage( 31 | "hackernews_userscript", 32 | sidebar_items_as_stringified_json 33 | ); 34 | } 35 | 36 | pub fn write_settings_as_stringified_json_to_local_storage( 37 | settings_as_stringified_json: &str 38 | ) 39 | { 40 | set_item_in_local_storage( 41 | "settings", 42 | settings_as_stringified_json 43 | ); 44 | } 45 | 46 | pub fn write_athing_id_to_submission_title_as_stringified_json_to_local_storage( 47 | athing_id_to_submission_title_as_stringified_json: &str 48 | ) 49 | { 50 | set_item_in_local_storage( 51 | "softhidden_things", 52 | athing_id_to_submission_title_as_stringified_json 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/sidebar.html: -------------------------------------------------------------------------------- 1 |