├── .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 |
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 |
42 | Open Safari Settings
43 | Open Settings
44 |
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 |
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 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
mute a word
101 |
105 |
106 |
125 |
126 |
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 | 
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 | 
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=''
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 |
35 |
36 |
37 |
38 |
39 |
Use Mutable
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
This Website
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
mute list
67 |
71 |
72 |
73 |
74 |
77 |
78 |
82 |
gamma
83 | x
84 |
85 |
beta
86 | x
87 |
88 |
alpha
89 | x
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
100 |
101 |
102 |
103 |
104 |
105 |
settings
106 |
110 |
111 |
112 |
113 |
114 | What would you like to do with muted posts?
115 |
116 |
117 |
118 | blur them out
119 | blur with text preview
120 | hide completely
121 | replace with kittens
122 | replace with puppies
123 | replace with hedgehogs
124 |
125 |
126 |
127 |
128 |
129 | Enabled by Default
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 | Debug Mode
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
Custom Site Settings
148 |
149 |
150 |
alpha
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
beta
159 | x
160 |
161 |
gamma
162 | x
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
thanks
173 |
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 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
mute a word
219 |
223 |
224 |
243 |
244 |
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 |
187 |
188 |
189 |
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 | });
--------------------------------------------------------------------------------