├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.sh ├── dev-testing └── info.txt ├── node_modules ├── .package-lock.json └── webextension-polyfill │ ├── LICENSE │ ├── README.md │ ├── dist │ ├── browser-polyfill.js │ ├── browser-polyfill.js.map │ ├── browser-polyfill.min.js │ └── browser-polyfill.min.js.map │ └── package.json ├── package-lock.json ├── package.json └── src ├── _locales ├── de │ └── messages.json ├── en │ └── messages.json └── fr │ └── messages.json ├── background.js ├── content-communityNotFound.js ├── content-general.js ├── content-sidebar.js ├── img ├── icon-copy.png ├── icon-external.png ├── icon-home.png ├── icon-lemm-noIcon.png ├── icon-lemm-nsf.png ├── lemming128.png ├── lemming128_dev.png ├── lemming16.png ├── lemming16_dev.png ├── lemming32.png ├── lemming32_dev.png ├── lemming48.png └── lemming48_dev.png ├── m3-background.js ├── manifest_chrome.json ├── manifest_edge.json ├── manifest_firefox.json ├── manifest_opera.json ├── manifest_safari.json ├── page-options ├── options.css ├── options.html └── options.js ├── page-popup ├── popup.css ├── popup.html └── popup.js ├── page-search ├── search.css ├── search.html └── search.js ├── page-settings ├── settings.css ├── settings.html └── settings.js ├── page-sidebar ├── sidebar.css ├── sidebar.html └── sidebar.js ├── styles.css └── utils.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [cynber] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: cynber 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dev-resources/ 3 | dev-deprecated/ 4 | web-ext-artifacts/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

Instance Assistant

4 | 5 |
6 |

7 | 8 |     9 | 10 |     11 | 12 |        13 | 14 |

