├── .gitattributes ├── .gitignore ├── LICENSE ├── Mutable ├── Mutable.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Shared (App) │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── foil-1024.png │ │ │ ├── foil-128.png │ │ │ ├── foil-16.png │ │ │ ├── foil-256 1.png │ │ │ ├── foil-256.png │ │ │ ├── foil-32 1.png │ │ │ ├── foil-32.png │ │ │ ├── foil-512 1.png │ │ │ ├── foil-512.png │ │ │ ├── foil-64.png │ │ │ └── logo-1024.png │ │ ├── Contents.json │ │ └── LargeIcon.imageset │ │ │ ├── Contents.json │ │ │ ├── icon-foil 1.png │ │ │ ├── icon-foil 2.png │ │ │ └── icon-foil.png │ ├── Base.lproj │ │ └── Main.html │ ├── Resources │ │ ├── Icon.png │ │ ├── Script.js │ │ ├── Style.css │ │ ├── foil.jpg │ │ ├── ios-extension-example.png │ │ ├── ios-extension-settings.png │ │ ├── ios-mutable-settings.png │ │ ├── ios-safari-menu.png │ │ ├── ios-safari-settings.jpg │ │ ├── ios-settings.jpg │ │ └── mac-extension-example.png │ └── ViewController.swift ├── Shared (Extension) │ └── SafariWebExtensionHandler.swift ├── iOS (App) │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ └── SceneDelegate.swift ├── iOS (Extension) │ └── Info.plist ├── macOS (App) │ ├── AppDelegate.swift │ ├── Base.lproj │ │ └── Main.storyboard │ ├── Info.plist │ └── Mutable.entitlements └── macOS (Extension) │ ├── Info.plist │ └── Mutable.entitlements ├── README.md ├── application.js ├── build.sh ├── docs ├── CNAME ├── Gemfile ├── Gemfile.lock ├── _config.yml ├── icon.png ├── index.md ├── preview.png └── stylesheet.css ├── icons ├── foil-1024.png ├── foil-128.png ├── foil-16.png ├── foil-256.png ├── foil-32.png ├── foil-48.png ├── foil-512.png ├── foil-64.png ├── foil-96.png ├── icon-48.png ├── icon-96.png ├── icon-foil.png ├── logo-1024.png ├── logo-transparent-48.png ├── logo-transparent-96.png └── logo-transparent.png ├── images ├── hedgehogs │ ├── 1.jpg │ ├── 10.jpg │ ├── 11.jpg │ ├── 12.jpg │ ├── 13.jpg │ ├── 14.jpg │ ├── 15.jpg │ ├── 16.jpg │ ├── 17.jpg │ ├── 18.jpg │ ├── 19.jpg │ ├── 2.jpg │ ├── 20.jpg │ ├── 3.jpg │ ├── 4.jpg │ ├── 5.jpg │ ├── 6.jpg │ ├── 7.jpg │ ├── 8.jpg │ └── 9.jpg ├── kittens │ ├── 1.jpg │ ├── 10.jpg │ ├── 11.jpg │ ├── 12.jpg │ ├── 13.jpg │ ├── 14.jpg │ ├── 15.jpg │ ├── 16.jpg │ ├── 17.jpg │ ├── 18.jpg │ ├── 19.jpg │ ├── 2.jpg │ ├── 20.jpg │ ├── 3.jpg │ ├── 4.jpg │ ├── 5.jpg │ ├── 6.jpg │ ├── 7.jpg │ ├── 8.jpg │ └── 9.jpg └── puppies │ ├── 1.jpg │ ├── 10.jpg │ ├── 11.jpg │ ├── 12.jpg │ ├── 13.jpg │ ├── 14.jpg │ ├── 15.jpg │ ├── 16.jpg │ ├── 17.jpg │ ├── 18.jpg │ ├── 19.jpg │ ├── 2.jpg │ ├── 20.jpg │ ├── 3.jpg │ ├── 4.jpg │ ├── 5.jpg │ ├── 6.jpg │ ├── 7.jpg │ ├── 8.jpg │ └── 9.jpg ├── jquery.js ├── manifest.json ├── mutable-stylesheet.css ├── package-lock.json ├── package.json ├── pako.js ├── replace.py ├── settings ├── example.html ├── foil.jpg ├── settings-stylesheet.css ├── settings.html └── settings.js ├── shared.js └── tests └── tests.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | mutable-packaged.zip 4 | Mutable/Mutable.xcodeproj/xcuserdata/ 5 | Mutable/Mutable.xcodeproj/project.xcworkspace/xcuserdata/idrees.xcuserdatad/UserInterfaceState.xcuserstate 6 | node_modules/ 7 | Mutable.code-workspace 8 | docs/_site/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /Mutable/Mutable.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Mutable/Mutable.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Mutable/Shared (App)/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Mutable/Shared (App)/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "logo-1024.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | }, 9 | { 10 | "filename" : "foil-16.png", 11 | "idiom" : "mac", 12 | "scale" : "1x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "foil-32 1.png", 17 | "idiom" : "mac", 18 | "scale" : "2x", 19 | "size" : "16x16" 20 | }, 21 | { 22 | "filename" : "foil-32.png", 23 | "idiom" : "mac", 24 | "scale" : "1x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "foil-64.png", 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "32x32" 32 | }, 33 | { 34 | "filename" : "foil-128.png", 35 | "idiom" : "mac", 36 | "scale" : "1x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "foil-256.png", 41 | "idiom" : "mac", 42 | "scale" : "2x", 43 | "size" : "128x128" 44 | }, 45 | { 46 | "filename" : "foil-256 1.png", 47 | "idiom" : "mac", 48 | "scale" : "1x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "foil-512.png", 53 | "idiom" : "mac", 54 | "scale" : "2x", 55 | "size" : "256x256" 56 | }, 57 | { 58 | "filename" : "foil-512 1.png", 59 | "idiom" : "mac", 60 | "scale" : "1x", 61 | "size" : "512x512" 62 | }, 63 | { 64 | "filename" : "foil-1024.png", 65 | "idiom" : "mac", 66 | "scale" : "2x", 67 | "size" : "512x512" 68 | } 69 | ], 70 | "info" : { 71 | "author" : "xcode", 72 | "version" : 1 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Mutable/Shared (App)/Assets.xcassets/AppIcon.appiconset/foil-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/Mutable/Shared (App)/Assets.xcassets/AppIcon.appiconset/foil-1024.png -------------------------------------------------------------------------------- /Mutable/Shared (App)/Assets.xcassets/AppIcon.appiconset/foil-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/Mutable/Shared (App)/Assets.xcassets/AppIcon.appiconset/foil-128.png -------------------------------------------------------------------------------- /Mutable/Shared (App)/Assets.xcassets/AppIcon.appiconset/foil-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/Mutable/Shared (App)/Assets.xcassets/AppIcon.appiconset/foil-16.png -------------------------------------------------------------------------------- /Mutable/Shared (App)/Assets.xcassets/AppIcon.appiconset/foil-256 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/Mutable/Shared (App)/Assets.xcassets/AppIcon.appiconset/foil-256 1.png -------------------------------------------------------------------------------- /Mutable/Shared (App)/Assets.xcassets/AppIcon.appiconset/foil-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/Mutable/Shared (App)/Assets.xcassets/AppIcon.appiconset/foil-256.png -------------------------------------------------------------------------------- /Mutable/Shared (App)/Assets.xcassets/AppIcon.appiconset/foil-32 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/Mutable/Shared (App)/Assets.xcassets/AppIcon.appiconset/foil-32 1.png -------------------------------------------------------------------------------- /Mutable/Shared (App)/Assets.xcassets/AppIcon.appiconset/foil-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/Mutable/Shared (App)/Assets.xcassets/AppIcon.appiconset/foil-32.png -------------------------------------------------------------------------------- /Mutable/Shared (App)/Assets.xcassets/AppIcon.appiconset/foil-512 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/Mutable/Shared (App)/Assets.xcassets/AppIcon.appiconset/foil-512 1.png -------------------------------------------------------------------------------- /Mutable/Shared (App)/Assets.xcassets/AppIcon.appiconset/foil-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/Mutable/Shared (App)/Assets.xcassets/AppIcon.appiconset/foil-512.png -------------------------------------------------------------------------------- /Mutable/Shared (App)/Assets.xcassets/AppIcon.appiconset/foil-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/Mutable/Shared (App)/Assets.xcassets/AppIcon.appiconset/foil-64.png -------------------------------------------------------------------------------- /Mutable/Shared (App)/Assets.xcassets/AppIcon.appiconset/logo-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/Mutable/Shared (App)/Assets.xcassets/AppIcon.appiconset/logo-1024.png -------------------------------------------------------------------------------- /Mutable/Shared (App)/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Mutable/Shared (App)/Assets.xcassets/LargeIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon-foil 2.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "icon-foil.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "icon-foil 1.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Mutable/Shared (App)/Assets.xcassets/LargeIcon.imageset/icon-foil 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/Mutable/Shared (App)/Assets.xcassets/LargeIcon.imageset/icon-foil 1.png -------------------------------------------------------------------------------- /Mutable/Shared (App)/Assets.xcassets/LargeIcon.imageset/icon-foil 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/Mutable/Shared (App)/Assets.xcassets/LargeIcon.imageset/icon-foil 2.png -------------------------------------------------------------------------------- /Mutable/Shared (App)/Assets.xcassets/LargeIcon.imageset/icon-foil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/Mutable/Shared (App)/Assets.xcassets/LargeIcon.imageset/icon-foil.png -------------------------------------------------------------------------------- /Mutable/Shared (App)/Base.lproj/Main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
Mutable
17 |
Turn Down the Noise
18 |
19 |
20 |
21 |
22 | 24 | 25 | 26 | 27 | 28 |
29 |
how to enable
30 |
31 |
32 |
33 |
34 |
35 |
36 |

You can turn on Mutable’s Safari extension in Settings by going to Settings → Safari → Extensions → Mutable

37 |

You can turn on Mutable’s extension in Safari Extensions preferences.

38 |

Mutable’s extension is currently on. You can turn it off in Safari Extensions preferences.

39 |

Mutable’s extension is currently off. You can turn it on in Safari Extensions preferences.

40 |
41 | 45 |
46 |

Open the Settings app and scroll down to the "Safari" section

47 | 48 |

Scroll down to the "General" category and tap on "Extensions"

49 | 50 |

Tap on "Mutable"

51 | 52 |

Enable the extension and allow access to all websites

53 | 54 |
55 |
56 |
57 |
58 |
59 | 60 | 61 | 62 | 63 | 64 |
65 |
how to use
66 |
67 |
68 |
69 |
70 |
71 |
72 |

73 | Using Mutable is as simple as opening Safari and clicking the website settings icon in the bottom left next to the URL (circled below). 74 |

75 | 76 |

77 | From there, click "Mutable" and give it permissions to run on every page so that it can detect social media sites. If Mutable isn't listed, be sure to follow the directions above for enabling it via settings first! 78 |

79 | 80 |

Using Mutable is as easy as opening Safari and clicking the Mutable icon next to the search bar (circled below).

81 | 82 |

Once Mutable is installed and enabled, just add some keywords to the mute list and visit any site to try it out! You can select from multiple options when choosing how Mutable deals with offending posts, from blurring or hiding them to even replacing them with pictures of cute animals! And if you'd rather have Mutable only run on sites you select, toggle "Enable by Default" to false and manually enable each site.

83 |

Enjoy, and if you have any questions or concerns feel free to contact me at idrees+mutable@idreesinc.com

84 |
85 |
86 | 89 |
90 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /Mutable/Shared (App)/Resources/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/Mutable/Shared (App)/Resources/Icon.png -------------------------------------------------------------------------------- /Mutable/Shared (App)/Resources/Script.js: -------------------------------------------------------------------------------- 1 | function show(platform, enabled, useSettingsInsteadOfPreferences) { 2 | document.body.classList.add(`platform-${platform}`); 3 | 4 | if (useSettingsInsteadOfPreferences) { 5 | document.getElementsByClassName('platform-mac state-on')[0].innerText = "Mutable’s extension is currently on. You can turn it off in the Extensions section of Safari Settings."; 6 | document.getElementsByClassName('platform-mac state-off')[0].innerText = "Mutable’s extension is currently off. You can turn it on in the Extensions section of Safari Settings."; 7 | document.getElementsByClassName('platform-mac state-unknown')[0].innerText = "You can turn on Mutable’s extension in the Extensions section of Safari Settings."; 8 | document.getElementsByClassName('platform-mac open-preferences')[0].innerText = "Quit and Open Safari Settings…"; 9 | } 10 | 11 | if (typeof enabled === "boolean") { 12 | document.body.classList.toggle(`state-on`, enabled); 13 | document.body.classList.toggle(`state-off`, !enabled); 14 | } else { 15 | document.body.classList.remove(`state-on`); 16 | document.body.classList.remove(`state-off`); 17 | } 18 | } 19 | 20 | function openPreferences() { 21 | webkit.messageHandlers.controller.postMessage("open-preferences"); 22 | } 23 | 24 | document.querySelector("button.open-preferences").addEventListener("click", openPreferences); 25 | -------------------------------------------------------------------------------- /Mutable/Shared (App)/Resources/Style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Epilogue:wght@400;500&display=swap'); 2 | 3 | * { 4 | -webkit-user-select: none; 5 | -webkit-user-drag: none; 6 | cursor: default; 7 | } 8 | 9 | :root { 10 | /* color-scheme: light dark;*/ 11 | --spacing: 20px; 12 | } 13 | 14 | html { 15 | height: 100%; 16 | } 17 | 18 | /*body {*/ 19 | /* display: flex;*/ 20 | /* align-items: center;*/ 21 | /* justify-content: center;*/ 22 | /* flex-direction: column;*/ 23 | /**/ 24 | /* gap: var(--spacing);*/ 25 | /* margin: 0 calc(var(--spacing) * 2);*/ 26 | /* height: 100%;*/ 27 | /**/ 28 | /* font: -apple-system-short-body;*/ 29 | /* text-align: center;*/ 30 | /*}*/ 31 | 32 | body:not(.platform-mac, .platform-ios) :is(.platform-mac, .platform-ios) { 33 | display: none; 34 | } 35 | 36 | body.platform-ios .platform-mac { 37 | display: none; 38 | } 39 | 40 | body.platform-mac .platform-ios { 41 | display: none; 42 | } 43 | 44 | body.platform-ios .platform-mac { 45 | display: none; 46 | } 47 | 48 | body:not(.state-on, .state-off) :is(.state-on, .state-off) { 49 | display: none; 50 | } 51 | 52 | body.state-on :is(.state-off, .state-unknown) { 53 | display: none; 54 | } 55 | 56 | body.state-off :is(.state-on, .state-unknown) { 57 | display: none; 58 | } 59 | 60 | button { 61 | font-size: 1em; 62 | color: black; 63 | } 64 | 65 | :root { 66 | --border-radius: 20px; 67 | --stroke: 2px; 68 | --light-stroke: 1.5px; 69 | } 70 | 71 | html, body { 72 | font-family: Epilogue, sans-serif; 73 | line-height: 1.25; 74 | min-width: 360px; 75 | padding: 0; 76 | margin: 0; 77 | color: black; 78 | } 79 | 80 | html { 81 | background: #f4d6d8; 82 | /* background: #d1dcf0;*/ 83 | /* background: #e1ecd1;*/ 84 | } 85 | 86 | body { 87 | margin-top: 60px; 88 | } 89 | 90 | #background { 91 | position: fixed; 92 | top: 0; 93 | left: 0; 94 | width: 100%; 95 | height: 100%; 96 | background-image: url("./foil.jpg"); 97 | background-size: cover; 98 | z-index: -1; 99 | /* opacity: 0.75; */ 100 | } 101 | 102 | #container { 103 | width: 100%; 104 | height: 100%; 105 | padding: 0 7% 0 7%; 106 | display: flex; 107 | flex-direction: column; 108 | box-sizing: border-box; 109 | } 110 | 111 | #title-container { 112 | width: 100%; 113 | height: 100px; 114 | margin-top: 35px; 115 | margin-bottom: -10px; 116 | display: flex; 117 | flex-direction: column; 118 | justify-content: center; 119 | align-items: center; 120 | } 121 | 122 | #title { 123 | font-family: Epilogue, sans-serif; 124 | font-size: 60px; 125 | } 126 | 127 | #tagline { 128 | margin-top: 10px; 129 | font-family: Epilogue, sans-serif; 130 | font-size: 20px; 131 | } 132 | 133 | .window { 134 | width: 100%; 135 | border: var(--stroke) solid black; 136 | box-sizing: border-box; 137 | border-radius: var(--border-radius); 138 | box-shadow: 4px 4px 0px black; 139 | margin-top: 40px; 140 | background: #fffdfa; 141 | } 142 | 143 | .window-top-bar { 144 | width: calc(100% + 2 * var(--stroke)); 145 | height: calc(var(--border-radius) * 2); 146 | margin-top: calc(-1 * var(--stroke)); 147 | margin-left: calc(-1 * var(--stroke)); 148 | border: var(--stroke) solid black; 149 | box-sizing: border-box; 150 | border-radius: var(--border-radius) var(--border-radius) var(--border-radius) 0; 151 | display: flex; 152 | justify-content: space-between; 153 | align-items: center; 154 | } 155 | 156 | .window-icon, .window-controls { 157 | width: 20%; 158 | } 159 | 160 | .window-icon { 161 | padding-left: calc(var(--border-radius) * 0.5); 162 | display: flex; 163 | align-items: center; 164 | } 165 | 166 | .window-title { 167 | text-align: center; 168 | font-family: Epilogue; 169 | font-size: 20px; 170 | } 171 | 172 | .window-controls { 173 | display: flex; 174 | justify-content: right; 175 | align-items: center; 176 | padding-right: calc(var(--border-radius) * 0.5); 177 | box-sizing: border-box; 178 | } 179 | 180 | .window-button { 181 | height: calc(var(--border-radius) * 0.85); 182 | aspect-ratio: 1; 183 | border-radius: 100px; 184 | border: var(--light-stroke) solid black; 185 | box-sizing: border-box; 186 | } 187 | 188 | .left-window-button { 189 | background: #71FFCC; 190 | margin-right: calc(var(--border-radius) * 0.25); 191 | } 192 | 193 | .right-window-button { 194 | background: #FFF6A4; 195 | } 196 | 197 | .window-content { 198 | padding: 10px 20px 20px 20px; 199 | box-sizing: border-box; 200 | font-size: 17px; 201 | /* display: flex;*/ 202 | /* flex-direction: column;*/ 203 | } 204 | 205 | .window-content > img { 206 | width: 100%; 207 | } 208 | 209 | #enable-window > .window-top-bar { 210 | background: linear-gradient(180deg, #C3FFE2 0%, #FFF 100%); 211 | } 212 | 213 | #mute-window > .window-top-bar { 214 | background: linear-gradient(180deg, #C3EAFF 0%, #FFF 100%); 215 | } 216 | 217 | .website { 218 | width: 100%; 219 | border: var(--stroke) solid black; 220 | display: flex; 221 | align-items: center; 222 | justify-content: space-between; 223 | box-sizing: border-box; 224 | padding: 7px; 225 | padding-left: 10px; 226 | } 227 | 228 | #websites-content > .website:not(:last-child) { 229 | margin-bottom: 13px; 230 | } 231 | 232 | /* #website-twitter { 233 | background: linear-gradient(90deg, #DAF2FF 0%, white 80%); 234 | } 235 | 236 | #website-bluesky { 237 | background: linear-gradient(90deg, #DAF8FF 0%, white 80%); 238 | } 239 | 240 | #website-reddit { 241 | background: linear-gradient(90deg, #fff6d6 0%, white 80%); 242 | } 243 | 244 | #website-instagram { 245 | background: linear-gradient(90deg, #ffd7e5 0%, white 80%); 246 | } */ 247 | 248 | .website-name { 249 | font-family: Epilogue; 250 | font-size: 18px; 251 | padding-top: 4px; 252 | } 253 | 254 | .toggle-switch { 255 | position: relative; 256 | display: inline-block; 257 | width: 72px; 258 | height: 26px; 259 | border: var(--stroke) solid black; 260 | border-radius: 1000px; 261 | } 262 | 263 | .toggle-switch input { 264 | opacity: 0; 265 | width: 0; 266 | height: 0; 267 | } 268 | 269 | .toggle-inner { 270 | position: absolute; 271 | cursor: pointer; 272 | top: 0; 273 | left: 0px; 274 | right: 0; 275 | bottom: 0; 276 | -webkit-transition: .4s; 277 | transition: .4s; 278 | border-radius: 34px; 279 | } 280 | 281 | .toggle-inner:before { 282 | position: absolute; 283 | content: ""; 284 | height: 30px; 285 | width: 50px; 286 | left: -2px; 287 | bottom: -2px; 288 | background-color: #FFC3C3; 289 | -webkit-transition: .4s; 290 | transition: .4s; 291 | border: var(--stroke) solid black; 292 | box-sizing: border-box; 293 | content: "off"; 294 | font-family: Epilogue; 295 | font-size: 16px; 296 | display: flex; 297 | align-items: center; 298 | justify-content: center; 299 | padding-top: 2px; 300 | border-radius: 34px; 301 | } 302 | 303 | input:checked + .toggle-inner:before { 304 | transform: translateX(28px); 305 | content: "on"; 306 | padding-top: 1px; 307 | background-color: #C3FFE2; 308 | } 309 | 310 | .group { 311 | min-height: 100px; 312 | width: 100%; 313 | border: var(--light-stroke) solid black; 314 | box-sizing: border-box; 315 | border-radius: calc(var(--border-radius) * 0.5); 316 | box-shadow: 2.5px 2.5px 0px black; 317 | } 318 | 319 | .group-top-bar { 320 | width: calc(100% + 2 * var(--light-stroke)); 321 | margin-top: calc(-1 * var(--light-stroke)); 322 | margin-left: calc(-1 * var(--light-stroke)); 323 | background: rgba(217, 217, 217, 0.40); 324 | border: var(--light-stroke) solid black; 325 | box-sizing: border-box; 326 | border-radius: calc(var(--border-radius) * 0.5) calc(var(--border-radius) * 0.5) 0 0; 327 | display: flex; 328 | justify-content: space-between; 329 | align-items: center; 330 | padding: 8px; 331 | padding-bottom: 5px; 332 | } 333 | 334 | .group-name { 335 | text-align: center; 336 | font-family: Epilogue; 337 | font-size: 16px; 338 | } 339 | 340 | .group-content { 341 | display: flex; 342 | flex-direction: column; 343 | padding: 12px; 344 | padding-right: 6px; 345 | } 346 | 347 | .group-element { 348 | margin-bottom: 10px; 349 | display: flex; 350 | justify-content: space-between; 351 | align-items: center; 352 | } 353 | 354 | .element-delete { 355 | display: inline; 356 | font-family: 'Courier New', Courier, monospace; 357 | font-size: 20px; 358 | color: grey; 359 | background-color: transparent; 360 | border: none; 361 | box-sizing: border-box; 362 | } 363 | 364 | .element-delete:hover { 365 | color: black; 366 | } 367 | 368 | .element-delete:active { 369 | color: red; 370 | } 371 | 372 | .add-button { 373 | width: 105px; 374 | border-radius: 0px 8px 8px 8px; 375 | border: var(--light-stroke) solid #000; 376 | background: #F9F9F9; 377 | box-shadow: 1px 1px 0px 0px #000; 378 | padding: 2px; 379 | padding-left: 2px; 380 | box-sizing: border-box; 381 | display: flex; 382 | align-items: center; 383 | justify-content: center; 384 | cursor: pointer; 385 | user-select: none; 386 | } 387 | 388 | .add-button-plus { 389 | font-family: "Consolas"; 390 | font-size: 18px; 391 | margin-left: 6px; 392 | } 393 | 394 | .add-button-empty { 395 | margin-top: 7px; 396 | } 397 | 398 | #modal-container { 399 | position: fixed; 400 | top: 0; 401 | left: 0; 402 | width: 100%; 403 | height: 100%; 404 | padding: 0 8% 0 8%; 405 | box-sizing: border-box; 406 | background: rgba(0,0,0,0.1); 407 | backdrop-filter: blur(5px); 408 | display: flex; 409 | flex-direction: column; 410 | justify-content: center; 411 | align-items: center; 412 | display: none; 413 | } 414 | 415 | #add-word-modal > .window-top-bar { 416 | background: linear-gradient(180deg, #dcd0ff 0%, #FFF 100%); 417 | } 418 | 419 | .settings-text-input, .settings-block { 420 | width: 100%; 421 | min-height: 32px; 422 | margin-top: 12px; 423 | border: var(--light-stroke) solid black; 424 | box-sizing: border-box; 425 | font-family: Epilogue; 426 | font-size: 16px; 427 | } 428 | 429 | .settings-text-input { 430 | padding-left: 10px; 431 | } 432 | 433 | .settings-block { 434 | padding-left: 10px; 435 | display: flex; 436 | align-items: center; 437 | justify-content: space-between; 438 | } 439 | 440 | .settings-toggle { 441 | height: 20px; 442 | margin-right: 5px; 443 | border-width: var(--light-stroke); 444 | } 445 | 446 | .settings-toggle-inner::before { 447 | height: 23px; 448 | border-width: var(--light-stroke); 449 | bottom: calc(-1 * var(--light-stroke)); 450 | } 451 | 452 | .keyword-input { 453 | border-radius: 5px 5px 0 0; 454 | } 455 | 456 | .button-block { 457 | width: 100%; 458 | display: flex; 459 | margin-top: 12px; 460 | } 461 | 462 | .submit-button, .cancel-button { 463 | /* width: 105px; */ 464 | height: 32px; 465 | flex-grow: 1; 466 | border: var(--light-stroke) solid #000; 467 | background: #F9F9F9; 468 | box-shadow: 1px 1px 0px 0px #000; 469 | padding: 2px; 470 | padding-left: 2px; 471 | box-sizing: border-box; 472 | display: flex; 473 | align-items: center; 474 | justify-content: center; 475 | cursor: pointer; 476 | user-select: none; 477 | } 478 | 479 | .submit-button { 480 | border-radius: 0 8px 8px 8px; 481 | background: linear-gradient(180deg, #e1fdff 0%, rgba(249, 249, 249, 0.00) 100%); 482 | } 483 | 484 | .cancel-button { 485 | margin-left: 5px; 486 | border-radius: 8px 0 8px 8px; 487 | background: linear-gradient(180deg, #ffe6e6 0%, rgba(249, 249, 249, 0.00) 100%); 488 | } 489 | 490 | #footer { 491 | width: 100%; 492 | height: 90px; 493 | display: flex; 494 | flex-direction: column; 495 | justify-content: center; 496 | align-items: center; 497 | } 498 | 499 | #settings-window > .window-top-bar { 500 | background: linear-gradient(180deg, #BCE7FF 0%, #FFF 100%); 501 | } 502 | 503 | #settings-window-content > .settings-block:first-child { 504 | margin-top: 0px; 505 | } 506 | 507 | .global-settings-block { 508 | padding: 8px; 509 | } 510 | 511 | .vertical-settings-block { 512 | display: flex; 513 | flex-direction: column; 514 | align-items: baseline; 515 | } 516 | 517 | .settings-text { 518 | font-size: 16px; 519 | box-sizing: border-box; 520 | } 521 | 522 | .settings-select { 523 | height: 32px; 524 | text-overflow: ellipsis; 525 | font-size: 16px; 526 | width: 100%; 527 | border: var(--light-stroke) solid #000; 528 | background: #EAFEFF; 529 | padding-left: 6px; 530 | padding-right: 19px; 531 | box-sizing: border-box; 532 | appearance: none; 533 | } 534 | 535 | .select-container { 536 | display: flex; 537 | justify-content: center; 538 | align-items: center; 539 | margin-top: 5px; 540 | width: 100%; 541 | } 542 | 543 | .select-container:after { 544 | width: 0; 545 | content: "▼"; 546 | font-size: 10px; 547 | transform: translateX(-20px); 548 | margin-top: 1px; 549 | pointer-events: none; 550 | } 551 | 552 | #donations-window > .window-top-bar { 553 | background: linear-gradient(180deg, #f4d7ff 0%, #FFF 100%); 554 | } 555 | 556 | #donations-window-content { 557 | align-items: center; 558 | } 559 | 560 | #donate { 561 | width: 100%; 562 | height: 32px; 563 | border: var(--light-stroke) solid #000; 564 | background: #F9F9F9; 565 | box-shadow: 1px 1px 0px 0px #000; 566 | border-radius: 8px; 567 | padding: 2px; 568 | padding-top: 3px; 569 | box-sizing: border-box; 570 | text-align: center; 571 | display: flex; 572 | align-items: center; 573 | justify-content: center; 574 | background: linear-gradient(to bottom right, #fce4ff 0%, #FFF 100%); 575 | cursor: pointer; 576 | } 577 | 578 | #donate-link { 579 | text-decoration: none; 580 | color: black; 581 | width: 100%; 582 | margin-top: 12px; 583 | } 584 | -------------------------------------------------------------------------------- /Mutable/Shared (App)/Resources/foil.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/Mutable/Shared (App)/Resources/foil.jpg -------------------------------------------------------------------------------- /Mutable/Shared (App)/Resources/ios-extension-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/Mutable/Shared (App)/Resources/ios-extension-example.png -------------------------------------------------------------------------------- /Mutable/Shared (App)/Resources/ios-extension-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/Mutable/Shared (App)/Resources/ios-extension-settings.png -------------------------------------------------------------------------------- /Mutable/Shared (App)/Resources/ios-mutable-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/Mutable/Shared (App)/Resources/ios-mutable-settings.png -------------------------------------------------------------------------------- /Mutable/Shared (App)/Resources/ios-safari-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/Mutable/Shared (App)/Resources/ios-safari-menu.png -------------------------------------------------------------------------------- /Mutable/Shared (App)/Resources/ios-safari-settings.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/Mutable/Shared (App)/Resources/ios-safari-settings.jpg -------------------------------------------------------------------------------- /Mutable/Shared (App)/Resources/ios-settings.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/Mutable/Shared (App)/Resources/ios-settings.jpg -------------------------------------------------------------------------------- /Mutable/Shared (App)/Resources/mac-extension-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/Mutable/Shared (App)/Resources/mac-extension-example.png -------------------------------------------------------------------------------- /Mutable/Shared (App)/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Shared (App) 4 | // 5 | // Created by Idrees Hassan on 8/11/23. 6 | // 7 | 8 | import WebKit 9 | 10 | #if os(iOS) 11 | import UIKit 12 | typealias PlatformViewController = UIViewController 13 | #elseif os(macOS) 14 | import Cocoa 15 | import SafariServices 16 | typealias PlatformViewController = NSViewController 17 | #endif 18 | 19 | let extensionBundleIdentifier = "com.idreesinc.Mutable.Extension" 20 | 21 | class ViewController: PlatformViewController, WKNavigationDelegate, WKScriptMessageHandler { 22 | 23 | @IBOutlet var webView: WKWebView! 24 | 25 | override func viewDidLoad() { 26 | super.viewDidLoad() 27 | 28 | self.webView.navigationDelegate = self 29 | 30 | #if os(iOS) 31 | // self.webView.scrollView.isScrollEnabled = false 32 | #endif 33 | 34 | self.webView.configuration.userContentController.add(self, name: "controller") 35 | 36 | self.webView.loadFileURL(Bundle.main.url(forResource: "Main", withExtension: "html")!, allowingReadAccessTo: Bundle.main.resourceURL!) 37 | } 38 | 39 | func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { 40 | #if os(iOS) 41 | webView.evaluateJavaScript("show('ios')") 42 | #elseif os(macOS) 43 | webView.evaluateJavaScript("show('mac')") 44 | 45 | SFSafariExtensionManager.getStateOfSafariExtension(withIdentifier: extensionBundleIdentifier) { (state, error) in 46 | guard let state = state, error == nil else { 47 | // Insert code to inform the user that something went wrong. 48 | return 49 | } 50 | 51 | DispatchQueue.main.async { 52 | if #available(macOS 13, *) { 53 | webView.evaluateJavaScript("show('mac', \(state.isEnabled), true)") 54 | } else { 55 | webView.evaluateJavaScript("show('mac', \(state.isEnabled), false)") 56 | } 57 | } 58 | } 59 | #endif 60 | } 61 | 62 | func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { 63 | if (message.body as! String != "open-preferences") { 64 | return; 65 | } 66 | #if os(iOS) 67 | guard let url = URL(string: UIApplication.openSettingsURLString) else { 68 | return 69 | } 70 | UIApplication.shared.open(url) 71 | #elseif os(macOS) 72 | SFSafariApplication.showPreferencesForExtension(withIdentifier: extensionBundleIdentifier) { error in 73 | guard error == nil else { 74 | // Insert code to inform the user that something went wrong. 75 | return 76 | } 77 | 78 | DispatchQueue.main.async { 79 | NSApplication.shared.terminate(nil) 80 | } 81 | } 82 | #endif 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /Mutable/Shared (Extension)/SafariWebExtensionHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SafariWebExtensionHandler.swift 3 | // Shared (Extension) 4 | // 5 | // Created by Idrees Hassan on 8/11/23. 6 | // 7 | 8 | import SafariServices 9 | import os.log 10 | 11 | let SFExtensionMessageKey = "message" 12 | 13 | class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { 14 | 15 | func beginRequest(with context: NSExtensionContext) { 16 | let item = context.inputItems[0] as! NSExtensionItem 17 | let message = item.userInfo?[SFExtensionMessageKey] 18 | os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@", message as! CVarArg) 19 | 20 | let response = NSExtensionItem() 21 | response.userInfo = [ SFExtensionMessageKey: [ "Response to": message ] ] 22 | 23 | context.completeRequest(returningItems: [response], completionHandler: nil) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Mutable/iOS (App)/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // iOS (App) 4 | // 5 | // Created by Idrees Hassan on 8/11/23. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 21 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Mutable/iOS (App)/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /Mutable/iOS (App)/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /Mutable/iOS (App)/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LSApplicationCategoryType 6 | public.app-category.social-networking 7 | SFSafariWebExtensionConverterVersion 8 | 14.3.1 9 | UIApplicationSceneManifest 10 | 11 | UIApplicationSupportsMultipleScenes 12 | 13 | UISceneConfigurations 14 | 15 | UIWindowSceneSessionRoleApplication 16 | 17 | 18 | UISceneConfigurationName 19 | Default Configuration 20 | UISceneDelegateClassName 21 | $(PRODUCT_MODULE_NAME).SceneDelegate 22 | UISceneStoryboardFile 23 | Main 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Mutable/iOS (App)/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // iOS (App) 4 | // 5 | // Created by Idrees Hassan on 8/11/23. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 15 | guard let _ = (scene as? UIWindowScene) else { return } 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Mutable/iOS (Extension)/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.Safari.web-extension 9 | NSExtensionPrincipalClass 10 | $(PRODUCT_MODULE_NAME).SafariWebExtensionHandler 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Mutable/macOS (App)/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // macOS (App) 4 | // 5 | // Created by Idrees Hassan on 8/11/23. 6 | // 7 | 8 | import Cocoa 9 | 10 | @main 11 | class AppDelegate: NSObject, NSApplicationDelegate { 12 | 13 | func applicationDidFinishLaunching(_ notification: Notification) { 14 | // Override point for customization after application launch. 15 | } 16 | 17 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 18 | return true 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Mutable/macOS (App)/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /Mutable/macOS (App)/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LSApplicationCategoryType 6 | public.app-category.social-networking 7 | SFSafariWebExtensionConverterVersion 8 | 14.3.1 9 | 10 | 11 | -------------------------------------------------------------------------------- /Mutable/macOS (App)/Mutable.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Mutable/macOS (Extension)/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.Safari.web-extension 9 | NSExtensionPrincipalClass 10 | $(PRODUCT_MODULE_NAME).SafariWebExtensionHandler 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Mutable/macOS (Extension)/Mutable.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mutable 2 | 3 | ## hide* content across the web and save your sanity 4 | 5 | _*or replace with cute pics of 🐱, 🐶, or 🦔, up to you_ 6 | 7 | 8 | Take control of the websites you browse every day and eliminate the content you don't want to see! Mutable helps you mute unwanted content across multiple sites at once. Just pick the keywords you want to hide and Mutable will blur, hide, or replace any posts it finds containing those terms! 9 | 10 | 11 | ![updated-chrome](https://github.com/IdreesInc/Mutable/assets/4875804/deb34901-e614-4468-a406-7afcedd0dfba) 12 | 13 | ## Platforms 14 | 15 | - [iOS and MacOS App](https://apps.apple.com/app/id6462700419) 16 | - [Chrome Extension](https://chrome.google.com/webstore/detail/mutable/daniknejbbnjhfmcgolfpaedkpcfkaop) 17 | - [Microsoft Edge Addon](https://microsoftedge.microsoft.com/addons/detail/mutable/eljpclpdfpmlicjlldnfeehfpfhljgbf) 18 | - [Firefox Extension](https://addons.mozilla.org/en-US/firefox/addon/mutable/) 19 | 20 | ## Features 21 | 22 | - Control what you see (and what you don't) across the entire web! 23 | - Create keyword mute lists that work across all social media sites, including Twitter/X, Facebook, Mastodon, Reddit, Threads, and Bluesky 24 | - Choose between blurring out posts, blurring with a preview, or hiding them altogether 25 | - You can even replace posts with cute images of kittens, puppies, or hedgehogs for that additional serotonin! 26 | 27 | ![updated-chrome-preview](https://github.com/IdreesInc/Mutable/assets/4875804/71e0ee6f-c4b8-4ba4-97df-e619dab740ab) 28 | 29 | Mutable is still being developed and I would love to hear from you. For questions, comments, or concerns, feel free to reach out! 30 | -------------------------------------------------------------------------------- /application.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /// 3 | 4 | const kittens = [ 5 | { 6 | "name": "1", 7 | "src": "https://unsplash.com/photos/Y0WXj3xqJz0" 8 | }, 9 | { 10 | "name": "2", 11 | "src": "https://unsplash.com/photos/WNoYQaAtCfo" 12 | }, 13 | { 14 | "name": "3", 15 | "src": "https://unsplash.com/photos/uhnZZUaTIOs" 16 | }, 17 | { 18 | "name": "4", 19 | "src": "https://unsplash.com/photos/bhonzdJMVjY" 20 | }, 21 | { 22 | "name": "5", 23 | "src": "https://unsplash.com/photos/lAjk-UJa85c" 24 | }, 25 | { 26 | "name": "6", 27 | "src": "https://unsplash.com/photos/klH-f7mw2Ws" 28 | }, 29 | { 30 | "name": "7", 31 | "src": "https://unsplash.com/photos/_867Jy8LCkI" 32 | }, 33 | { 34 | "name": "8", 35 | "src": "https://unsplash.com/photos/7nPxC8id3Ss" 36 | }, 37 | { 38 | "name": "9", 39 | "src": "https://unsplash.com/photos/eSceitc-s30" 40 | }, 41 | { 42 | "name": "10", 43 | "src": "https://unsplash.com/photos/MJymGVEazyY" 44 | }, 45 | { 46 | "name": "11", 47 | "src": "https://unsplash.com/photos/jMOSPjsZXtg" 48 | }, 49 | { 50 | "name": "12", 51 | "src": "https://unsplash.com/photos/ZJWUZ6JKAF0" 52 | }, 53 | { 54 | "name": "13", 55 | "src": "https://unsplash.com/photos/rxvocsh0DUw" 56 | }, 57 | { 58 | "name": "14", 59 | "src": "https://unsplash.com/photos/OmWw4SfE2DI" 60 | }, 61 | { 62 | "name": "15", 63 | "src": "https://unsplash.com/photos/Px2Y-sio6-c" 64 | }, 65 | { 66 | "name": "16", 67 | "src": "https://unsplash.com/photos/bXnoQq1sIBM" 68 | }, 69 | { 70 | "name": "17", 71 | "src": "https://unsplash.com/photos/WHTLPrTPBk0" 72 | }, 73 | { 74 | "name": "18", 75 | "src": "https://unsplash.com/photos/7qNZYwUbPic" 76 | }, 77 | { 78 | "name": "19", 79 | "src": "https://unsplash.com/photos/VpzlatUJFfo" 80 | }, 81 | { 82 | "name": "20", 83 | "src": "https://unsplash.com/photos/SAKLELG-pO8" 84 | } 85 | ]; 86 | const puppies = [ 87 | { 88 | "name": "1", 89 | "src": "https://unsplash.com/photos/fk4tiMlDFF0" 90 | }, 91 | { 92 | "name": "2", 93 | "src": "https://unsplash.com/photos/9LkqymZFLrE" 94 | }, 95 | { 96 | "name": "3", 97 | "src": "https://unsplash.com/photos/atOlntWcO4k" 98 | }, 99 | { 100 | "name": "4", 101 | "src": "https://unsplash.com/photos/Qb7D1xw28Co" 102 | }, 103 | { 104 | "name": "5", 105 | "src": "https://unsplash.com/photos/k4vhuUHv08o" 106 | }, 107 | { 108 | "name": "6", 109 | "src": "https://unsplash.com/photos/eoqnr8ikwFE" 110 | }, 111 | { 112 | "name": "7", 113 | "src": "https://unsplash.com/photos/wxfZi8eYdEk" 114 | }, 115 | { 116 | "name": "8", 117 | "src": "https://unsplash.com/photos/m8v7BDLV8yE" 118 | }, 119 | { 120 | "name": "9", 121 | "src": "https://unsplash.com/photos/z_U6bPp_Rjg" 122 | }, 123 | { 124 | "name": "10", 125 | "src": "https://unsplash.com/photos/zr0beNrnvgQ" 126 | }, 127 | { 128 | "name": "11", 129 | "src": "https://unsplash.com/photos/vMNr5gbeeTk" 130 | }, 131 | { 132 | "name": "12", 133 | "src": "https://unsplash.com/photos/ORzQG8jKOO4" 134 | }, 135 | { 136 | "name": "13", 137 | "src": "https://unsplash.com/photos/TzjMd7i5WQI" 138 | }, 139 | { 140 | "name": "14", 141 | "src": "https://unsplash.com/photos/DsGeUBaJPwc" 142 | }, 143 | { 144 | "name": "15", 145 | "src": "https://unsplash.com/photos/JK8w20Zantg" 146 | }, 147 | { 148 | "name": "16", 149 | "src": "https://unsplash.com/photos/oO5MBxRCadY" 150 | }, 151 | { 152 | "name": "17", 153 | "src": "https://unsplash.com/photos/6uPsI12Xqjk" 154 | }, 155 | { 156 | "name": "18", 157 | "src": "https://unsplash.com/photos/AsCYNjt6IF0" 158 | }, 159 | { 160 | "name": "19", 161 | "src": "https://unsplash.com/photos/7T8ammfU8-s" 162 | }, 163 | { 164 | "name": "20", 165 | "src": "https://unsplash.com/photos/VQPD1fc_Y8g" 166 | } 167 | ]; 168 | const hedgehogs = [ 169 | { 170 | "name": "1", 171 | "src": "https://unsplash.com/photos/D09Nooc3XQw" 172 | }, 173 | { 174 | "name": "2", 175 | "src": "https://unsplash.com/photos/OMCgkp1oZ3Q" 176 | }, 177 | { 178 | "name": "3", 179 | "src": "https://unsplash.com/photos/aM7r5lqKhiY" 180 | }, 181 | { 182 | "name": "4", 183 | "src": "https://unsplash.com/photos/eHMLxD3W_m4" 184 | }, 185 | { 186 | "name": "5", 187 | "src": "https://unsplash.com/photos/8wkkhqEYN0A" 188 | }, 189 | { 190 | "name": "6", 191 | "src": "https://unsplash.com/photos/k_E7DpVgftw" 192 | }, 193 | { 194 | "name": "7", 195 | "src": "https://unsplash.com/photos/iJ9o00UeAWk" 196 | }, 197 | { 198 | "name": "8", 199 | "src": "https://unsplash.com/photos/GXMr7BadXQo" 200 | }, 201 | { 202 | "name": "9", 203 | "src": "https://unsplash.com/photos/dktikEL51dM" 204 | }, 205 | { 206 | "name": "10", 207 | "src": "https://unsplash.com/photos/g5tj75PJ4w4" 208 | }, 209 | { 210 | "name": "11", 211 | "src": "https://unsplash.com/photos/MrCsc_ZL5dU" 212 | }, 213 | { 214 | "name": "12", 215 | "src": "https://unsplash.com/photos/QWELA5fl1KY" 216 | }, 217 | { 218 | "name": "13", 219 | "src": "https://unsplash.com/photos/6apx2KP_SEo" 220 | }, 221 | { 222 | "name": "14", 223 | "src": "https://unsplash.com/photos/zjHbH8vkhgk" 224 | }, 225 | { 226 | "name": "15", 227 | "src": "https://unsplash.com/photos/S6BR5GOqB4A" 228 | }, 229 | { 230 | "name": "16", 231 | "src": "https://unsplash.com/photos/60QYdXjUd8o" 232 | }, 233 | { 234 | "name": "17", 235 | "src": "https://unsplash.com/photos/lO9UKYIbV_g" 236 | }, 237 | { 238 | "name": "18", 239 | "src": "https://unsplash.com/photos/qbFyc2w9q90" 240 | }, 241 | { 242 | "name": "19", 243 | "src": "https://unsplash.com/photos/jczICIeZtos" 244 | }, 245 | { 246 | "name": "20", 247 | "src": "https://unsplash.com/photos/JnZWFenBmx0" 248 | } 249 | ]; 250 | 251 | /** @type {Settings} */ 252 | let settings = new Settings(); 253 | let shuffledBag = []; 254 | /** @type {HTMLElement|undefined} */ 255 | let debugWindow; 256 | /** @type {HTMLElement|undefined} */ 257 | let debugHost; 258 | /** @type {HTMLElement|undefined} */ 259 | let debugParsers; 260 | /** @type {HTMLElement|undefined} */ 261 | let debugTotalPosts; 262 | /** @type {HTMLElement|undefined} */ 263 | let debugPostsMuted; 264 | 265 | let totalPostCount = 0; 266 | let totalPostsMuted = 0; 267 | 268 | init(); 269 | 270 | function init() { 271 | debug("Mutable has been loaded successfully!"); 272 | getSettings((result) => { 273 | console.log("Settings loaded"); 274 | settings = result; 275 | initParsing(); 276 | }, (msg) => { 277 | console.error(msg); 278 | console.log("No settings found, creating default settings"); 279 | initParsing(); 280 | }); 281 | subscribeToSettings((result) => { 282 | debug("Settings updated"); 283 | resetPosts(); 284 | settings = result; 285 | if (settings.debugMode) { 286 | createDebugWindow(); 287 | } else { 288 | removeDebugWindow(); 289 | } 290 | }); 291 | } 292 | 293 | /** 294 | * Create the debug window element. 295 | */ 296 | function createDebugWindow() { 297 | if (document.getElementById("mutable-debug-window")) { 298 | document.getElementById("mutable-debug-window")?.remove(); 299 | } 300 | debugWindow = document.createElement("div"); 301 | debugWindow.id = "mutable-debug-window"; 302 | const title = document.createElement("div"); 303 | title.textContent = "Mutable Debug Window"; 304 | title.classList.add("mutable-debug-title"); 305 | debugWindow.appendChild(title); 306 | debugHost = document.createElement("div"); 307 | debugHost.classList.add("mutable-debug-item"); 308 | debugWindow.appendChild(debugHost); 309 | debugParsers = document.createElement("div"); 310 | debugParsers.classList.add("mutable-debug-item"); 311 | debugWindow.appendChild(debugParsers); 312 | debugTotalPosts = document.createElement("div"); 313 | debugTotalPosts.classList.add("mutable-debug-item"); 314 | debugWindow.appendChild(debugTotalPosts); 315 | debugPostsMuted = document.createElement("div"); 316 | debugPostsMuted.classList.add("mutable-debug-item"); 317 | debugWindow.appendChild(debugPostsMuted); 318 | document.body.appendChild(debugWindow); 319 | debug("Debug window created"); 320 | } 321 | 322 | /** 323 | * Update the values in the debug window. 324 | * @param {string} parserNames 325 | */ 326 | function updateDebugWindow(parserNames) { 327 | if (!settings.debugMode) { 328 | removeDebugWindow(); 329 | return; 330 | } 331 | if (!debugWindow) { 332 | createDebugWindow(); 333 | } 334 | if (debugHost) { 335 | debugHost.textContent = "Host: " + window.location.hostname; 336 | } 337 | if (debugParsers) { 338 | debugParsers.textContent = "Parsers: " + parserNames; 339 | } 340 | if (debugTotalPosts) { 341 | debugTotalPosts.textContent = "Posts Found: " + totalPostCount; 342 | } 343 | if (debugPostsMuted) { 344 | debugPostsMuted.textContent = "Posts Muted: " + totalPostsMuted; 345 | } 346 | } 347 | 348 | /** 349 | * Remove the debug window element. 350 | */ 351 | function removeDebugWindow() { 352 | if (debugWindow) { 353 | debugWindow.remove(); 354 | debugWindow = undefined; 355 | debug("Debug window removed"); 356 | } 357 | // Remove the debug class from all posts 358 | for (let post of document.querySelectorAll(".mutable-debug-post")) { 359 | post.classList.remove("mutable-debug-post"); 360 | } 361 | } 362 | 363 | function initParsing() { 364 | // Every second, parse the page for new posts 365 | setInterval(() => { 366 | parse(); 367 | }, 1000); 368 | // Also parse whenever the page is scrolled (but only once per second) 369 | let scrollTimeout = null; 370 | document.addEventListener("wheel", () => { 371 | if (scrollTimeout) { 372 | clearTimeout(scrollTimeout); 373 | } else { 374 | parse(); 375 | } 376 | scrollTimeout = setTimeout(() => { 377 | parse(); 378 | }, 100); 379 | }); 380 | parse(); 381 | } 382 | 383 | /** 384 | * Parse the page for posts and hide any that match the mute patterns. 385 | */ 386 | function parse() { 387 | let host = window.location.hostname; 388 | if (!settings.mutableEnabled) { 389 | debug("Mutable is disabled"); 390 | updateDebugWindow("Disabled globally"); 391 | return; 392 | } 393 | if (!settings.isSiteEnabled(host)) { 394 | debug("Site is disabled"); 395 | updateDebugWindow("Disabled for site"); 396 | return; 397 | } 398 | if (!settings.enabledByDefault && !settings.isSiteExplicitlyEnabled(host)) { 399 | debug("Mutable is in whitelist mode and this site is not explicitly enabled"); 400 | updateDebugWindow("Not enabled for site"); 401 | return; 402 | } 403 | let posts = []; 404 | /** @type {string[]} */ 405 | let parsersApplied = []; 406 | // First check if any specialized parser applies to this page 407 | for (let parser of Parser.specializedParsers()) { 408 | if (parser.appliesToPage()) { 409 | debug(`Applying parser: ${parser.parserName}`); 410 | posts.push(...parser.getPosts()); 411 | parsersApplied.push(parser.parserName); 412 | } 413 | } 414 | totalPostCount += posts.length; 415 | // If the specialized parser hasn't found any posts on this site so far, use the universal parser 416 | if (totalPostCount === 0) { 417 | debug(`Applying universal parser`); 418 | posts.push(...UniversalParser.getPosts()); 419 | parsersApplied.push(UniversalParser.parserName); 420 | } 421 | if (parsersApplied.length > 0) { 422 | debug(`Found ${posts.length} posts on ${parsersApplied.join(", ")}`); 423 | for (let post of posts) { 424 | post.postElement.setAttribute(PROCESSED_INDICATOR, "true"); 425 | if (settings.debugMode && !post.postElement.classList.contains("mutable-debug-post")) { 426 | // console.log(post.authorHandle()); 427 | // console.log(post.authorName()); 428 | // console.log(post.postContents()); 429 | if (!parsersApplied.includes(UniversalParser.parserName)) { 430 | post.postElement.classList.add("mutable-debug-post"); 431 | // Add tooltip with debug info 432 | post.postElement.setAttribute("title", `Author: ${post.authorName()}\nHandle: ${post.authorHandle()}\nAlt: ${post.mediaAltText()}\nContents: ${post.postContents()?.substring(0, 100)}`); 433 | } 434 | } 435 | let matchText = match(post); 436 | if (matchText !== null) { 437 | hidePost(post.postElement, matchText); 438 | totalPostsMuted++; 439 | } 440 | } 441 | } 442 | updateDebugWindow(parsersApplied.length > 0 ? parsersApplied.join(", ") : "none"); 443 | } 444 | 445 | /** 446 | * Determine whether the provided post matches any of the mute patterns. 447 | * @param {Post} post The post to check 448 | * @returns {string|null} The pattern that matched, or null if no pattern matched 449 | */ 450 | function match(post) { 451 | const contents = post.postContents(); 452 | const groups = settings.getGroupsList(); 453 | if (contents) { 454 | for (let group of groups) { 455 | for (let pattern of group.patterns) { 456 | if (pattern.isMatch(contents)) { 457 | return pattern.plaintext(); 458 | } 459 | } 460 | } 461 | } 462 | const altTexts = post.mediaAltText(); 463 | if (altTexts) { 464 | for (let altText of altTexts) { 465 | for (let group of groups) { 466 | for (let pattern of group.patterns) { 467 | if (pattern.isMatch(altText)) { 468 | return pattern.plaintext(); 469 | } 470 | } 471 | } 472 | } 473 | } 474 | return null; 475 | } 476 | 477 | /** 478 | * @param {HTMLElement} element 479 | * @param {string} reason 480 | */ 481 | function hidePost(element, reason) { 482 | if (settings.globalMuteAction === "blur") { 483 | element.classList.add("mutable-blur"); 484 | element.addEventListener("click", function (event) { 485 | if (element.classList.contains("mutable-blur")) { 486 | element.classList.remove("mutable-blur"); 487 | event.stopPropagation(); 488 | event.preventDefault(); 489 | // Remove from children too 490 | for (let child of element.querySelectorAll(".mutable-blur")) { 491 | if (child instanceof HTMLElement) { 492 | child.classList.remove("mutable-blur"); 493 | } 494 | } 495 | } 496 | }); 497 | } else if (settings.globalMuteAction === "blur-preview") { 498 | element.classList.add("mutable-blur-explanation"); 499 | element.setAttribute("data-mutable-match", reason); 500 | element.addEventListener("click", function (event) { 501 | if (element.classList.contains("mutable-blur-explanation")) { 502 | element.classList.remove("mutable-blur-explanation"); 503 | event.stopPropagation(); 504 | event.preventDefault(); 505 | // Remove from children too 506 | for (let child of element.querySelectorAll(".mutable-blur-explanation")) { 507 | if (child instanceof HTMLElement) { 508 | child.classList.remove("mutable-blur-explanation"); 509 | } 510 | } 511 | } 512 | }); 513 | } else if (settings.globalMuteAction === "hide") { 514 | element.classList.add("mutable-hide"); 515 | } else if (settings.globalMuteAction === "kittens") { 516 | element.classList.add("mutable-image-overlay"); 517 | const kittenSrc = chrome.runtime.getURL(`./images/kittens/${kittens[getIndexFromBag(kittens.length)].name}.jpg`); 518 | element.style.setProperty("--overlay-image", `url("${kittenSrc}")`); 519 | element.addEventListener("click", function (event) { 520 | removeOverlay(element, event); 521 | }); 522 | } else if (settings.globalMuteAction === "puppies") { 523 | element.classList.add("mutable-image-overlay"); 524 | const puppySrc = chrome.runtime.getURL(`./images/puppies/${puppies[getIndexFromBag(puppies.length)].name}.jpg`); 525 | element.style.setProperty("--overlay-image", `url("${puppySrc}")`); 526 | element.addEventListener("click", function (event) { 527 | removeOverlay(element, event); 528 | }); 529 | } else if (settings.globalMuteAction === "hedgehogs") { 530 | element.classList.add("mutable-image-overlay"); 531 | const hedgehogSrc = chrome.runtime.getURL(`./images/hedgehogs/${hedgehogs[getIndexFromBag(hedgehogs.length)].name}.jpg`); 532 | element.style.setProperty("--overlay-image", `url("${hedgehogSrc}")`); 533 | element.addEventListener("click", function (event) { 534 | removeOverlay(element, event); 535 | }); 536 | } else { 537 | console.error(`Unknown global mute action, defaulting to 'blur': ${settings.globalMuteAction}`); 538 | element.classList.add("mutable-blur"); 539 | element.addEventListener("click", function (event) { 540 | if (element.classList.contains("mutable-blur")) { 541 | element.classList.remove("mutable-blur"); 542 | event.stopPropagation(); 543 | event.preventDefault(); 544 | } 545 | }); 546 | } 547 | } 548 | 549 | /** 550 | * @param {HTMLElement} element 551 | * @param {MouseEvent} event 552 | */ 553 | function removeOverlay(element, event) { 554 | if (element.classList.contains("mutable-image-overlay")) { 555 | element.classList.remove("mutable-image-overlay"); 556 | element.style.setProperty("--overlay-image", ""); 557 | event.stopPropagation(); 558 | event.preventDefault(); 559 | // Remove from children too 560 | for (let child of element.querySelectorAll(".mutable-image-overlay")) { 561 | if (child instanceof HTMLElement) { 562 | child.classList.remove("mutable-image-overlay"); 563 | child.style.setProperty("--overlay-image", ""); 564 | } 565 | } 566 | } 567 | } 568 | 569 | /** 570 | * Get a random index from the bag. 571 | * @param {number} listSize 572 | * @returns {number} A random index less than listSize 573 | */ 574 | function getIndexFromBag(listSize) { 575 | if (shuffledBag.length === 0) { 576 | for (let i = 0; i < listSize; i++) { 577 | shuffledBag.push(i); 578 | } 579 | shuffledBag = shuffle(shuffledBag); 580 | } 581 | return shuffledBag.pop() % listSize; 582 | } 583 | 584 | /** 585 | * Fisher-Yates shuffle. 586 | * https://stackoverflow.com/a/2450976/1330144 587 | * @param {Array.} array 588 | * @returns {Array.} 589 | * @template T 590 | */ 591 | function shuffle(array) { 592 | let currentIndex = array.length, randomIndex; 593 | // While there remain elements to shuffle. 594 | while (currentIndex != 0) { 595 | // Pick a remaining element. 596 | randomIndex = Math.floor(Math.random() * currentIndex); 597 | currentIndex--; 598 | // And swap it with the current element. 599 | [array[currentIndex], array[randomIndex]] = [ 600 | array[randomIndex], array[currentIndex]]; 601 | } 602 | return array; 603 | } 604 | 605 | /** 606 | * Reset all posts that have been hidden by Mutable. 607 | */ 608 | function resetPosts() { 609 | for (let post of document.querySelectorAll(`[${PROCESSED_INDICATOR}]`)) { 610 | post.removeAttribute(PROCESSED_INDICATOR); 611 | post.classList.remove("mutable-blur"); 612 | post.classList.remove("mutable-hide"); 613 | post.classList.remove("mutable-blur-explanation"); 614 | post.classList.remove("mutable-image-overlay"); 615 | } 616 | } 617 | 618 | /** 619 | * @param {any} message 620 | */ 621 | function log(message) { 622 | console.log(`Mutable: ${message}`); 623 | } 624 | 625 | /** 626 | * @param {any} message 627 | */ 628 | function debug(message) { 629 | if (settings.debugMode) { 630 | console.debug(`Mutable: ${message}`); 631 | } 632 | } 633 | 634 | /** 635 | * @param {any} message 636 | */ 637 | function error(message) { 638 | console.error(`Mutable: ${message}`); 639 | } 640 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Build Mutable for Firefox and Chrome (but not Safari) 3 | # python ./replace.py ./settings/settings.html 4 | zip -r mutable-packaged.zip . -x "Mutable/*" -x "*/.DS_Store" -x ".DS_Store" -x "build.sh" -x "replace.py" -x "mutable-packaged.zip" -x "node_modules/*" -x "Mutable.code-workspace" -x ".git/*" -x ".gitignore" -x ".gitattributes" 5 | # python ./replace.py ./settings/settings.html true -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | mutable.idreesinc.com -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | gem "jekyll" 2 | gem "webrick" 3 | gem "jekyll-remote-theme" -------------------------------------------------------------------------------- /docs/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | specs: 3 | addressable (2.8.7) 4 | public_suffix (>= 2.0.2, < 7.0) 5 | colorator (1.1.0) 6 | concurrent-ruby (1.3.4) 7 | em-websocket (0.5.3) 8 | eventmachine (>= 0.12.9) 9 | http_parser.rb (~> 0) 10 | eventmachine (1.2.7) 11 | ffi (1.17.0-arm64-darwin) 12 | forwardable-extended (2.6.0) 13 | http_parser.rb (0.8.0) 14 | i18n (1.8.11) 15 | concurrent-ruby (~> 1.0) 16 | jekyll (4.3.3) 17 | addressable (~> 2.4) 18 | colorator (~> 1.0) 19 | em-websocket (~> 0.5) 20 | i18n (~> 1.0) 21 | jekyll-sass-converter (>= 2.0, < 4.0) 22 | jekyll-watch (~> 2.0) 23 | kramdown (~> 2.3, >= 2.3.1) 24 | kramdown-parser-gfm (~> 1.0) 25 | liquid (~> 4.0) 26 | mercenary (>= 0.3.6, < 0.5) 27 | pathutil (~> 0.9) 28 | rouge (>= 3.0, < 5.0) 29 | safe_yaml (~> 1.0) 30 | terminal-table (>= 1.8, < 4.0) 31 | webrick (~> 1.7) 32 | jekyll-remote-theme (0.4.3) 33 | addressable (~> 2.0) 34 | jekyll (>= 3.5, < 5.0) 35 | jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0) 36 | rubyzip (>= 1.3.0, < 3.0) 37 | jekyll-sass-converter (2.1.0) 38 | sassc (> 2.0.1, < 3.0) 39 | jekyll-watch (2.2.1) 40 | listen (~> 3.0) 41 | kramdown (2.3.1) 42 | rexml 43 | kramdown-parser-gfm (1.1.0) 44 | kramdown (~> 2.0) 45 | liquid (4.0.3) 46 | listen (3.9.0) 47 | rb-fsevent (~> 0.10, >= 0.10.3) 48 | rb-inotify (~> 0.9, >= 0.9.10) 49 | mercenary (0.4.0) 50 | pathutil (0.16.2) 51 | forwardable-extended (~> 2.6) 52 | public_suffix (4.0.7) 53 | rb-fsevent (0.11.2) 54 | rb-inotify (0.11.1) 55 | ffi (~> 1.0) 56 | rexml (3.3.5) 57 | strscan 58 | rouge (3.26.1) 59 | rubyzip (2.3.2) 60 | safe_yaml (1.0.5) 61 | sassc (2.4.0) 62 | ffi (~> 1.9) 63 | strscan (3.1.0) 64 | terminal-table (2.0.0) 65 | unicode-display_width (~> 1.1, >= 1.1.1) 66 | unicode-display_width (1.8.0) 67 | webrick (1.7.0) 68 | 69 | PLATFORMS 70 | arm64-darwin 71 | 72 | DEPENDENCIES 73 | jekyll 74 | jekyll-remote-theme 75 | webrick 76 | 77 | BUNDLED WITH 78 | 2.5.17 79 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | remote_theme: IdreesInc/Jekyll-Project-Theme 2 | plugins: 3 | - jekyll-remote-theme -------------------------------------------------------------------------------- /docs/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/docs/icon.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | window_title: "Get Mutable - The Smart Content Blocker" 3 | title: "Mutable" 4 | subtitle: "hide* content across the web and save your sanity" 5 | annotation: "*or replace with cute pics of 🐱, 🐶, or 🦔, up to you" 6 | source_code: "https://github.com/IdreesInc/Mutable" 7 | background: "linear-gradient(90deg, #f4d7ff, #BCE7FF, #daf0d3, #eee2d1)" 8 | layout: "project" 9 | --- 10 | 11 | {% include link.html 12 | url="https://apps.apple.com/app/id6462700419" 13 | text="ios/mac app" 14 | %} 15 | 16 | {% include link.html 17 | url="https://chrome.google.com/webstore/detail/mutable/daniknejbbnjhfmcgolfpaedkpcfkaop" 18 | text="chrome extension" 19 | %} 20 | 21 | {% include link.html 22 | url="https://microsoftedge.microsoft.com/addons/detail/mutable/eljpclpdfpmlicjlldnfeehfpfhljgbf" 23 | text="microsoft edge addon" 24 | %} 25 | 26 | {% include link.html 27 | url="https://addons.mozilla.org/en-US/firefox/addon/mutable/" 28 | text="firefox extension" 29 | %} 30 | 31 | {% include link.html 32 | url="https://socials.idreesinc.com" 33 | text="follow for updates" 34 | %} -------------------------------------------------------------------------------- /docs/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/docs/preview.png -------------------------------------------------------------------------------- /docs/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Epilogue:wght@400;500&display=swap'); 2 | 3 | html { 4 | font-family: 'Epilogue', sans-serif; 5 | } 6 | 7 | html, body { 8 | margin: 0; 9 | padding: 0; 10 | } 11 | 12 | body { 13 | display: flex; 14 | flex-direction: column; 15 | color: black; 16 | } 17 | 18 | #foil { 19 | position: absolute; 20 | z-index: -1; 21 | width: 60%; 22 | height: 15%; 23 | left: 15%; 24 | top: 30%; 25 | transform: rotate(-20deg); 26 | background-image: url('foil.jpg'); 27 | background-size: cover; 28 | background-position: center; 29 | } 30 | 31 | #intro-page { 32 | height: 100vh; 33 | width: 65%; 34 | margin-left: 30%; 35 | display: flex; 36 | flex-direction: column; 37 | justify-content: center; 38 | } 39 | 40 | #title { 41 | font-size: 100px; 42 | } 43 | 44 | #subtitle { 45 | font-size: 35px; 46 | margin-top: 25px; 47 | } 48 | 49 | #links { 50 | font-family: Epilogue; 51 | font-size: 30px; 52 | margin-top: 30px; 53 | text-decoration-line: underline; 54 | } 55 | 56 | .link { 57 | margin-bottom: 20px; 58 | } 59 | 60 | .link a { 61 | color: black; 62 | text-decoration: none; 63 | } 64 | 65 | #github a { 66 | color: black; 67 | } 68 | 69 | #github { 70 | margin-top: 10px; 71 | } 72 | 73 | #annotation { 74 | font-size: 20px; 75 | position: absolute; 76 | bottom: 40px; 77 | right: 40px; 78 | margin-left: 20px; 79 | } 80 | 81 | #me { 82 | font-size: 16px; 83 | position: absolute; 84 | bottom: 12px; 85 | right: 40px; 86 | margin-left: 20px; 87 | } 88 | 89 | #me a { 90 | color: black; 91 | } 92 | 93 | @media screen and (orientation:portrait) and (max-width: 85vh) { 94 | #foil { 95 | width: 90%; 96 | height: 11%; 97 | left: 5%; 98 | top: 27%; 99 | transform: rotate(-20deg); 100 | } 101 | 102 | #intro-page { 103 | width: 80%; 104 | margin-top: -10%; 105 | margin-left: 10%; 106 | } 107 | 108 | #title { 109 | font-size: 16vw; 110 | } 111 | 112 | #subtitle { 113 | font-size: 6.4vw; 114 | margin-top: 50px; 115 | } 116 | 117 | #links { 118 | font-size: 6vw; 119 | margin-top: 110px; 120 | } 121 | 122 | .link { 123 | margin-bottom: 50px; 124 | } 125 | 126 | #annotation { 127 | font-size: 3vw; 128 | position: absolute; 129 | bottom: 6vw; 130 | right: 25px; 131 | } 132 | 133 | #me { 134 | font-size: 3vw; 135 | position: absolute; 136 | bottom: 1.5vw; 137 | right: 25px; 138 | } 139 | } -------------------------------------------------------------------------------- /icons/foil-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/icons/foil-1024.png -------------------------------------------------------------------------------- /icons/foil-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/icons/foil-128.png -------------------------------------------------------------------------------- /icons/foil-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/icons/foil-16.png -------------------------------------------------------------------------------- /icons/foil-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/icons/foil-256.png -------------------------------------------------------------------------------- /icons/foil-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/icons/foil-32.png -------------------------------------------------------------------------------- /icons/foil-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/icons/foil-48.png -------------------------------------------------------------------------------- /icons/foil-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/icons/foil-512.png -------------------------------------------------------------------------------- /icons/foil-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/icons/foil-64.png -------------------------------------------------------------------------------- /icons/foil-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/icons/foil-96.png -------------------------------------------------------------------------------- /icons/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/icons/icon-48.png -------------------------------------------------------------------------------- /icons/icon-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/icons/icon-96.png -------------------------------------------------------------------------------- /icons/icon-foil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/icons/icon-foil.png -------------------------------------------------------------------------------- /icons/logo-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/icons/logo-1024.png -------------------------------------------------------------------------------- /icons/logo-transparent-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/icons/logo-transparent-48.png -------------------------------------------------------------------------------- /icons/logo-transparent-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/icons/logo-transparent-96.png -------------------------------------------------------------------------------- /icons/logo-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/icons/logo-transparent.png -------------------------------------------------------------------------------- /images/hedgehogs/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/hedgehogs/1.jpg -------------------------------------------------------------------------------- /images/hedgehogs/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/hedgehogs/10.jpg -------------------------------------------------------------------------------- /images/hedgehogs/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/hedgehogs/11.jpg -------------------------------------------------------------------------------- /images/hedgehogs/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/hedgehogs/12.jpg -------------------------------------------------------------------------------- /images/hedgehogs/13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/hedgehogs/13.jpg -------------------------------------------------------------------------------- /images/hedgehogs/14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/hedgehogs/14.jpg -------------------------------------------------------------------------------- /images/hedgehogs/15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/hedgehogs/15.jpg -------------------------------------------------------------------------------- /images/hedgehogs/16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/hedgehogs/16.jpg -------------------------------------------------------------------------------- /images/hedgehogs/17.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/hedgehogs/17.jpg -------------------------------------------------------------------------------- /images/hedgehogs/18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/hedgehogs/18.jpg -------------------------------------------------------------------------------- /images/hedgehogs/19.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/hedgehogs/19.jpg -------------------------------------------------------------------------------- /images/hedgehogs/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/hedgehogs/2.jpg -------------------------------------------------------------------------------- /images/hedgehogs/20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/hedgehogs/20.jpg -------------------------------------------------------------------------------- /images/hedgehogs/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/hedgehogs/3.jpg -------------------------------------------------------------------------------- /images/hedgehogs/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/hedgehogs/4.jpg -------------------------------------------------------------------------------- /images/hedgehogs/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/hedgehogs/5.jpg -------------------------------------------------------------------------------- /images/hedgehogs/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/hedgehogs/6.jpg -------------------------------------------------------------------------------- /images/hedgehogs/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/hedgehogs/7.jpg -------------------------------------------------------------------------------- /images/hedgehogs/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/hedgehogs/8.jpg -------------------------------------------------------------------------------- /images/hedgehogs/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/hedgehogs/9.jpg -------------------------------------------------------------------------------- /images/kittens/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/kittens/1.jpg -------------------------------------------------------------------------------- /images/kittens/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/kittens/10.jpg -------------------------------------------------------------------------------- /images/kittens/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/kittens/11.jpg -------------------------------------------------------------------------------- /images/kittens/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/kittens/12.jpg -------------------------------------------------------------------------------- /images/kittens/13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/kittens/13.jpg -------------------------------------------------------------------------------- /images/kittens/14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/kittens/14.jpg -------------------------------------------------------------------------------- /images/kittens/15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/kittens/15.jpg -------------------------------------------------------------------------------- /images/kittens/16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/kittens/16.jpg -------------------------------------------------------------------------------- /images/kittens/17.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/kittens/17.jpg -------------------------------------------------------------------------------- /images/kittens/18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/kittens/18.jpg -------------------------------------------------------------------------------- /images/kittens/19.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/kittens/19.jpg -------------------------------------------------------------------------------- /images/kittens/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/kittens/2.jpg -------------------------------------------------------------------------------- /images/kittens/20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/kittens/20.jpg -------------------------------------------------------------------------------- /images/kittens/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/kittens/3.jpg -------------------------------------------------------------------------------- /images/kittens/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/kittens/4.jpg -------------------------------------------------------------------------------- /images/kittens/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/kittens/5.jpg -------------------------------------------------------------------------------- /images/kittens/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/kittens/6.jpg -------------------------------------------------------------------------------- /images/kittens/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/kittens/7.jpg -------------------------------------------------------------------------------- /images/kittens/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/kittens/8.jpg -------------------------------------------------------------------------------- /images/kittens/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/kittens/9.jpg -------------------------------------------------------------------------------- /images/puppies/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/puppies/1.jpg -------------------------------------------------------------------------------- /images/puppies/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/puppies/10.jpg -------------------------------------------------------------------------------- /images/puppies/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/puppies/11.jpg -------------------------------------------------------------------------------- /images/puppies/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/puppies/12.jpg -------------------------------------------------------------------------------- /images/puppies/13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/puppies/13.jpg -------------------------------------------------------------------------------- /images/puppies/14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/puppies/14.jpg -------------------------------------------------------------------------------- /images/puppies/15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/puppies/15.jpg -------------------------------------------------------------------------------- /images/puppies/16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/puppies/16.jpg -------------------------------------------------------------------------------- /images/puppies/17.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/puppies/17.jpg -------------------------------------------------------------------------------- /images/puppies/18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/puppies/18.jpg -------------------------------------------------------------------------------- /images/puppies/19.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/puppies/19.jpg -------------------------------------------------------------------------------- /images/puppies/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/puppies/2.jpg -------------------------------------------------------------------------------- /images/puppies/20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/puppies/20.jpg -------------------------------------------------------------------------------- /images/puppies/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/puppies/3.jpg -------------------------------------------------------------------------------- /images/puppies/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/puppies/4.jpg -------------------------------------------------------------------------------- /images/puppies/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/puppies/5.jpg -------------------------------------------------------------------------------- /images/puppies/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/puppies/6.jpg -------------------------------------------------------------------------------- /images/puppies/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/puppies/7.jpg -------------------------------------------------------------------------------- /images/puppies/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/puppies/8.jpg -------------------------------------------------------------------------------- /images/puppies/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/images/puppies/9.jpg -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Mutable - Customizable Content Filtering", 4 | "description": "Improve your browsing experience by muting keywords across the web! Now open source!", 5 | "version": "2.0.0", 6 | "homepage_url": "https://idreesinc.com", 7 | "icons": { 8 | "48": "icons/foil-48.png", 9 | "96": "icons/foil-96.png", 10 | "128": "icons/foil-128.png" 11 | }, 12 | "content_scripts": [ 13 | { 14 | "matches": [""], 15 | "js": ["jquery.js", "pako.js", "shared.js", "application.js"], 16 | "css": ["mutable-stylesheet.css"] 17 | } 18 | ], 19 | "action": { 20 | "default_popup": "settings/settings.html" 21 | }, 22 | "permissions": [ 23 | "storage" 24 | ], 25 | "web_accessible_resources": [ 26 | { 27 | "resources": ["images/*"], 28 | "matches": [""] 29 | } 30 | ], 31 | "browser_specific_settings": { 32 | "gecko": { 33 | "id": "mutable@idreesinc.com" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /mutable-stylesheet.css: -------------------------------------------------------------------------------- 1 | .mutable-blur { 2 | filter: blur(10px) !important; 3 | opacity: 0.5 !important; 4 | } 5 | 6 | .mutable-blur > * { 7 | pointer-events: none !important; 8 | } 9 | 10 | .mutable-blur-explanation { 11 | position: relative; 12 | } 13 | 14 | .mutable-blur-explanation::after { 15 | text-align: center; 16 | content: 'Muted by Mutable\A Contains: "' attr(data-mutable-match) '"'; 17 | white-space: pre-wrap; 18 | box-sizing: border-box; 19 | font-size: 18px !important; 20 | color: transparent; 21 | font-family: "Helvetica", "Arial", sans-serif !important; 22 | font-weight: normal !important; 23 | position: absolute; 24 | display: flex; 25 | align-items: center; 26 | justify-content: center; 27 | top: 0; 28 | left: 0; 29 | width: 100%; 30 | height: 100%; 31 | background-image: linear-gradient(to right, #e5cfee, #baecff, #efddcf, #ddf1d1, #f2d5d7); 32 | -webkit-background-clip: text; 33 | background-clip: text; 34 | backdrop-filter: blur(10px) brightness(80%) !important; 35 | -webkit-backdrop-filter: blur(10px) brightness(80%) !important; 36 | z-index: 3; 37 | } 38 | 39 | 40 | .mutable-hide { 41 | display: none !important; 42 | } 43 | 44 | .mutable-image-overlay { 45 | position: relative; 46 | } 47 | 48 | .mutable-image-overlay::after { 49 | box-sizing: border-box; 50 | position: absolute; 51 | display: flex; 52 | align-items: center; 53 | justify-content: center; 54 | top: 0; 55 | left: 0; 56 | width: 100%; 57 | height: 100%; 58 | content: ''; 59 | background-image: var(--overlay-image); 60 | background-size: cover; 61 | background-position: center; 62 | z-index: 1001; 63 | } 64 | 65 | #mutable-debug-window { 66 | position: fixed; 67 | display: flex; 68 | flex-direction: column; 69 | bottom: 0; 70 | right: 15px; 71 | min-width: 200px; 72 | max-width: 50%; 73 | min-height: 100px; 74 | max-height: 50%; 75 | background: rgba(255, 255, 255, 0.70); 76 | backdrop-filter: blur(5px); 77 | -webkit-backdrop-filter: blur(5px); 78 | border-style: solid; 79 | border-width: 1px; 80 | border-color: rgba(0, 0, 0, 0.50); 81 | padding: 10px; 82 | padding-bottom: 15px; 83 | font-family: "Helvetica", "Arial", sans-serif; 84 | border-radius: 10px 10px 0 0; 85 | z-index: 1000000; 86 | font-size: 15px; 87 | color: black; 88 | } 89 | 90 | .mutable-debug-title { 91 | text-align: center; 92 | margin-bottom: 5px; 93 | } 94 | 95 | #mutable-debug-window > :not(:last-child) { 96 | margin-bottom: 5px; 97 | } 98 | 99 | .mutable-debug-post { 100 | border-style: solid !important; 101 | border-width: 2px !important; 102 | border-image: linear-gradient(to bottom, #ff8ff0, #70f8ff, #81ff92) 30 !important; 103 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "test": "concat -o built_tests.js ./shared.js ./tests/tests.js && mocha ./built_tests.js --require chai/register-assert.js ; rm ./built_tests.js" 4 | }, 5 | "devDependencies": { 6 | "@types/chrome": "^0.0.242", 7 | "@types/jquery": "^3.5.16", 8 | "chai": "^5.1.1", 9 | "concat": "^1.0.3", 10 | "mocha": "^10.6.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /replace.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import os 4 | import tempfile 5 | 6 | template='' 7 | code='
donate
Want to support Mutable?
' 8 | tmp=tempfile.mkstemp() 9 | 10 | with open(sys.argv[1]) as fd1, open(tmp[1],'w') as fd2: 11 | for line in fd1: 12 | if len(sys.argv) == 3: 13 | line = line.replace(code, template) 14 | else: 15 | line = line.replace(template, code) 16 | fd2.write(line) 17 | 18 | os.rename(tmp[1],sys.argv[1]) -------------------------------------------------------------------------------- /settings/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /settings/foil.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdreesInc/Mutable/a65c4e72a7aa5e3c0f3698cb877e1ea1563da555/settings/foil.jpg -------------------------------------------------------------------------------- /settings/settings-stylesheet.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Epilogue:wght@400;500&display=swap'); 2 | 3 | :root { 4 | --border-radius: 20px; 5 | --stroke: 2px; 6 | --light-stroke: 2px; 7 | } 8 | 9 | html, body { 10 | font-family: Epilogue, sans-serif; 11 | font-size: 16px; 12 | min-width: 360px; 13 | padding: 0; 14 | margin: 0; 15 | } 16 | 17 | html { 18 | background: #fffdfa; 19 | } 20 | 21 | #background { 22 | position: fixed; 23 | top: 0; 24 | left: 0; 25 | width: 100%; 26 | height: 100%; 27 | background-image: url("./foil.jpg"); 28 | background-size: cover; 29 | z-index: -1; 30 | /* opacity: 0.75; */ 31 | } 32 | 33 | #container { 34 | width: 100%; 35 | height: 100%; 36 | padding: 0 10% 0 10%; 37 | display: flex; 38 | flex-direction: column; 39 | box-sizing: border-box; 40 | } 41 | 42 | #title-container { 43 | width: 100%; 44 | height: 100px; 45 | margin-top: 35px; 46 | margin-bottom: -10px; 47 | display: flex; 48 | flex-direction: column; 49 | justify-content: center; 50 | align-items: center; 51 | } 52 | 53 | #title { 54 | font-family: Epilogue, sans-serif; 55 | font-size: 60px; 56 | } 57 | 58 | #tagline { 59 | margin-top: 10px; 60 | font-family: Epilogue, sans-serif; 61 | font-size: 18px; 62 | } 63 | 64 | .window { 65 | width: 100%; 66 | border: var(--stroke) solid black; 67 | box-sizing: border-box; 68 | border-radius: var(--border-radius); 69 | box-shadow: 4px 4px 0px black; 70 | margin-top: 40px; 71 | background: #fffdfa; 72 | } 73 | 74 | .window-top-bar { 75 | width: calc(100% + 2 * var(--stroke)); 76 | height: calc(var(--border-radius) * 2); 77 | margin-top: calc(-1 * var(--stroke)); 78 | margin-left: calc(-1 * var(--stroke)); 79 | border: var(--stroke) solid black; 80 | box-sizing: border-box; 81 | border-radius: var(--border-radius) var(--border-radius) var(--border-radius) 0; 82 | display: flex; 83 | justify-content: space-between; 84 | align-items: center; 85 | } 86 | 87 | .window-icon, .window-controls { 88 | width: 20%; 89 | } 90 | 91 | .window-icon { 92 | padding-left: calc(var(--border-radius) * 0.5); 93 | display: flex; 94 | align-items: center; 95 | } 96 | 97 | .window-title { 98 | text-align: center; 99 | font-family: Epilogue; 100 | font-size: 20px; 101 | } 102 | 103 | .window-controls { 104 | display: flex; 105 | justify-content: right; 106 | align-items: center; 107 | padding-right: calc(var(--border-radius) * 0.5); 108 | box-sizing: border-box; 109 | } 110 | 111 | .window-button { 112 | height: calc(var(--border-radius) * 0.85); 113 | aspect-ratio: 1; 114 | border-radius: 100px; 115 | border: var(--light-stroke) solid black; 116 | box-sizing: border-box; 117 | } 118 | 119 | .left-window-button { 120 | background: #71FFCC; 121 | margin-right: calc(var(--border-radius) * 0.25); 122 | } 123 | 124 | .right-window-button { 125 | background: #FFF6A4; 126 | } 127 | 128 | .window-content { 129 | padding: 20px; 130 | box-sizing: border-box; 131 | display: flex; 132 | flex-direction: column; 133 | } 134 | 135 | #websites-window > .window-top-bar { 136 | background: linear-gradient(180deg, #AAFFD6 0%, #FFF 100%); 137 | } 138 | 139 | #mute-window > .window-top-bar { 140 | background: linear-gradient(180deg, #CBB9FF 0%, #FFF 100%); 141 | } 142 | 143 | .top-level-toggle { 144 | width: 100%; 145 | border: var(--stroke) solid black; 146 | display: flex; 147 | align-items: center; 148 | justify-content: space-between; 149 | box-sizing: border-box; 150 | padding: 7px; 151 | padding-left: 10px; 152 | } 153 | 154 | #parser-toggles { 155 | width: 100%; 156 | display: flex; 157 | flex-direction: column; 158 | } 159 | 160 | /* Parser toggles every child but last */ 161 | #parser-toggles > div:not(:last-child) { 162 | margin-bottom: 8px; 163 | } 164 | 165 | #experimental-parsers { 166 | margin-top: 15px; 167 | font-size: 17px; 168 | text-align: center; 169 | } 170 | 171 | #experimental-parsers > span { 172 | margin-bottom: 10px; 173 | /* opacity: 0.5; */ 174 | } 175 | 176 | /* #website-twitter { 177 | background: linear-gradient(90deg, #DAF2FF 0%, white 80%); 178 | } 179 | 180 | #website-bluesky { 181 | background: linear-gradient(90deg, #DAF8FF 0%, white 80%); 182 | } 183 | 184 | #website-reddit { 185 | background: linear-gradient(90deg, #fff6d6 0%, white 80%); 186 | } 187 | 188 | #website-instagram { 189 | background: linear-gradient(90deg, #ffd7e5 0%, white 80%); 190 | } */ 191 | 192 | .toggle-name { 193 | font-family: Epilogue; 194 | font-size: 18px; 195 | padding-top: 4px; 196 | max-width: 60%; 197 | } 198 | 199 | .toggle-switch { 200 | position: relative; 201 | display: inline-block; 202 | width: 72px; 203 | min-width: 72px; 204 | height: 26px; 205 | border: var(--stroke) solid black; 206 | border-radius: 1000px; 207 | } 208 | 209 | .toggle-switch input { 210 | opacity: 0; 211 | width: 0; 212 | height: 0; 213 | } 214 | 215 | .toggle-inner { 216 | position: absolute; 217 | cursor: pointer; 218 | top: 0; 219 | left: 0px; 220 | right: 0; 221 | bottom: 0; 222 | -webkit-transition: .4s; 223 | transition: .4s; 224 | border-radius: 34px; 225 | } 226 | 227 | .toggle-inner:before { 228 | position: absolute; 229 | content: ""; 230 | height: 30px; 231 | width: 50px; 232 | left: -2px; 233 | bottom: -2px; 234 | background-color: #FFC3C3; 235 | -webkit-transition: .4s; 236 | transition: .4s; 237 | border: var(--stroke) solid black; 238 | box-sizing: border-box; 239 | content: "off"; 240 | font-family: Epilogue; 241 | font-size: 16px; 242 | display: flex; 243 | align-items: center; 244 | justify-content: center; 245 | padding-top: 2px; 246 | border-radius: 34px; 247 | } 248 | 249 | input:checked + .toggle-inner:before { 250 | transform: translateX(28px); 251 | content: "on"; 252 | padding-top: 1px; 253 | background-color: #C3FFE2; 254 | } 255 | 256 | #custom-sites { 257 | margin-top: 10px; 258 | } 259 | 260 | .group { 261 | overflow-x: hidden; 262 | width: 100%; 263 | border: var(--light-stroke) solid black; 264 | box-sizing: border-box; 265 | border-radius: calc(var(--border-radius) * 0.5); 266 | box-shadow: 2.5px 2.5px 0px black; 267 | } 268 | 269 | .group-top-bar { 270 | width: calc(100% + 2 * var(--light-stroke)); 271 | margin-top: calc(-1 * var(--light-stroke)); 272 | margin-left: calc(-1 * var(--light-stroke)); 273 | /* background: rgba(217, 217, 217, 0.40); */ 274 | background: linear-gradient(to bottom, #e7f7ff 0%, rgba(249, 249, 249, 0.00) 130%); 275 | border: var(--light-stroke) solid black; 276 | /* Border only on bottom */ 277 | border-top: none; 278 | border-left: none; 279 | border-right: none; 280 | box-sizing: border-box; 281 | border-radius: calc(var(--border-radius) * 0.5) calc(var(--border-radius) * 0.5) 0 0; 282 | display: flex; 283 | justify-content: space-between; 284 | align-items: center; 285 | padding: 8px; 286 | padding-bottom: 5px; 287 | } 288 | 289 | .group-name { 290 | text-align: center; 291 | font-family: Epilogue; 292 | font-size: 16px; 293 | } 294 | 295 | .group-content { 296 | display: flex; 297 | flex-direction: column; 298 | padding: 12px; 299 | padding-right: 6px; 300 | max-height: 340px; 301 | overflow-y: scroll; 302 | } 303 | 304 | .mute-list { 305 | flex-direction: column-reverse; 306 | } 307 | 308 | .group-element { 309 | padding-top: 3px; 310 | padding-bottom: 3px; 311 | display: flex; 312 | justify-content: space-between; 313 | align-items: center; 314 | } 315 | 316 | .group-element .element-delete { 317 | opacity: 0; 318 | } 319 | 320 | .group-element:hover .element-delete { 321 | opacity: 1; 322 | } 323 | 324 | .element-delete { 325 | display: inline; 326 | font-family: 'Courier New', Courier, monospace; 327 | font-size: 20px; 328 | color: grey; 329 | background-color: transparent; 330 | border: none; 331 | box-sizing: border-box; 332 | cursor: pointer; 333 | } 334 | 335 | .element-delete:hover { 336 | color: black; 337 | } 338 | 339 | .element-delete:active { 340 | color: red; 341 | } 342 | 343 | .site-delete { 344 | margin-left: auto; 345 | margin-right: 5px; 346 | } 347 | 348 | .add-button { 349 | width: 110px; 350 | border-radius: 0px 8px 8px 8px; 351 | border: var(--light-stroke) solid #000; 352 | background: #F9F9F9; 353 | box-shadow: 1px 1px 0px 0px #000; 354 | padding: 2px; 355 | padding-top: 4px; 356 | padding-left: 2px; 357 | margin-top: 6px; 358 | box-sizing: border-box; 359 | display: flex; 360 | align-items: center; 361 | justify-content: center; 362 | cursor: pointer; 363 | user-select: none; 364 | background: linear-gradient(to left, #f0fff6 0%, rgba(249, 249, 249, 0.00) 130%); 365 | 366 | } 367 | 368 | .add-button:hover { 369 | background: #e7fff4; 370 | } 371 | 372 | .add-button-plus { 373 | font-family: "Arial", "Helvetica", sans-serif; 374 | font-size: 16px; 375 | margin-left: 5px; 376 | box-sizing: border-box; 377 | } 378 | 379 | .add-button-empty { 380 | margin-top: 7px; 381 | } 382 | 383 | #modal-container { 384 | position: fixed; 385 | top: 0; 386 | left: 0; 387 | width: 100%; 388 | height: 100%; 389 | padding: 0 8% 0 8%; 390 | box-sizing: border-box; 391 | background: rgba(0,0,0,0.1); 392 | backdrop-filter: blur(5px); 393 | display: flex; 394 | flex-direction: column; 395 | justify-content: center; 396 | align-items: center; 397 | display: none; 398 | } 399 | 400 | #add-word-modal > .window-top-bar { 401 | background: linear-gradient(180deg, #dcd0ff 0%, #FFF 100%); 402 | } 403 | 404 | .settings-text-input, .settings-block { 405 | width: 100%; 406 | min-height: 32px; 407 | margin-top: 10px; 408 | border: var(--light-stroke) solid black; 409 | box-sizing: border-box; 410 | font-family: Epilogue; 411 | font-size: 16px; 412 | } 413 | 414 | .settings-text-input { 415 | padding-left: 10px; 416 | } 417 | 418 | .settings-block { 419 | padding-left: 10px; 420 | display: flex; 421 | align-items: center; 422 | justify-content: space-between; 423 | } 424 | 425 | .settings-toggle { 426 | height: 20px; 427 | margin-right: 5px; 428 | border-width: var(--light-stroke); 429 | } 430 | 431 | .settings-toggle-inner::before { 432 | height: calc(20px + 2 * var(--light-stroke)); 433 | border-width: var(--light-stroke); 434 | bottom: calc(-1 * var(--light-stroke)); 435 | } 436 | 437 | .keyword-input { 438 | border-radius: 5px 5px 0 0; 439 | } 440 | 441 | .button-block { 442 | width: 100%; 443 | display: flex; 444 | margin-top: 10px; 445 | } 446 | 447 | .submit-button, .cancel-button { 448 | /* width: 105px; */ 449 | height: 32px; 450 | flex-grow: 1; 451 | border: var(--light-stroke) solid #000; 452 | background: #F9F9F9; 453 | box-shadow: 1px 1px 0px 0px #000; 454 | padding: 2px; 455 | padding-left: 2px; 456 | box-sizing: border-box; 457 | display: flex; 458 | align-items: center; 459 | justify-content: center; 460 | cursor: pointer; 461 | user-select: none; 462 | } 463 | 464 | .submit-button { 465 | border-radius: 0 8px 8px 8px; 466 | background: linear-gradient(180deg, #e1fdff 0%, rgba(249, 249, 249, 0.00) 100%); 467 | } 468 | 469 | .cancel-button { 470 | margin-left: 5px; 471 | border-radius: 8px 0 8px 8px; 472 | background: linear-gradient(180deg, #ffe6e6 0%, rgba(249, 249, 249, 0.00) 100%); 473 | } 474 | 475 | #footer { 476 | width: 100%; 477 | height: 90px; 478 | display: flex; 479 | flex-direction: column; 480 | justify-content: center; 481 | align-items: center; 482 | padding-top: 8px; 483 | padding-bottom: 8px; 484 | } 485 | 486 | #settings-window > .window-top-bar { 487 | background: linear-gradient(180deg, #BCE7FF 0%, #FFF 100%); 488 | } 489 | 490 | #settings-window-content > .settings-block:first-child { 491 | margin-top: 0px; 492 | } 493 | 494 | .global-settings-block { 495 | padding: 8px; 496 | } 497 | 498 | .vertical-settings-block { 499 | display: flex; 500 | flex-direction: column; 501 | align-items: baseline; 502 | } 503 | 504 | .settings-text { 505 | font-size: 16px; 506 | box-sizing: border-box; 507 | } 508 | 509 | .settings-select { 510 | height: 32px; 511 | text-overflow: ellipsis; 512 | font-size: 16px; 513 | width: 100%; 514 | border: var(--light-stroke) solid #000; 515 | background: #EAFEFF; 516 | padding-left: 6px; 517 | padding-right: 19px; 518 | box-sizing: border-box; 519 | appearance: none; 520 | color: black; 521 | } 522 | 523 | .select-container { 524 | display: flex; 525 | justify-content: center; 526 | align-items: center; 527 | margin-top: 5px; 528 | width: 100%; 529 | } 530 | 531 | .select-container:after { 532 | width: 0; 533 | content: "▼"; 534 | font-size: 10px; 535 | transform: translateX(-20px); 536 | margin-top: 1px; 537 | pointer-events: none; 538 | } 539 | 540 | #donations-window > .window-top-bar { 541 | background: linear-gradient(180deg, #f4d7ff 0%, #FFF 100%); 542 | } 543 | 544 | #donations-window-content { 545 | align-items: center; 546 | } 547 | 548 | #donate { 549 | width: 100%; 550 | height: 32px; 551 | border: var(--light-stroke) solid #000; 552 | background: #F9F9F9; 553 | box-shadow: 1px 1px 0px 0px #000; 554 | border-radius: 8px; 555 | padding: 2px; 556 | padding-top: 3px; 557 | box-sizing: border-box; 558 | text-align: center; 559 | display: flex; 560 | align-items: center; 561 | justify-content: center; 562 | background: linear-gradient(to bottom right, #fce4ff 0%, #FFF 100%); 563 | cursor: pointer; 564 | } 565 | 566 | #donate-link { 567 | text-decoration: none; 568 | color: black; 569 | width: 100%; 570 | margin-top: 10px; 571 | } 572 | 573 | #acknowledgements-window > .window-top-bar { 574 | background: linear-gradient(180deg, #f4d7ff 0%, #FFF 100%); 575 | } 576 | 577 | #acknowledgements-window { 578 | display: none; 579 | } 580 | 581 | .acknowledgements-link { 582 | margin-bottom: 8px; 583 | text-decoration: underline; 584 | cursor: pointer; 585 | } 586 | 587 | .acknowledgements-link a { 588 | color: black; 589 | } -------------------------------------------------------------------------------- /settings/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 |
11 |
Mutable
12 |
Turn Down the Noise
13 |
14 |
15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |
website
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
Use Mutable
40 | 45 |
46 |
47 |
This Website
48 | 53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | 61 | 62 | 63 | 64 | 65 |
66 |
mute list
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
Default Group
76 |
77 |
78 |
79 |
add word
80 |
+
81 |
82 |
gamma 83 | 84 |
85 |
beta 86 | 87 |
88 |
alpha 89 | 90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | 100 | 101 | 102 | 103 | 104 |
105 |
settings
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 | What would you like to do with muted posts? 115 |
116 |
117 | 125 |
126 |
127 |
128 |
129 | Enabled by Default 130 |
131 | 135 |
136 |
137 |
138 | Debug Mode 139 |
140 | 144 |
145 |
146 |
147 |
Custom Site Settings
148 |
149 |
150 |
alpha 151 |
152 | 156 |
157 |
158 |
beta 159 | 160 |
161 |
gamma 162 | 163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
thanks
173 |
174 |
175 |
176 |
177 |
178 |
179 |

180 | Resource Credits 181 |
182 | Foil background by kjpargeter 183 |

184 | Special thanks to the following people for their help and support: 185 |
186 |

187 | Beta Testers 188 |
189 | Miranda 190 |
191 | Marlow 192 |
193 | Max 194 |
195 | Saif 196 |
197 | Michael 198 |

199 |
200 |
201 | 202 | 207 |
208 | 245 | 246 | 247 | 248 | 249 | -------------------------------------------------------------------------------- /settings/settings.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /// 3 | 4 | /** @type {HTMLElement} */ 5 | // @ts-ignore 6 | const groupsContainer = document.getElementById("groups-container"); 7 | /** @type {HTMLElement} */ 8 | // @ts-ignore 9 | const customSites = document.getElementById("custom-sites"); 10 | /** @type {HTMLElement} */ 11 | // @ts-ignore 12 | const customSitesList = document.getElementById("custom-sites-list"); 13 | /** @type {HTMLElement} */ 14 | // @ts-ignore 15 | const websitesContent = document.getElementById("websites-content"); 16 | /** @type {HTMLInputElement} */ 17 | // @ts-ignore 18 | const mutableEnabled = document.getElementById("mutable-enabled"); 19 | /** @type {HTMLInputElement} */ 20 | // @ts-ignore 21 | const toggleThisWebsite = document.getElementById("toggle-this-website"); 22 | /** @type {HTMLElement} */ 23 | // @ts-ignore 24 | const background = document.getElementById("background"); 25 | /** @type {HTMLElement} */ 26 | // @ts-ignore 27 | const modalContainer = document.getElementById("modal-container"); 28 | /** @type {HTMLElement} */ 29 | // @ts-ignore 30 | const addWordModal = document.getElementById("add-word-modal"); 31 | /** @type {HTMLInputElement} */ 32 | // @ts-ignore 33 | const addWordInput = document.getElementById("add-word-input"); 34 | /** @type {HTMLInputElement} */ 35 | // @ts-ignore 36 | const addWordCaseSensitive = document.getElementById("add-word-case-sensitive"); 37 | /** @type {HTMLElement} */ 38 | // @ts-ignore 39 | const addWordSubmit = document.getElementById("add-word-submit"); 40 | /** @type {((keyword: string, caseSensitive: boolean) => void)} */ 41 | let addWordSubmitCallback = () => {}; 42 | /** @type {HTMLElement} */ 43 | // @ts-ignore 44 | const addWordCancel = document.getElementById("add-word-cancel"); 45 | /** @type {HTMLSelectElement} */ 46 | // @ts-ignore 47 | const globalMuteAction = document.getElementById("global-mute-action"); 48 | /** @type {HTMLElement} */ 49 | // @ts-ignore 50 | const acknowledgementsWindow = document.getElementById("acknowledgements-window"); 51 | /** @type {HTMLElement} */ 52 | // @ts-ignore 53 | const acknowledgements = document.getElementById("acknowledgements"); 54 | /** @type {HTMLInputElement} */ 55 | // @ts-ignore 56 | const debugMode = document.getElementById("debug-mode"); 57 | /** @type {HTMLInputElement} */ 58 | // @ts-ignore 59 | const enabledByDefault = document.getElementById("enabled-by-default"); 60 | 61 | let currentSettings = new Settings(); 62 | let deletedLegacySettings = false; 63 | 64 | init(); 65 | 66 | function init() { 67 | getSettings((result) => { 68 | currentSettings = result; 69 | initSettings(); 70 | renderSettings(); 71 | }, (msg) => { 72 | console.error(msg); 73 | console.log("No settings found, creating default settings"); 74 | initSettings(); 75 | renderSettings(); 76 | }); 77 | let scrollRatio = 0; 78 | let mouseRatio = 0; 79 | // Move background x position as a product of the scroll y position 80 | document.addEventListener("scroll", () => { 81 | // calculate scroll ratio (0 at top, 1 at bottom) 82 | scrollRatio = window.scrollY / (document.body.scrollHeight - window.innerHeight); 83 | updateFoil(scrollRatio, mouseRatio); 84 | }); 85 | // Move background x position as a product of the mouse y position 86 | document.addEventListener("mousemove", (event) => { 87 | mouseRatio = event.clientY / window.innerHeight; 88 | updateFoil(scrollRatio, mouseRatio); 89 | }); 90 | acknowledgements.addEventListener("click", () => { 91 | acknowledgementsWindow.style.display = "block"; 92 | }); 93 | initModals(); 94 | } 95 | 96 | function updateFoil(scrollRatio, mouseRatio) { 97 | let ratio = scrollRatio * 0.5 + mouseRatio * 0.5; 98 | background.style.backgroundPositionX = `${ratio * 80}%`; 99 | } 100 | 101 | function initSettings() { 102 | // Update the settings when the toggle is changed 103 | toggleThisWebsite.addEventListener("change", () => { 104 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 105 | let currentTab = tabs[0]; 106 | if (currentTab.url !== undefined) { 107 | let url = new URL(currentTab.url); 108 | let hostname = url.hostname; 109 | currentSettings.setWebsiteEnabled(hostname, toggleThisWebsite.checked); 110 | updateSettings(); 111 | } 112 | }); 113 | }); 114 | globalMuteAction.addEventListener("change", () => { 115 | currentSettings.globalMuteAction = globalMuteAction.value; 116 | updateSettings(); 117 | }); 118 | debugMode.addEventListener("change", () => { 119 | currentSettings.debugMode = debugMode.checked; 120 | updateSettings(); 121 | }); 122 | enabledByDefault.addEventListener("change", () => { 123 | currentSettings.enabledByDefault = enabledByDefault.checked; 124 | updateSettings(); 125 | }); 126 | } 127 | 128 | function renderSettings() { 129 | mutableEnabled.checked = currentSettings.mutableEnabled; 130 | mutableEnabled.addEventListener("change", () => { 131 | currentSettings.mutableEnabled = mutableEnabled.checked; 132 | updateSettings(); 133 | }); 134 | // Load toggle's initial state based on the current tab 135 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 136 | let currentTab = tabs[0]; 137 | if (currentTab.url !== undefined) { 138 | let url = new URL(currentTab.url); 139 | let hostname = url.hostname; 140 | if (currentSettings.enabledByDefault) { 141 | toggleThisWebsite.checked = currentSettings.isSiteEnabled(hostname); 142 | } else { 143 | toggleThisWebsite.checked = currentSettings.isSiteExplicitlyEnabled(hostname); 144 | } 145 | toggleThisWebsite.disabled = false; 146 | } else { 147 | // Disable the toggle on pages like the browser settings 148 | toggleThisWebsite.checked = false; 149 | toggleThisWebsite.disabled = true; 150 | } 151 | }); 152 | customSitesList.innerHTML = ""; 153 | const websiteRules = currentSettings.getWebsiteRulesList(); 154 | if (websiteRules.length === 0) { 155 | customSites.style.display = "none"; 156 | } else { 157 | customSites.style.display = "block"; 158 | } 159 | // Sort the website rules by host in alphabetical order 160 | websiteRules.sort((a, b) => { 161 | return a.host.localeCompare(b.host); 162 | }); 163 | const hostLabelMaxLength = 22; 164 | for (let site of websiteRules) { 165 | const siteElement = document.createElement("div"); 166 | siteElement.classList.add("group-element"); 167 | // Truncate the host if it's too long 168 | let hostLabel = site.host; 169 | if (hostLabel.length > hostLabelMaxLength) { 170 | hostLabel = hostLabel.substring(0, hostLabelMaxLength) + "..."; 171 | } 172 | siteElement.textContent = hostLabel; 173 | const siteDelete = document.createElement("button"); 174 | siteDelete.classList.add("element-delete"); 175 | siteDelete.classList.add("site-delete"); 176 | siteDelete.innerText = "x"; 177 | if (siteDelete !== null) { 178 | siteDelete.addEventListener("click", () => { 179 | currentSettings.deleteSiteRule(site.host); 180 | updateSettings(); 181 | }); 182 | } 183 | siteElement.appendChild(siteDelete); 184 | const siteToggle = document.createElement("label"); 185 | siteToggle.innerHTML = ` 186 | 190 | `; 191 | const siteToggleInput = siteToggle.querySelector("input"); 192 | if (siteToggleInput === null) { 193 | throw new Error("Could not find input element in site toggle"); 194 | } 195 | siteToggleInput.checked = site.enabled; 196 | siteToggleInput.addEventListener("change", () => { 197 | site.enabled = siteToggleInput.checked; 198 | updateSettings(); 199 | }); 200 | siteElement.appendChild(siteToggle); 201 | customSitesList.appendChild(siteElement); 202 | } 203 | 204 | // Render the muted keywords 205 | groupsContainer.innerHTML = ""; 206 | for (let group of currentSettings.getGroupsList()) { 207 | let groupElement = document.createElement("div"); 208 | groupElement.classList.add("group"); 209 | let groupTopBar = document.createElement("div"); 210 | groupTopBar.classList.add("group-top-bar"); 211 | let groupTitle = document.createElement("div"); 212 | groupTitle.classList.add("group-name"); 213 | groupTitle.textContent = group.name; 214 | groupTopBar.appendChild(groupTitle); 215 | groupElement.appendChild(groupTopBar); 216 | let groupContent = document.createElement("div"); 217 | groupContent.classList.add("group-content"); 218 | groupContent.classList.add("mute-list"); 219 | for (let pattern of group.patterns) { 220 | let groupElement = document.createElement("div"); 221 | groupElement.classList.add("group-element"); 222 | groupElement.textContent = pattern.plaintext(); 223 | let deleteButton = document.createElement("button"); 224 | deleteButton.classList.add("element-delete"); 225 | deleteButton.textContent = "x"; 226 | deleteButton.addEventListener("click", () => { 227 | group.deletePattern(pattern.id); 228 | updateSettings(); 229 | }); 230 | groupElement.appendChild(deleteButton); 231 | groupContent.prepend(groupElement); 232 | } 233 | let addButton = document.createElement("div"); 234 | addButton.classList.add("add-button"); 235 | addButton.innerHTML = ` 236 |
add word
237 |
+
238 | `; 239 | if (group.patterns.length === 0) { 240 | addButton.classList.add("add-button-empty"); 241 | } 242 | addButton.addEventListener("click", () => { 243 | displayAddWordModal((keyword, caseSensitive) => { 244 | if (keyword.trim().length === 0) { 245 | return; 246 | } 247 | group.addPattern(new KeywordMute(generateId(), keyword, caseSensitive)); 248 | updateSettings(); 249 | }); 250 | }); 251 | groupContent.prepend(addButton); 252 | groupElement.appendChild(groupContent); 253 | groupsContainer.appendChild(groupElement); 254 | } 255 | globalMuteAction.value = currentSettings.globalMuteAction; 256 | debugMode.checked = currentSettings.debugMode; 257 | enabledByDefault.checked = currentSettings.enabledByDefault; 258 | } 259 | 260 | function updateSettings() { 261 | putSettings(currentSettings, () => { 262 | if (!deletedLegacySettings) { 263 | deletedLegacySettings = true; 264 | // Remove legacy settings now that we have successfully saved the new settings 265 | // TODO: Remove this in a few versions 266 | deleteSettings("settings"); 267 | } 268 | renderSettings(); 269 | }); 270 | } 271 | 272 | 273 | function initModals() { 274 | addWordCancel.addEventListener("click", () => { 275 | hideModals(); 276 | }); 277 | addWordSubmit.addEventListener("click", () => { 278 | let keyword = addWordInput.value; 279 | if (!keyword) { 280 | alert("Please enter a keyword"); 281 | return; 282 | } 283 | hideModals(); 284 | addWordSubmitCallback(keyword, addWordCaseSensitive.checked); 285 | }); 286 | // Get the nested child of class window-controls from addWordModal 287 | let windowControls = addWordModal.getElementsByClassName("window-controls")[0]; 288 | windowControls.addEventListener("click", () => { 289 | hideModals(); 290 | }); 291 | } 292 | 293 | /** 294 | * @param {((keyword: string, caseSensitive: boolean) => void)} submitCallback 295 | */ 296 | function displayAddWordModal(submitCallback) { 297 | addWordInput.value = ""; 298 | addWordCaseSensitive.checked = false; 299 | addWordSubmitCallback = submitCallback; 300 | 301 | addWordModal.style.display = ""; 302 | modalContainer.style.display = "flex"; 303 | 304 | addWordInput.focus(); 305 | } 306 | 307 | function hideModals() { 308 | addWordModal.style.display = "none"; 309 | modalContainer.style.display = "none"; 310 | } -------------------------------------------------------------------------------- /shared.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const PROCESSED_INDICATOR = "mutable-parsed"; 4 | 5 | /** 6 | * An abstract class representing a post on a content feed. 7 | * This class is designed to lazy-load the parsed elements of the post only when requested and cache the values. 8 | */ 9 | class Post { 10 | /** 11 | * The full text contents of the post element. 12 | * @type {string|null} 13 | */ 14 | #postText = null; 15 | /** 16 | * The text contents of the post without the author's name or handle (if present). 17 | * @type {string|null} 18 | */ 19 | #postContents = null; 20 | /** 21 | * The name of the author. 22 | * @type {string|null} 23 | */ 24 | #authorName = null; 25 | /** 26 | * The handle of the author. 27 | * @type {string|null} 28 | */ 29 | #authorHandle = null; 30 | /** 31 | * The alt text of the media in the post. 32 | * @type {string[]|null} 33 | */ 34 | #mediaAltText = null; 35 | 36 | /** 37 | * @param {HTMLElement} postElement 38 | */ 39 | constructor(postElement) { 40 | this.postElement = postElement; 41 | } 42 | 43 | /** 44 | * @returns {string|null} 45 | */ 46 | postText() { 47 | this.#postText = this.#postText ?? this.getPostText(); 48 | return this.#postText; 49 | } 50 | 51 | /** 52 | * @returns {string|null} 53 | */ 54 | getPostText() { 55 | return null; 56 | } 57 | 58 | /** 59 | * @returns {string|null} 60 | */ 61 | postContents() { 62 | this.#postContents = this.#postContents ?? this.getPostContents(); 63 | return this.#postContents; 64 | } 65 | 66 | /** 67 | * @returns {string|null} 68 | */ 69 | getPostContents() { 70 | return null; 71 | } 72 | 73 | /** 74 | * @returns {string|null} 75 | */ 76 | authorName() { 77 | this.#authorName = this.#authorName ?? this.getAuthorName(); 78 | return this.#authorName; 79 | } 80 | 81 | /** 82 | * @returns {string|null} 83 | */ 84 | getAuthorName() { 85 | return null; 86 | } 87 | 88 | /** 89 | * @returns {string|null} 90 | */ 91 | authorHandle() { 92 | this.#authorHandle = this.#authorHandle ?? this.getAuthorHandle(); 93 | return this.#authorHandle; 94 | } 95 | 96 | /** 97 | * @returns {string|null} 98 | */ 99 | getAuthorHandle() { 100 | return null; 101 | } 102 | 103 | /** 104 | * @returns {string[]|null} 105 | */ 106 | mediaAltText() { 107 | this.#mediaAltText = this.#mediaAltText ?? this.getMediaAltText(); 108 | return this.#mediaAltText; 109 | } 110 | 111 | /** 112 | * @returns {string[]|null} 113 | */ 114 | getMediaAltText() { 115 | return null; 116 | } 117 | } 118 | 119 | class TwitterPost extends Post { 120 | getPostText() { 121 | return this.postElement.innerText; 122 | } 123 | 124 | getPostContents() { 125 | const text = this.postText(); 126 | return text === null ? null : text.replace(this.authorName() ?? "", "").replace(this.authorHandle() ?? "", ""); 127 | } 128 | 129 | getAuthorName() { 130 | try { 131 | const usernameElement = $(this.postElement).find('[data-testid="User-Name"]'); 132 | return usernameElement.first().text().split("@")[0].trim(); 133 | } catch (ex) { 134 | console.error(ex); 135 | console.error("Could not find author name"); 136 | } 137 | return null; 138 | } 139 | 140 | getAuthorHandle() { 141 | try { 142 | const usernameElement = $(this.postElement).find('[data-testid="User-Name"]'); 143 | const authorHandleElement = usernameElement.find("span").filter(function () { 144 | return $(this).text().trim().startsWith("@"); 145 | }).first(); 146 | return authorHandleElement.text(); 147 | } catch (ex) { 148 | console.error(ex); 149 | console.error("Could not find author handle"); 150 | } 151 | return null; 152 | } 153 | } 154 | 155 | class BlueskyPost extends Post { 156 | getPostText() { 157 | return this.postElement.innerText; 158 | } 159 | 160 | getPostContents() { 161 | const text = this.postText(); 162 | return text === null ? null : text.replace(this.authorName() ?? "", "").replace(this.authorHandle() ?? "", ""); 163 | } 164 | 165 | getAuthorName() { 166 | try { 167 | const authorNameElement = $(this.postElement).find("span").filter(function () { 168 | return $(this).text().trim().startsWith("@"); 169 | }).first().parent(); 170 | return authorNameElement.text().replace(this.authorHandle() ?? "", "").trim(); 171 | } catch (ex) { 172 | console.error(ex); 173 | console.error("Could not find author name"); 174 | } 175 | return null; 176 | } 177 | 178 | getAuthorHandle() { 179 | try { 180 | const authorHandleElement = $(this.postElement).find("span").filter(function () { 181 | return $(this).text().trim().startsWith("@"); 182 | }).first(); 183 | return authorHandleElement.text(); 184 | } catch (ex) { 185 | console.error(ex); 186 | console.error("Could not find author handle"); 187 | } 188 | return null; 189 | } 190 | 191 | getMediaAltText() { 192 | try { 193 | let altTexts = []; 194 | $(this.postElement).find(".expo-image-container").parent().each((index, element) => { 195 | const altText = element.getAttribute("aria-label"); 196 | if (altText) { 197 | altTexts.push(altText); 198 | } 199 | }); 200 | return altTexts; 201 | } catch (ex) { 202 | console.error(ex); 203 | console.error("Could not find media alt text"); 204 | } 205 | return null; 206 | } 207 | } 208 | 209 | class MastodonPost extends Post { 210 | getPostText() { 211 | return this.postElement.innerText; 212 | } 213 | 214 | getPostContents() { 215 | const text = this.postText(); 216 | return text === null ? null : text.replace(this.authorName() ?? "", "").replace(this.authorHandle() ?? "", ""); 217 | } 218 | 219 | getAuthorName() { 220 | try { 221 | return $(this.postElement).find(".display-name__html").first().text(); 222 | } catch (ex) { 223 | console.error(ex); 224 | console.error("Could not find author name"); 225 | } 226 | return null; 227 | } 228 | 229 | getAuthorHandle() { 230 | try { 231 | return $(this.postElement).find(".display-name__account").first().text(); 232 | } catch (ex) { 233 | console.error(ex); 234 | console.error("Could not find author handle"); 235 | } 236 | return null; 237 | } 238 | } 239 | 240 | 241 | class FacebookPost extends Post { 242 | getPostText() { 243 | return this.postElement.innerText; 244 | } 245 | 246 | getPostContents() { 247 | const text = this.postText(); 248 | return text; 249 | } 250 | } 251 | 252 | class ThreadsPost extends Post { 253 | getPostText() { 254 | return this.postElement.innerText; 255 | } 256 | 257 | getPostContents() { 258 | const text = this.postText(); 259 | return text; 260 | } 261 | } 262 | 263 | class Parser { 264 | 265 | /** 266 | * The unique id of the parser. 267 | * @abstract 268 | * @type {string} 269 | */ 270 | static id = ""; 271 | /** 272 | * The name of the parser. 273 | * @abstract 274 | * @type {string} 275 | */ 276 | static parserName = ""; 277 | /** 278 | * The washed-out color of the parser's branding. 279 | * @abstract 280 | * @type {string} 281 | */ 282 | static brandColor = "#000000"; 283 | 284 | /** 285 | * Whether this parser is experimental and should be disabled by default. 286 | * @abstract 287 | * @type {boolean} 288 | */ 289 | static experimental = false; 290 | 291 | /** 292 | * Whether this parser applies to the current page. 293 | * @abstract 294 | * @returns {boolean} 295 | */ 296 | static appliesToPage() { 297 | throw "Not implemented"; 298 | } 299 | 300 | /** 301 | * Get all posts on the page. 302 | * @abstract 303 | * @returns {Post[]} 304 | */ 305 | static getPosts() { 306 | throw "Not implemented"; 307 | } 308 | 309 | /** 310 | * @returns {typeof Parser[]} 311 | */ 312 | static specializedParsers() { 313 | return [TwitterParser, RedditParser, FacebookParser, MastodonParser, BlueskyParser, ThreadsParser]; 314 | } 315 | 316 | /** 317 | * Get all parsers 318 | * @returns {typeof Parser[]} 319 | */ 320 | static parsers() { 321 | return [...Parser.specializedParsers(), UniversalParser]; 322 | } 323 | 324 | /** 325 | * @param {string} id 326 | */ 327 | static getParserById(id) { 328 | // TODO: Cache this 329 | for (let parser of Parser.parsers()) { 330 | if (parser.id === id) { 331 | return parser; 332 | } 333 | } 334 | return null; 335 | } 336 | 337 | /** 338 | * @param {string} id 339 | */ 340 | static isParserExperimental(id) { 341 | const parser = Parser.getParserById(id); 342 | return parser && parser.experimental; 343 | } 344 | } 345 | 346 | class TwitterParser extends Parser { 347 | 348 | static id = "twitter"; 349 | static parserName = "Twitter/X"; 350 | static brandColor = "#DAF2FF"; 351 | 352 | static appliesToPage() { 353 | return window.location.host === "twitter.com" || window.location.host === "mobile.twitter.com" || window.location.host === "x.com"; 354 | } 355 | 356 | /** 357 | * @returns {Post[]} 358 | */ 359 | static getPosts() { 360 | let postContainers = $(document).find('[data-testid="tweet"]').parent().filter('[' + PROCESSED_INDICATOR + '!="true"]'); 361 | let posts = []; 362 | postContainers.each((index) => { 363 | let postElement = postContainers[index]; 364 | let post = new TwitterPost(postElement); 365 | posts.push(post); 366 | }); 367 | return posts; 368 | } 369 | } 370 | 371 | class MastodonParser extends Parser { 372 | 373 | static id = "mastodon"; 374 | static parserName = "Mastodon"; 375 | static brandColor = "#efebff"; 376 | 377 | static appliesToPage() { 378 | return $("body").children().first().attr("id") === "mastodon"; 379 | } 380 | 381 | /** 382 | * @returns {Post[]} 383 | */ 384 | static getPosts() { 385 | let postContainers = $(".status.status-public").filter('[' + PROCESSED_INDICATOR + '!="true"]'); 386 | let posts = []; 387 | postContainers.each((index) => { 388 | let postElement = postContainers[index]; 389 | let post = new MastodonPost(postElement); 390 | posts.push(post); 391 | }); 392 | return posts; 393 | } 394 | } 395 | 396 | class BlueskyParser extends Parser { 397 | 398 | static id = "bluesky"; 399 | static parserName = "Bluesky"; 400 | static brandColor = "#DAF8FF"; 401 | 402 | static appliesToPage() { 403 | return window.location.host === "bsky.app"; 404 | } 405 | 406 | /** 407 | * @returns {Post[]} 408 | */ 409 | static getPosts() { 410 | let postContainers = $(document).find('[data-testid^="feedItem"][' + PROCESSED_INDICATOR + '!="true"]'); 411 | let posts = []; 412 | postContainers.each((index) => { 413 | let postElement = postContainers[index]; 414 | let post = new BlueskyPost(postElement); 415 | posts.push(post); 416 | }); 417 | return posts; 418 | } 419 | } 420 | 421 | class RedditParser extends Parser { 422 | 423 | static id = "reddit"; 424 | static parserName = "Reddit"; 425 | static brandColor = "#fff0df"; 426 | 427 | static appliesToPage() { 428 | return window.location.host === "reddit.com" || window.location.host === "www.reddit.com" || window.location.host === "old.reddit.com"; 429 | } 430 | 431 | /** 432 | * @returns {Post[]} 433 | */ 434 | static getPosts() { 435 | if (window.location.host.includes("old.reddit.com")) { 436 | let postContainers = $(document).find('[' + PROCESSED_INDICATOR + '!="true"].thing'); 437 | let posts = []; 438 | postContainers.each((index) => { 439 | let postElement = postContainers[index]; 440 | let post = new RedditPost(postElement); 441 | posts.push(post); 442 | }); 443 | return posts; 444 | } else { 445 | let postContainers = $(document).find('[data-testid="post-container"][' + PROCESSED_INDICATOR + '!="true"]'); 446 | let posts = []; 447 | if (postContainers.length === 0) { 448 | // Mobile site 449 | postContainers = $(document).find('article[class^="Post "][' + PROCESSED_INDICATOR + '!="true"]'); 450 | if (postContainers.length === 0) { 451 | // Mobile site with new layout 452 | postContainers = $(document).find('shreddit-post[' + PROCESSED_INDICATOR + '!="true"]'); 453 | } 454 | postContainers.each((index) => { 455 | let postElement = postContainers[index]; 456 | let post = new RedditMobilePost(postElement); 457 | posts.push(post); 458 | }); 459 | } else { 460 | // Desktop site 461 | postContainers.each((index) => { 462 | let postElement = postContainers[index]; 463 | let post = new RedditPost(postElement); 464 | posts.push(post); 465 | }); 466 | } 467 | return posts; 468 | } 469 | } 470 | } 471 | 472 | class RedditPost extends Post { 473 | getPostText() { 474 | return this.postElement.innerText; 475 | } 476 | 477 | getPostContents() { 478 | const text = this.postText(); 479 | return text === null ? null : text.replace(this.authorName() ?? "", "").replace(this.authorHandle() ?? "", ""); 480 | } 481 | 482 | getAuthorHandle() { 483 | try { 484 | return $(this.postElement).find('[data-testid="post_author_link"]').text(); 485 | } catch (ex) { 486 | console.error(ex); 487 | console.error("Could not find author handle"); 488 | } 489 | return null; 490 | } 491 | } 492 | 493 | class RedditMobilePost extends Post { 494 | getPostText() { 495 | return this.postElement.innerText; 496 | } 497 | 498 | getPostContents() { 499 | const text = this.postText(); 500 | return text; 501 | } 502 | } 503 | 504 | class FacebookParser extends Parser { 505 | 506 | static id = "facebook"; 507 | static parserName = "Facebook"; 508 | static brandColor = "#d9ecff"; 509 | 510 | static appliesToPage() { 511 | return window.location.host === "www.facebook.com" || window.location.host === "m.facebook.com"; 512 | } 513 | 514 | /** 515 | * @returns {Post[]} 516 | */ 517 | static getPosts() { 518 | let postContainers; 519 | if (window.location.host === "m.facebook.com") { 520 | postContainers = $(document).find('[data-tracking-duration-id][' + PROCESSED_INDICATOR + '!="true"]'); 521 | } else { 522 | postContainers = $(document).find('[aria-labelledby][aria-describedby][' + PROCESSED_INDICATOR + '!="true"]'); 523 | } 524 | let posts = []; 525 | postContainers.each((index) => { 526 | let postElement = postContainers[index]; 527 | let post = new FacebookPost(postElement); 528 | posts.push(post); 529 | }); 530 | return posts; 531 | } 532 | } 533 | 534 | class ThreadsParser extends Parser { 535 | 536 | static id = "threads"; 537 | static parserName = "Threads"; 538 | static brandColor = "#d9ecff"; 539 | 540 | static appliesToPage() { 541 | return window.location.host === "www.threads.net"; 542 | } 543 | 544 | /** 545 | * @returns {Post[]} 546 | */ 547 | static getPosts() { 548 | let postContainers = $(document).find('[data-pressable-container][' + PROCESSED_INDICATOR + '!="true"]'); 549 | let posts = []; 550 | postContainers.each((index) => { 551 | let postElement = postContainers[index]; 552 | let post = new ThreadsPost(postElement); 553 | posts.push(post); 554 | }); 555 | return posts; 556 | } 557 | } 558 | 559 | class UniversalParser extends Parser { 560 | 561 | static id = "universal-experimental"; 562 | static parserName = "Any Website"; 563 | static brandColor = "#e9ffe0"; 564 | static experimental = true; 565 | 566 | static appliesToPage() { 567 | return true; 568 | } 569 | 570 | /** 571 | * @returns {Post[]} 572 | */ 573 | static getPosts() { 574 | // Using jQuery, get every leaf or element that has text aside from the contents of its children 575 | let postContainers = $(document).find("*" + '[' + PROCESSED_INDICATOR + '!="true"]').filter(function () { 576 | return $(this).contents().filter(function () { 577 | return this.nodeType === 3; 578 | }).text().trim().length > 0; 579 | }); 580 | // If the element is a not a div, replace it with its parent 581 | postContainers = postContainers.map(function () { 582 | // If this is a span or custom element, replace it with its parent 583 | let element = this; 584 | while ((element.tagName.toLowerCase() === "span" || element.tagName.includes("-")) && element.parentElement && element.parentElement.getAttribute(PROCESSED_INDICATOR) !== "true") { 585 | element = element.parentElement; 586 | } 587 | return element; 588 | }); 589 | let posts = []; 590 | postContainers.each((index) => { 591 | let postElement = postContainers[index]; 592 | let post = new EverythingPost(postElement); 593 | posts.push(post); 594 | }); 595 | return posts; 596 | } 597 | } 598 | 599 | class EverythingPost extends Post { 600 | getPostText() { 601 | return this.postElement.innerText; 602 | } 603 | 604 | getPostContents() { 605 | const text = this.postText(); 606 | return text; 607 | } 608 | } 609 | 610 | class Group { 611 | /** 612 | * @param {string} id 613 | * @param {string} name 614 | * @param {MutePattern[]} patterns 615 | */ 616 | constructor(id, name, patterns) { 617 | this.id = id; 618 | this.name = name; 619 | this.patterns = patterns; 620 | } 621 | 622 | /** 623 | * @param {MutePattern} pattern 624 | */ 625 | addPattern(pattern) { 626 | this.patterns.push(pattern); 627 | } 628 | 629 | /** 630 | * @param {string} patternId 631 | */ 632 | deletePattern(patternId) { 633 | this.patterns = this.patterns.filter((pattern) => pattern.id !== patternId); 634 | } 635 | 636 | /** 637 | * Deserialize a group from JSON. 638 | * @param {object} json 639 | * @returns {Group|null} The deserialized group, or null if the group could not be deserialized 640 | */ 641 | static fromJson(json) { 642 | if (json.id === undefined || typeof json.id !== "string") { 643 | console.error("Missing id property: " + JSON.stringify(json)); 644 | return null; 645 | } 646 | if (json.name === undefined || typeof json.name !== "string") { 647 | console.error("Missing name property: " + JSON.stringify(json)); 648 | return null; 649 | } 650 | if (json.patterns === undefined) { 651 | console.error("Missing patterns property: " + JSON.stringify(json)); 652 | return null; 653 | } 654 | let patterns = []; 655 | for (let pattern of json.patterns) { 656 | let deserializedPattern = MutePattern.fromJson(pattern); 657 | if (deserializedPattern) { 658 | patterns.push(deserializedPattern); 659 | } 660 | } 661 | return new Group(json.id, json.name, patterns); 662 | } 663 | } 664 | 665 | class MutePattern { 666 | /** 667 | * @param {string} id 668 | * @param {string} patternType 669 | */ 670 | constructor(id, patternType) { 671 | this.id = id; 672 | this.patternType = patternType; 673 | } 674 | /** 675 | * Determine whether the provided text matches this pattern. 676 | * @abstract 677 | * @param {string} contents The contents to test the match against 678 | * @returns {boolean} Whether the text matches this pattern 679 | */ 680 | isMatch(contents) { 681 | return false; 682 | } 683 | 684 | /** 685 | * Get the plaintext representation of this pattern. 686 | * @abstract 687 | * @returns {string} The plaintext representation of this pattern 688 | */ 689 | plaintext() { 690 | return ""; 691 | } 692 | 693 | /** 694 | * Deserialize a pattern from JSON. 695 | * @param {object} json 696 | * @returns {MutePattern|null} The deserialized pattern, or null if the pattern could not be deserialized 697 | */ 698 | static fromJson(json) { 699 | if (json.id === undefined || typeof json.id !== "string") { 700 | console.error("Missing id property: " + JSON.stringify(json)); 701 | return null; 702 | } 703 | if (json.patternType === "keyword") { 704 | if (json.word === undefined) { 705 | console.error("Missing word property: " + JSON.stringify(json)); 706 | return null; 707 | } 708 | if (json.caseSensitive === undefined) { 709 | console.error("Missing caseSensitive property: " + JSON.stringify(json)); 710 | return null; 711 | } 712 | return new KeywordMute(json.id, json.word, json.caseSensitive); 713 | } else { 714 | console.error(`Unknown pattern type: ${json.patternType}`); 715 | return null; 716 | } 717 | } 718 | } 719 | 720 | /** 721 | * @param {string} string 722 | * @returns {string} 723 | */ 724 | function escapeRegExp(string) { 725 | return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); 726 | } 727 | 728 | class KeywordMute extends MutePattern { 729 | /** 730 | * @param {string} id 731 | * @param {string} word 732 | */ 733 | constructor(id, word, caseSensitive = false) { 734 | super(id, "keyword"); 735 | this.word = word; 736 | this.caseSensitive = caseSensitive; 737 | const PART = "|\\s|[.!\\?,:;\\-\\_\\+\\=(\\)[\\]{\\}\\'\\\"\\<\\>])"; 738 | this.regex = new RegExp("(^" + PART + escapeRegExp(this.word) + "($" + PART, this.caseSensitive ? "" : "i"); 739 | } 740 | 741 | /** 742 | * @param {string} contents 743 | */ 744 | isMatch(contents) { 745 | return this.regex.test(contents); 746 | } 747 | 748 | plaintext() { 749 | return this.word; 750 | } 751 | } 752 | 753 | class WebsiteRule { 754 | /** 755 | * @param {string} host 756 | * @param {boolean} enabled 757 | */ 758 | constructor(host, enabled) { 759 | this.host = host; 760 | this.enabled = enabled; 761 | } 762 | } 763 | 764 | class Settings { 765 | /** 766 | * @param {Object} [groups] 767 | * @param {string} [globalMuteAction] 768 | * @param {boolean} [debugMode] 769 | * @param {Object} [websiteRules] 770 | * @param {boolean} [mutableEnabled] 771 | * @param {boolean} [enabledByDefault] 772 | */ 773 | constructor(groups, websiteRules, globalMuteAction="blur", debugMode, mutableEnabled=true, enabledByDefault=true) { 774 | this.groups = groups ?? { "default": new Group("default", "Default Group", [])}; 775 | this.websiteRules = websiteRules ?? {}; 776 | this.globalMuteAction = globalMuteAction; 777 | this.debugMode = debugMode ?? false; 778 | this.mutableEnabled = mutableEnabled; 779 | this.enabledByDefault = enabledByDefault; 780 | } 781 | 782 | /** 783 | * Return the list of custom website rules. 784 | * @returns {WebsiteRule[]} 785 | */ 786 | getWebsiteRulesList() { 787 | return Object.values(this.websiteRules); 788 | } 789 | 790 | /** 791 | * Return the list of groups. 792 | * @returns {Group[]} 793 | */ 794 | getGroupsList() { 795 | return Object.values(this.groups); 796 | } 797 | 798 | /** 799 | * Whether Mutable is enabled on the current website. 800 | * @param {string} host The host of the website 801 | * @returns {boolean} 802 | */ 803 | isSiteEnabled(host) { 804 | if (host.startsWith("www.")) { 805 | host = host.substring(4); 806 | } 807 | return this.websiteRules[host]?.enabled ?? true; 808 | } 809 | 810 | /** 811 | * Whether Mutable is explicitly enabled on the current website. 812 | * @param {string} host The host of the website 813 | * @returns 814 | */ 815 | isSiteExplicitlyEnabled(host) { 816 | if (host.startsWith("www.")) { 817 | host = host.substring(4); 818 | } 819 | return this.websiteRules[host]?.enabled ?? false; 820 | } 821 | 822 | /** 823 | * @param {string} host 824 | * @param {boolean} enabled 825 | */ 826 | setWebsiteEnabled(host, enabled) { 827 | if (host.startsWith("www.")) { 828 | host = host.substring(4); 829 | } 830 | this.websiteRules[host] = new WebsiteRule(host, enabled); 831 | } 832 | 833 | /** 834 | * @param {string} host 835 | */ 836 | deleteSiteRule(host) { 837 | if (host.startsWith("www.")) { 838 | host = host.substring(4); 839 | } 840 | delete this.websiteRules[host]; 841 | } 842 | 843 | /** 844 | * Deserialize settings from JSON. 845 | * @param {object} json 846 | * @returns {Settings|null} The deserialized settings, or null if the settings could not be deserialized 847 | */ 848 | static fromJson(json) { 849 | if (json.groups === undefined) { 850 | console.error("Missing groups property: " + JSON.stringify(json)); 851 | return null; 852 | } 853 | /** @type {Object} */ 854 | let groups = {}; 855 | for (let [groupId, group] of Object.entries(json.groups)) { 856 | if (typeof groupId !== "string") { 857 | console.error("Group id is not a string: " + JSON.stringify(groupId)); 858 | continue; 859 | } 860 | let deserializedGroup = Group.fromJson(group); 861 | if (deserializedGroup) { 862 | groups[groupId] = deserializedGroup; 863 | } 864 | } 865 | if (Object.keys(groups).length === 0) { 866 | console.error("No groups were deserialized"); 867 | return null; 868 | } else if (!groups["default"]) { 869 | console.error("Default group was not found"); 870 | return null; 871 | } 872 | 873 | let websiteRules = json.websiteRules; 874 | if (websiteRules === undefined || typeof websiteRules !== "object") { 875 | websiteRules = {}; 876 | } 877 | 878 | // TODO: Remove this in the future once most users have migrated 879 | if (json.disabledParsers !== undefined) { 880 | // Import legacy disabled parsers and convert them to website rules 881 | /** @type {string[]} */ 882 | let disabledParsers = json.disabledParsers; 883 | for (let parserId of disabledParsers) { 884 | if (parserId === "twitter") { 885 | websiteRules["twitter.com"] = new WebsiteRule("twitter.com", false); 886 | websiteRules["x.com"] = new WebsiteRule("x.com", false); 887 | } else if (parserId === "reddit") { 888 | websiteRules["reddit.com"] = new WebsiteRule("reddit.com", false); 889 | } else if (parserId === "bluesky") { 890 | websiteRules["bsky.app"] = new WebsiteRule("bsky.app", false); 891 | } else if (parserId === "facebook") { 892 | websiteRules["facebook.com"] = new WebsiteRule("facebook.com", false); 893 | } 894 | } 895 | } 896 | 897 | let globalMuteAction = json.globalMuteAction; 898 | if (globalMuteAction === undefined || typeof globalMuteAction !== "string") { 899 | console.warn("Missing or invalid globalMuteAction property: " + JSON.stringify(json)); 900 | globalMuteAction = undefined; 901 | } 902 | 903 | let debugMode = json.debugMode; 904 | if (typeof debugMode !== "boolean") { 905 | console.warn("Invalid debugMode property: " + JSON.stringify(json)); 906 | debugMode = undefined; 907 | } 908 | 909 | let mutableEnabled = json.mutableEnabled; 910 | if (mutableEnabled !== undefined && typeof mutableEnabled !== "boolean") { 911 | console.warn("Invalid mutableEnabled property: " + JSON.stringify(json)); 912 | mutableEnabled = undefined; 913 | } 914 | 915 | let enabledByDefault = json.enabledByDefault; 916 | if (enabledByDefault !== undefined && typeof enabledByDefault !== "boolean") { 917 | console.warn("Invalid enabledByDefault property: " + JSON.stringify(json)); 918 | enabledByDefault = undefined; 919 | } 920 | 921 | return new Settings(groups, websiteRules, globalMuteAction, debugMode, mutableEnabled, enabledByDefault); 922 | } 923 | } 924 | 925 | function generateId() { 926 | // From nanoid: https://github.com/ai/nanoid 927 | // With length of 9, 1% chance of collision at 19 million IDs 928 | let t = 9; 929 | return crypto.getRandomValues(new Uint8Array(t)).reduce(((t,e)=>t+=(e&=63)<36?e.toString(36):e<62?(e-26).toString(36).toUpperCase():e>62?"-":"_"),""); 930 | } 931 | 932 | /** 933 | * Get the metadata for the serialized settings from the web extension sync storage. 934 | * @returns {Promise} A promise that resolves to the metadata 935 | */ 936 | function getSettingsMetadata() { 937 | return new Promise((resolve, reject) => { 938 | chrome.storage.sync.get("metadata", function (result) { 939 | if (chrome.runtime.lastError) { 940 | console.error(chrome.runtime.lastError); 941 | reject(chrome.runtime.lastError); 942 | } else { 943 | resolve(result.metadata); 944 | } 945 | }); 946 | }); 947 | } 948 | 949 | /** 950 | * Asynchronously get the settings from the web extension sync storage. 951 | * @param {(arg0: Settings) => void} onSuccess The function to call when the settings have been loaded successfully 952 | * @param {(msg: string) => void} [onFail] The function to call when the settings could not be loaded 953 | */ 954 | function getSettings(onSuccess, onFail = (msg) => {console.error(msg)}) { 955 | console.log("Loading settings"); 956 | if (typeof chrome === "undefined") { 957 | onFail("Chrome API not found"); 958 | return; 959 | } 960 | getSettingsMetadata().then((result) => { 961 | if (result !== undefined) { 962 | if (result.v !== 1) { 963 | onFail("Settings version not supported, expected v1 but got " + result.v); 964 | } else if (result.n === undefined) { 965 | onFail("Compressed settings missing number of parts"); 966 | } else { 967 | // Retrieve the serialized settings in parts 968 | let keysToGet = ["metadata"]; 969 | for (let i = 0; i < result.n; i++) { 970 | keysToGet.push("p" + i); 971 | } 972 | chrome.storage.sync.get(keysToGet, function (result) { 973 | if (chrome.runtime.lastError) { 974 | console.error(chrome.runtime.lastError); 975 | onFail("Error loading serialized settings from sync storage"); 976 | } else { 977 | // Reconstruct the serialized settings 978 | deserializeSettings(result, (deserialized) => { 979 | let restored = Settings.fromJson(deserialized); 980 | if (restored) { 981 | onSuccess(restored); 982 | } else { 983 | onFail("Compressed settings could not be deserialized"); 984 | } 985 | }, onFail); 986 | } 987 | }); 988 | } 989 | } else { 990 | // Possible that the settings were not migrated yet 991 | getLegacySettings((settings) => { 992 | onSuccess(settings); 993 | }, onFail); 994 | } 995 | }); 996 | } 997 | 998 | /** 999 | * Asynchronously get legacy settings should they exist. 1000 | * @param {(arg0: Settings) => void} onSuccess 1001 | * @param {(msg: string) => void} onFail 1002 | */ 1003 | function getLegacySettings(onSuccess, onFail) { 1004 | chrome.storage.sync.get("settings", function (result) { 1005 | if (chrome.runtime.lastError) { 1006 | console.error(chrome.runtime.lastError); 1007 | onFail("Error loading legacy settings from sync storage") 1008 | } else if (result.settings === undefined) { 1009 | onFail("Legacy settings not found"); 1010 | } else { 1011 | let restored = Settings.fromJson(result.settings); 1012 | if (restored) { 1013 | console.log("Legacy settings restored successfully"); 1014 | onSuccess(restored); 1015 | } else { 1016 | onFail("Legacy settings could not be restored"); 1017 | } 1018 | } 1019 | }); 1020 | } 1021 | 1022 | /** 1023 | * Compress and serialize the settings in parts to account for the 8KB limit on the web extension sync storage. 1024 | * @param {Settings} settings 1025 | * @param {(arg0: Object) => void} cb The function to call with the serialized settings 1026 | */ 1027 | function serializeSettings(settings, cb) { 1028 | let serialized = JSON.stringify(settings); 1029 | // @ts-ignore 1030 | const compressed = pako.deflate(serialized, { to: "string" }); 1031 | const base64 = btoa(String.fromCharCode(...compressed)); 1032 | const PART_SIZE = 8000; 1033 | let parts = []; 1034 | for (let i = 0; i < base64.length; i += PART_SIZE) { 1035 | parts.push(base64.substring(i, i + PART_SIZE)); 1036 | } 1037 | let serializedSettings = { 1038 | "metadata": { 1039 | "v": 1, // Version 1040 | "n": parts.length, // Number of parts 1041 | } 1042 | }; 1043 | for (let i = 0; i < parts.length; i++) { 1044 | serializedSettings["p" + i] = parts[i]; 1045 | } 1046 | cb(serializedSettings); 1047 | } 1048 | 1049 | /** 1050 | * Decompress and deserialize the settings. 1051 | * @param {Object} serialized 1052 | * @param {(json: object) => void} cb The function to call with the deserialized settings 1053 | * @param {(msg: string) => void} onFail The function to call when the settings could not be deserialized 1054 | */ 1055 | function deserializeSettings(serialized, cb, onFail) { 1056 | if (serialized.metadata === undefined) { 1057 | onFail("Serialized settings missing metadata"); 1058 | return; 1059 | } 1060 | if (serialized.metadata.v !== 1) { 1061 | onFail("Serialized settings version not supported, expected v1 but got " + serialized.metadata.v); 1062 | return; 1063 | } 1064 | if (serialized.metadata.n === undefined) { 1065 | onFail("Serialized settings missing number of parts"); 1066 | return; 1067 | } 1068 | let encoded = ""; 1069 | for (let i = 0; i < serialized.metadata.n; i++) { 1070 | if (serialized["p" + i] === undefined) { 1071 | onFail("Serialized settings missing part " + i); 1072 | return; 1073 | } 1074 | encoded += serialized["p" + i]; 1075 | } 1076 | try { 1077 | const compressedData = atob(encoded); 1078 | const compressedArray = Uint8Array.from([...compressedData].map((char) => char.charCodeAt(0))); 1079 | //@ts-ignore 1080 | const decompressedArray = pako.inflate(compressedArray); 1081 | const decompressedString = new TextDecoder().decode(decompressedArray); 1082 | cb(JSON.parse(decompressedString)); 1083 | } catch (ex) { 1084 | console.error(ex); 1085 | onFail("Failed to decompress serialized settings: " + ex); 1086 | } 1087 | } 1088 | 1089 | /** 1090 | * Save the settings to the web extension sync storage. 1091 | * @param {Settings} settings The settings to upload 1092 | * @param {() => void} [onSuccess] The function to call when the settings have been saved successfully 1093 | * @param {(msg: string) => void} [onFail] The function to call when the settings could not be saved 1094 | */ 1095 | function putSettings(settings, onSuccess= () => {}, onFail = (msg) => {console.error(msg)}) { 1096 | if (typeof chrome === "undefined") { 1097 | onFail("Chrome API not found"); 1098 | return; 1099 | } 1100 | try { 1101 | serializeSettings(settings, (serializedSettings) => { 1102 | chrome.storage.sync.set(serializedSettings, function () { 1103 | if (chrome.runtime.lastError) { 1104 | console.error(chrome.runtime.lastError); 1105 | onFail(("Settings could not be saved due to a browser storage sync error")); 1106 | chrome.storage.sync.getBytesInUse("settings", function (bytesInUse) { 1107 | console.log("Bytes in use: " + bytesInUse); 1108 | console.log(JSON.stringify(settings)); 1109 | }); 1110 | } else { 1111 | console.log("Settings saved successfully"); 1112 | onSuccess(); 1113 | } 1114 | }); 1115 | }); 1116 | } catch (ex) { 1117 | console.error(ex); 1118 | onFail("Failed to serialize settings"); 1119 | } 1120 | } 1121 | 1122 | /** 1123 | * Delete the settings from the web extension sync storage. 1124 | * @param {string} key The key of the settings to delete 1125 | */ 1126 | function deleteSettings(key) { 1127 | console.log("Deleting settings with key '" + key + "'"); 1128 | if (typeof chrome === "undefined") { 1129 | console.error("Chrome API not found"); 1130 | return; 1131 | } 1132 | chrome.storage.sync.remove(key, function () { 1133 | if (chrome.runtime.lastError) { 1134 | console.error(chrome.runtime.lastError); 1135 | } else { 1136 | console.log("Settings with key '" + key + "' deleted successfully"); 1137 | } 1138 | }); 1139 | } 1140 | 1141 | /** 1142 | * Subscribe to changes in the settings. 1143 | * @param {(arg0: Settings) => void} callback 1144 | */ 1145 | function subscribeToSettings(callback) { 1146 | if (typeof chrome === "undefined") { 1147 | console.error("Chrome API not found"); 1148 | return; 1149 | } 1150 | chrome.storage.onChanged.addListener((changes, areaName) => { 1151 | if (areaName === "sync" && changes["metadata"]) { 1152 | getSettings(callback); 1153 | } 1154 | }); 1155 | } -------------------------------------------------------------------------------- /tests/tests.js: -------------------------------------------------------------------------------- 1 | // Assert dependency is registered in the test script in package.json 2 | 3 | describe('Parsing tests', () => { 4 | describe("Keyword mute pattern", () => { 5 | it('Whole content match', () => { 6 | const pattern = new KeywordMute("#test", "test"); 7 | const result = pattern.isMatch("test"); 8 | assert.isTrue(result); 9 | }); 10 | 11 | it('Word surrounded by spaces', () => { 12 | const pattern = new KeywordMute("#test", "test"); 13 | const result = pattern.isMatch(" test "); 14 | assert.isTrue(result); 15 | }); 16 | 17 | it('Beginning of sentence', () => { 18 | const pattern = new KeywordMute("#test", "Test"); 19 | const result = pattern.isMatch("Test this or that."); 20 | assert.isTrue(result); 21 | }); 22 | 23 | it('Middle of sentence', () => { 24 | const pattern = new KeywordMute("#test", "this"); 25 | const result = pattern.isMatch("Test this or that."); 26 | assert.isTrue(result); 27 | }); 28 | 29 | it('End of sentence', () => { 30 | const pattern = new KeywordMute("#test", "that"); 31 | const result = pattern.isMatch("Test this or that."); 32 | assert.isTrue(result); 33 | }); 34 | 35 | it('Case insensitivity whole word', () => { 36 | const pattern = new KeywordMute("#test", "TEST"); 37 | const result = pattern.isMatch("test"); 38 | assert.isTrue(result); 39 | }); 40 | 41 | it('Case sensitive beginning of sentence', () => { 42 | const pattern = new KeywordMute("#test", "Test", true); 43 | const result = pattern.isMatch("Test this or that."); 44 | assert.isTrue(result); 45 | }); 46 | 47 | it('Case sensitive middle of sentence', () => { 48 | const pattern = new KeywordMute("#test", "ThIs", true); 49 | const result = pattern.isMatch("Test ThIs or that."); 50 | assert.isTrue(result); 51 | }); 52 | 53 | it('Case sensitive end of sentence', () => { 54 | const pattern = new KeywordMute("#test", "That", true); 55 | const result = pattern.isMatch("Test this or That."); 56 | assert.isTrue(result); 57 | }); 58 | 59 | it('Symbols within word', () => { 60 | const symbols = "!@#$%^&*()_+-=[]{}|;':,.<>/?"; 61 | for (let i = 0; i < symbols.length; i++) { 62 | const pattern = new KeywordMute("#test", "test" + symbols[i] + "test"); 63 | const result = pattern.isMatch("test" + symbols[i] + "test"); 64 | assert.isTrue(result, "Failed on '" + symbols[i] + "' with content: " + "test" + symbols[i] + "test"); 65 | } 66 | }); 67 | 68 | it('Surrounded by punctuation', () => { 69 | const pattern = new KeywordMute("#test", "test", true); 70 | const punctuation = ".,;:!?-_+=()[]{}'\"<>"; 71 | for (let i = 0; i < punctuation.length; i++) { 72 | const result = pattern.isMatch(`${punctuation[i]}test${punctuation[i]}`); 73 | assert.isTrue(result, "Failed on '" + punctuation[i] + "' with content: " + `${punctuation[i]}test${punctuation[i]}`); 74 | } 75 | }); 76 | 77 | it('Beginning of a word', () => { 78 | const pattern = new KeywordMute("#test", "test", true); 79 | const result = pattern.isMatch("testify"); 80 | assert.isFalse(result); 81 | }); 82 | 83 | it('Middle of a word', () => { 84 | const pattern = new KeywordMute("#test", "test", true); 85 | const result = pattern.isMatch("attested"); 86 | assert.isFalse(result); 87 | }); 88 | 89 | it('End of a word', () => { 90 | const pattern = new KeywordMute("#test", "test", true); 91 | const result = pattern.isMatch("attest"); 92 | assert.isFalse(result); 93 | }); 94 | 95 | it('Mute phrase', () => { 96 | const pattern = new KeywordMute("#test", "brown hen", true); 97 | const result = pattern.isMatch("the brown hen laid an egg"); 98 | assert.isTrue(result); 99 | }); 100 | 101 | it('Word with apostrophe', () => { 102 | const pattern = new KeywordMute("#test", "don't", true); 103 | const result = pattern.isMatch("I don't know"); 104 | assert.isTrue(result); 105 | }); 106 | 107 | it('Word with hyphen', () => { 108 | const pattern = new KeywordMute("#test", "well-being", true); 109 | const result = pattern.isMatch("I care for your well-being dude"); 110 | assert.isTrue(result); 111 | }); 112 | 113 | it('Word with hyphen at end of sentence', () => { 114 | const pattern = new KeywordMute("#test", "well-being", true); 115 | const result = pattern.isMatch("I care for your well-being"); 116 | assert.isTrue(result); 117 | }); 118 | 119 | it('Possessive word', () => { 120 | const pattern = new KeywordMute("#test", "Idrees"); 121 | const result = pattern.isMatch("Idrees's car"); 122 | assert.isTrue(result); 123 | }); 124 | }); 125 | }); --------------------------------------------------------------------------------