15 |
16 | 17 |
18 | 19 | # Downloads 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 33 | 38 | 43 | 44 | 45 | 46 |
Firefox Google Chrome Microsoft Edge
29 | 30 | Get 'Instance Assistant for Lemmy & Kbin' on Firefox 31 | 32 | 34 | 35 | Get 'Instance Assistant for Lemmy & Kbin' on Chrome 36 | 37 | 39 | 40 | Get 'Instance Assistant for Lemmy & Kbin' - Edge 41 | 42 |
47 | 48 | **Safari:** No immediate plans. While I can port the extension to Safari, I can't sign and publish it without a recent MacOS device. 49 | 50 | **Other browsers:** No immediate plans. For most other browsers, you should be able to download from either the Chrome or Firefox stores. 51 | 52 | If you have thoughts or want to contribute, you can join the discussion here: https://lemmy.ca/c/instance_assistant 53 | 54 |
55 | 56 | 57 | 58 | 59 | # Features 60 | 61 |
62 | 63 | **Support** for Lemmy and Kbin instances + custom frontends such as Alexandrite and Photon 64 | 65 |
66 | 67 | **Post to Lemmy/Kbin:** 68 | * Instance Assistant can generate a draft post with autofilled title/link/body 69 | * For most news sites, videos, and other webpages, you can use the popup menu 70 | * For images, you can right-click on the image itself 71 | 72 |
73 | 74 | **Search for posts:** 75 | * Find and open all posts that have a link to the site you are on. Use it to find posts about news articles, videos, and more 76 | 77 |
78 | 79 | **Search for communities and content without leaving the page:** 80 | * You can use the popup menu or sidebar to search for communities (powered by [lemmyverse.net](https://lemmyverse.net/communities)), and for content (powered by [search-lemmy.com](https://search-lemmy.com)) 81 | 82 |
83 | 84 | **Redirect to your home instance:** 85 | * When you are on a foreign instance, buttons will be available in the sidebar to open the **community**, **post**, or **user (NEW)** in your home instance, allowing you to participate immediately 86 | 87 |
88 | 89 | **Open links in home instance:** 90 | * If you come across a Lemmy/Kbin link anywhere on the web, you can right click to open it in your home instance 91 | 92 |
93 | 94 | **Upgraded Pages:** 95 | * 'Community not found' pages now have better information and buttons to trigger a fetch, open the community in the source instance, and more. This also supports alternative front-ends. [See it in action here](https://lemmy.ca/c/fakecommunity@example.com) 96 | * `/communities` pages will have the non-functional 'subscribe' buttons replaced, to make it easier when browsing other instances. 97 | 98 |
99 | 100 | **Customizable:** 101 | * Customizable list of instances in popup/sidebar to let you quickly switch home instances. This is great for if you have multiple accounts on different instances 102 | * You can change the behaviour of the extension and customize the actual Lemmy/Kbin site interface, making your browsing experience your own. 103 | 104 |
105 |
106 | 107 | ### I'm new to Lemmy/Kbin, what is this? 108 | 109 | Lemmy and Kbin are a part of the Fediverse, a network of interconnected social media platforms that work similar to Reddit. This extension is designed to make it easier to use these platforms and make the most of decentralized social media while mimizing the difficulties that may come with it. 110 | 111 | Say, for example, you may make an account on `lemmy.ca`. You may google a community or topic and come across a page on a different instance (ex. `lemmy.ml/c/technology`). If you want to subscribe to the community, you will need to copy the code (`!technology@lemmy.ml`), open your home instance, and then paste it into the search page. If you want to comment on a post, you would then try to track it down by scrolling through the community. 112 | 113 | This extension will let you jump to the version on your home instance, allowing you to subscribe and participate immediately. 114 | 115 | There are many other features as well, and hopefully many more to come! :) 116 | 117 |
118 | 119 | # Contributing 120 | 121 | You can contribute to this project in a number of ways: 122 | 123 | 🐛 **Report bugs & suggest features:** If you find a bug or want a new feature, you can discuss in the [Support Community](https://lemmy.ca/c/instance_assistant) or open an issue on GitHub. 124 | 125 | 💻 **Contribute code:** You can find guidance [on the wiki](https://github.com/cynber/lemmy-instance-assistant/wiki/Development-Process) 126 | 127 | 🌐 **Translate:** If you want to help translate the extension, you will soon be able to do so. I will update this section once it is ready. 128 | 129 | 💛 **Donate:** If you want to support the project financially, you can do so by clicking the sponsor button on this page, or through [ko-fi.com/cynber](https://ko-fi.com/cynber) 130 | 131 | 132 | 133 |
134 | 135 | # Other Links 136 | 137 | * [Recent Updates](https://github.com/cynber/lemmy-instance-assistant/wiki#recent-updates) 138 | * [Open Issues](https://github.com/cynber/lemmy-instance-assistant/issues) 139 | * [What is being worked on?](https://github.com/users/cynber/projects/1) 140 | * [Credits](https://github.com/cynber/lemmy-instance-assistant/wiki#credits) 141 | 142 |
143 | 144 | # Screenshots 145 | 146 | This is not a complete list of features, but it should give you an idea of what the extension can do. I will update these screenshots every now and then, so the UI may have changed since these were taken. 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 |
Popup Menu with redirect, community search, and moreImproved 'community not found' page, with button to triger a fetch and more
firefox-sc-3 firefox-sc-4
158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 |
Redirect button in sidebar of foreign Lemmy instanceRedirect button in sidebar of foreign Kbin instance
firefox-sc-1 firefox-sc-2
169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 |
Redirect button for Photon FrontendRedirect button for Alexandrite Frontend
firefox-sc-0 firefox-sc-0
180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 |
Improved '/communities' page on foreign instances Persistent sidebar version of popup menu
firefox-sc-0 firefox-sc-5
191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 |
Right click context menu to open any links in your home instance Change display settings, such as hiding the default sidebar
firefox-sc-0 firefox-sc-0
202 | 203 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function init_build() { 4 | # GET VERSION AND MANIFEST VERSION ========================================== 5 | version=$(grep -o '"version": "[^"]*' src/manifest_$1.json | cut -d'"' -f4) 6 | manifest=$(grep -o '"manifest_version": [0-9]*' src/manifest_$1.json | cut -d' ' -f2) 7 | 8 | # CREATE BUILD DIRECTORY ==================================================== 9 | echo "Building $1 v-$version$suffix (Manifest $manifest)..." 10 | directory="build/$1/instance-assistant-$version$suffix" 11 | if [ -d "$directory" ]; then 12 | rm -rf $directory 13 | fi 14 | mkdir -p $directory 15 | 16 | # COPY FILES ================================================================ 17 | cp src/manifest_$1.json $directory/manifest.json 18 | cp LICENSE $directory/LICENSE 19 | cp src/styles.css $directory/styles.css 20 | cp src/utils.js $directory/utils.js 21 | cp src/content-sidebar.js $directory/content-sidebar.js 22 | cp src/content-communityNotFound.js $directory/content-communityNotFound.js 23 | cp src/content-general.js $directory/content-general.js 24 | cp -r node_modules $directory/node_modules 25 | cp -r src/img $directory/img 26 | # cp -r src/_locales $directory/_locales # TODO: Fix translations 27 | cp -r src/page-options $directory/page-options 28 | cp -r src/page-popup $directory/page-popup 29 | cp -r src/page-settings $directory/page-settings 30 | cp -r src/page-sidebar $directory/page-sidebar 31 | cp -r src/page-search $directory/page-search 32 | 33 | # COPY SCRIPT FILES BASED ON MANIFEST VERSION ================================ 34 | if [ "$manifest" = 2 ]; then 35 | cp src/background.js $directory/background.js 36 | else 37 | cp src/m3-background.js $directory/background.js 38 | fi 39 | 40 | # REPLACE DEV IMAGES WITH PRODUCTION IMAGES ================================== 41 | if [ "$isDev" = false ]; then 42 | sed -i 's/_dev.png/.png/' $directory/manifest.json 43 | fi 44 | 45 | # CHECK IF ZIP FILE ALREADY EXISTS AND CONFIRM OVERWRITE (PRODUCTION ONLY) === 46 | if [ -f "build/$1/instance-assistant-$1-$version$suffix.zip" ] && [ "$isDev" = false ]; then 47 | read -p "Zip file already exists. Overwrite? (y/n): " confirm 48 | if [[ $confirm == "y" ]]; then 49 | rm "build/$1/instance-assistant-$1-$version$suffix.zip" 50 | else 51 | echo "Build process canceled." 52 | exit 1 53 | fi 54 | fi 55 | 56 | # ZIP FILES, REMOVE BUILD DIRECTORY, AND PRINT SUCCESS MESSAGE =============== 57 | cd build/$1/instance-assistant-$version$suffix 58 | zip -r ../instance-assistant-$1-$version$suffix.zip * >/dev/null 2>&1 59 | cd ../../.. 60 | # rm -rf $directory 61 | echo -e "\e[32mDone building $1 v-$version$suffix\e[0m" 62 | } 63 | 64 | # ================================================================================= 65 | # BUILD SCRIPT 66 | # ================================================================================= 67 | 68 | suffix="" 69 | isDev=false 70 | 71 | if [ "$1" == "-dev" ]; then 72 | suffix="-DEV" 73 | isDev=true 74 | fi 75 | 76 | init_build "chrome" 77 | init_build "firefox" 78 | init_build "edge" 79 | init_build "opera" 80 | #init_build "safari" -------------------------------------------------------------------------------- /dev-testing/info.txt: -------------------------------------------------------------------------------- 1 | This folder has code that's still being tested -------------------------------------------------------------------------------- /node_modules/.package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lemmy-instance-assistant", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "node_modules/webextension-polyfill": { 7 | "version": "0.10.0", 8 | "resolved": "https://registry.npmjs.org/webextension-polyfill/-/webextension-polyfill-0.10.0.tgz", 9 | "integrity": "sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g==", 10 | "dev": true 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /node_modules/webextension-polyfill/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 | -------------------------------------------------------------------------------- /node_modules/webextension-polyfill/README.md: -------------------------------------------------------------------------------- 1 | # WebExtension `browser` API Polyfill 2 | 3 | This library allows extensions that use the Promise-based WebExtension/BrowserExt API being standardized by the 4 | [W3 Browser Extensions][w3-browserext] group to run on Google Chrome with minimal or no changes. 5 | 6 | [![CircleCI](https://circleci.com/gh/mozilla/webextension-polyfill.svg?style=svg)](https://circleci.com/gh/mozilla/webextension-polyfill) 7 | [![codecov](https://codecov.io/gh/mozilla/webextension-polyfill/branch/master/graph/badge.svg)](https://codecov.io/gh/mozilla/webextension-polyfill) 8 | [![devDependency Status](https://david-dm.org/mozilla/webextension-polyfill/dev-status.svg)](https://david-dm.org/mozilla/webextension-polyfill#info=devDependencies) 9 | [![npm version](https://badge.fury.io/js/webextension-polyfill.svg)](https://badge.fury.io/js/webextension-polyfill) 10 | 11 | > This library doesn't (and it is not going to) polyfill API methods or options that are missing on Chrome but natively provided 12 | > on Firefox, and so the extension has to do its own "runtime feature detection" in those cases (and then eventually polyfill the 13 | > missing feature on its own or enable/disable some of the features accordingly). 14 | 15 | [w3-browserext]: https://www.w3.org/community/browserext/ 16 | 17 | Table of contents 18 | ================= 19 | 20 | * [Supported Browsers](#supported-browsers) 21 | * [Installation](#installation) 22 | * [Basic Setup](#basic-setup) 23 | * [Basic Setup with ES6 module loader](#basic-setup-with-es6-module-loader) 24 | * [Basic Setup with module bundlers](#basic-setup-with-module-bundlers) 25 | * [Usage with webpack without bundling](#usage-with-webpack-without-bundling) 26 | * [Using the Promise-based APIs](#using-the-promise-based-apis) 27 | * [Examples](#examples) 28 | * [Usage with TypeScript](#usage-with-typescript) 29 | * [Known Limitations and Incompatibilities](#known-limitations-and-incompatibilities) 30 | * [Contributing to this project](#contributing-to-this-project) 31 | 32 | Supported Browsers 33 | ================== 34 | 35 | | Browser | Support Level | 36 | | ------------------------- | -------------------------------------------------------------------------------------------------- | 37 | | Chrome | *Officially Supported* (with automated tests) | 38 | | Firefox | *Officially Supported as a NO-OP* (with automated tests for comparison with the behaviors on Chrome) | 39 | | Opera / Edge (>=79.0.309) | *Unofficially Supported* as a Chrome-compatible target (but not explicitly tested in automation) | 40 | 41 | The polyfill is being tested explicitly (with automated tests that run on every pull request) on **officially supported** 42 | browsers (that are currently the last stable versions of Chrome and Firefox). 43 | 44 | On Firefox, this library is actually acting as a NO-OP: it detects that the `browser` API object is already defined 45 | and it does not create any custom wrappers. 46 | Firefox is still included in the automated tests, to ensure that no wrappers are being created when running on Firefox, 47 | and for comparison with the behaviors implemented by the library on Chrome. 48 | 49 | ## Installation 50 | 51 | A new version of the library is built from this repository and released as an npm package. 52 | 53 | The npm package is named after this repo: [webextension-polyfill](https://www.npmjs.com/package/webextension-polyfill). 54 | 55 | For the extension that already include a package.json file, the last released version of this library can be quickly installed using: 56 | 57 | ``` 58 | npm install --save-dev webextension-polyfill 59 | ``` 60 | 61 | Inside the `dist/` directory of the npm package, there are both the minified and non-minified builds (and their related source map files): 62 | 63 | - node_modules/webextension-polyfill/dist/browser-polyfill.js 64 | - node_modules/webextension-polyfill/dist/browser-polyfill.min.js 65 | 66 | For extensions that do not include a package.json file and/or prefer to download and add the library directly into their own code repository, all the versions released on npm are also available for direct download from unpkg.com: 67 | 68 | - https://unpkg.com/webextension-polyfill/dist/ 69 | 70 | and linked to the Github releases: 71 | 72 | - https://github.com/mozilla/webextension-polyfill/releases 73 | 74 | ## Basic Setup 75 | 76 | In order to use the polyfill, it must be loaded into any context where `browser` APIs are accessed. The most common cases 77 | are background and content scripts, which can be specified in `manifest.json` (make sure to include the `browser-polyfill.js` script before any other scripts that use it): 78 | 79 | ```javascript 80 | { 81 | // ... 82 | 83 | "background": { 84 | "scripts": [ 85 | "browser-polyfill.js", 86 | "background.js" 87 | ] 88 | }, 89 | 90 | "content_scripts": [{ 91 | // ... 92 | "js": [ 93 | "browser-polyfill.js", 94 | "content.js" 95 | ] 96 | }] 97 | } 98 | ``` 99 | 100 | For HTML documents, such as `browserAction` popups, or tab pages, it must be 101 | included more explicitly: 102 | 103 | ```html 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | ``` 113 | 114 | And for dynamically-injected content scripts loaded by `tabs.executeScript`, 115 | it must be injected by a separate `executeScript` call, unless it has 116 | already been loaded via a `content_scripts` declaration in 117 | `manifest.json`: 118 | 119 | ```javascript 120 | browser.tabs.executeScript({file: "browser-polyfill.js"}); 121 | browser.tabs.executeScript({file: "content.js"}).then(result => { 122 | // ... 123 | }); 124 | ``` 125 | 126 | ### Basic Setup with ES6 module loader 127 | 128 | The polyfill can also be loaded using the native ES6 module loader available in 129 | the recent browsers versions. 130 | 131 | Be aware that the polyfill module does not export the `browser` API object, 132 | but defines the `browser` object in the global namespace (i.e. `window`). 133 | 134 | ```html 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | ``` 144 | 145 | ```javascript 146 | // In background.js (loaded after browser-polyfill.js) the `browser` 147 | // API object is already defined and provides the promise-based APIs. 148 | browser.runtime.onMessage.addListener(...); 149 | ``` 150 | 151 | ### Basic Setup with module bundlers 152 | 153 | This library is built as a **UMD module** (Universal Module Definition), and so it can also be used with module bundlers (and explicitly tested on both **webpack** and **browserify**) or AMD module loaders. 154 | 155 | **src/background.js**: 156 | ```javascript 157 | var browser = require("webextension-polyfill"); 158 | 159 | browser.runtime.onMessage.addListener(async (msg, sender) => { 160 | console.log("BG page received message", msg, "from", sender); 161 | console.log("Stored data", await browser.storage.local.get()); 162 | }); 163 | 164 | browser.browserAction.onClicked.addListener(() => { 165 | browser.tabs.executeScript({file: "content.js"}); 166 | }); 167 | ``` 168 | 169 | **src/content.js**: 170 | ```javascript 171 | var browser = require("webextension-polyfill"); 172 | 173 | browser.storage.local.set({ 174 | [window.location.hostname]: document.title, 175 | }).then(() => { 176 | browser.runtime.sendMessage(`Saved document title for ${window.location.hostname}`); 177 | }); 178 | ``` 179 | 180 | By using `require("webextension-polyfill")`, the module bundler will use the non-minified version of this library, and the extension is supposed to minify the entire generated bundles as part of its own build steps. 181 | 182 | If the extension doesn't minify its own sources, it is still possible to explicitly ask the module bundler to use the minified version of this library, e.g.: 183 | 184 | ```javascript 185 | var browser = require("webextension-polyfill/dist/browser-polyfill.min"); 186 | 187 | ... 188 | ``` 189 | 190 | ### Usage with webpack without bundling 191 | 192 | The previous section explains how to bundle `webextension-polyfill` in each script. An alternative method is to include a single copy of the library in your extension, and load the library as shown in [Basic Setup](#basic-setup). You will need to install [copy-webpack-plugin](https://www.npmjs.com/package/copy-webpack-plugin): 193 | 194 | ```sh 195 | npm install --save-dev copy-webpack-plugin 196 | ``` 197 | 198 | **In `webpack.config.js`,** import the plugin and configure it this way. It will copy the minified file into your _output_ folder, wherever your other webpack files are generated. 199 | 200 | ```js 201 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 202 | 203 | module.exports = { 204 | /* Your regular webpack config, probably including something like this: 205 | output: { 206 | path: path.join(__dirname, 'distribution'), 207 | filename: '[name].js' 208 | }, 209 | */ 210 | plugins: [ 211 | new CopyWebpackPlugin({ 212 | patterns: [{ 213 | from: 'node_modules/webextension-polyfill/dist/browser-polyfill.js', 214 | }], 215 | }) 216 | ] 217 | } 218 | ``` 219 | 220 | And then include the file in each context, using the `manifest.json` just like in [Basic Setup](#basic-setup). 221 | 222 | ## Using the Promise-based APIs 223 | 224 | The Promise-based APIs in the `browser` namespace work, for the most part, 225 | very similarly to the callback-based APIs in Chrome's `chrome` namespace. 226 | The major differences are: 227 | 228 | * Rather than receiving a callback argument, every async function returns a 229 | `Promise` object, which resolves or rejects when the operation completes. 230 | 231 | * Rather than checking the `chrome.runtime.lastError` property from every 232 | callback, code which needs to explicitly deal with errors registers a 233 | separate Promise rejection handler. 234 | 235 | * Rather than receiving a `sendResponse` callback to send a response, 236 | `onMessage` listeners simply return a Promise whose resolution value is 237 | used as a reply. 238 | 239 | * Rather than nesting callbacks when a sequence of operations depend on each 240 | other, Promise chaining is generally used instead. 241 | 242 | * The resulting Promises can be also used with `async` and `await`, rather 243 | than dealt with directly. 244 | 245 | ## Examples 246 | 247 | The following code will retrieve a list of URLs patterns from the `storage` 248 | API, retrieve a list of tabs which match any of them, reload each of those 249 | tabs, and notify the user that is has been done: 250 | 251 | ```javascript 252 | browser.storage.local.get("urls").then(({urls}) => { 253 | return browser.tabs.query({url: urls}); 254 | }).then(tabs => { 255 | return Promise.all( 256 | Array.from(tabs, tab => browser.tabs.reload(tab.id)) 257 | ); 258 | }).then(() => { 259 | return browser.notifications.create({ 260 | type: "basic", 261 | iconUrl: "icon.png", 262 | title: "Tabs reloaded", 263 | message: "Your tabs have been reloaded", 264 | }); 265 | }).catch(error => { 266 | console.error(`An error occurred while reloading tabs: ${error.message}`); 267 | }); 268 | ``` 269 | 270 | Or, using an async function: 271 | 272 | ```javascript 273 | async function reloadTabs() { 274 | try { 275 | let {urls} = await browser.storage.local.get("urls"); 276 | 277 | let tabs = await browser.tabs.query({url: urls}); 278 | 279 | await Promise.all( 280 | Array.from(tabs, tab => browser.tabs.reload(tab.id)) 281 | ); 282 | 283 | await browser.notifications.create({ 284 | type: "basic", 285 | iconUrl: "icon.png", 286 | title: "Tabs reloaded", 287 | message: "Your tabs have been reloaded", 288 | }); 289 | } catch (error) { 290 | console.error(`An error occurred while reloading tabs: ${error.message}`); 291 | } 292 | } 293 | ``` 294 | 295 | It's also possible to use Promises effectively using two-way messaging. 296 | Communication between a background page and a tab content script, for example, 297 | looks something like this from the background page side: 298 | 299 | ```javascript 300 | browser.tabs.sendMessage(tabId, "get-ids").then(results => { 301 | processResults(results); 302 | }); 303 | ``` 304 | 305 | And like this from the content script: 306 | 307 | ```javascript 308 | browser.runtime.onMessage.addListener(msg => { 309 | if (msg == "get-ids") { 310 | return browser.storage.local.get("idPattern").then(({idPattern}) => { 311 | return Array.from(document.querySelectorAll(idPattern), 312 | elem => elem.textContent); 313 | }); 314 | } 315 | }); 316 | ``` 317 | 318 | or: 319 | 320 | ```javascript 321 | browser.runtime.onMessage.addListener(async function(msg) { 322 | if (msg == "get-ids") { 323 | let {idPattern} = await browser.storage.local.get("idPattern"); 324 | 325 | return Array.from(document.querySelectorAll(idPattern), 326 | elem => elem.textContent); 327 | } 328 | }); 329 | ``` 330 | 331 | Or vice versa. 332 | 333 | ## Usage with TypeScript 334 | 335 | There are multiple projects that add TypeScript support to your web-extension project: 336 | 337 | | Project | Description | 338 | | ------------- | ------------- | 339 | | [@types/webextension-polyfill](https://www.npmjs.com/package/@types/webextension-polyfill) | Types and JS-Doc are automatically generated from the mozilla schema files, so it is always up-to-date with the latest APIs. Formerly known as [webextension-polyfill-ts](https://github.com/Lusito/webextension-polyfill-ts). | 340 | | [web-ext-types](https://github.com/kelseasy/web-ext-types) | Manually maintained types based on MDN's documentation. No JS-Doc included. | 341 | | [@types/chrome](https://www.npmjs.com/package/@types/chrome) | Manually maintained types and JS-Doc. Only contains types for chrome extensions though! | 342 | 343 | ## Known Limitations and Incompatibilities 344 | 345 | This library tries to minimize the amount of "special handling" that a cross-browser extension has to do to be able to run on the supported browsers from a single codebase, but there are still cases when polyfillling the missing or incompatible behaviors or features is not possible or out of the scope of this polyfill. 346 | 347 | This section aims to keep track of the most common issues that an extension may have. 348 | 349 | ### No callback supported by the Promise-based APIs on Chrome 350 | 351 | While some of the asynchronous API methods in Firefox (the ones that return a promise) also support the callback parameter (mostly as a side effect of the backward compatibility with the callback-based APIs available on Chrome), the Promise-based APIs provided by this library do not support the callback parameter (See ["#102 Cannot call browser.storage.local.get with callback"][I-102]). 352 | 353 | ### No promise returned on Chrome for some API methods 354 | 355 | This library takes its knowledge of the APIs to wrap and their signatures from a metadata JSON file: 356 | [api-metadata.json](api-metadata.json). 357 | 358 | If an API method is not yet included in this "API metadata" file, it will not be recognized. 359 | Promises are not supported for unrecognized APIs, and callbacks have to be used for them. 360 | 361 | Chrome-only APIs have no promise version, because extensions that use such APIs 362 | would not be compatible with Firefox. 363 | 364 | File an issue in this repository for API methods that support callbacks in Chrome *and* 365 | Firefox but are currently missing from the "API metadata" file. 366 | 367 | ### Issues that happen only when running on Firefox 368 | 369 | When an extension that uses this library doesn't behave as expected on Firefox, it is almost never an issue in this polyfill, but an issue with the native implementation in Firefox. 370 | 371 | "Firefox only" issues should be reported upstream on Bugzilla: 372 | - https://bugzilla.mozilla.org/enter_bug.cgi?product=WebExtensions&component=Untriaged 373 | 374 | ### API methods or options that are only available when running in Firefox 375 | 376 | This library does not provide any polyfill for API methods and options that are only available on Firefox, and they are actually considered out of the scope of this library. 377 | 378 | ### tabs.executeScript 379 | 380 | On Firefox `browser.tabs.executeScript` returns a promise which resolves to the result of the content script code that has been executed, which can be an immediate value or a Promise. 381 | 382 | On Chrome, the `browser.tabs.executeScript` API method as polyfilled by this library also returns a promise which resolves to the result of the content script code, but only immediate values are supported. 383 | If the content script code result is a Promise, the promise returned by `browser.tabs.executeScript` will be resolved to `undefined`. 384 | 385 | ### MSEdge support 386 | 387 | MSEdge versions >= 79.0.309 are unofficially supported as a Chrome-compatible target (as for Opera or other Chrome-based browsers that also support extensions). 388 | 389 | MSEdge versions older than 79.0.309 are **unsupported**, for extension developers that still have to work on extensions for older MSEdge versions, the MSEdge `--ms-preload` manifest key and the [Microsoft Edge Extension Toolkit](https://docs.microsoft.com/en-us/microsoft-edge/extensions/guides/porting-chrome-extensions)'s Chrome API bridge can be used to be able to load the webextension-polyfill without any MSEdge specific changes. 390 | 391 | The following Github repository provides some additional detail about this strategy and a minimal test extension that shows how to put it together: 392 | 393 | - https://github.com/rpl/example-msedge-extension-with-webextension-polyfill 394 | 395 | ## Contributing to this project 396 | 397 | Read the [contributing section](CONTRIBUTING.md) for additional information about how to build the library from this repository and how to contribute and test changes. 398 | 399 | [PR-114]: https://github.com/mozilla/webextension-polyfill/pull/114 400 | [I-102]: https://github.com/mozilla/webextension-polyfill/issues/102#issuecomment-379365343 401 | -------------------------------------------------------------------------------- /node_modules/webextension-polyfill/dist/browser-polyfill.min.js: -------------------------------------------------------------------------------- 1 | (function(a,b){if("function"==typeof define&&define.amd)define("webextension-polyfill",["module"],b);else if("undefined"!=typeof exports)b(module);else{var c={exports:{}};b(c),a.browser=c.exports}})("undefined"==typeof globalThis?"undefined"==typeof self?this:self:globalThis,function(a){"use strict";if(!globalThis.chrome?.runtime?.id)throw new Error("This script should only be loaded in a browser extension.");if("undefined"==typeof globalThis.browser||Object.getPrototypeOf(globalThis.browser)!==Object.prototype){a.exports=(a=>{const b={alarms:{clear:{minArgs:0,maxArgs:1},clearAll:{minArgs:0,maxArgs:0},get:{minArgs:0,maxArgs:1},getAll:{minArgs:0,maxArgs:0}},bookmarks:{create:{minArgs:1,maxArgs:1},get:{minArgs:1,maxArgs:1},getChildren:{minArgs:1,maxArgs:1},getRecent:{minArgs:1,maxArgs:1},getSubTree:{minArgs:1,maxArgs:1},getTree:{minArgs:0,maxArgs:0},move:{minArgs:2,maxArgs:2},remove:{minArgs:1,maxArgs:1},removeTree:{minArgs:1,maxArgs:1},search:{minArgs:1,maxArgs:1},update:{minArgs:2,maxArgs:2}},browserAction:{disable:{minArgs:0,maxArgs:1,fallbackToNoCallback:!0},enable:{minArgs:0,maxArgs:1,fallbackToNoCallback:!0},getBadgeBackgroundColor:{minArgs:1,maxArgs:1},getBadgeText:{minArgs:1,maxArgs:1},getPopup:{minArgs:1,maxArgs:1},getTitle:{minArgs:1,maxArgs:1},openPopup:{minArgs:0,maxArgs:0},setBadgeBackgroundColor:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setBadgeText:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setIcon:{minArgs:1,maxArgs:1},setPopup:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setTitle:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0}},browsingData:{remove:{minArgs:2,maxArgs:2},removeCache:{minArgs:1,maxArgs:1},removeCookies:{minArgs:1,maxArgs:1},removeDownloads:{minArgs:1,maxArgs:1},removeFormData:{minArgs:1,maxArgs:1},removeHistory:{minArgs:1,maxArgs:1},removeLocalStorage:{minArgs:1,maxArgs:1},removePasswords:{minArgs:1,maxArgs:1},removePluginData:{minArgs:1,maxArgs:1},settings:{minArgs:0,maxArgs:0}},commands:{getAll:{minArgs:0,maxArgs:0}},contextMenus:{remove:{minArgs:1,maxArgs:1},removeAll:{minArgs:0,maxArgs:0},update:{minArgs:2,maxArgs:2}},cookies:{get:{minArgs:1,maxArgs:1},getAll:{minArgs:1,maxArgs:1},getAllCookieStores:{minArgs:0,maxArgs:0},remove:{minArgs:1,maxArgs:1},set:{minArgs:1,maxArgs:1}},devtools:{inspectedWindow:{eval:{minArgs:1,maxArgs:2,singleCallbackArg:!1}},panels:{create:{minArgs:3,maxArgs:3,singleCallbackArg:!0},elements:{createSidebarPane:{minArgs:1,maxArgs:1}}}},downloads:{cancel:{minArgs:1,maxArgs:1},download:{minArgs:1,maxArgs:1},erase:{minArgs:1,maxArgs:1},getFileIcon:{minArgs:1,maxArgs:2},open:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},pause:{minArgs:1,maxArgs:1},removeFile:{minArgs:1,maxArgs:1},resume:{minArgs:1,maxArgs:1},search:{minArgs:1,maxArgs:1},show:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0}},extension:{isAllowedFileSchemeAccess:{minArgs:0,maxArgs:0},isAllowedIncognitoAccess:{minArgs:0,maxArgs:0}},history:{addUrl:{minArgs:1,maxArgs:1},deleteAll:{minArgs:0,maxArgs:0},deleteRange:{minArgs:1,maxArgs:1},deleteUrl:{minArgs:1,maxArgs:1},getVisits:{minArgs:1,maxArgs:1},search:{minArgs:1,maxArgs:1}},i18n:{detectLanguage:{minArgs:1,maxArgs:1},getAcceptLanguages:{minArgs:0,maxArgs:0}},identity:{launchWebAuthFlow:{minArgs:1,maxArgs:1}},idle:{queryState:{minArgs:1,maxArgs:1}},management:{get:{minArgs:1,maxArgs:1},getAll:{minArgs:0,maxArgs:0},getSelf:{minArgs:0,maxArgs:0},setEnabled:{minArgs:2,maxArgs:2},uninstallSelf:{minArgs:0,maxArgs:1}},notifications:{clear:{minArgs:1,maxArgs:1},create:{minArgs:1,maxArgs:2},getAll:{minArgs:0,maxArgs:0},getPermissionLevel:{minArgs:0,maxArgs:0},update:{minArgs:2,maxArgs:2}},pageAction:{getPopup:{minArgs:1,maxArgs:1},getTitle:{minArgs:1,maxArgs:1},hide:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setIcon:{minArgs:1,maxArgs:1},setPopup:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setTitle:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},show:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0}},permissions:{contains:{minArgs:1,maxArgs:1},getAll:{minArgs:0,maxArgs:0},remove:{minArgs:1,maxArgs:1},request:{minArgs:1,maxArgs:1}},runtime:{getBackgroundPage:{minArgs:0,maxArgs:0},getPlatformInfo:{minArgs:0,maxArgs:0},openOptionsPage:{minArgs:0,maxArgs:0},requestUpdateCheck:{minArgs:0,maxArgs:0},sendMessage:{minArgs:1,maxArgs:3},sendNativeMessage:{minArgs:2,maxArgs:2},setUninstallURL:{minArgs:1,maxArgs:1}},sessions:{getDevices:{minArgs:0,maxArgs:1},getRecentlyClosed:{minArgs:0,maxArgs:1},restore:{minArgs:0,maxArgs:1}},storage:{local:{clear:{minArgs:0,maxArgs:0},get:{minArgs:0,maxArgs:1},getBytesInUse:{minArgs:0,maxArgs:1},remove:{minArgs:1,maxArgs:1},set:{minArgs:1,maxArgs:1}},managed:{get:{minArgs:0,maxArgs:1},getBytesInUse:{minArgs:0,maxArgs:1}},sync:{clear:{minArgs:0,maxArgs:0},get:{minArgs:0,maxArgs:1},getBytesInUse:{minArgs:0,maxArgs:1},remove:{minArgs:1,maxArgs:1},set:{minArgs:1,maxArgs:1}}},tabs:{captureVisibleTab:{minArgs:0,maxArgs:2},create:{minArgs:1,maxArgs:1},detectLanguage:{minArgs:0,maxArgs:1},discard:{minArgs:0,maxArgs:1},duplicate:{minArgs:1,maxArgs:1},executeScript:{minArgs:1,maxArgs:2},get:{minArgs:1,maxArgs:1},getCurrent:{minArgs:0,maxArgs:0},getZoom:{minArgs:0,maxArgs:1},getZoomSettings:{minArgs:0,maxArgs:1},goBack:{minArgs:0,maxArgs:1},goForward:{minArgs:0,maxArgs:1},highlight:{minArgs:1,maxArgs:1},insertCSS:{minArgs:1,maxArgs:2},move:{minArgs:2,maxArgs:2},query:{minArgs:1,maxArgs:1},reload:{minArgs:0,maxArgs:2},remove:{minArgs:1,maxArgs:1},removeCSS:{minArgs:1,maxArgs:2},sendMessage:{minArgs:2,maxArgs:3},setZoom:{minArgs:1,maxArgs:2},setZoomSettings:{minArgs:1,maxArgs:2},update:{minArgs:1,maxArgs:2}},topSites:{get:{minArgs:0,maxArgs:0}},webNavigation:{getAllFrames:{minArgs:1,maxArgs:1},getFrame:{minArgs:1,maxArgs:1}},webRequest:{handlerBehaviorChanged:{minArgs:0,maxArgs:0}},windows:{create:{minArgs:0,maxArgs:1},get:{minArgs:1,maxArgs:2},getAll:{minArgs:0,maxArgs:1},getCurrent:{minArgs:0,maxArgs:1},getLastFocused:{minArgs:0,maxArgs:1},remove:{minArgs:1,maxArgs:1},update:{minArgs:2,maxArgs:2}}};if(0===Object.keys(b).length)throw new Error("api-metadata.json has not been included in browser-polyfill");class c extends WeakMap{constructor(a,b=void 0){super(b),this.createItem=a}get(a){return this.has(a)||this.set(a,this.createItem(a)),super.get(a)}}const d=a=>a&&"object"==typeof a&&"function"==typeof a.then,e=(b,c)=>(...d)=>{a.runtime.lastError?b.reject(new Error(a.runtime.lastError.message)):c.singleCallbackArg||1>=d.length&&!1!==c.singleCallbackArg?b.resolve(d[0]):b.resolve(d)},f=a=>1==a?"argument":"arguments",g=(a,b)=>function(c,...d){if(d.lengthb.maxArgs)throw new Error(`Expected at most ${b.maxArgs} ${f(b.maxArgs)} for ${a}(), got ${d.length}`);return new Promise((f,g)=>{if(b.fallbackToNoCallback)try{c[a](...d,e({resolve:f,reject:g},b))}catch(e){console.warn(`${a} API method doesn't seem to support the callback parameter, `+"falling back to call it without a callback: ",e),c[a](...d),b.fallbackToNoCallback=!1,b.noCallback=!0,f()}else b.noCallback?(c[a](...d),f()):c[a](...d,e({resolve:f,reject:g},b))})},h=(a,b,c)=>new Proxy(b,{apply(b,d,e){return c.call(d,a,...e)}});let i=Function.call.bind(Object.prototype.hasOwnProperty);const j=(a,b={},c={})=>{let d=Object.create(null),e=Object.create(a);return new Proxy(e,{has(b,c){return c in a||c in d},get(e,f){if(f in d)return d[f];if(!(f in a))return;let k=a[f];if("function"==typeof k){if("function"==typeof b[f])k=h(a,a[f],b[f]);else if(i(c,f)){let b=g(f,c[f]);k=h(a,a[f],b)}else k=k.bind(a);}else if("object"==typeof k&&null!==k&&(i(b,f)||i(c,f)))k=j(k,b[f],c[f]);else if(i(c,"*"))k=j(k,b[f],c["*"]);else return Object.defineProperty(d,f,{configurable:!0,enumerable:!0,get(){return a[f]},set(b){a[f]=b}}),k;return d[f]=k,k},set(b,c,e){return c in d?d[c]=e:a[c]=e,!0},defineProperty(a,b,c){return Reflect.defineProperty(d,b,c)},deleteProperty(a,b){return Reflect.deleteProperty(d,b)}})},k=a=>({addListener(b,c,...d){b.addListener(a.get(c),...d)},hasListener(b,c){return b.hasListener(a.get(c))},removeListener(b,c){b.removeListener(a.get(c))}}),l=new c(a=>"function"==typeof a?function(b){const c=j(b,{},{getContent:{minArgs:0,maxArgs:0}});a(c)}:a),m=new c(a=>"function"==typeof a?function(b,c,e){let f,g,h=!1,i=new Promise(a=>{f=function(b){h=!0,a(b)}});try{g=a(b,c,f)}catch(a){g=Promise.reject(a)}const j=!0!==g&&d(g);if(!0!==g&&!j&&!h)return!1;const k=a=>{a.then(a=>{e(a)},a=>{let b;b=a&&(a instanceof Error||"string"==typeof a.message)?a.message:"An unexpected error occurred",e({__mozWebExtensionPolyfillReject__:!0,message:b})}).catch(a=>{console.error("Failed to send onMessage rejected reply",a)})};return j?k(g):k(i),!0}:a),n=({reject:b,resolve:c},d)=>{a.runtime.lastError?a.runtime.lastError.message==="The message port closed before a response was received."?c():b(new Error(a.runtime.lastError.message)):d&&d.__mozWebExtensionPolyfillReject__?b(new Error(d.message)):c(d)},o=(a,b,c,...d)=>{if(d.lengthb.maxArgs)throw new Error(`Expected at most ${b.maxArgs} ${f(b.maxArgs)} for ${a}(), got ${d.length}`);return new Promise((a,b)=>{const e=n.bind(null,{resolve:a,reject:b});d.push(e),c.sendMessage(...d)})},p={devtools:{network:{onRequestFinished:k(l)}},runtime:{onMessage:k(m),onMessageExternal:k(m),sendMessage:o.bind(null,"sendMessage",{minArgs:1,maxArgs:3})},tabs:{sendMessage:o.bind(null,"sendMessage",{minArgs:2,maxArgs:3})}},q={clear:{minArgs:1,maxArgs:1},get:{minArgs:1,maxArgs:1},set:{minArgs:1,maxArgs:1}};return b.privacy={network:{"*":q},services:{"*":q},websites:{"*":q}},j(a,p,b)})(chrome)}else a.exports=globalThis.browser}); 2 | //# sourceMappingURL=browser-polyfill.min.js.map 3 | 4 | // webextension-polyfill v.0.10.0 (https://github.com/mozilla/webextension-polyfill) 5 | 6 | /* This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | -------------------------------------------------------------------------------- /node_modules/webextension-polyfill/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webextension-polyfill", 3 | "version": "0.10.0", 4 | "description": "A lightweight polyfill library for Promise-based WebExtension APIs in Chrome.", 5 | "main": "dist/browser-polyfill.js", 6 | "files": [ 7 | "dist" 8 | ], 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/mozilla/webextension-polyfill.git" 12 | }, 13 | "author": "Mozilla", 14 | "license": "MPL-2.0", 15 | "bugs": { 16 | "url": "https://github.com/mozilla/webextension-polyfill/issues" 17 | }, 18 | "homepage": "https://github.com/mozilla/webextension-polyfill", 19 | "devDependencies": { 20 | "@babel/core": "7.18.10", 21 | "@babel/preset-env": "7.18.10", 22 | "@babel/register": "7.18.9", 23 | "@babel/eslint-parser": "7.18.9", 24 | "babel-preset-minify": "0.5.2", 25 | "browserify": "17.0.0", 26 | "chai": "4.3.6", 27 | "chromedriver": "104.0.0", 28 | "cross-env": "7.0.3", 29 | "eslint": "8.21.0", 30 | "finalhandler": "1.2.0", 31 | "geckodriver": "3.0.2", 32 | "global-replaceify": "1.0.0", 33 | "grunt": "1.5.3", 34 | "grunt-babel": "8.0.0", 35 | "grunt-contrib-concat": "2.1.0", 36 | "grunt-replace": "2.0.2", 37 | "istanbul-lib-instrument": "5.2.0", 38 | "jsdom": "20.0.0", 39 | "mocha": "10.0.0", 40 | "nyc": "15.1.0", 41 | "selenium-webdriver": "4.4.0", 42 | "serve-static": "1.15.0", 43 | "shelljs": "0.8.5", 44 | "sinon": "14.0.0", 45 | "tape": "5.5.3", 46 | "tape-async": "2.3.0", 47 | "tmp": "0.2.1" 48 | }, 49 | "nyc": { 50 | "reporter": [ 51 | "lcov", 52 | "text", 53 | "html" 54 | ], 55 | "instrument": false 56 | }, 57 | "scripts": { 58 | "build": "npm run lint && grunt", 59 | "prepublish": "npm run build && npm run test", 60 | "test": "mocha", 61 | "lint": "eslint *.js src/*.js scripts/**/*.js test/**/*.js", 62 | "test-coverage": "cross-env COVERAGE=y nyc mocha", 63 | "test-minified": "cross-env TEST_MINIFIED_POLYFILL=1 mocha", 64 | "test-integration": "tape test/integration/test-*", 65 | "test-integration:chrome": "cross-env TEST_BROWSER_TYPE=chrome npm run test-integration", 66 | "test-integration:firefox": "cross-env TEST_BROWSER_TYPE=firefox npm run test-integration" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lemmy-instance-assistant", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "devDependencies": { 8 | "webextension-polyfill": "^0.10.0" 9 | } 10 | }, 11 | "node_modules/webextension-polyfill": { 12 | "version": "0.10.0", 13 | "resolved": "https://registry.npmjs.org/webextension-polyfill/-/webextension-polyfill-0.10.0.tgz", 14 | "integrity": "sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g==", 15 | "dev": true 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "webextension-polyfill": "^0.10.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/_locales/de/messages.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /src/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /src/_locales/fr/messages.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | // -------------------------------------- 2 | // COPIED FROM utils.js 3 | function getStorageAPI() { 4 | let storageAPI; 5 | if (typeof browser !== 'undefined' && browser.storage && browser.storage.local) { 6 | storageAPI = browser.storage.local; 7 | } else if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) { 8 | storageAPI = chrome.storage.local; 9 | } else { 10 | throw new Error('Storage API is not supported in this browser.'); 11 | } 12 | 13 | return storageAPI; 14 | } 15 | function getBrowserAPI() { 16 | let browserAPI; 17 | if (typeof browser !== 'undefined' && browser.storage && browser.storage.local) { 18 | browserAPI = browser; 19 | } else if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) { 20 | browserAPI = chrome; 21 | } else { 22 | throw new Error('Browser API is not supported in this browser.'); 23 | } 24 | return browserAPI; 25 | } 26 | async function getAllSettings() { 27 | const storageAPI = getStorageAPI(); 28 | const allSettings = await storageAPI.get('settings'); 29 | if (!allSettings || !allSettings.settings) { 30 | await setAllSettings(defaultSettings); 31 | return defaultSettings; 32 | } 33 | return await storageAPI.get('settings'); 34 | } 35 | async function getSetting(settingName) { 36 | const allSettings = await getAllSettings(); 37 | return allSettings.settings[settingName]; 38 | } 39 | async function hasSelectedInstance() { 40 | const selectedInstance = await getSetting('selectedInstance'); 41 | return selectedInstance !== undefined && selectedInstance !== ""; 42 | } 43 | async function hasSelectedType() { 44 | const selectedType = await getSetting('selectedType'); 45 | return selectedType !== undefined && selectedType !== ""; 46 | } 47 | async function loadStorage(key) { 48 | const { value } = await browser.storage.local.get(key); 49 | return value; 50 | } 51 | async function p2l_getPostData() { 52 | const storageAPI = getBrowserAPI(); 53 | return new Promise((resolve, reject) => { 54 | storageAPI.tabs.query({ active: true, currentWindow: true }, function (tabs) { 55 | const activeTab = tabs[0]; 56 | const postData = { 57 | title: activeTab.title, 58 | url: activeTab.url 59 | }; 60 | resolve(postData); 61 | }); 62 | }); 63 | } 64 | // -------------------------------------- 65 | 66 | 67 | // -------------------------------------- 68 | // Handle redirects within a Lemmy site 69 | // - Sometimes when navigating within a Lemmy site, the content scripts won't run despite the URL matching the pattern. This is a workaround for that. 70 | // -------------------------------------- 71 | browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { 72 | if (changeInfo.status === "complete") { 73 | if ( 74 | /^https?:\/\/.*\/c\//.test(tab.url) || 75 | /^https?:\/\/.*\/communities\//.test(tab.url) || 76 | /^https?:\/\/.*\/post\//.test(tab.url) 77 | ) { 78 | browser.tabs.executeScript(tabId, { file: "utils.js" }) 79 | browser.tabs.executeScript(tabId, { file: "content-general.js" }) 80 | browser.tabs.executeScript(tabId, { file: "content-sidebar.js" }) 81 | } 82 | } 83 | }); 84 | 85 | 86 | // -------------------------------------- 87 | // Handle context menu clicks 88 | // -------------------------------------- 89 | browser.contextMenus.onClicked.addListener(async (info, tab) => { 90 | if (info.menuItemId === "redirect" && info.linkUrl) { 91 | 92 | let sourceHost = new URL(info.linkUrl).hostname; 93 | let sourcePath = new URL(info.linkUrl).pathname; 94 | 95 | if (sourcePath.includes("/c/") || sourcePath.includes("/m/")) { 96 | const communityName = sourcePath.match(/\/[cm]\/([^/@]+)/)[1]; 97 | const sourceInstance = sourcePath.includes("@") ? 98 | sourcePath.match(/\/[cm]\/[^/@]+@([^/]+)/)[1] : sourceHost; 99 | 100 | async function loadStorage() { 101 | 102 | const selectedInstance = await getSetting('selectedInstance'); 103 | 104 | const { selectedType } = await browser.storage.local.get('selectedType'); 105 | communityPrefix = selectedType ? (selectedType === "lemmy" ? "/c/" : "/m/") : "/c/"; 106 | const redirectURL = selectedInstance + communityPrefix + communityName + '@' + sourceInstance; 107 | browser.tabs.update(tab.id, { url: redirectURL }); 108 | } 109 | loadStorage(); 110 | } else { 111 | browser.tabs.update(tab.id, { url: 'https://github.com/cynber/lemmy-instance-assistant/wiki/Sorry-that-didn\'t-work...' }); 112 | // TODO: Add a popup to explain this 113 | } 114 | } else if (info.menuItemId === "post-image" && info.srcUrl) { 115 | 116 | if (await hasSelectedInstance() && await hasSelectedType()) { 117 | 118 | const type = await getSetting("selectedType"); 119 | const instance = await getSetting("selectedInstance"); 120 | const postData = await p2l_getPostData(); 121 | 122 | if (type === "lemmy") { 123 | const url = instance + "/create_post"; 124 | const createdTab = await browser.tabs.create({ url: url }); 125 | 126 | // Listen for tab updates to check for loading completion 127 | browser.tabs.onUpdated.addListener(function listener(tabId, changeInfo) { 128 | if (tabId === createdTab.id && changeInfo.status === "complete") { 129 | browser.tabs.onUpdated.removeListener(listener); // Remove the listener 130 | 131 | // Fill in form after the tab is fully loaded 132 | browser.tabs.executeScript(createdTab.id, { 133 | code: ` 134 | const EVENT_OPTIONS = {bubbles: true, cancelable: false, composed: true}; 135 | const EVENTS = { 136 | BLUR: new Event("blur", EVENT_OPTIONS), 137 | CHANGE: new Event("change", EVENT_OPTIONS), 138 | INPUT: new Event("input", EVENT_OPTIONS), 139 | }; 140 | 141 | const postTitleInput = document.querySelector("#post-title"); 142 | const postURLInput = document.querySelector("#post-url"); 143 | const postBodyInput = document.querySelector("textarea[id^='markdown-textarea-']"); 144 | 145 | postTitleInput.select(); 146 | postTitleInput.value = "${postData.title}"; 147 | postTitleInput.dispatchEvent(EVENTS.INPUT); 148 | 149 | postURLInput.select(); 150 | postURLInput.value = "${info.srcUrl}"; 151 | postURLInput.dispatchEvent(EVENTS.INPUT); 152 | 153 | postBodyInput.select(); 154 | postBodyInput.value = "Source: ${postData.url}"; 155 | postBodyInput.dispatchEvent(EVENTS.INPUT); 156 | ` 157 | }); 158 | 159 | window.close(); // Close the popup 160 | } 161 | }); 162 | 163 | } else if (type === "kbin") { 164 | const url = instance + "/new?url=" + info.srcUrl + "&title=" + postData.title + "&body=Source: " + postData.url; 165 | await browser.tabs.create({ url: url }); 166 | } 167 | 168 | } else { alert("No valid instance has been set. Please select an instance in the popup using 'Change my home instance'."); } 169 | } 170 | }); 171 | 172 | browser.contextMenus.create({ 173 | id: "redirect", 174 | title: "Redirect to home instance", 175 | contexts: ["link"], 176 | targetUrlPatterns: ["http://*/c/*", "https://*/c/*", "http://*/p/*", "https://*/p/*", "http://*/m/*", "https://*/m/*"], 177 | }, () => void browser.runtime.lastError, 178 | ); 179 | 180 | browser.contextMenus.create({ 181 | id: "post-image", 182 | title: "Post this image", 183 | contexts: ["image"], 184 | targetUrlPatterns: ["http://*/*", "https://*/*"] 185 | }, () => void browser.runtime.lastError, 186 | ); 187 | 188 | 189 | // -------------------------------------- 190 | // Set default values on install/update 191 | // -------------------------------------- 192 | 193 | browser.runtime.onInstalled.addListener(async ({ reason }) => { 194 | // Set default values on install/update 195 | // TODO: fix the utils import so we can just use initializeSettingsWithDefaults() 196 | if (reason === 'install' || reason === 'update') { 197 | 198 | async function backgroundInitializeSettings() { 199 | 200 | const defaultSettings = { 201 | hideSidebarLemmy: false, 202 | hideSidebarKbin: false, 203 | instanceList: [ 204 | { name: "lemmy.world", url: "https://lemmy.world" }, 205 | { name: "lemmy.ca", url: "https://lemmy.ca" }, 206 | { name: "lemm.ee", url: "https://lemm.ee" }, 207 | { name: "kbin.social", url: "https://kbin.social" }, 208 | ], 209 | runOnCommunitySidebar: true, 210 | runOnCommunityNotFound: true, 211 | hideHelp: false, 212 | selectedInstance: '', // users are forced to set this 213 | selectedType: 'lemmy', // lemmy or kbin 214 | theme: 'dark', // **NOT IMPLEMENTED YET** 215 | toolSearchCommunity_openInLemmyverse: false, 216 | }; 217 | 218 | let storageAPI = browser.storage.local; 219 | let allSettings = await storageAPI.get('settings'); 220 | 221 | if (!allSettings || !allSettings.settings) { 222 | await storageAPI.set({ 'settings': defaultSettings }); 223 | } else { 224 | for (const settingName of Object.keys(defaultSettings)) { 225 | if (!allSettings.settings.hasOwnProperty(settingName)) { 226 | allSettings.settings[settingName] = defaultSettings[settingName]; 227 | } 228 | } 229 | await storageAPI.set({ 'settings': allSettings.settings }); 230 | } 231 | } 232 | await backgroundInitializeSettings(); 233 | } 234 | 235 | // Open settings page when extension is first installed 236 | if (reason === 'install') { 237 | browser.tabs.create({ url: 'page-settings/settings.html' }); 238 | } 239 | }); -------------------------------------------------------------------------------- /src/content-communityNotFound.js: -------------------------------------------------------------------------------- 1 | // ============================================================= // 2 | // Injects buttons and links into the community not found page to help users find their way. // 3 | // ============================================================= // 4 | 5 | setTimeout(() => { 6 | const testURL = window.location.href; 7 | 8 | if ((isLemmyCommunityNotFound(testURL))) { 9 | async function loadSelectedInstance() { 10 | 11 | // ------ Set up general variables ------ // 12 | 13 | const CURRENT_HOST = new URL(window.location.href).hostname; 14 | const CURRENT_PATH = new URL(window.location.href).pathname; 15 | const targetCommunity = CURRENT_PATH.match(/\/c\/(.+?)@/)[1]; 16 | const targetInstance = CURRENT_PATH.match(/@(.+)/)[1]; 17 | 18 | const selectedInstance = await getSetting('selectedInstance'); 19 | let TARGET_ELEMENT = document.querySelector('.error-page'); 20 | 21 | // --------- Set up injectables --------- // 22 | let createButton = (text) => { 23 | const button = document.createElement('button'); 24 | button.setAttribute('type', 'button'); 25 | button.textContent = text; 26 | button.setAttribute('id', 'instance-assistant-sidebar'); 27 | return button; 28 | }; 29 | 30 | const createMessage = (text) => { 31 | const paragraph = document.createElement('p'); 32 | paragraph.innerHTML = text; 33 | paragraph.setAttribute('id', 'instance-assistant-sidebar'); 34 | return paragraph; 35 | }; 36 | 37 | const createDropdown = (text, options) => { 38 | const container = document.createElement('div'); 39 | const dropdownText = document.createElement('p'); 40 | const dropdownList = document.createElement('ul'); 41 | dropdownText.innerHTML = "▼ " + text + " ▼"; 42 | dropdownText.style.cssText = `cursor: pointer; font-size: 0.8rem; color: #939496;; text-decoration: underline;`; 43 | dropdownList.style.cssText = `list-style: none; padding: 0; margin: 0; font-size: 0.8rem; color: #939496;`; 44 | 45 | dropdownList.style.display = 'none'; 46 | dropdownText.addEventListener('click', () => { 47 | dropdownList.style.display = dropdownList.style.display === 'none' ? 'block' : 'none'; 48 | dropdownText.innerHTML = dropdownList.style.display === 'none' ? "▼ " + text + " ▼" : "▲ " + text + " ▲"; 49 | }); 50 | 51 | options.forEach((option) => { 52 | const listItem = document.createElement('li'); 53 | listItem.innerHTML = option; 54 | listItem.style.cssText = `padding: 0.5rem 0rem;`; 55 | dropdownList.appendChild(listItem); 56 | }); 57 | container.appendChild(dropdownText); 58 | container.appendChild(dropdownList); 59 | return container; 60 | }; 61 | 62 | const container = document.createElement('div'); 63 | container.setAttribute('id', 'instance-assistant-sidebar'); 64 | container.style.cssText = ` 65 | background-color: #1f1f1f; 66 | padding: 10px; 67 | border-radius: 5px; 68 | border: 2px solid #2f2f2f; 69 | margin-bottom: 10px; 70 | `; 71 | 72 | const txtErrorPage = createMessage(`Did you arrive here from Instance Assistant?

The community ` + targetCommunity + ` does not exist on this instance (yet). This can happen if you are the first person to try and open it in this instance. Someone will need to prompt this instance to fetch the community from the original instance. This task can be trigerred by entering the community URL (ex. ` + targetInstance + `/c/` + targetCommunity + `) or identifier (ex. !` + targetCommunity + `@` + targetInstance + `) into the search page (reference).

You can do this by clicking on the button below, and then coming back after some time. Don't worry about the "No results" message, the fetch process would have started in the background. Alternatively, you can copy one of the codes above and do the search manually at https://` + CURRENT_HOST + `/search.\n\n You can also just view the community on the foreign instance.

`) 73 | 74 | let btnOpenSearchLemmy = createButton('Trigger a search'); 75 | btnOpenSearchLemmy.style.cssText = ` 76 | padding: .375rem .75rem; 77 | margin: 0rem 2rem 2rem 2rem; 78 | width: 50%; 79 | border: none; 80 | border-radius: 5px; 81 | font-weight: 400; 82 | text-align: center; 83 | color: white; 84 | `; 85 | btnOpenSearchLemmy.style.backgroundColor = '#175a4c'; 86 | 87 | let btnHomeLemmy = createButton('Go to my home instance'); 88 | btnHomeLemmy.style.cssText = ` 89 | padding: .375rem .75rem; 90 | margin: 1rem 2rem .5rem 2rem; 91 | width: 50%; 92 | border: none; 93 | border-radius: 5px; 94 | font-weight: 400; 95 | text-align: center; 96 | color: white; 97 | `; 98 | btnHomeLemmy.style.backgroundColor = '#5f35ae'; 99 | 100 | let btnCommunityLemmy = createButton('Open community on foreign instance'); 101 | btnCommunityLemmy.style.cssText = ` 102 | padding: .375rem .75rem; 103 | margin: 1rem 2rem .5rem 2rem; 104 | width: 50%; 105 | border: none; 106 | border-radius: 5px; 107 | font-weight: 400; 108 | text-align: center; 109 | color: white; 110 | `; 111 | btnCommunityLemmy.style.backgroundColor = '#363636'; 112 | 113 | let txtHomeInstance = selectedInstance ? createMessage(`Your home instance is ${selectedInstance}.`) : createMessage(`You have not set a home instance yet.`); 114 | 115 | const changeInstanceInstructions = [ 116 | '1) Click on the extension icon in the browser toolbar', 117 | '2) Press "Change my home instance" and type in your home instance URL', 118 | '3) Press "Toggle home instance type" to switch between "Lemmy" and "Kbin". (default is "Lemmy")', 119 | ]; 120 | 121 | const txtChangeInstance = createDropdown('How to change home instance', changeInstanceInstructions); 122 | 123 | // --------- Add Event Listeners -------- // 124 | btnHomeLemmy.addEventListener('click', () => { 125 | if (selectedInstance) { 126 | window.location.href = selectedInstance; 127 | } else { alert('No valid instance has been set.') } 128 | }); 129 | 130 | btnOpenSearchLemmy.addEventListener('click', () => { 131 | window.location.href = 'https://' + CURRENT_HOST + '/search?q=' + '!' + targetCommunity + '%40' + targetInstance + '&type=All&listingType=All&page=1&sort=TopAll'; 132 | }); 133 | 134 | btnCommunityLemmy.addEventListener('click', () => { 135 | window.location.href = 'https://' + targetInstance + '/c/' + targetCommunity; 136 | }); 137 | 138 | // ---------- Append elements ----------- // 139 | if (!document.querySelector('#instance-assistant-sidebar') && (await getSetting('runOnCommunityNotFound'))) { // prevent duplicate elements 140 | const hideHelp = await getSetting('hideHelp'); 141 | if (!hideHelp) {container.appendChild(txtErrorPage);} 142 | container.appendChild(btnOpenSearchLemmy) 143 | container.appendChild(btnCommunityLemmy); 144 | container.appendChild(btnHomeLemmy); 145 | container.appendChild(txtHomeInstance); 146 | if (!hideHelp) {container.appendChild(txtChangeInstance);} 147 | TARGET_ELEMENT.insertBefore(container, TARGET_ELEMENT.firstChild); 148 | } 149 | 150 | if (mayBeFrontend(targetInstance)) { 151 | 152 | console.log('may be frontend') 153 | 154 | let txtAlternateRedirect = createMessage('

IMPORTANT: Based on your URL ' + targetInstance + ', you may be using a custom frontend. You can try getting the community from the main domain:

'); 155 | 156 | let realInstance = getRealHostname(targetInstance); 157 | 158 | console.log(realInstance) 159 | 160 | 161 | let btnAlternateRedirect = createButton('Search in "' + realInstance + '"'); 162 | btnAlternateRedirect.style.cssText = ` 163 | padding: .375rem .75rem; 164 | margin: .5rem 2rem 1rem 2rem; 165 | width: 50%; 166 | border: none; 167 | border-radius: 5px; 168 | font-weight: 400; 169 | text-align: center; 170 | color: white; 171 | `; 172 | btnAlternateRedirect.style.backgroundColor = '#5f35ae'; 173 | 174 | btnAlternateRedirect.addEventListener('click', () => { 175 | window.location.href = 'https://' + CURRENT_HOST + '/c/' + targetCommunity + '@' + realInstance; 176 | }); 177 | 178 | container.insertBefore(txtAlternateRedirect, container.children[2]); 179 | container.insertBefore(btnAlternateRedirect, container.children[3]); 180 | 181 | } 182 | } 183 | loadSelectedInstance(); 184 | } 185 | }, "500"); 186 | -------------------------------------------------------------------------------- /src/content-general.js: -------------------------------------------------------------------------------- 1 | // ============================================================= // 2 | // General content manipulation 3 | // ============================================================= // 4 | 5 | setTimeout(async () => { 6 | if (isLemmyCommunity(window.location.href) || 7 | isKbinCommunity(window.location.href) || 8 | isLemmyCommunityList(window.location.href)) { 9 | 10 | // -------------------------------------- 11 | // Hide sidebar if setting is enabled 12 | // -------------------------------------- 13 | const hideSidebarLemmy = await getSetting('hideSidebarLemmy'); 14 | const hideSidebarKbin = await getSetting('hideSidebarKbin'); 15 | 16 | // Hide on Lemmy 17 | if (hideSidebarLemmy && isLemmyCommunity(window.location.href)) { 18 | const sidebarSubscribed = document.getElementById("sidebarContainer"); 19 | sidebarSubscribed.style.display = "none"; 20 | removeClassByWildcard("site-sideba*"); 21 | const serverInfo = document.getElementById("sidebarInfo"); 22 | serverInfo.style.display = "none"; 23 | 24 | const mainElement = document.querySelector('.community.container-lg main.col-12.col-md-8.col-lg-9'); 25 | if (mainElement) { 26 | // if mainElement exists, remove the classes that make it small 27 | mainElement.classList.remove('col-12', 'col-md-8', 'col-lg-9'); 28 | mainElement.classList.add('col-12'); 29 | } 30 | } 31 | 32 | // Hide on Kbin 33 | if (hideSidebarKbin && isKbinCommunity(window.location.href)) { 34 | // TODO: Hide sidebar 35 | } 36 | 37 | 38 | // -------------------------------------- 39 | // Replace 'Subscribe' button with redirect button on '/communities' pages on foreign instances 40 | // -------------------------------------- 41 | if (isLemmyCommunityList(window.location.href) && !(await isHomeInstance(window.location.href)) && !(isLoggedInLemmy())) { 42 | 43 | const subscribeButtons = document.querySelectorAll('#community_table tbody tr td:last-child .btn-link'); 44 | 45 | subscribeButtons.forEach(async button => { 46 | const communityName = button.closest('tr').querySelector('td:first-child a').getAttribute('href').split('/c/')[1]; 47 | const domain = new URL(window.location.href).hostname; 48 | const inputURL = `https://${domain}/c/${communityName}`; 49 | 50 | const newURL = await getCommunityRedirectURL(inputURL); 51 | 52 | const newButton = document.createElement('a'); 53 | newButton.classList.add('btn', 'btn-link', 'd-inline-block'); 54 | newButton.style = ` 55 | background-color: #054da7; 56 | color: #e0e0e0; 57 | border-radius: 5px; 58 | /* Add any other custom styles here */ 59 | `; 60 | newButton.textContent = 'Open in Home Instance'; 61 | newButton.href = newURL; 62 | button.parentNode.replaceChild(newButton, button); 63 | }); 64 | 65 | const lastColumnTitle = document.querySelector('#community_table thead tr th:last-child'); 66 | lastColumnTitle.textContent = 'Open in Home Instance'; 67 | } 68 | 69 | 70 | } 71 | }, "500"); 72 | -------------------------------------------------------------------------------- /src/content-sidebar.js: -------------------------------------------------------------------------------- 1 | // =========================================================================== // 2 | // Injects buttons and links into the sidebar of Lemmy communities and posts. // 3 | // =========================================================================== // 4 | 5 | setTimeout(() => { 6 | const pageURL = window.location.href; 7 | 8 | if (isLemmyCommunity(pageURL) || 9 | isLemmyPost(pageURL) || 10 | isLemmyUser(pageURL) || 11 | isKbinCommunity(pageURL) || 12 | isLemmyPhoton() || 13 | isLemmyAlexandrite()) { 14 | 15 | async function loadSelectedInstance() { 16 | 17 | const selectedInstance = await getSetting('selectedInstance'); 18 | const hideHelp = await getSetting('hideHelp'); 19 | 20 | // ====================================== // 21 | // --------- Set up injectables --------- // 22 | // ====================================== // 23 | 24 | let createButton = (text) => { 25 | const button = document.createElement('button'); 26 | button.setAttribute('type', 'button'); 27 | button.textContent = text; 28 | button.setAttribute('id', 'instance-assistant-sidebar'); 29 | return button; 30 | }; 31 | 32 | const createMessage = (text) => { 33 | const paragraph = document.createElement('p'); 34 | paragraph.style.cssText = `font-size: 0.8rem; color: #939496;`; 35 | paragraph.innerHTML = text; 36 | paragraph.setAttribute('id', 'instance-assistant-sidebar'); 37 | return paragraph; 38 | }; 39 | 40 | const createDropdown = (text, options) => { 41 | const container = document.createElement('div'); 42 | const dropdownText = document.createElement('p'); 43 | const dropdownList = document.createElement('ul'); 44 | dropdownText.innerHTML = "▼ " + text + " ▼"; 45 | dropdownText.style.cssText = `cursor: pointer; font-size: 0.8rem; color: #939496;; text-decoration: underline;`; 46 | dropdownList.style.cssText = `list-style: none; padding: 0; margin: 0; font-size: 0.8rem; color: #939496;`; 47 | 48 | dropdownList.style.display = 'none'; 49 | dropdownText.addEventListener('click', () => { 50 | dropdownList.style.display = dropdownList.style.display === 'none' ? 'block' : 'none'; 51 | dropdownText.innerHTML = dropdownList.style.display === 'none' ? "▼ " + text + " ▼" : "▲ " + text + " ▲"; 52 | }); 53 | 54 | options.forEach((option) => { 55 | const listItem = document.createElement('li'); 56 | listItem.innerHTML = option; 57 | listItem.style.cssText = `padding: 0.5rem 0rem;`; 58 | dropdownList.appendChild(listItem); 59 | }); 60 | 61 | container.appendChild(dropdownText); 62 | container.appendChild(dropdownList); 63 | return container; 64 | }; 65 | 66 | let btnRedirectKbin = createButton('Open in my home instance'); 67 | btnRedirectKbin.style.cssText = ` 68 | padding: 0.75rem; 69 | margin: 1rem 0rem .5rem 0rem; 70 | width: 100%; 71 | height: 100%; 72 | display: block; 73 | border: var(--kbin-button-secondary-border); 74 | text-align: center; 75 | color: white; 76 | font-size: 0.85rem; 77 | font-weight: 400; 78 | cursor: pointer; 79 | `; 80 | btnRedirectKbin.style.backgroundColor = '#5f35ae'; 81 | 82 | let btnRedirectLemmy = createButton('Open in my home instance'); 83 | btnRedirectLemmy.style.cssText = ` 84 | padding: .375rem .75rem; 85 | margin: 1rem 0rem .5rem 0rem; 86 | width: 100%; 87 | border: none; 88 | border-radius: 5px; 89 | font-weight: 400; 90 | text-align: center; 91 | color: white; 92 | `; 93 | btnRedirectLemmy.style.backgroundColor = '#5f35ae'; 94 | 95 | let btnRedirectLemmyAlexandrite = createButton('Open in my home instance'); 96 | btnRedirectLemmyAlexandrite.style.cssText = ` 97 | padding: .375rem .75rem; 98 | margin: 1rem 0rem .5rem 0rem; 99 | width: 60%; 100 | border: none; 101 | border-radius: 5px; 102 | font-weight: 400; 103 | text-align: center; 104 | color: white; 105 | `; 106 | btnRedirectLemmyAlexandrite.style.backgroundColor = '#5f35ae'; 107 | 108 | let containerRedirectLemmyAlexandrite = document.createElement('div'); 109 | containerRedirectLemmyAlexandrite.setAttribute('id', 'instance-assistant-sidebar'); 110 | containerRedirectLemmyAlexandrite.style.cssText = ` 111 | background-color: #19101e; 112 | padding: 0.5rem; 113 | border-radius: 5px; 114 | margin: 0.5rem 0rem 0.5rem 0rem; 115 | text-align: center; 116 | `; 117 | 118 | let btnRedirectLemmyPhoton = createButton('Open in my home instance'); 119 | btnRedirectLemmyPhoton.style.cssText = ` 120 | padding: .375rem .75rem; 121 | margin: .5rem 0rem .5rem 0rem; 122 | width: 100%; 123 | border: none; 124 | border-radius: 5px; 125 | font-weight: 400; 126 | text-align: center; 127 | color: white; 128 | `; 129 | btnRedirectLemmyPhoton.style.backgroundColor = '#5f35ae'; 130 | 131 | let containerRedirectLemmyPhoton = document.createElement('div'); 132 | containerRedirectLemmyPhoton.setAttribute('id', 'instance-assistant-sidebar'); 133 | containerRedirectLemmyPhoton.style.cssText = ` 134 | background-color: #18181b; 135 | padding: 10px; 136 | border-radius: 5px; 137 | border-color: #232326; 138 | border-width: 1px; 139 | margin: 0.5rem -1rem 0.5rem 0rem; 140 | `; 141 | 142 | let btnToPostLemmy = createButton('Find in my home instance'); 143 | btnToPostLemmy.style.cssText = ` 144 | padding: .375rem .75rem; 145 | margin: 1rem 0rem .5rem 0rem; 146 | width: 100%; 147 | border: none; 148 | border-radius: 5px; 149 | font-weight: 400; 150 | text-align: center; 151 | color: white; 152 | `; 153 | btnToPostLemmy.style.backgroundColor = '#27175a'; 154 | 155 | let btnToUserLemmy = createButton('Find user in my home instance'); 156 | btnToUserLemmy.style.cssText = ` 157 | padding: .375rem .75rem; 158 | margin: 1rem 0rem .5rem 0rem; 159 | width: 100%; 160 | border: none; 161 | border-radius: 5px; 162 | font-weight: 400; 163 | text-align: center; 164 | color: white; 165 | `; 166 | btnToUserLemmy.style.backgroundColor = '#432684'; 167 | 168 | // let btnToPostKbin = createButton('Find in my home instance'); 169 | // btnToPostKbin.style.cssText = ` 170 | // padding: 0.75rem; 171 | // margin: 1rem 0rem .5rem 0rem; 172 | // width: 100%; 173 | // height: 100%; 174 | // display: block; 175 | // border: var(--kbin-button-secondary-border); 176 | // text-align: center; 177 | // color: white; 178 | // font-size: 0.85rem; 179 | // font-weight: 400; 180 | // cursor: pointer; 181 | // `; 182 | // btnToPostKbin.style.backgroundColor = '#27175a'; 183 | 184 | 185 | let txtHomeInstance = selectedInstance ? createMessage(`Home Instance: ${selectedInstance}`) : createMessage(`You have not set a home instance yet.`); 186 | 187 | const changeInstanceInstructions = [ 188 | '1) Click on the extension icon in the browser toolbar', 189 | '2) Press "Change my home instance" and type in your home instance URL', 190 | '3) Press "Toggle home instance type" to switch between "Lemmy" and "Kbin". (default is "Lemmy")', 191 | ]; 192 | 193 | const txtChangeInstance = createDropdown('How to change home instance', changeInstanceInstructions); 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | const canRedirect = await hasSelectedInstance(); 206 | let TARGET_ELEMENT; 207 | let redirectURL = ''; 208 | 209 | // ---------- Set Target Element -------- // 210 | switch (true) { 211 | case isKbinCommunity(pageURL) && !isKbinPost(pageURL): 212 | TARGET_ELEMENT = document.querySelector('.section.intro') || document.querySelector('#sidebar .magazine .row'); 213 | break; 214 | 215 | case isKbinPost(pageURL): 216 | TARGET_ELEMENT = document.querySelector('.section.intro') || document.querySelector('#sidebar .magazine .row'); 217 | break; 218 | 219 | case isLemmyUser(pageURL): 220 | TARGET_ELEMENT = document.querySelector('.person-listing'); 221 | break; 222 | 223 | case (isLemmyCommunity(pageURL) && !isLemmyPost(pageURL) && !isLemmyUser(pageURL)): 224 | TARGET_ELEMENT = document.querySelector('.card-body'); 225 | break; 226 | 227 | case isLemmyPost(pageURL): 228 | TARGET_ELEMENT = document.querySelector('#sidebarMain').children[0]; 229 | break; 230 | 231 | case isLemmyPhoton() && !isLemmyPhotonPost(pageURL): 232 | TARGET_ELEMENT = document.querySelector('.hidden.xl\\:block aside').children[2]; 233 | break; 234 | 235 | case isLemmyPhotonPost(pageURL): 236 | TARGET_ELEMENT = document.querySelector('main.p-3'); 237 | break; 238 | 239 | case isLemmyAlexandrite(): 240 | TARGET_ELEMENT = document.querySelector('.sidebar').children[1]; 241 | break; 242 | } 243 | 244 | 245 | 246 | 247 | // ---------- Add Event Listeners -------- // 248 | 249 | // For community redirects 250 | if (!(isLemmyPost(pageURL) || 251 | isLemmyPhotonPost(pageURL) || 252 | isLemmyAlexandritePost(pageURL) || 253 | isLemmyUser(pageURL))) { 254 | redirectURL = await getCommunityRedirectURL(pageURL); 255 | } 256 | function redirectToInstance() { 257 | canRedirect 258 | ? window.location.href = redirectURL 259 | : alert('No valid instance has been set.'); 260 | } 261 | 262 | btnRedirectLemmy.addEventListener('click', redirectToInstance); 263 | btnRedirectKbin.addEventListener('click', redirectToInstance); 264 | btnRedirectLemmyAlexandrite.addEventListener('click', redirectToInstance); 265 | btnRedirectLemmyPhoton.addEventListener('click', redirectToInstance); 266 | 267 | // For post redirects 268 | btnToPostLemmy.addEventListener('click', async () => { 269 | if (canRedirect) { 270 | 271 | // prepare variables for fetching 272 | let og_instance = getRealHostname(pageURL) 273 | let postId = (window.location.pathname).split('/post/')[1]; 274 | 275 | // if on a photon instance, further split the url 276 | if (isLemmyPhotonPost(pageURL)) { 277 | postId = window.location.pathname.split('/post/')[1].split('/')[1]; 278 | } 279 | 280 | // fetch the filtered post list from home instance 281 | const og_Post = await fetchPostFromID("https://"+og_instance, postId); 282 | const matchingPosts = await fetchPostsFromTitle(selectedInstance, og_Post.post.name); 283 | const filteredPosts = await filterPostsByPost(og_Post, matchingPosts); 284 | 285 | // if only one post is found, redirect to it 286 | // else ask if user wants to open all matching posts 287 | if (filteredPosts.length > 0) { 288 | console.log(filteredPosts[0]); 289 | const newPostId = filteredPosts[0].post.id; 290 | const community = filteredPosts[0].community.name; 291 | openPostFromID(selectedInstance, newPostId, community); 292 | //window.location.href = selectedInstance + '/post/' + newPostId; 293 | } else if (filteredPosts.length > 1) { 294 | const approve = confirm(filteredPosts.length + ' matching posts were found. Do you want to open them all?'); 295 | if (approve) { 296 | filteredPosts.forEach(post => { 297 | console.log(post); 298 | const community = post.community.name; 299 | openPostFromID(selectedInstance, post.post.id, community); 300 | //window.open(selectedInstance + '/post/' + post.post.id); 301 | 302 | }); 303 | } 304 | } else { alert("Post not found in home instance"); } 305 | } else { alert('No valid instance has been set.') } 306 | }); 307 | 308 | // btnToPostKbin.addEventListener('click', async () => { 309 | // if (canRedirect) { 310 | // console 311 | // let community; 312 | // let instance; 313 | // let postID; 314 | // const pathParts = window.location.pathname.split('/'); 315 | // if (pageURL.includes('@')) { 316 | // community = pathParts[2]; 317 | // instance = pathParts[3].split('@')[1]; 318 | // postID = pathParts[5]; 319 | // } else { 320 | // community = pathParts[2]; 321 | // instance = window.location.hostname; 322 | // postID = pathParts[4]; 323 | // } 324 | // openPostFromID(selectedInstance, postID, community); 325 | // } 326 | // }); 327 | 328 | // For user redirects 329 | 330 | if(isLemmyUser(pageURL)) { 331 | redirectURL = await getUserRedirectURL(pageURL); 332 | } 333 | function redirectToUser() { 334 | canRedirect 335 | ? window.location.href = redirectURL 336 | : alert('No valid instance has been set.'); 337 | } 338 | 339 | btnToUserLemmy.addEventListener('click', redirectToUser); 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | // ---------- Append elements ----------- // 349 | if (!document.querySelector('#instance-assistant-sidebar') && (await getSetting('runOnCommunitySidebar')) && !(await isHomeInstance(pageURL))) { // Prevent duplicate elements 350 | // append community redirect button to community page 351 | if (isLemmyCommunity(pageURL)) { 352 | TARGET_ELEMENT.appendChild(btnRedirectLemmy); 353 | TARGET_ELEMENT.appendChild(txtHomeInstance); 354 | if (!hideHelp) {TARGET_ELEMENT.appendChild(txtChangeInstance);} 355 | } 356 | if (isLemmyPost(pageURL)) { 357 | TARGET_ELEMENT.appendChild(btnToPostLemmy); 358 | TARGET_ELEMENT.appendChild(txtHomeInstance); 359 | if (!hideHelp) {TARGET_ELEMENT.appendChild(txtChangeInstance);} 360 | } 361 | if (isKbinCommunity(pageURL)) { 362 | TARGET_ELEMENT.appendChild(btnRedirectKbin); 363 | TARGET_ELEMENT.appendChild(txtHomeInstance); 364 | if (!hideHelp) {TARGET_ELEMENT.appendChild(txtChangeInstance);} 365 | } 366 | // if (isKbinPost(pageURL)) { 367 | // TARGET_ELEMENT.appendChild(btnToPostKbin); 368 | // TARGET_ELEMENT.appendChild(txtHomeInstance); 369 | // if (!hideHelp) {TARGET_ELEMENT.appendChild(txtChangeInstance);} 370 | // } 371 | if (isLemmyPhoton() && !isLemmyPhotonPost(pageURL)) { 372 | if (!document.querySelector('#instance-assistant-sidebar')) { 373 | containerRedirectLemmyPhoton.appendChild(btnRedirectLemmyPhoton); 374 | containerRedirectLemmyPhoton.appendChild(txtHomeInstance); 375 | if (!hideHelp) {TARGET_ELEMENT.appendChild(txtChangeInstance);} 376 | TARGET_ELEMENT.appendChild(containerRedirectLemmyPhoton); 377 | } 378 | } 379 | // append post redirect button to post page 380 | if (isLemmyPhotonPost(pageURL)) { 381 | TARGET_ELEMENT.insertBefore(btnToPostLemmy, TARGET_ELEMENT.firstChild); 382 | } 383 | if (isLemmyAlexandrite() && !isLemmyAlexandritePost(pageURL)) { 384 | if (!document.querySelector('#instance-assistant-sidebar')) { 385 | containerRedirectLemmyAlexandrite.appendChild(btnRedirectLemmyAlexandrite); 386 | containerRedirectLemmyAlexandrite.appendChild(txtHomeInstance); 387 | if (!hideHelp) {TARGET_ELEMENT.appendChild(txtChangeInstance);} 388 | TARGET_ELEMENT.appendChild(containerRedirectLemmyAlexandrite); 389 | } 390 | } 391 | if (isLemmyAlexandritePost(pageURL)) { 392 | TARGET_ELEMENT.appendChild(btnToPostLemmy); 393 | TARGET_ELEMENT.appendChild(txtHomeInstance); 394 | if (!hideHelp) {TARGET_ELEMENT.appendChild(txtChangeInstance);} 395 | } 396 | // append user redirect button to user page 397 | if (isLemmyUser(pageURL)) { 398 | const userContainer = document.createElement('div'); 399 | userContainer.style.cssText = ` 400 | display: flex; 401 | flex-direction: column; 402 | align-items: center; 403 | justify-content: center; 404 | `; 405 | userContainer.appendChild(btnToUserLemmy); 406 | userContainer.appendChild(txtHomeInstance); 407 | if (!hideHelp) {userContainer.appendChild(txtChangeInstance);} 408 | TARGET_ELEMENT.parentElement.appendChild(userContainer); 409 | } 410 | } 411 | } 412 | loadSelectedInstance(); 413 | } 414 | }, "800"); -------------------------------------------------------------------------------- /src/img/icon-copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cynber/lemmy-instance-assistant/668ee6930eff9a5111f403e170e608a2bf546d0f/src/img/icon-copy.png -------------------------------------------------------------------------------- /src/img/icon-external.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cynber/lemmy-instance-assistant/668ee6930eff9a5111f403e170e608a2bf546d0f/src/img/icon-external.png -------------------------------------------------------------------------------- /src/img/icon-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cynber/lemmy-instance-assistant/668ee6930eff9a5111f403e170e608a2bf546d0f/src/img/icon-home.png -------------------------------------------------------------------------------- /src/img/icon-lemm-noIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cynber/lemmy-instance-assistant/668ee6930eff9a5111f403e170e608a2bf546d0f/src/img/icon-lemm-noIcon.png -------------------------------------------------------------------------------- /src/img/icon-lemm-nsf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cynber/lemmy-instance-assistant/668ee6930eff9a5111f403e170e608a2bf546d0f/src/img/icon-lemm-nsf.png -------------------------------------------------------------------------------- /src/img/lemming128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cynber/lemmy-instance-assistant/668ee6930eff9a5111f403e170e608a2bf546d0f/src/img/lemming128.png -------------------------------------------------------------------------------- /src/img/lemming128_dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cynber/lemmy-instance-assistant/668ee6930eff9a5111f403e170e608a2bf546d0f/src/img/lemming128_dev.png -------------------------------------------------------------------------------- /src/img/lemming16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cynber/lemmy-instance-assistant/668ee6930eff9a5111f403e170e608a2bf546d0f/src/img/lemming16.png -------------------------------------------------------------------------------- /src/img/lemming16_dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cynber/lemmy-instance-assistant/668ee6930eff9a5111f403e170e608a2bf546d0f/src/img/lemming16_dev.png -------------------------------------------------------------------------------- /src/img/lemming32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cynber/lemmy-instance-assistant/668ee6930eff9a5111f403e170e608a2bf546d0f/src/img/lemming32.png -------------------------------------------------------------------------------- /src/img/lemming32_dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cynber/lemmy-instance-assistant/668ee6930eff9a5111f403e170e608a2bf546d0f/src/img/lemming32_dev.png -------------------------------------------------------------------------------- /src/img/lemming48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cynber/lemmy-instance-assistant/668ee6930eff9a5111f403e170e608a2bf546d0f/src/img/lemming48.png -------------------------------------------------------------------------------- /src/img/lemming48_dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cynber/lemmy-instance-assistant/668ee6930eff9a5111f403e170e608a2bf546d0f/src/img/lemming48_dev.png -------------------------------------------------------------------------------- /src/m3-background.js: -------------------------------------------------------------------------------- 1 | // -------------------------------------- 2 | // COPIED FROM utils.js 3 | function getStorageAPI() { 4 | let storageAPI; 5 | if (typeof browser !== 'undefined' && browser.storage && browser.storage.local) { 6 | storageAPI = browser.storage.local; 7 | } else if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) { 8 | storageAPI = chrome.storage.local; 9 | } else { 10 | throw new Error('Storage API is not supported in this browser.'); 11 | } 12 | 13 | return storageAPI; 14 | } 15 | function getBrowserAPI() { 16 | let browserAPI; 17 | if (typeof browser !== 'undefined' && browser.storage && browser.storage.local) { 18 | browserAPI = browser; 19 | } else if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) { 20 | browserAPI = chrome; 21 | } else { 22 | throw new Error('Browser API is not supported in this browser.'); 23 | } 24 | return browserAPI; 25 | } 26 | async function getAllSettings() { 27 | const storageAPI = getStorageAPI(); 28 | const allSettings = await storageAPI.get('settings'); 29 | if (!allSettings || !allSettings.settings) { 30 | await setAllSettings(defaultSettings); 31 | return defaultSettings; 32 | } 33 | return await storageAPI.get('settings'); 34 | } 35 | async function getSetting(settingName) { 36 | const allSettings = await getAllSettings(); 37 | return allSettings.settings[settingName]; 38 | } 39 | async function hasSelectedInstance() { 40 | const selectedInstance = await getSetting('selectedInstance'); 41 | return selectedInstance !== undefined && selectedInstance !== ""; 42 | } 43 | async function hasSelectedType() { 44 | const selectedType = await getSetting('selectedType'); 45 | return selectedType !== undefined && selectedType !== ""; 46 | } 47 | async function loadStorage(key) { 48 | const { value } = await browser.storage.local.get(key); 49 | return value; 50 | } 51 | async function p2l_getPostData() { 52 | const storageAPI = getBrowserAPI(); 53 | return new Promise((resolve, reject) => { 54 | storageAPI.tabs.query({ active: true, currentWindow: true }, function (tabs) { 55 | const activeTab = tabs[0]; 56 | const postData = { 57 | title: activeTab.title, 58 | url: activeTab.url 59 | }; 60 | resolve(postData); 61 | }); 62 | }); 63 | } 64 | // -------------------------------------- 65 | 66 | // -------------------------------------- 67 | // Handle redirects within a Lemmy site 68 | // - Sometimes when navigating within a Lemmy site, the content scripts won't run despite the URL matching the pattern. This is a workaround for that. 69 | // -------------------------------------- 70 | chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { 71 | if (tab.url?.startsWith("chrome://") || tab.url?.startsWith("chrome-extension://")) { 72 | return undefined; 73 | } 74 | if (changeInfo.status === "complete") { 75 | if ( 76 | /^https?:\/\/.*\/c\//.test(tab.url) || 77 | /^https?:\/\/.*\/communities\//.test(tab.url) || 78 | /^https?:\/\/.*\/post\//.test(tab.url) 79 | ) { 80 | chrome.scripting.executeScript({ 81 | target: { tabId: tabId }, 82 | files: ["utils.js", "content-sidebar.js", "content-general.js"] 83 | }); 84 | } 85 | } 86 | }); 87 | 88 | 89 | // -------------------------------------- 90 | // Handle context menu clicks 91 | // -------------------------------------- 92 | chrome.contextMenus.onClicked.addListener(async (info, tab) => { 93 | if (info.menuItemId === "redirect" && info.linkUrl) { 94 | 95 | let sourceHost = new URL(info.linkUrl).hostname; 96 | let sourcePath = new URL(info.linkUrl).pathname; 97 | 98 | if (sourcePath.includes("/c/") || sourcePath.includes("/m/")) { 99 | const communityName = sourcePath.match(/\/[cm]\/([^/@]+)/)[1]; 100 | const sourceInstance = sourcePath.includes("@") ? 101 | sourcePath.match(/\/[cm]\/[^/@]+@([^/]+)/)[1] : sourceHost; 102 | const selectedType = await getSetting("selectedType"); 103 | const selectedInstance = await getSetting("selectedInstance"); 104 | 105 | if (!selectedInstance) { 106 | chrome.tabs.update(tab.id, { url: 'https://github.com/cynber/lemmy-instance-assistant#setup' }); 107 | return false; 108 | } 109 | 110 | const communityPrefix = selectedType ? (selectedType === "lemmy" ? "/c/" : "/m/") : "/c/"; 111 | const redirectURL = selectedInstance + communityPrefix + communityName + '@' + sourceInstance; 112 | 113 | chrome.tabs.update(tab.id, { url: redirectURL }); 114 | 115 | } else { 116 | chrome.tabs.update(tab.id, { url: 'https://github.com/cynber/lemmy-instance-assistant/wiki/Sorry-that-didn\'t-work...' }); 117 | // TODO: Add a popup to explain this 118 | } 119 | } else if (info.menuItemId === "post-image" && info.srcUrl) { 120 | 121 | if (await hasSelectedInstance() && await hasSelectedType()) { 122 | 123 | const type = await getSetting("selectedType"); 124 | const instance = await getSetting("selectedInstance"); 125 | const postData = await p2l_getPostData(); 126 | 127 | if (type === "lemmy") { 128 | const url = instance + "/create_post"; 129 | console.log(url, postData, info, tab, type, instance) 130 | const createdTab = await chrome.tabs.create({ url: url }); 131 | 132 | 133 | // Listen for tab updates to check for loading completion 134 | chrome.tabs.onUpdated.addListener(function listener(tabId, changeInfo) { 135 | console.log(changeInfo.status) 136 | if (tabId === createdTab.id) { 137 | chrome.tabs.onUpdated.removeListener(listener); // Remove the listener 138 | console.log(postData.title); 139 | console.log(postData.url); 140 | 141 | // Fill in form after the tab is fully loaded 142 | chrome.tabs.onUpdated.addListener(function listener(tabId, changeInfo) { 143 | if (tabId === createdTab.id && changeInfo.status === "complete") { 144 | chrome.tabs.onUpdated.removeListener(listener); // Remove the listener 145 | 146 | // Fill in form after the tab is fully loaded 147 | chrome.scripting.executeScript({ 148 | target: { tabId: createdTab.id }, 149 | function: fillFormScript, 150 | args: [{ 151 | postDataTitle: postData.title, 152 | infoSrcUrl: info.srcUrl, 153 | postDataUrl: postData.url 154 | }] 155 | }); 156 | } 157 | }); 158 | 159 | function fillFormScript(args) { 160 | const EVENT_OPTIONS = { bubbles: true, cancelable: false, composed: true }; 161 | const EVENTS = { 162 | BLUR: new Event("blur", EVENT_OPTIONS), 163 | CHANGE: new Event("change", EVENT_OPTIONS), 164 | INPUT: new Event("input", EVENT_OPTIONS), 165 | }; 166 | 167 | const postTitleInput = document.querySelector("#post-title"); 168 | const postURLInput = document.querySelector("#post-url"); 169 | const postBodyInput = document.querySelector("textarea[id^='markdown-textarea-']"); 170 | 171 | postTitleInput.select(); 172 | postTitleInput.value = args.postDataTitle; 173 | postTitleInput.dispatchEvent(EVENTS.INPUT); 174 | 175 | postURLInput.select(); 176 | postURLInput.value = args.infoSrcUrl; 177 | postURLInput.dispatchEvent(EVENTS.INPUT); 178 | 179 | postBodyInput.select(); 180 | postBodyInput.value = "Source: " + args.postDataUrl; 181 | postBodyInput.dispatchEvent(EVENTS.INPUT); 182 | } 183 | } 184 | }); 185 | 186 | } else if (type === "kbin") { 187 | const url = instance + "/new?url=" + info.srcUrl + "&title=" + postData.title + "&body=Source: " + postData.url; 188 | await chrome.tabs.create({ url: url }); 189 | } 190 | 191 | } else { alert("No valid instance has been set. Please select an instance in the popup using 'Change my home instance'."); } 192 | } 193 | }); 194 | 195 | chrome.runtime.onInstalled.addListener(() => { 196 | chrome.contextMenus.create({ 197 | id: "redirect", 198 | title: "Redirect to home instance", 199 | contexts: ["link"], 200 | targetUrlPatterns: ["http://*/c/*", "https://*/c/*", "http://*/p/*", "https://*/p/*", "http://*/m/*", "https://*/m/*"], 201 | }); 202 | chrome.contextMenus.create({ 203 | id: "post-image", 204 | title: "Post this image", 205 | contexts: ["image"], 206 | targetUrlPatterns: ["http://*/*", "https://*/*"] 207 | }, () => void chrome.runtime.lastError, 208 | ); 209 | }); 210 | 211 | 212 | 213 | // -------------------------------------- 214 | // Set default values on install/update 215 | // -------------------------------------- 216 | 217 | chrome.runtime.onInstalled.addListener(async ({ reason }) => { 218 | 219 | // Set default values on install/update 220 | // TODO: fix the utils import so we can just use initializeSettingsWithDefaults() 221 | if (reason === 'install' || reason === 'update') { 222 | 223 | async function backgroundInitializeSettings() { 224 | 225 | const defaultSettings = { 226 | hideSidebarLemmy: false, 227 | hideSidebarKbin: false, 228 | instanceList: [ 229 | { name: "lemmy.world", url: "https://lemmy.world" }, 230 | { name: "lemmy.ca", url: "https://lemmy.ca" }, 231 | { name: "lemm.ee", url: "https://lemm.ee" }, 232 | { name: "kbin.social", url: "https://kbin.social" }, 233 | ], 234 | runOnCommunitySidebar: true, 235 | runOnCommunityNotFound: true, 236 | hideHelp: false, 237 | selectedInstance: '', // users are forced to set this 238 | selectedType: 'lemmy', // lemmy or kbin 239 | theme: 'dark', // **NOT IMPLEMENTED YET** 240 | toolSearchCommunity_openInLemmyverse: false, 241 | }; 242 | 243 | let storageAPI = chrome.storage.local; 244 | let allSettings = await storageAPI.get('settings'); 245 | 246 | if (!allSettings || !allSettings.settings) { 247 | await storageAPI.set({ 'settings': defaultSettings }); 248 | } else { 249 | for (const settingName of Object.keys(defaultSettings)) { 250 | if (!allSettings.settings.hasOwnProperty(settingName)) { 251 | allSettings.settings[settingName] = defaultSettings[settingName]; 252 | } 253 | } 254 | await storageAPI.set({ 'settings': allSettings.settings }); 255 | } 256 | } 257 | await backgroundInitializeSettings(); 258 | } 259 | 260 | // Open settings page when extension is first installed 261 | if (reason === 'install') { 262 | chrome.tabs.create({ url: 'page-settings/settings.html' }); 263 | } 264 | }); 265 | -------------------------------------------------------------------------------- /src/manifest_chrome.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Instance Assistant for Lemmy & Kbin", 3 | "short_name": "Inst. Assist", 4 | "author": "Cynber", 5 | "description": "Simplify your Lemmy & Kbin experience with tools for your instance and communities", 6 | "homepage_url": "https://github.com/cynber/lemmy-instance-assistant", 7 | "version": "1.2.6", 8 | "manifest_version": 3, 9 | "icons": { 10 | "48": "img/lemming48_dev.png", 11 | "128": "img/lemming128_dev.png" 12 | }, 13 | "permissions": [ 14 | "storage", 15 | "activeTab", 16 | "scripting", 17 | "contextMenus", 18 | "sidePanel" 19 | ], 20 | "host_permissions": [ 21 | "*://*/communities", 22 | "*://*/c/*", 23 | "*://*/m/*", 24 | "*://*/post/*" 25 | ], 26 | "action": { 27 | "default_popup": "page-popup/popup.html", 28 | "default_icon": { 29 | "16": "img/lemming16_dev.png", 30 | "32": "img/lemming32_dev.png" 31 | } 32 | }, 33 | "content_scripts": [ 34 | { 35 | "matches": [ 36 | "*://*/communities", 37 | "*://*/c/*", 38 | "*://*/m/*", 39 | "*://*/post/*", 40 | "*://*/u/*" 41 | ], 42 | "js": [ 43 | "node_modules/webextension-polyfill/dist/browser-polyfill.js", 44 | "utils.js", 45 | "content-sidebar.js", 46 | "content-general.js", 47 | "content-communityNotFound.js" 48 | ], 49 | "run_at": "document_end" 50 | } 51 | ], 52 | "background": { 53 | "service_worker": "background.js" 54 | }, 55 | "side_panel": { 56 | "default_title": "Instance Assistant", 57 | "default_icon": { 58 | "16": "img/lemming16_dev.png", 59 | "32": "img/lemming32_dev.png" 60 | }, 61 | "default_path": "page-sidebar/sidebar.html" 62 | } 63 | } -------------------------------------------------------------------------------- /src/manifest_edge.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Instance Assistant for Lemmy & Kbin", 3 | "short_name": "Inst. Assist", 4 | "author": "Cynber", 5 | "description": "Simplify your Lemmy & Kbin experience with tools for your instance and communities", 6 | "homepage_url": "https://github.com/cynber/lemmy-instance-assistant", 7 | "version": "1.2.6", 8 | "manifest_version": 3, 9 | "icons": { 10 | "48": "img/lemming48_dev.png", 11 | "128": "img/lemming128_dev.png" 12 | }, 13 | "permissions": [ 14 | "storage", 15 | "activeTab", 16 | "scripting", 17 | "contextMenus" 18 | ], 19 | "host_permissions": [ 20 | "*://*/communities", 21 | "*://*/c/*", 22 | "*://*/m/*", 23 | "*://*/post/*" 24 | ], 25 | "action": { 26 | "default_popup": "page-popup/popup.html", 27 | "default_icon": { 28 | "16": "img/lemming16_dev.png", 29 | "32": "img/lemming32_dev.png" 30 | } 31 | }, 32 | "content_scripts": [ 33 | { 34 | "matches": [ 35 | "*://*/communities", 36 | "*://*/c/*", 37 | "*://*/m/*", 38 | "*://*/post/*", 39 | "*://*/u/*" 40 | ], 41 | "js": [ 42 | "node_modules/webextension-polyfill/dist/browser-polyfill.js", 43 | "utils.js", 44 | "content-sidebar.js", 45 | "content-general.js", 46 | "content-communityNotFound.js" 47 | ], 48 | "run_at": "document_end" 49 | } 50 | ], 51 | "background": { 52 | "service_worker": "background.js" 53 | }, 54 | "side_panel": { 55 | "default_title": "Instance Assistant", 56 | "default_icon": { 57 | "16": "img/lemming16_dev.png", 58 | "32": "img/lemming32_dev.png" 59 | }, 60 | "default_path": "page-sidebar/sidebar.html" 61 | } 62 | } -------------------------------------------------------------------------------- /src/manifest_firefox.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Instance Assistant for Lemmy & Kbin", 3 | "short_name": "Inst. Assist", 4 | "author": "Cynber", 5 | "description": "Simplify your Lemmy & Kbin experience with tools for your instance and communities", 6 | "homepage_url": "https://github.com/cynber/lemmy-instance-assistant", 7 | "version": "1.2.6", 8 | "manifest_version": 2, 9 | "icons": { 10 | "48": "img/lemming48_dev.png", 11 | "128": "img/lemming128_dev.png" 12 | }, 13 | "permissions": [ 14 | "storage", 15 | "activeTab", 16 | "scripting", 17 | "contextMenus", 18 | "*://*/c/*", 19 | "*://*/m/*", 20 | "*://*/post/*" 21 | ], 22 | "browser_action": { 23 | "default_popup": "page-popup/popup.html", 24 | "default_icon": { 25 | "16": "img/lemming16_dev.png", 26 | "32": "img/lemming32_dev.png" 27 | } 28 | }, 29 | "content_scripts": [ 30 | { 31 | "matches": [ 32 | "*://*/communities", 33 | "*://*/c/*", 34 | "*://*/m/*", 35 | "*://*/post/*", 36 | "*://*/u/*" 37 | ], 38 | "js": [ 39 | "node_modules/webextension-polyfill/dist/browser-polyfill.js", 40 | "utils.js", 41 | "content-sidebar.js", 42 | "content-general.js", 43 | "content-communityNotFound.js" 44 | ], 45 | "run_at": "document_end" 46 | } 47 | ], 48 | "background": { 49 | "scripts": [ 50 | "background.js" 51 | ], 52 | "persistent": false 53 | }, 54 | "sidebar_action": { 55 | "default_title": "Instance Assistant", 56 | "default_icon": { 57 | "16": "img/lemming16_dev.png", 58 | "32": "img/lemming32_dev.png" 59 | }, 60 | "default_panel": "page-sidebar/sidebar.html", 61 | "open_at_install": false 62 | }, 63 | "options_ui": { 64 | "page": "page-options/options.html", 65 | "open_in_tab": false, 66 | "browser_style": false 67 | }, 68 | "browser_specific_settings": { 69 | "gecko": { 70 | "id": "{49f79358-ada8-438f-a6b3-649ce7638a34}" 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /src/manifest_opera.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Instance Assistant for Lemmy & Kbin", 3 | "short_name": "Inst. Assist", 4 | "author": "Cynber", 5 | "description": "Simplify your Lemmy & Kbin experience with tools for your instance and communities", 6 | "homepage_url": "https://github.com/cynber/lemmy-instance-assistant", 7 | "version": "1.2.6", 8 | "manifest_version": 3, 9 | "icons": { 10 | "48": "img/lemming48_dev.png", 11 | "128": "img/lemming128_dev.png" 12 | }, 13 | "permissions": [ 14 | "storage", 15 | "activeTab", 16 | "scripting", 17 | "contextMenus" 18 | ], 19 | "host_permissions": [ 20 | "*://*/communities", 21 | "*://*/c/*", 22 | "*://*/m/*", 23 | "*://*/post/*" 24 | ], 25 | "action": { 26 | "default_popup": "page-popup/popup.html", 27 | "default_icon": { 28 | "16": "img/lemming16_dev.png", 29 | "32": "img/lemming32_dev.png" 30 | } 31 | }, 32 | "content_scripts": [ 33 | { 34 | "matches": [ 35 | "*://*/communities", 36 | "*://*/c/*", 37 | "*://*/m/*", 38 | "*://*/post/*", 39 | "*://*/u/*" 40 | ], 41 | "js": [ 42 | "node_modules/webextension-polyfill/dist/browser-polyfill.js", 43 | "utils.js", 44 | "content-sidebar.js", 45 | "content-general.js", 46 | "content-communityNotFound.js" 47 | ], 48 | "run_at": "document_end" 49 | } 50 | ], 51 | "background": { 52 | "service_worker": "background.js" 53 | }, 54 | "sidebar_action": { 55 | "default_title": "Instance Assistant", 56 | "default_icon": { 57 | "16": "img/lemming16_dev.png", 58 | "32": "img/lemming32_dev.png" 59 | }, 60 | "default_panel": "page-sidebar/sidebar.html", 61 | "open_at_install": false 62 | } 63 | } -------------------------------------------------------------------------------- /src/manifest_safari.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cynber/lemmy-instance-assistant/668ee6930eff9a5111f403e170e608a2bf546d0f/src/manifest_safari.json -------------------------------------------------------------------------------- /src/page-options/options.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: white; 3 | width: 80%; 4 | } 5 | 6 | p { 7 | color: black; 8 | } -------------------------------------------------------------------------------- /src/page-options/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Instance Assistant Options 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Please go to the settings page instead of using the options menu:

14 |
15 | 16 |
17 |
18 |

This was done in order to consolidate the options & settings pages.

19 | 20 | 21 | -------------------------------------------------------------------------------- /src/page-options/options.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', () => { 2 | const settingsButton = document.getElementById('settings-button'); 3 | 4 | settingsButton.addEventListener('click', () => { 5 | browser.tabs.create({ url: '../page-settings/settings.html' }); 6 | }); 7 | }); -------------------------------------------------------------------------------- /src/page-popup/popup.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-bg-1: #1f1f1f; 3 | --color-bg-2: #252525; 4 | --color-bg-3: #111111; 5 | --color-text: #e0e0e0; 6 | --color-btn: #363636; 7 | --color-btn-hover: #3e4347; 8 | --color-btn-active: #4b5156; 9 | --color-btn-redirect: #472783; 10 | --color-btn-redirect-hover: #5a3a9e; 11 | --color-btn-redirect-active: #6c4db7; 12 | --color-btn-tools: #2e515e; 13 | --color-btn-tools-hover: #3a6b7a; 14 | --color-btn-tools-active: #468494; 15 | --color-btn-change: #571a1a; 16 | --color-btn-change-hover: #6b1a1a; 17 | --color-btn-change-active: #7f1a1a; 18 | } 19 | 20 | body { 21 | width: 400px; 22 | min-height: 150px; 23 | padding-bottom: 1px; 24 | } 25 | 26 | .subtle-text { 27 | font-size: 12px; 28 | color: var(--color-text); 29 | opacity: 0.8; 30 | margin: 3px 0px 1px 0px; 31 | } 32 | 33 | .no-instance-warning { 34 | font-size: 12px; 35 | color: red; 36 | padding: 5px; 37 | text-align: center; 38 | } 39 | 40 | .page-header { 41 | display: flex; 42 | margin: 0px 0px 10px 0px; 43 | justify-content: space-between; 44 | align-items: center; 45 | } 46 | 47 | .page-header p { 48 | margin: 0px 0px 0px 0px; 49 | padding: 0px 0px 0px 0px; 50 | } 51 | 52 | .page-section { 53 | background: var(--color-bg-2); 54 | padding: 10px 10px 10px 10px; 55 | margin: 10px 0px 10px 0px; 56 | border-radius: 5px; 57 | } 58 | 59 | .page-section:last-child { 60 | margin-bottom: 2rem; 61 | } 62 | 63 | .section-header-text { 64 | margin: 0px 0px 5px 0px; 65 | padding: 0px 0px 0px 0px; 66 | color: var(--color-text); 67 | } 68 | 69 | .icon-external { 70 | width: 10px; 71 | height: 10px; 72 | margin-left: 5px; 73 | } 74 | 75 | /* ----------------------------------------- */ 76 | /* GENERAL BUTTON STYLES */ 77 | /* ----------------------------------------- */ 78 | 79 | #btn-settings { 80 | width: auto; 81 | margin-left: 10px; 82 | align-items: right; 83 | } 84 | 85 | #btn-open-settings { 86 | margin: 0.25rem 0rem 0rem 0rem; 87 | background-color: var(--color-btn) 88 | } 89 | 90 | #btn-open-settings:hover { 91 | background-color: var(--color-btn-hover); 92 | } 93 | 94 | #btn-open-settings:active { 95 | animation: buttonClick 0.1s; 96 | } 97 | 98 | #btn-redirect-instance { 99 | padding: 0.7rem .75rem; 100 | margin: 0.2rem 0rem .2rem 0rem; 101 | background-color: var(--color-btn-redirect); 102 | } 103 | 104 | #btn-redirect-instance:hover { 105 | background-color: var(--color-btn-redirect-hover); 106 | } 107 | 108 | #btn-redirect-instance:active { 109 | animation: buttonClick 0.1s; 110 | } 111 | 112 | /* ----------------------------------------- */ 113 | /* TOOLS */ 114 | /* ----------------------------------------- */ 115 | 116 | .page-tools { 117 | margin: 0.25rem 0rem 0rem 0rem; 118 | background-color: var(--color-btn-tools); 119 | } 120 | 121 | .page-tools:hover { 122 | background-color: var(--color-btn-tools-hover); 123 | } 124 | 125 | .page-tools:active { 126 | animation: buttonClick 0.1s; 127 | } 128 | 129 | /* Flex container for the search bar and button */ 130 | .search-bar-communities, 131 | .search-bar-content { 132 | display: flex; 133 | justify-content: space-between; 134 | align-items: baseline; 135 | padding: 5px 5px 5px 5px; 136 | } 137 | 138 | /* Search bar input */ 139 | #searchInputCommunities, 140 | #searchInputContent { 141 | flex: 3; 142 | margin-right: 5px; 143 | height: 19px; 144 | } 145 | 146 | /* Search button */ 147 | #btn-tool-search-community, 148 | #btn-tool-search-content { 149 | flex: 1; 150 | margin-left: 5px; 151 | } 152 | 153 | /* ----------------------------------------- */ 154 | /* SETTINGS */ 155 | /* ----------------------------------------- */ 156 | 157 | #btn-change-instance { 158 | margin: 0.25rem 0rem 0rem 0rem; 159 | background-color: var(--color-btn-change); 160 | } 161 | 162 | #btn-change-instance:hover { 163 | background-color: var(--color-btn-change-hover); 164 | } 165 | 166 | #btn-change-instance:active { 167 | animation: buttonClick 0.1s; 168 | } 169 | 170 | #btn-change-type { 171 | margin: 0.25rem 0rem 0rem 0rem; 172 | background-color: var(--color-btn) 173 | } 174 | 175 | #btn-change-type:hover { 176 | background-color: var(--color-btn-hover); 177 | } 178 | 179 | #btn-change-type:active { 180 | animation: buttonClick 0.1s; 181 | } 182 | 183 | ul#instance-list { 184 | margin: 5px 0px 0px 0px; 185 | list-style-type: none; 186 | padding-left: 0; 187 | columns: 2; 188 | -webkit-columns: 2; 189 | -moz-columns: 2; 190 | } 191 | 192 | .btn-instance-list { 193 | background-color: var(--color-btn); 194 | color: var(--color-text); 195 | padding: .375rem .75rem; 196 | padding-left: 36px; 197 | margin: 3px; 198 | width: 95%; 199 | border: 2px; 200 | border-radius: 5px; 201 | cursor: pointer; 202 | text-align: left; 203 | transition: background-color 0.2s; 204 | background-image: url("../img/icon-copy.png"); 205 | background-repeat: no-repeat; 206 | background-position: left center; 207 | background-size: 24px 16px; 208 | } 209 | 210 | .btn-instance-list:hover { 211 | background-color: var(--color-btn-hover); 212 | } 213 | 214 | .btn-instance-list:active { 215 | animation: buttonClick 0.1s; 216 | } -------------------------------------------------------------------------------- /src/page-popup/popup.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | Instance Selector 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 27 | 28 |
29 | 30 |
31 | 32 | 33 | 34 |
35 |
36 |

Posts related to this webpage

37 |
38 |
39 | 42 | 45 |
46 |

'Open posts' will pull from your home instance (potential risks)

49 |
50 | 51 |
52 |
53 | 54 | 57 |
58 |
59 | 60 | 63 |
64 |
65 | 66 |
67 |
68 |

Quick Settings

69 |
70 | 71 | 75 |
    76 |

    You can modify this list in SETTINGS

    77 |
    78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /src/page-popup/popup.js: -------------------------------------------------------------------------------- 1 | // THIS PAGE SHOULD BE IDENTICAL TO popup.js 2 | 3 | document.addEventListener("DOMContentLoaded", function () { 4 | async function createPage() { 5 | 6 | const instanceList = document.getElementById("instance-list"), 7 | btnChangeInstance = document.getElementById("btn-change-instance"), 8 | btnChangeType = document.getElementById("btn-change-type"), 9 | btnRedirect = document.getElementById("btn-redirect-instance"), 10 | btnOpenSettings = document.getElementById("btn-open-settings"), 11 | txtHomeInstance = document.getElementById("homeInstance"), 12 | txtInstanceType = document.getElementById("instance-type"); 13 | txtInstanceWarn = document.getElementById("no-instance-warning"); 14 | 15 | // --------------------------------------------------------- 16 | // ------------------- Setup Display ----------------------- 17 | // --------------------------------------------------------- 18 | 19 | const selectedInstance = await getSetting("selectedInstance"); 20 | const selectedType = await getSetting("selectedType"); 21 | 22 | txtHomeInstance.textContent = selectedInstance ? selectedInstance : "unknown"; 23 | txtInstanceType.textContent = selectedType ? selectedType : "unknown"; 24 | txtInstanceWarn.textContent = selectedInstance ? "" : "WARN - Instance Not Selected: Some features will not work as expected. Please click 'Change my home instance'."; 25 | 26 | let lemmyInstances = await getSetting("instanceList"); 27 | 28 | lemmyInstances.forEach((instance) => { 29 | const listItem = document.createElement("li"); 30 | const button = document.createElement("button"); 31 | button.type = "button"; 32 | button.textContent = instance.name; 33 | button.className = "btn-instance-list"; 34 | listItem.appendChild(button); 35 | instanceList.appendChild(listItem); 36 | }); 37 | 38 | // --------------------------------------------------------- 39 | // ------------------- Basic Functions --------------------- 40 | // --------------------------------------------------------- 41 | 42 | // Open settings page 43 | btnOpenSettings.addEventListener("click", (event) => { 44 | doOpenSettings(); 45 | }); 46 | 47 | // --------------------------------------------------------- 48 | // --------------- Quick Settings Functions ---------------- 49 | // --------------------------------------------------------- 50 | 51 | // Update home instance address 52 | btnChangeInstance.addEventListener("click", async () => { 53 | const inputInstance = prompt("Enter your instance URL: (ex. 'https://lemmy.ca')"); 54 | if (inputInstance === null) { return; } // exit without alerting if user cancels 55 | if (inputInstance && validInstanceURLPattern.test(inputInstance)) { 56 | await setSetting("selectedInstance", inputInstance.trim()); 57 | txtHomeInstance.textContent = inputInstance.trim(); 58 | 59 | } else { alert("Invalid URL format, please follow this format: \n 'https://lemmy.ca'"); } 60 | window.location.reload(); 61 | }); 62 | 63 | // Toggle home instance type 64 | btnChangeType.addEventListener("click", async () => { 65 | const currentType = await getSetting("selectedType"); 66 | const newType = currentType === "lemmy" ? "kbin" : "lemmy"; 67 | await setSetting("selectedType", newType); 68 | txtInstanceType.textContent = newType; 69 | }); 70 | 71 | // Copy URL 72 | instanceList.addEventListener("click", (event) => { 73 | const target = event.target; 74 | if (target.classList.contains("btn-instance-list")) { 75 | const url = lemmyInstances.find((instance) => instance.name === target.textContent).url; 76 | navigator.clipboard.writeText(url); 77 | } 78 | }); 79 | 80 | // --------------------------------------------- // 81 | // ---------------- Posting Tools -------------- // 82 | // --------------------------------------------- // 83 | 84 | const btn_post_to = document.getElementById("btn-tool-post-to"); 85 | const btnOpenPosts = document.getElementById("btn-tool-open-posts"); 86 | 87 | // Post to Community 88 | btn_post_to.addEventListener("click", async () => { 89 | doCreatePost(); 90 | }); 91 | 92 | // Open posts from home instance 93 | btnOpenPosts.addEventListener("click", async () => { 94 | const queryOptions = { active: true, currentWindow: true }; 95 | const [tab] = await browser.tabs.query(queryOptions); 96 | const testURL = tab.url; 97 | 98 | doOpenMatchingPostsLemmy(testURL); 99 | }); 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | // --------------------------------------------- // 109 | // ---------------- Search Tools --------------- // 110 | // --------------------------------------------- // 111 | 112 | // Search Lemmyverse 113 | const btnSearchCommunities = document.getElementById("btn-tool-search-community"); 114 | const searchInputCommunities = document.getElementById("searchInputCommunities"); 115 | 116 | async function performSearchCommunities() { 117 | await toolSearchCommunitiesLemmyverse(searchInputCommunities.value.trim()) 118 | } 119 | 120 | btnSearchCommunities.addEventListener("click", performSearchCommunities); 121 | 122 | searchInputCommunities.addEventListener("keydown", (event) => { 123 | if (event.key === "Enter") { 124 | event.preventDefault(); // Prevent default form submission behavior 125 | performSearchCommunities(); 126 | } 127 | }); 128 | 129 | // Search content with Lemmy-Search 130 | const btnSearchContent = document.getElementById("btn-tool-search-content"); 131 | const searchInputContent = document.getElementById("searchInputContent"); 132 | 133 | function performSearchCommunitiesContent() { 134 | toolSearchContentLemmysearch(searchInputContent.value.trim()); 135 | } 136 | 137 | btnSearchContent.addEventListener("click", performSearchCommunitiesContent); 138 | 139 | searchInputContent.addEventListener("keydown", (event) => { 140 | if (event.key === "Enter") { 141 | event.preventDefault(); // Prevent default form submission behavior 142 | performSearchCommunitiesContent(); 143 | } 144 | }); 145 | 146 | // --------------------------------------------- // 147 | // -------------- Redirect Instance ------------ // 148 | // --------------------------------------------- // 149 | 150 | // Redirect to selected instance 151 | btnRedirect.addEventListener('click', async () => { 152 | const queryOptions = { active: true, currentWindow: true }; 153 | const [tab] = await browser.tabs.query(queryOptions); 154 | 155 | tabURL = tab.url; 156 | 157 | if ((await hasSelectedInstance())) { 158 | if (isLemmyCommunityWEAK(tabURL) || isKbinCommunityWEAK(tabURL)) { 159 | if (!(await isHomeInstance(tabURL))) { 160 | 161 | const redirectURL = await getCommunityRedirectURL(tabURL); 162 | await browser.tabs.update(tab.id, { url: redirectURL }); 163 | 164 | } else { alert('You are already on your home instance.'); } 165 | } else { alert('You are not on a Lemmy or Kbin community. Please navigate to a community page and try again.\n\nThe extension checks for links that have "/c/" or "/m/" in the URL'); } 166 | } else { alert('No valid instance has been set. Please select an instance in the popup using "Change my home instance".'); } 167 | }); 168 | } 169 | createPage(); 170 | }); 171 | -------------------------------------------------------------------------------- /src/page-search/search.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-bg-1: #1f1f1f; 3 | --color-bg-2: #252525; 4 | --color-bg-3: #111111; 5 | --color-text: #e0e0e0; 6 | --color-btn: #363636; 7 | --color-btn-hover: #3e4347; 8 | --color-btn-active: #4b5156; 9 | --color-btn-redirect: #5f35ae; 10 | --color-btn-redirect-hover: #6a3ebc; 11 | --color-btn-redirect-active: #7546ca; 12 | --color-btn-tools: #054da7; 13 | --color-btn-tools-hover: #0e5abf; 14 | --color-btn-tools-active: #1668d7; 15 | --color-btn-change: #571a1a; 16 | --color-btn-change-hover: #6b1a1a; 17 | --color-btn-change-active: #7f1a1a; 18 | } 19 | 20 | body { 21 | padding: 0; 22 | margin: 0; 23 | color: var(--color-text) 24 | } 25 | 26 | h2 { 27 | margin: 30px 0px 10px 0px; 28 | } 29 | 30 | /* ----------------------------------------- */ 31 | /* HEADER */ 32 | /* ----------------------------------------- */ 33 | 34 | header { 35 | margin: 0; 36 | background-color: var(--color-bg-3); 37 | padding: 0 40px; 38 | display: flex; 39 | justify-content: space-between; 40 | align-items: center; 41 | } 42 | 43 | nav { 44 | margin: 10px 0; 45 | } 46 | 47 | .btn-nav { 48 | display: inline-block; 49 | margin-right: 10px; 50 | padding: 8px 12px; 51 | background-color: var(--color-btn); 52 | text-decoration: none; 53 | border-radius: 5px; 54 | } 55 | 56 | .btn-nav:hover { 57 | background-color: var(--color-btn-hover); 58 | } 59 | 60 | .btn-nav:active { 61 | animation: buttonClick 0.05s; 62 | } 63 | 64 | .logo { 65 | height: 40px; 66 | margin: 10px; 67 | } 68 | 69 | /* ----------------------------------------- */ 70 | /* CONTENT */ 71 | /* ----------------------------------------- */ 72 | 73 | .search { 74 | padding: 40px; 75 | } 76 | 77 | #searchForm { 78 | font-size: large; 79 | margin: 40px; 80 | text-align: center; 81 | } 82 | 83 | label { 84 | display: block; 85 | margin-bottom: 5px; 86 | } 87 | 88 | .text-field { 89 | margin-bottom: 20px; 90 | } 91 | 92 | .text-field p { 93 | margin-bottom: 5px; 94 | } 95 | 96 | .text-field-input { 97 | width: 100%; 98 | } 99 | 100 | .text-field label { 101 | font-weight: bold; 102 | margin-right: 10px; 103 | } 104 | 105 | input[type="checkbox"], 106 | input[type="text"] { 107 | margin-left: 5px; 108 | } 109 | 110 | input[type="text"] { 111 | width: 30%; 112 | } 113 | 114 | .welcome { 115 | margin-bottom: 20px; 116 | font-size: 16px; 117 | color: var(--color-text); 118 | text-align: center; 119 | background-color: var(--color-btn-active); 120 | } 121 | 122 | #toast-container { 123 | position: fixed; 124 | bottom: 20px; 125 | left: 50%; 126 | transform: translateX(-50%); 127 | z-index: 9999; 128 | } 129 | 130 | #toast-message { 131 | background-color: var(--color-btn-active); 132 | color: var(--color-text); 133 | padding: 10px 20px; 134 | border-radius: 4px; 135 | opacity: 0; 136 | transition: opacity 0.3s ease; 137 | } 138 | 139 | #toast-message.show { 140 | opacity: 1; 141 | } 142 | 143 | /* ----------------------------------------- */ 144 | /* RESULTS */ 145 | /* ----------------------------------------- */ 146 | 147 | #results { 148 | display: grid; 149 | grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); 150 | gap: 20px; 151 | justify-items: center; 152 | margin-top: 20px; 153 | } 154 | 155 | .community-card { 156 | background-color: var(--color-bg-3); 157 | color: var(--color-text); 158 | border: 2px solid var(--color-bg-2); 159 | border-radius: 5px; 160 | padding: 10px; 161 | width: 350px; 162 | height: 300px; 163 | display: flex; 164 | flex-direction: column; 165 | justify-content: space-between; 166 | } 167 | 168 | .community-card h3 { 169 | background-color: var(--color-btn-tools); 170 | border: 2px solid var(--color-btn-tools); 171 | border-radius: 5px; 172 | margin-bottom: 5px; 173 | padding: 5px; 174 | font-size: 14px; 175 | font-weight: bold; 176 | white-space: nowrap; 177 | overflow: hidden; 178 | text-overflow: ellipsis; 179 | text-align: center; 180 | } 181 | 182 | .community-card img { 183 | width: 100px; 184 | height: 100px; 185 | border-radius: 50%; 186 | object-fit: cover; 187 | margin: 0 auto; 188 | display: block; 189 | } 190 | 191 | .community-card p { 192 | margin: 3px 0; 193 | font-size: 12px; 194 | overflow: hidden; 195 | text-overflow: ellipsis; 196 | display: -webkit-box; 197 | -webkit-line-clamp: 3; 198 | -webkit-box-orient: vertical; 199 | } 200 | 201 | 202 | 203 | 204 | /* ----------------------------------------- */ 205 | /* FOOTER */ 206 | /* ----------------------------------------- */ 207 | 208 | .footer { 209 | padding: 20px 40px; 210 | display: flex; 211 | align-items: center; 212 | } -------------------------------------------------------------------------------- /src/page-search/search.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Search for Communities 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 |
    20 | 21 |

    Search for Communities

    22 |
    23 | 31 |
    32 | 33 | 52 | 53 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/page-search/search.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', () => { 2 | 3 | // Function to fetch and search the 'community' JSON file 4 | async function searchCommunities(query) { 5 | try { 6 | const [response] = await Promise.all([fetch('https://data.lemmyverse.net/data/community.full.json')]); 7 | const data = await response.json(); 8 | 9 | // Perform the search 10 | const searchTerm = query.toLowerCase(); 11 | const searchResults = data.filter((community) => 12 | community.name.toLowerCase().includes(searchTerm) 13 | ); 14 | 15 | // Sort the results by most monthly active users 16 | searchResults.sort((a, b) => b.counts.users_active_month - a.counts.users_active_month); 17 | 18 | return searchResults; 19 | 20 | } catch (error) { 21 | console.error('Error fetching or processing data:', error); 22 | return []; 23 | } 24 | } 25 | 26 | // Function to display search results on the page 27 | async function displayResults(results) { 28 | 29 | // get home instance & type 30 | const selectedInstance = await getSetting('selectedInstance'); 31 | const selectedType = await getSetting('selectedType'); 32 | 33 | let newURL = ''; 34 | let hasHomeInstance = false; 35 | 36 | if (!(await hasSelectedInstance()) || !(await hasSelectedType())) { 37 | // no instance or type selected, create unique link on each card 38 | } else { 39 | hasHomeInstance = true; 40 | newURL = (selectedType == 'lemmy') ? selectedInstance + '/c/' : selectedInstance + '/m/'; 41 | } 42 | 43 | const resultsContainer = document.getElementById('results'); 44 | resultsContainer.innerHTML = ''; 45 | 46 | if (results.length === 0) { 47 | resultsContainer.innerHTML = '

    No matching communities found.

    '; 48 | } else { 49 | results.forEach((community) => { 50 | const communityCard = document.createElement('div'); 51 | communityCard.classList.add('community-card'); 52 | 53 | // If no home instance is selected, use the community's baseurl 54 | if (hasHomeInstance == false) { newURL = 'https://' + community.baseurl + '/c/'; } 55 | 56 | // Use placeholder icon if none is provided, or if not tagged sfw 57 | if (!community.icon) { community.icon = '../img/icon-lemm-noIcon.png'; } 58 | if (community.nsfw == true) { community.icon = '../img/icon-lemm-nsf.png'; } 59 | 60 | // Create the HTML content for each community card 61 | const communityHTML = ` 62 | Community Icon 63 |

    ${community.name}@${community.baseurl}

    64 |

    Title: ${community.title}

    65 |

    Description: ${community.desc}

    66 |

    Subscribers: ${community.counts.subscribers}

    67 |

    Posts: ${community.counts.posts}

    68 |

    Comments: ${community.counts.comments}

    69 | `; 70 | 71 | communityCard.innerHTML = communityHTML; 72 | resultsContainer.appendChild(communityCard); 73 | }); 74 | } 75 | } 76 | 77 | // Event listener for the search form submission 78 | document.getElementById('searchForm').addEventListener('submit', async (event) => { 79 | event.preventDefault(); 80 | const searchQuery = document.getElementById('searchQuery').value; 81 | const searchResults = await searchCommunities(searchQuery); 82 | displayResults(searchResults); 83 | }); 84 | 85 | 86 | 87 | 88 | 89 | // --------------------------------------------------------- 90 | // ------------------- Handle Buttons ---------------------- 91 | // --------------------------------------------------------- 92 | 93 | // Open settings page 94 | document.getElementById('open-settings').addEventListener('click', () => { 95 | browser.tabs.create({ url: '../page-settings/settings.html' }); 96 | }); 97 | 98 | 99 | 100 | 101 | 102 | // --------------------------------------------------------- 103 | // ---------- Handle Searches from URL Parameters ---------- 104 | // --------------------------------------------------------- 105 | 106 | // Function to extract the search query from URL parameters 107 | function getSearchQueryFromURL() { 108 | const queryString = window.location.search; 109 | const urlParams = new URLSearchParams(queryString); 110 | return urlParams.get('query') || ''; // Return the query parameter or an empty string if not found 111 | } 112 | 113 | // Perform a search on page load using the search query from URL parameters 114 | const searchQueryFromURL = getSearchQueryFromURL(); 115 | if (searchQueryFromURL) { 116 | document.getElementById('searchQuery').value = searchQueryFromURL; 117 | searchCommunities(searchQueryFromURL) 118 | .then((searchResults) => displayResults(searchResults)) 119 | .catch((error) => console.error('Error during initial search:', error)); 120 | } 121 | }); 122 | -------------------------------------------------------------------------------- /src/page-settings/settings.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-bg-1: #1f1f1f; 3 | --color-bg-2: #252525; 4 | --color-bg-3: #111111; 5 | --color-text: #e0e0e0; 6 | --color-btn: #363636; 7 | --color-btn-hover: #3e4347; 8 | --color-btn-active: #4b5156; 9 | --color-btn-redirect: #5f35ae; 10 | --color-btn-redirect-hover: #6a3ebc; 11 | --color-btn-redirect-active: #7546ca; 12 | --color-btn-tools: #054da7; 13 | --color-btn-tools-hover: #0e5abf; 14 | --color-btn-tools-active: #1668d7; 15 | --color-btn-change: #571a1a; 16 | --color-btn-change-hover: #6b1a1a; 17 | --color-btn-change-active: #7f1a1a; 18 | } 19 | 20 | body { 21 | padding: 0; 22 | margin: 0; 23 | color: var(--color-text) 24 | } 25 | 26 | h2 { 27 | margin: 30px 0px 10px 0px; 28 | } 29 | 30 | /* ----------------------------------------- */ 31 | /* HEADER */ 32 | /* ----------------------------------------- */ 33 | 34 | header { 35 | margin: 0; 36 | background-color: var(--color-bg-3); 37 | padding: 0 40px; 38 | display: flex; 39 | justify-content: space-between; 40 | align-items: center; 41 | } 42 | 43 | nav { 44 | margin: 10px 0; 45 | } 46 | 47 | .btn-settings-nav { 48 | display: inline-block; 49 | margin-right: 10px; 50 | padding: 8px 12px; 51 | background-color: var(--color-btn); 52 | text-decoration: none; 53 | border-radius: 5px; 54 | } 55 | 56 | .btn-settings-nav:hover { 57 | background-color: var(--color-btn-hover); 58 | } 59 | 60 | .btn-settings-nav:active { 61 | animation: buttonClick 0.05s; 62 | } 63 | 64 | .logo { 65 | height: 40px; 66 | margin: 10px; 67 | } 68 | 69 | /* ----------------------------------------- */ 70 | /* CONTENT */ 71 | /* ----------------------------------------- */ 72 | 73 | .settings { 74 | padding: 0 40px; 75 | } 76 | 77 | .checkbox { 78 | margin-bottom: 5px; 79 | display: flex; 80 | } 81 | 82 | label { 83 | display: block; 84 | margin-bottom: 5px; 85 | } 86 | 87 | .text-field { 88 | margin-bottom: 20px; 89 | } 90 | 91 | .text-field p { 92 | margin-bottom: 5px; 93 | } 94 | 95 | .text-field-input { 96 | 97 | width: 100%; 98 | } 99 | 100 | .text-field label { 101 | font-weight: bold; 102 | margin-right: 10px; 103 | } 104 | 105 | input[type="checkbox"], 106 | input[type="text"] { 107 | margin-left: 5px; 108 | } 109 | 110 | input[type="text"] { 111 | width: 30%; 112 | } 113 | 114 | .radio-field { 115 | margin-bottom: 10px; 116 | } 117 | 118 | .radio-field label { 119 | display: inline-block; 120 | margin-right: 10px; 121 | font-size: 14px; 122 | color: var(--color-text) 123 | } 124 | 125 | .radio-field input[type="radio"] { 126 | margin-right: 5px; 127 | } 128 | 129 | .checkbox label { 130 | display: inline-block; 131 | margin-right: 10px; 132 | font-size: 14px; 133 | color: var(--color-text) 134 | } 135 | 136 | .validation-error { 137 | border-color: red; 138 | } 139 | 140 | .validation-message { 141 | color: red; 142 | font-size: 12px; 143 | margin-top: 5px; 144 | } 145 | 146 | .welcome { 147 | margin-bottom: 20px; 148 | font-size: 16px; 149 | color: var(--color-text); 150 | text-align: center; 151 | background-color: var(--color-btn-active); 152 | } 153 | 154 | #toast-container { 155 | position: fixed; 156 | bottom: 20px; 157 | left: 50%; 158 | transform: translateX(-50%); 159 | z-index: 9999; 160 | } 161 | 162 | #toast-message { 163 | background-color: var(--color-btn-active); 164 | color: var(--color-text); 165 | padding: 10px 20px; 166 | border-radius: 4px; 167 | opacity: 0; 168 | transition: opacity 0.3s ease; 169 | } 170 | 171 | #toast-message.show { 172 | opacity: 1; 173 | } 174 | 175 | /* ----------------------------------------- */ 176 | /* FOOTER */ 177 | /* ----------------------------------------- */ 178 | 179 | .footer { 180 | padding: 20px 40px; 181 | display: flex; 182 | align-items: center; 183 | } 184 | 185 | #save-btn { 186 | padding: 8px 12px; 187 | color: #111111; 188 | background-color: #e0e0e0; 189 | border: none; 190 | border-radius: 5px; 191 | cursor: pointer; 192 | } 193 | 194 | #save-btn:hover { 195 | background-color: #fffbca; 196 | } 197 | 198 | #save-btn.selected { 199 | background-color: #ffdede; 200 | } 201 | 202 | #save-btn:active { 203 | animation: buttonClick 0.05s; 204 | } 205 | 206 | #reset-btn { 207 | padding: 8px 12px; 208 | margin-left: 30px; 209 | color: var(--color-text); 210 | background-color: var(--color-btn-change); 211 | border: none; 212 | border-radius: 5px; 213 | cursor: pointer; 214 | } 215 | 216 | #reset-btn:hover { 217 | background-color: var(--color-btn-change-hover); 218 | } 219 | 220 | #reset-btn.selected { 221 | background-color: var(--color-btn-change-active); 222 | } 223 | 224 | #reset-btn:active { 225 | animation: buttonClick 0.05s; 226 | } -------------------------------------------------------------------------------- /src/page-settings/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IA Settings 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
    17 |
    18 | 19 |

    Instance Assistant Settings

    20 |
    21 | 29 |
    30 | 31 |
    32 | 33 |

    Welcome! Please enter your instance information below.

    34 | 35 |

    Change Instance

    36 |
    37 |

    Home Instance URL:

    38 |
    39 | 40 |
    Please enter a valid URL (ex. 'https://lemmy.ca")
    41 |
    42 |

    Home Instance Type:

    43 |
    44 | 45 | 46 |
    47 |
    48 | 49 | 50 |
    51 | 52 |

    Change Display

    53 |
    54 |

    You can modify the list of instances in the popup menu by editing this field. This is helpful if you have 55 | accounts on more than one instance, or even if you like jumping between instances. Remove the instances that you 56 | don't need, and if you make a mistake then you can 'Reset to Default'.

    57 |

    Please list one instance per line, and use the format "button-text, url"

    58 | 59 | 60 |
    61 | 62 |

    Show/Hide sections of original site:

    63 |
    64 | 65 | 66 |
    67 | 71 | 72 | 73 |

    Enable/Disable Functionality

    74 |
    75 |
    76 | 77 | 78 |
    79 |
    80 | 81 | 82 |
    83 |
    84 | 85 | 86 |
    87 | 88 |

    Miscellaneous

    89 |
    90 |
    91 | 92 | 93 |
    94 |
    95 |
    96 |
    97 |
    98 | 99 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /src/page-settings/settings.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', async function () { 2 | 3 | // --------------------------------------------------------- 4 | // ------------------- Initialization ---------------------- 5 | // --------------------------------------------------------- 6 | 7 | // Initialize settings with default values, if any are missing 8 | await initializeSettingsWithDefaults(); 9 | 10 | // --------------------------------------------------------- 11 | // ------------------- Setup Display ----------------------- 12 | // --------------------------------------------------------- 13 | 14 | // Get DOM elements 15 | const saveButton = document.getElementById('save-btn'); 16 | const resetButton = document.getElementById('reset-btn'); 17 | const validationMessage = document.querySelector('.validation-message'); 18 | 19 | const instanceField = document.getElementById('instance-field'); 20 | const lemmyRadio = document.getElementById('radio-lemmy'); 21 | const kbinRadio = document.getElementById('radio-kbin'); 22 | const hideSidebarLemmyCheckbox = document.getElementById('hideSidebarLemmy'); 23 | // const hideSidebarKbinCheckbox = document.getElementById('hideSidebarKbin'); 24 | const showSidebarCheckbox = document.getElementById('showSidebarButtons'); 25 | const showCommunityNotFoundCheckbox = document.getElementById('showCommunityNotFound'); 26 | const hideHelpCheckbox = document.getElementById('hideHelp'); 27 | const searchOpenLemmyverseCheckbox = document.getElementById('searchOpenLemmyverse'); 28 | const instanceListTextArea = document.getElementById('instance-list'); 29 | 30 | // Function to set field values based on settings 31 | async function setFieldValues() { 32 | try { 33 | const allSettings = (await getAllSettings()).settings; // Get all settings 34 | 35 | instanceField.value = allSettings.selectedInstance || ''; 36 | lemmyRadio.checked = allSettings.selectedType === 'lemmy'; 37 | kbinRadio.checked = allSettings.selectedType === 'kbin'; 38 | hideSidebarLemmyCheckbox.checked = allSettings.hideSidebarLemmy; 39 | // hideSidebarKbinCheckbox.checked = allSettings.hideSidebarKbin; 40 | showSidebarCheckbox.checked = allSettings.runOnCommunitySidebar; 41 | showCommunityNotFoundCheckbox.checked = allSettings.runOnCommunityNotFound; 42 | hideHelpCheckbox.checked = allSettings.hideHelp; 43 | searchOpenLemmyverseCheckbox.checked = allSettings.toolSearchCommunity_openInLemmyverse; 44 | instanceListTextArea.value = allSettings.instanceList.map(item => `${item.name}, ${item.url}`).join('\n'); 45 | } catch (error) { 46 | console.error('Error retrieving settings:', error); 47 | } 48 | } 49 | 50 | setFieldValues(); 51 | 52 | // --------------------------------------------------------- 53 | // ------------------- Validation -------------------------- 54 | // --------------------------------------------------------- 55 | 56 | // Function to show validation error message 57 | const showValidationError = (message) => { 58 | validationMessage.textContent = message; 59 | validationMessage.style.display = 'block'; 60 | instanceField.classList.add('validation-error'); 61 | }; 62 | 63 | // Function to hide validation error message 64 | const hideValidationError = () => { 65 | validationMessage.style.display = 'none'; 66 | instanceField.classList.remove('validation-error'); 67 | }; 68 | 69 | hideValidationError(); 70 | 71 | // --------------------------------------------------------- 72 | // ------------------- Basic Functions --------------------- 73 | // --------------------------------------------------------- 74 | 75 | // Event handler for input event on selectedInstance text field 76 | instanceField.addEventListener('input', function () { 77 | if (saveClicked) { 78 | const instanceValue = this.value.trim(); 79 | if (!validInstanceURLPattern.test(instanceValue)) { 80 | showValidationError("Please enter a valid URL: (ex. 'https://lemmy.ca')"); 81 | } else { 82 | hideValidationError(); 83 | } 84 | } 85 | }); 86 | 87 | function showSaveConfirmation(text) { 88 | const toastMessage = document.getElementById('toast-message'); 89 | toastMessage.innerText = text; 90 | toastMessage.classList.add('show'); 91 | 92 | setTimeout(() => { 93 | toastMessage.classList.remove('show'); 94 | }, 3000); 95 | } 96 | 97 | // Save button click event handler 98 | let saveClicked = false; 99 | saveButton.addEventListener('click', async function () { 100 | saveClicked = true; 101 | const instanceValue = instanceField.value.trim(); 102 | const platformValue = lemmyRadio.checked ? "lemmy" : "kbin"; 103 | const hideSidebarLemmy = hideSidebarLemmyCheckbox.checked; 104 | // const hideSidebarKbin = hideSidebarKbinCheckbox.checked; 105 | const toggleShowSidebarButtons = showSidebarCheckbox.checked; 106 | const toggleShowCommunityNotFound = showCommunityNotFoundCheckbox.checked; 107 | const toggleHideHelp = hideHelpCheckbox.checked; 108 | const toggleSearchOpenLemmyverse = searchOpenLemmyverseCheckbox.checked; 109 | 110 | // Validation check 111 | if (!validInstanceURLPattern.test(instanceValue)) { 112 | showValidationError("Please enter a valid URL: (ex. 'https://lemmy.ca')"); 113 | showSaveConfirmation("Settings could not be saved, see errors for details."); 114 | return; 115 | } else { 116 | hideValidationError(); 117 | } 118 | 119 | const websiteListTextArea = document.getElementById('instance-list'); 120 | const websiteListText = websiteListTextArea.value.trim(); 121 | const websitesArray = websiteListText 122 | .split('\n') 123 | .map(line => line.trim()) 124 | .filter(line => line !== "") 125 | .map(line => { 126 | const [name, url] = line.split(',').map(item => item.trim()); 127 | return { name, url }; 128 | }); 129 | 130 | // Store values to local storage 131 | await setSetting('selectedInstance', instanceValue); 132 | await setSetting('selectedType', platformValue); 133 | await setSetting('hideSidebarLemmy', hideSidebarLemmy); 134 | // await setSetting('hideSidebarKbin', hideSidebarKbin); 135 | await setSetting('runOnCommunitySidebar', toggleShowSidebarButtons); 136 | await setSetting('runOnCommunityNotFound', toggleShowCommunityNotFound); 137 | await setSetting('hideHelp', toggleHideHelp); 138 | await setSetting('toolSearchCommunity_openInLemmyverse', toggleSearchOpenLemmyverse); 139 | await setSetting('instanceList', websitesArray); 140 | 141 | showSaveConfirmation("Settings saved successfully!"); 142 | 143 | }); 144 | 145 | // Reset button click event handler 146 | resetButton.addEventListener('click', async function () { 147 | const confirmation = confirm("Are you sure you want to reset all settings?"); 148 | 149 | if (confirmation) { 150 | await resetAllSettingsToDefault(); 151 | console.log("Settings reset to default."); 152 | 153 | setFieldValues(); 154 | 155 | hideValidationError(); 156 | showSaveConfirmation("Settings reset to default."); 157 | 158 | } else { console.log("Settings reset cancelled."); } 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /src/page-sidebar/sidebar.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-bg-1: #1f1f1f; 3 | --color-bg-2: #252525; 4 | --color-bg-3: #111111; 5 | --color-text: #e0e0e0; 6 | --color-btn: #363636; 7 | --color-btn-hover: #3e4347; 8 | --color-btn-active: #4b5156; 9 | --color-btn-redirect: #472783; 10 | --color-btn-redirect-hover: #5a3a9e; 11 | --color-btn-redirect-active: #6c4db7; 12 | --color-btn-tools: #2e515e; 13 | --color-btn-tools-hover: #3a6b7a; 14 | --color-btn-tools-active: #468494; 15 | --color-btn-change: #571a1a; 16 | --color-btn-change-hover: #6b1a1a; 17 | --color-btn-change-active: #7f1a1a; 18 | } 19 | 20 | body { 21 | min-width: 200px; 22 | min-height: 150px; 23 | } 24 | 25 | .subtle-text { 26 | font-size: 12px; 27 | color: var(--color-text); 28 | opacity: 0.8; 29 | margin: 3px 0px 1px 0px; 30 | } 31 | 32 | .no-instance-warning { 33 | font-size: 12px; 34 | color: red; 35 | padding: 5px; 36 | text-align: center; 37 | } 38 | 39 | .page-header { 40 | display: flex; 41 | margin: 0px 0px 10px 0px; 42 | justify-content: space-between; 43 | align-items: center; 44 | } 45 | 46 | .page-header p { 47 | margin: 0px 0px 0px 0px; 48 | padding: 0px 0px 0px 0px; 49 | } 50 | 51 | .page-section { 52 | background: var(--color-bg-2); 53 | padding: 10px 10px 10px 10px; 54 | margin: 10px 0px 10px 0px; 55 | border-radius: 5px; 56 | } 57 | 58 | .section-header-text { 59 | margin: 0px 0px 5px 0px; 60 | padding: 0px 0px 0px 0px; 61 | color: var(--color-text); 62 | } 63 | 64 | .icon-external { 65 | width: 10px; 66 | height: 10px; 67 | margin-left: 5px; 68 | } 69 | 70 | /* ----------------------------------------- */ 71 | /* GENERAL BUTTON STYLES */ 72 | /* ----------------------------------------- */ 73 | 74 | #btn-settings { 75 | width: auto; 76 | margin-left: 10px; 77 | align-items: right; 78 | } 79 | 80 | #btn-open-settings { 81 | margin: 0.25rem 0rem 0rem 0rem; 82 | background-color: var(--color-btn) 83 | } 84 | 85 | #btn-open-settings:hover { 86 | background-color: var(--color-btn-hover); 87 | } 88 | 89 | #btn-open-settings:active { 90 | animation: buttonClick 0.1s; 91 | } 92 | 93 | #btn-redirect-instance { 94 | padding: 0.7rem .75rem; 95 | margin: 0.2rem 0rem .2rem 0rem; 96 | background-color: var(--color-btn-redirect); 97 | } 98 | 99 | #btn-redirect-instance:hover { 100 | background-color: var(--color-btn-redirect-hover); 101 | } 102 | 103 | #btn-redirect-instance:active { 104 | animation: buttonClick 0.1s; 105 | } 106 | 107 | /* ----------------------------------------- */ 108 | /* TOOLS */ 109 | /* ----------------------------------------- */ 110 | 111 | .page-tools { 112 | margin: 0.25rem 0rem 0rem 0rem; 113 | background-color: var(--color-btn-tools); 114 | } 115 | 116 | .page-tools:hover { 117 | background-color: var(--color-btn-tools-hover); 118 | } 119 | 120 | .page-tools:active { 121 | animation: buttonClick 0.1s; 122 | } 123 | 124 | /* Flex container for the search bar and button */ 125 | .search-bar-communities, 126 | .search-bar-content { 127 | display: flex; 128 | justify-content: space-between; 129 | align-items: baseline; 130 | padding: 5px 5px 5px 5px; 131 | } 132 | 133 | /* Search bar input */ 134 | #searchInputCommunities, 135 | #searchInputContent { 136 | flex: 3; 137 | margin-right: 5px; 138 | } 139 | 140 | /* Search button */ 141 | #btn-tool-search-community, 142 | #btn-tool-search-content { 143 | flex: 1; 144 | margin-left: 5px; 145 | } 146 | 147 | /* ----------------------------------------- */ 148 | /* SETTINGS */ 149 | /* ----------------------------------------- */ 150 | 151 | #btn-change-instance { 152 | margin: 0.25rem 0rem 0rem 0rem; 153 | background-color: var(--color-btn-change); 154 | } 155 | 156 | #btn-change-instance:hover { 157 | background-color: var(--color-btn-change-hover); 158 | } 159 | 160 | #btn-change-instance:active { 161 | animation: buttonClick 0.1s; 162 | } 163 | 164 | #btn-change-type { 165 | margin: 0.25rem 0rem 0rem 0rem; 166 | background-color: var(--color-btn) 167 | } 168 | 169 | #btn-change-type:hover { 170 | background-color: var(--color-btn-hover); 171 | } 172 | 173 | #btn-change-type:active { 174 | animation: buttonClick 0.1s; 175 | } 176 | 177 | ul#instance-list { 178 | margin-bottom: 0px; 179 | list-style-type: none; 180 | padding-left: 0; 181 | } 182 | 183 | .btn-instance-list { 184 | background-color: var(--color-btn); 185 | color: var(--color-text); 186 | padding: .375rem .75rem; 187 | padding-left: 36px; 188 | margin: 5px; 189 | width: 95%; 190 | border: 2px; 191 | border-radius: 5px; 192 | cursor: pointer; 193 | text-align: left; 194 | transition: background-color 0.2s; 195 | background-image: url("../img/icon-copy.png"); 196 | background-repeat: no-repeat; 197 | background-position: left center; 198 | background-size: 24px 16px; 199 | } 200 | 201 | .btn-instance-list:hover { 202 | background-color: var(--color-btn-hover); 203 | } 204 | 205 | .btn-instance-list:active { 206 | animation: buttonClick 0.1s; 207 | } -------------------------------------------------------------------------------- /src/page-sidebar/sidebar.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | Instance Selector 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 27 | 28 |
    29 | 30 |
    31 | 32 | 33 | 34 |
    35 |
    36 |

    Posts related to this webpage

    37 |
    38 |
    39 | 42 | 45 |
    46 |

    'Open posts' will pull from your home instance (potential risks)

    49 |
    50 | 51 |
    52 |
    53 | 54 | 57 |
    58 |
    59 | 60 | 63 |
    64 |
    65 | 66 |
    67 |
    68 |

    Quick Settings

    69 |
    70 | 71 | 75 |
      76 |

      You can modify this list in SETTINGS

      77 |
      78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /src/page-sidebar/sidebar.js: -------------------------------------------------------------------------------- 1 | // THIS PAGE SHOULD BE IDENTICAL TO popup.js 2 | 3 | document.addEventListener("DOMContentLoaded", function () { 4 | async function createPage() { 5 | 6 | const instanceList = document.getElementById("instance-list"), 7 | btnChangeInstance = document.getElementById("btn-change-instance"), 8 | btnChangeType = document.getElementById("btn-change-type"), 9 | btnRedirect = document.getElementById("btn-redirect-instance"), 10 | btnOpenSettings = document.getElementById("btn-open-settings"), 11 | txtHomeInstance = document.getElementById("homeInstance"), 12 | txtInstanceType = document.getElementById("instance-type"); 13 | txtInstanceWarn = document.getElementById("no-instance-warning"); 14 | 15 | // --------------------------------------------------------- 16 | // ------------------- Setup Display ----------------------- 17 | // --------------------------------------------------------- 18 | 19 | const selectedInstance = await getSetting("selectedInstance"); 20 | const selectedType = await getSetting("selectedType"); 21 | 22 | txtHomeInstance.textContent = selectedInstance ? selectedInstance : "unknown"; 23 | txtInstanceType.textContent = selectedType ? selectedType : "unknown"; 24 | txtInstanceWarn.textContent = selectedInstance ? "" : "WARN - Instance Not Selected: Some features will not work as expected. Please click 'Change my home instance'."; 25 | 26 | let lemmyInstances = await getSetting("instanceList"); 27 | 28 | lemmyInstances.forEach((instance) => { 29 | const listItem = document.createElement("li"); 30 | const button = document.createElement("button"); 31 | button.type = "button"; 32 | button.textContent = instance.name; 33 | button.className = "btn-instance-list"; 34 | listItem.appendChild(button); 35 | instanceList.appendChild(listItem); 36 | }); 37 | 38 | // --------------------------------------------------------- 39 | // ------------------- Basic Functions --------------------- 40 | // --------------------------------------------------------- 41 | 42 | // Open settings page 43 | btnOpenSettings.addEventListener("click", (event) => { 44 | doOpenSettings(); 45 | }); 46 | 47 | // --------------------------------------------------------- 48 | // --------------- Quick Settings Functions ---------------- 49 | // --------------------------------------------------------- 50 | 51 | // Update home instance address 52 | btnChangeInstance.addEventListener("click", async () => { 53 | const inputInstance = prompt("Enter your instance URL: (ex. 'https://lemmy.ca')"); 54 | if (inputInstance === null) { return; } // exit without alerting if user cancels 55 | if (inputInstance && validInstanceURLPattern.test(inputInstance)) { 56 | await setSetting("selectedInstance", inputInstance.trim()); 57 | txtHomeInstance.textContent = inputInstance.trim(); 58 | } else { alert("Invalid URL format, please follow this format: \n 'https://lemmy.ca'"); } 59 | window.location.reload(); 60 | }); 61 | 62 | // Toggle home instance type 63 | btnChangeType.addEventListener("click", async () => { 64 | const currentType = await getSetting("selectedType"); 65 | const newType = currentType === "lemmy" ? "kbin" : "lemmy"; 66 | await setSetting("selectedType", newType); 67 | txtInstanceType.textContent = newType; 68 | }); 69 | 70 | // Copy URL 71 | instanceList.addEventListener("click", (event) => { 72 | const target = event.target; 73 | if (target.classList.contains("btn-instance-list")) { 74 | const url = lemmyInstances.find((instance) => instance.name === target.textContent).url; 75 | navigator.clipboard.writeText(url); 76 | } 77 | }); 78 | 79 | // --------------------------------------------- // 80 | // ---------------- Posting Tools -------------- // 81 | // --------------------------------------------- // 82 | 83 | const btn_post_to = document.getElementById("btn-tool-post-to"); 84 | const btnOpenPosts = document.getElementById("btn-tool-open-posts"); 85 | 86 | // Post to Community 87 | btn_post_to.addEventListener("click", async () => { 88 | doCreatePost(); 89 | }); 90 | 91 | // Open posts from home instance 92 | btnOpenPosts.addEventListener("click", async () => { 93 | const queryOptions = { active: true, currentWindow: true }; 94 | const [tab] = await browser.tabs.query(queryOptions); 95 | const testURL = tab.url; 96 | 97 | doOpenMatchingPostsLemmy(testURL); 98 | }); 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | // --------------------------------------------- // 108 | // ---------------- Search Tools --------------- // 109 | // --------------------------------------------- // 110 | 111 | // Search Lemmyverse 112 | const btnSearchCommunities = document.getElementById("btn-tool-search-community"); 113 | const searchInputCommunities = document.getElementById("searchInputCommunities"); 114 | 115 | async function performSearchCommunities() { 116 | await toolSearchCommunitiesLemmyverse(searchInputCommunities.value.trim()) 117 | } 118 | 119 | btnSearchCommunities.addEventListener("click", performSearchCommunities); 120 | 121 | searchInputCommunities.addEventListener("keydown", (event) => { 122 | if (event.key === "Enter") { 123 | event.preventDefault(); // Prevent default form submission behavior 124 | performSearchCommunities(); 125 | } 126 | }); 127 | 128 | // Search content with Lemmy-Search 129 | const btnSearchContent = document.getElementById("btn-tool-search-content"); 130 | const searchInputContent = document.getElementById("searchInputContent"); 131 | 132 | function performSearchCommunitiesContent() { 133 | toolSearchContentLemmysearch(searchInputContent.value.trim()); 134 | } 135 | 136 | btnSearchContent.addEventListener("click", performSearchCommunitiesContent); 137 | 138 | searchInputContent.addEventListener("keydown", (event) => { 139 | if (event.key === "Enter") { 140 | event.preventDefault(); // Prevent default form submission behavior 141 | performSearchCommunitiesContent(); 142 | } 143 | }); 144 | 145 | // --------------------------------------------- // 146 | // -------------- Redirect Instance ------------ // 147 | // --------------------------------------------- // 148 | 149 | // Redirect to selected instance 150 | btnRedirect.addEventListener('click', async () => { 151 | const queryOptions = { active: true, currentWindow: true }; 152 | const [tab] = await browser.tabs.query(queryOptions); 153 | 154 | tabURL = tab.url; 155 | 156 | if ((await hasSelectedInstance())) { 157 | if (isLemmyCommunityWEAK(tabURL) || isKbinCommunityWEAK(tabURL)) { 158 | if (!(await isHomeInstance(tabURL))) { 159 | 160 | const redirectURL = await getCommunityRedirectURL(tabURL); 161 | await browser.tabs.update(tab.id, { url: redirectURL }); 162 | 163 | } else { alert('You are already on your home instance.'); } 164 | } else { alert('You are not on a Lemmy or Kbin community. Please navigate to a community page and try again.\n\nThe extension checks for links that have "/c/" or "/m/" in the URL'); } 165 | } else { alert('No valid instance has been set. Please select an instance in the popup using "Change my home instance".'); } 166 | }); 167 | } 168 | createPage(); 169 | }); 170 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-bg-1: #1f1f1f; 3 | --color-bg-2: #252525; 4 | --color-bg-3: #111111; 5 | --color-text: #e0e0e0; 6 | --color-btn: #363636; 7 | --color-btn-hover: #3e4347; 8 | --color-btn-active: #4b5156; 9 | --color-btn-redirect: #5f35ae; 10 | --color-btn-redirect-hover: #6a3ebc; 11 | --color-btn-redirect-active: #7546ca; 12 | --color-btn-tools: #054da7; 13 | --color-btn-tools-hover: #0e5abf; 14 | --color-btn-tools-active: #1668d7; 15 | --color-btn-change: #571a1a; 16 | --color-btn-change-hover: #6b1a1a; 17 | --color-btn-change-active: #7f1a1a; 18 | } 19 | 20 | body { 21 | background: var(--color-bg-1); 22 | padding: 10px; 23 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif, sans-serif; 24 | } 25 | 26 | /* --------------------- */ 27 | /* TEXT & TYPOGRAPHY */ 28 | /* --------------------- */ 29 | 30 | p, 31 | a { 32 | color: var(--color-text); 33 | font-size: 14px; 34 | } 35 | 36 | /* --------------------- */ 37 | /* BUTTONS */ 38 | /* --------------------- */ 39 | 40 | .button { 41 | color: var(--color-text); 42 | padding: .375rem .75rem; 43 | margin: 0rem 0rem 0rem 0rem; 44 | width: 100%; 45 | border: none; 46 | border-radius: 5px; 47 | cursor: pointer; 48 | font-weight: 400; 49 | text-align: center; 50 | text-decoration: none; 51 | background-color: var(--color-btn); 52 | } 53 | 54 | /* --------------------- */ 55 | /* OTHER */ 56 | /* --------------------- */ 57 | 58 | .message { 59 | background: var(--color-btn-active); 60 | padding: 10px; 61 | border-radius: 5px; 62 | margin-bottom: 10px; 63 | } 64 | 65 | /* --------------------- */ 66 | /* ICONS */ 67 | /* --------------------- */ 68 | 69 | .info-icon { 70 | display: inline-block; 71 | margin-left: 5px; 72 | font-size: 14px; 73 | cursor: help; 74 | } 75 | 76 | .icon-external { 77 | width: 10px; 78 | height: 10px; 79 | margin-left: 5px; 80 | } 81 | 82 | .icon-home { 83 | width: 10px; 84 | height: 10px; 85 | margin-left: 5px; 86 | } 87 | 88 | /* --------------------- */ 89 | /* ANIMS */ 90 | /* --------------------- */ 91 | 92 | @keyframes buttonClick { 93 | 0% { 94 | transform: scale(1); 95 | box-shadow: none; 96 | } 97 | 98 | 50% { 99 | transform: scale(0.95); 100 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); 101 | } 102 | 103 | 100% { 104 | transform: scale(1); 105 | box-shadow: none; 106 | } 107 | } -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | function testFunction() { 2 | console.log("This is a test function."); 3 | } 4 | 5 | let validInstanceURLPattern = /^(http|https):\/\/(?:[\w-]+\.)?[\w.-]+\.[a-zA-Z]{2,}$/; 6 | 7 | // Utility function to get the appropriate storage API based on the browser 8 | function getStorageAPI() { 9 | let storageAPI; 10 | if (typeof browser !== 'undefined' && browser.storage && browser.storage.local) { 11 | storageAPI = browser.storage.local; 12 | } else if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) { 13 | storageAPI = chrome.storage.local; 14 | } else { 15 | throw new Error('Storage API is not supported in this browser.'); 16 | } 17 | 18 | return storageAPI; 19 | } 20 | 21 | function getBrowserAPI() { 22 | let browserAPI; 23 | if (typeof browser !== 'undefined' && browser.storage && browser.storage.local) { 24 | browserAPI = browser; 25 | } else if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) { 26 | browserAPI = chrome; 27 | } else { 28 | throw new Error('Browser API is not supported in this browser.'); 29 | } 30 | return browserAPI; 31 | } 32 | 33 | function doOpenSettings() { 34 | const browserAPI = getBrowserAPI(); 35 | browserAPI.tabs.create({ url: '../page-settings/settings.html' }); 36 | } 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | // ---------------------------------------------- 47 | // SETTINGS FUNCTIONS 48 | // ---------------------------------------------- 49 | 50 | // Get all settings 51 | // - returns { settings: { ... } } 52 | async function getAllSettings() { 53 | const storageAPI = getStorageAPI(); 54 | const allSettings = await storageAPI.get('settings'); 55 | if (!allSettings || !allSettings.settings) { 56 | await setAllSettings(defaultSettings); 57 | return defaultSettings; 58 | } 59 | return await storageAPI.get('settings'); 60 | } 61 | 62 | // Get a single setting 63 | // - returns 64 | async function getSetting(settingName) { 65 | const allSettings = await getAllSettings(); 66 | return allSettings.settings[settingName]; 67 | } 68 | 69 | // Set all settings 70 | // - accepts 71 | async function setAllSettings(settingsObj) { 72 | const storageAPI = getStorageAPI(); 73 | await storageAPI.set({ 'settings': settingsObj }); 74 | } 75 | 76 | // Set a single setting 77 | // - accepts 78 | async function setSetting(settingName, settingValue) { 79 | const allSettings = await getAllSettings(); 80 | allSettings.settings[settingName] = settingValue; 81 | await setAllSettings(allSettings.settings); 82 | } 83 | 84 | // Update multiple settings 85 | // - Usage: 86 | // const settingsToUpdate = { 87 | // theme: 'dark', 88 | // runOnCommunitySidebar: false, 89 | // toolSearchCommunity_openInLemmyverse: false, 90 | // }; 91 | // await updateSettings(settingsToUpdate); 92 | async function updateSettings(settingsToUpdate) { 93 | const allSettings = await getAllSettings(); 94 | const updatedSettings = { ...allSettings.settings, ...settingsToUpdate }; 95 | await setAllSettings(updatedSettings); 96 | } 97 | 98 | const defaultSettings = { 99 | hideSidebarLemmy: false, 100 | hideSidebarKbin: false, 101 | instanceList: [ 102 | { name: "lemmy.world", url: "https://lemmy.world" }, 103 | { name: "lemmy.ca", url: "https://lemmy.ca" }, 104 | { name: "lemm.ee", url: "https://lemm.ee" }, 105 | { name: "kbin.social", url: "https://kbin.social" }, 106 | ], 107 | runOnCommunitySidebar: true, 108 | runOnCommunityNotFound: true, 109 | hideHelp: false, 110 | selectedInstance: '', // users are forced to set this 111 | selectedType: 'lemmy', // lemmy or kbin 112 | theme: 'dark', // **NOT IMPLEMENTED YET** 113 | toolSearchCommunity_openInLemmyverse: false, 114 | }; 115 | 116 | // Reset all settings to the default values 117 | async function resetAllSettingsToDefault() { 118 | await setAllSettings(defaultSettings); 119 | } 120 | 121 | // Reset a single setting to the default value 122 | // - Usage: 123 | // await resetSettingToDefault('theme'); 124 | async function resetSettingToDefault(settingName) { 125 | const allSettings = await getAllSettings(); 126 | if (defaultSettings.hasOwnProperty(settingName)) { 127 | allSettings.settings[settingName] = defaultSettings[settingName]; 128 | await setAllSettings(allSettings.settings); 129 | } else { 130 | throw new Error(`Setting "${settingName}" does not exist in the default settings.`); 131 | } 132 | } 133 | 134 | // Initialize settings with default values 135 | // - This is called when the extension is first installed or updated 136 | // - It checks for missing settings and adds them 137 | async function initializeSettingsWithDefaults() { 138 | const allSettings = await getAllSettings(); 139 | if (!allSettings.hasOwnProperty('settings')) { 140 | await setAllSettings(defaultSettings); 141 | } else { 142 | // Check for missing settings and update them 143 | for (const settingName of Object.keys(defaultSettings)) { 144 | if (!allSettings.settings.hasOwnProperty(settingName)) { 145 | allSettings.settings[settingName] = defaultSettings[settingName]; 146 | } 147 | } 148 | await setAllSettings(allSettings.settings); 149 | } 150 | } 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | // ---------------------------------------------- 162 | // Determine type of page 163 | // ---------------------------------------------- 164 | 165 | function isLoggedInLemmy() { 166 | const loginLink = document.querySelector('a[href="/login"]'); 167 | const signupLink = document.querySelector('a[href="/signup"]'); 168 | return !(loginLink && signupLink); 169 | } 170 | 171 | function isLoggedInKbin() { 172 | const loginLink = document.querySelectorAll('a.login[href="/login"]'); 173 | return !loginLink; 174 | } 175 | 176 | function isLemmySite() { 177 | const metaTag = document.querySelector('meta[name="Description"]'); 178 | if (metaTag) { 179 | return metaTag.content === "Lemmy"; 180 | } else { 181 | return false; 182 | } 183 | } 184 | 185 | function isLemmyCommunityList(sourceURL) { 186 | const CURRENT_PATH = new URL(sourceURL).pathname; 187 | return (isLemmySite() && CURRENT_PATH === "/communities") 188 | } 189 | 190 | function isLemmyCommunity(sourceURL) { 191 | const CURRENT_PATH = new URL(sourceURL).pathname; 192 | return (isLemmySite() && CURRENT_PATH.includes("/c/")) 193 | } 194 | 195 | function isLemmyCommunityWEAK(sourceURL) { 196 | // For when you can't check the meta tag, like when only the URL is available 197 | const CURRENT_PATH = new URL(sourceURL).pathname; 198 | return (CURRENT_PATH.includes("/c/")) 199 | } 200 | 201 | function isLemmyPost(sourceURL) { 202 | const CURRENT_PATH = new URL(sourceURL).pathname; 203 | return (isLemmySite() && (CURRENT_PATH.includes("/post/"))) 204 | } 205 | 206 | function isLemmyUser(sourceURL) { 207 | const CURRENT_PATH = new URL(sourceURL).pathname; 208 | return (isLemmySite() && (CURRENT_PATH.includes("/u/"))) 209 | } 210 | 211 | function isLemmyLoadOtherInstance(sourceURL) { 212 | const CURRENT_PATH = new URL(sourceURL).pathname; 213 | return (isLemmyCommunity(sourceURL) && CURRENT_PATH.includes("@")) 214 | } 215 | 216 | function isLemmyCommunityNotFound(sourceURL) { 217 | const hasErrorContainer = document.querySelector('.error-page'); 218 | return (isLemmyLoadOtherInstance(sourceURL) && hasErrorContainer) 219 | } 220 | 221 | function isKbinSite() { 222 | // TODO: Add a check for the meta tag 223 | return true; 224 | } 225 | 226 | function isKbinCommunity(sourceURL) { 227 | const CURRENT_PATH = new URL(sourceURL).pathname; 228 | return (isKbinSite() && CURRENT_PATH.includes("/m/")) 229 | } 230 | 231 | function isKbinCommunityWEAK(sourceURL) { 232 | // For when you can't check the meta tag, like when only the URL is available 233 | const CURRENT_PATH = new URL(sourceURL).pathname; 234 | return (CURRENT_PATH.includes("/m/")) 235 | } 236 | 237 | function isKbinPost(sourceURL) { 238 | const CURRENT_PATH = new URL(sourceURL).pathname; 239 | return (isKbinSite() && (CURRENT_PATH.includes("/t/"))) 240 | } 241 | 242 | // -------------- Other Frontends --------------- 243 | 244 | function isLemmyPhoton() { 245 | // look for meta tag name="description", and see if it contains "Photon" 246 | const metaTag = document.querySelector('meta[name="description"]'); 247 | if (metaTag) { 248 | return metaTag.content.includes("Photon: An alternative lemmy client with a sleek design."); 249 | } else { 250 | return false; 251 | } 252 | } 253 | 254 | function isLemmyAlexandrite() { 255 | return !!document.querySelector('div.sx-stack.f-row.gap-1.align-items-center.mx-4.sx-badge-gray.sx-font-size-2 a[href="https://github.com/sheodox/alexandrite"]'); 256 | } 257 | 258 | function isLemmyPhotonPost(sourceURL) { 259 | const CURRENT_PATH = new URL(sourceURL).pathname; 260 | return (isLemmyPhoton() && CURRENT_PATH.includes("/post/")) 261 | } 262 | 263 | function isLemmyAlexandritePost(sourceURL) { 264 | const CURRENT_PATH = new URL(sourceURL).pathname; 265 | return (isLemmyAlexandrite() && CURRENT_PATH.includes("/post/")) 266 | } 267 | 268 | function mayBeFrontend(testURL) { 269 | let testURLHost = testURL; 270 | 271 | // Check if the input is a valid URL 272 | try { 273 | const urlObj = new URL(testURL); 274 | testURLHost = urlObj.hostname; 275 | } catch (error) { 276 | // Input is not a valid URL, use it as is 277 | } 278 | 279 | const hostParts = testURLHost.split('.'); 280 | return hostParts.length > 2; // tests for number of parts in the hostname 281 | } 282 | 283 | function getRealHostname(testURL) { 284 | let testURLHost = testURL; 285 | 286 | // Check if the input is a valid URL 287 | try { 288 | const urlObj = new URL(testURL); 289 | testURLHost = urlObj.hostname; 290 | } catch (error) { 291 | // Input is not a valid URL, use it as is 292 | } 293 | // check if testURLHost contains > 1 dot 294 | 295 | if (testURLHost.split('.').length > 2) { 296 | const hostParts = testURLHost.split('.'); 297 | return hostParts.slice(1).join('.'); 298 | } else { 299 | return testURLHost; 300 | } 301 | } 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | // ---------------------------------------------- 313 | // SELECTED INSTANCE FUNCTIONS 314 | // ---------------------------------------------- 315 | 316 | async function hasSelectedInstance() { 317 | const selectedInstance = await getSetting('selectedInstance'); 318 | return selectedInstance !== undefined && selectedInstance !== ""; 319 | } 320 | 321 | async function hasSelectedType() { 322 | const selectedType = await getSetting('selectedType'); 323 | return selectedType !== undefined && selectedType !== ""; 324 | } 325 | 326 | async function isHomeInstance(testURL) { 327 | if (!(await hasSelectedInstance())) { 328 | console.log("No selected instance"); 329 | return false; 330 | } else { 331 | console.log("Has selected instance"); 332 | const selectedInstance = await getSetting('selectedInstance'); 333 | const testURLHost = new URL(testURL).hostname; 334 | const selectedInstanceHost = new URL(selectedInstance).hostname; 335 | return testURLHost.endsWith(selectedInstanceHost); 336 | } 337 | } 338 | 339 | async function getCommunityRedirectURL(oldURL) { 340 | const selectedInstance = await getSetting('selectedInstance'); 341 | const selectedType = await getSetting('selectedType'); 342 | 343 | const communityPrefix = selectedType ? (selectedType === "lemmy" ? "/c/" : "/m/") : "/c/"; 344 | 345 | const oldHost = new URL(oldURL).hostname; 346 | const oldPath = new URL(oldURL).pathname; 347 | 348 | const communityName = oldPath.match(/\/[cm]\/([^/@]+)/)[1]; 349 | const oldInstance = oldPath.includes("@") ? 350 | oldPath.match(/\/[cm]\/[^/@]+@([^/]+)/)[1] : oldHost; 351 | 352 | const newURL = selectedInstance + communityPrefix + communityName + '@' + oldInstance; 353 | 354 | return newURL; 355 | } 356 | 357 | async function getUserRedirectURL(oldURL) { 358 | const selectedInstance = await getSetting('selectedInstance'); 359 | const selectedType = await getSetting('selectedType'); 360 | 361 | const oldHost = new URL(oldURL).hostname; 362 | const oldPath = new URL(oldURL).pathname; 363 | 364 | console.log("oldURL:", oldURL, "oldHost:", oldHost, "oldPath:", oldPath); 365 | 366 | const userName = oldPath.match(/\/u\/([^/@]+)/)[1]; 367 | const userInstance = oldPath.includes("@") ? 368 | oldPath.match(/\/u\/[^/@]+@([^/]+)/)[1] : oldHost; 369 | 370 | const newURL = selectedInstance + "/u/" + userName + '@' + userInstance; 371 | 372 | console.log("newURL:", newURL); 373 | 374 | return newURL; 375 | } 376 | 377 | 378 | async function getPostRedirectURL(oldURL) { 379 | return oldURL; 380 | } 381 | 382 | async function toggleInstanceType() { 383 | const currentType = await getSetting("selectedType"); 384 | const newType = currentType === "lemmy" ? "kbin" : "lemmy"; 385 | await setSetting("selectedType", newType); 386 | } 387 | 388 | // INPUT: instance URL (string), post ID (string) 389 | // OUTPUT: post object (JSON) 390 | async function fetchPostFromID(instance, postID) { 391 | console.log("fetch request:", instance + '/api/v3/post?id=' + postID) 392 | const options = { method: 'GET', headers: { accept: 'application/json' } }; 393 | try { 394 | const response = await fetch(instance + '/api/v3/post?id=' + postID, options); 395 | const apiResponse = await response.json(); 396 | return apiResponse.post_view; 397 | } catch (err) { 398 | console.error(err); 399 | } 400 | } 401 | 402 | // INPUT: instance URL (string), post title (string) 403 | // OUTPUT: post search results (JSON) 404 | async function fetchPostsFromTitle(instance, postTitle) { 405 | console.log("fetch request:", instance + '/api/v3/search?q=' + encodeURIComponent(postTitle) + '&type_=Posts'); 406 | const options = { method: 'GET', headers: { accept: 'application/json' } }; 407 | try { 408 | console.log("fetch request:", instance + '/api/v3/search?q=' + encodeURIComponent(postTitle) + '&type_=Posts'); 409 | const response = await fetch(instance + '/api/v3/search?q=' + encodeURIComponent(postTitle) + '&type_=Posts', options); 410 | const apiResponse = await response.json(); 411 | return apiResponse.posts; 412 | } catch (err) { 413 | console.error(err); 414 | } 415 | } 416 | 417 | // INPUT: post object (JSON), post search results (JSON) 418 | // OUTPUT: filtered post search results (JSON) 419 | async function filterPostsByPost(testPost, inputPosts) { 420 | const og_communityName = testPost.community.name; 421 | const og_creatorName = testPost.creator.name; 422 | const og_title = testPost.post.name; 423 | const og_url = testPost.post.url; 424 | 425 | let filteredPosts = inputPosts.filter(post => 426 | post.community.name === og_communityName && 427 | post.creator.name === og_creatorName && 428 | post.post.name === og_title 429 | ); 430 | 431 | return filteredPosts; 432 | } 433 | 434 | async function openPostFromID(instance, postID, community) { 435 | const type = await getSetting("selectedType"); 436 | 437 | (type === "lemmy") ? 438 | window.location.href = instance + '/post/' + postID : 439 | window.location.href = instance + '/m/' + community + '/t/' + postID; 440 | } 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | // ---------------------------------------------- 454 | // ------------- External Tool ---------------- 455 | // ---------------------------------------------- 456 | 457 | // External Tool: Search Community through Lemmyverse 458 | async function toolSearchCommunitiesLemmyverse(searchTerm) { 459 | const browserAPI = getBrowserAPI(); 460 | if (searchTerm !== "") { 461 | if (await getSetting('toolSearchCommunity_openInLemmyverse')) { 462 | const baseUrl = "https://lemmyverse.net/communities"; 463 | const encodedSearchTerm = encodeURIComponent(searchTerm); 464 | browserAPI.tabs.create({ url: `${baseUrl}?query=${encodedSearchTerm}` }); 465 | } else { 466 | browserAPI.tabs.create({ url: `../page-search/search.html?query=${encodeURIComponent(searchTerm)}` }); 467 | } 468 | } else { 469 | console.log("CommunitySearch: Search term is empty"); 470 | } 471 | } 472 | 473 | // External Tool: Search Content through Lemmysearch 474 | function toolSearchContentLemmysearch(searchTerm) { 475 | const browserAPI = getBrowserAPI(); 476 | if (searchTerm !== "") { 477 | const baseUrl = "https://www.search-lemmy.com/results"; 478 | const encodedSearchTerm = encodeURIComponent(searchTerm); 479 | const finalUrl = `${baseUrl}?query=${encodedSearchTerm}`; 480 | browserAPI.tabs.create({ url: finalUrl }); 481 | } 482 | } 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | // ---------------------------------------------- 494 | // ------------- Posting Tools ---------------- 495 | // ---------------------------------------------- 496 | 497 | // Helper functions to post a webpage to a community 498 | 499 | // Get the post data from the current tab 500 | // - returns { title: "title", url: "url" } 501 | async function p2l_getPostData() { 502 | const browserAPI = getBrowserAPI(); 503 | const tabs = await browserAPI.tabs.query({ active: true, currentWindow: true }); 504 | const activeTab = tabs[0]; 505 | const postData = { 506 | title: activeTab.title, 507 | url: activeTab.url 508 | }; 509 | return postData; 510 | } 511 | 512 | // when on a webpage, open the matching posts in a new tab 513 | async function doOpenMatchingPostsLemmy(testURL) { 514 | const browserAPI = getBrowserAPI(); 515 | const selectedType = await getSetting('selectedType'); 516 | 517 | if (await hasSelectedInstance() && await hasSelectedType()) { 518 | if (selectedType === "lemmy") { 519 | const selectedInstance = await getSetting('selectedInstance'); 520 | 521 | const searchURL = selectedInstance + "/api/v3/search?q=" + testURL; 522 | const [lemmyPostResponse_URL, lemmyPostResponse_BODY] = await Promise.all([ 523 | fetch(searchURL + "&type_=Url"), 524 | fetch(searchURL + "&type_=All") 525 | ]); 526 | lemmyPostData = { 527 | posts: [ 528 | ...(await lemmyPostResponse_URL.json()).posts, 529 | ...(await lemmyPostResponse_BODY.json()).posts 530 | ] 531 | }; 532 | if (lemmyPostData.posts.length <= 0) { 533 | alert("No posts found for this URL."); 534 | } else if (lemmyPostData.posts.length === 1) { 535 | lemmyPostData.posts.forEach(post => { 536 | const post_id = post.counts.post_id; 537 | console.log("Post ID:", post_id); 538 | browserAPI.tabs.create({ url: selectedInstance + "/post/" + post_id }); 539 | }); 540 | } else if (lemmyPostData.posts.length > 1) { 541 | // tell user how many posts there are and ask if it's ok to open them 542 | const confirmOpen = confirm("There are " + lemmyPostData.posts.length + " posts for this URL. Open them all?"); 543 | if (confirmOpen) { 544 | lemmyPostData.posts.forEach(post => { 545 | const post_id = post.counts.post_id; 546 | console.log("Post ID:", post_id); 547 | browserAPI.tabs.create({ url: selectedInstance + "/post/" + post_id }); 548 | }); 549 | } 550 | } 551 | } else if (selectedType === "kbin") { 552 | alert("This feature is not yet available for Kbin instances."); 553 | } 554 | } else { alert("No valid instance has been set. Please select an instance in the popup using 'Change my home instance'."); } 555 | } 556 | 557 | async function doOpenMatchingPostsKbin(testURL) { 558 | // TODO: Implement this 559 | alert("This feature is not yet available for Kbin instances."); 560 | } 561 | 562 | // When on a webpage, post it to a community 563 | async function doCreatePost() { 564 | const browserAPI = getBrowserAPI(); 565 | if (await hasSelectedInstance() && await hasSelectedType()) { 566 | const postData = await p2l_getPostData(); 567 | 568 | const instance = await getSetting("selectedInstance"); 569 | const type = await getSetting("selectedType"); 570 | 571 | if (type === "lemmy") { 572 | const url = instance + "/create_post"; 573 | const createdTab = await browserAPI.tabs.create({ url: url }); 574 | const listener = (tabId, changeInfo) => { 575 | if (tabId === createdTab.id && (changeInfo.status === "complete" || changeInfo.status === "loading")) { 576 | 577 | 578 | browserAPI.tabs.onUpdated.removeListener(listener); 579 | 580 | // Fill in form after the tab is fully loaded 581 | browserAPI.scripting.executeScript({ 582 | target: { tabId: createdTab.id }, 583 | func: async (postData) => { 584 | const EVENT_OPTIONS = {bubbles: true, cancelable: false, composed: true}; 585 | const EVENTS = { 586 | BLUR: new Event("blur", EVENT_OPTIONS), 587 | CHANGE: new Event("change", EVENT_OPTIONS), 588 | INPUT: new Event("input", EVENT_OPTIONS), 589 | }; 590 | 591 | const postTitleInput = document.querySelector("#post-title"); 592 | const postURLInput = document.querySelector("#post-url"); 593 | 594 | postTitleInput.select(); 595 | postTitleInput.value = postData.title; 596 | postTitleInput.dispatchEvent(EVENTS.INPUT); 597 | 598 | postURLInput.select(); 599 | postURLInput.value = postData.url; 600 | postURLInput.dispatchEvent(EVENTS.INPUT); 601 | }, 602 | args: [postData] 603 | }).catch(error => { 604 | console.error("Script execution error:", error); 605 | }); 606 | 607 | window.close(); 608 | } 609 | }; 610 | 611 | browserAPI.tabs.onUpdated.addListener(listener); 612 | 613 | } else if (type === "kbin") { 614 | const url = instance + "/new?url=" + postData.url + "&title=" + postData.title; 615 | await browserAPI.tabs.create({ url: url }); 616 | } 617 | } else { alert("No valid instance has been set. Please select an instance in the popup using 'Change my home instance'."); } 618 | } 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | // ---------------------------------------------- 630 | // ---------- General DOM Manipulation ---------- 631 | // ---------------------------------------------- 632 | 633 | //Used for offset removal 634 | function removeClassByWildcard(divClass) { 635 | // If the class ends with a "*", then it matches all classes that start with the given class name. 636 | if (divClass.endsWith("*")) { 637 | divClass = divClass.replace("*", ""); 638 | // Get all elements with the given class name. 639 | const elements = document.getElementsByTagName("div"); 640 | const re = new RegExp("(^|s)" + divClass + "(s|$)"); 641 | const result = []; 642 | let className = ""; 643 | 644 | for (let i = 0; i < elements.length; i++) { 645 | if (re.test(elements[i].className)) { 646 | console.log("Match: " + elements[i]); 647 | result.push(elements[i]); 648 | for (let y = 0; y < elements[i].classList.length; y++) { 649 | if (elements[i].classList[y].indexOf(divClass) !== -1) { 650 | className = elements[i].classList[y]; 651 | console.log(className); 652 | } 653 | } 654 | } 655 | } 656 | // Remove the class from all elements. 657 | for (let i = 0; i < result.length; i++) { 658 | result[i].classList.remove(className); 659 | } 660 | } else { 661 | // Otherwise, the class must match exactly. 662 | const elements = document.querySelectorAll("[class=" + divClass + "]"); 663 | 664 | // Remove the class from all elements. 665 | for (let i = 0; i < elements.length; i++) { 666 | elements[i].classList.remove(divClass); 667 | } 668 | } 669 | } --------------------------------------------------------------------------------