├── .browserslistrc ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── doc ├── browser support.txt ├── build on windows.txt ├── codingStyle.txt ├── create a CSS module.txt ├── eslint.txt ├── greasyfork_description.txt ├── importantInterfaces.txt ├── listJSON.txt ├── to a future maintainer.txt └── using Automail without persistent storage.txt ├── icons ├── automail-128.png ├── automail-16.png ├── automail-2.png ├── automail-3.png ├── automail-32.png ├── automail-48.png ├── automail-8.png ├── automail-96.png └── automail.svg ├── package.json └── src ├── HOWTO.css ├── HOWTO.js ├── README.txt ├── alias.js ├── automail.m4 ├── boneless.m4 ├── cache.js ├── conditionalStyles.js ├── controller.js ├── css ├── CSSfavs.js ├── SFWmode.css ├── compactBrowse.css ├── displayBox.css ├── expandFeedFilters.css ├── footerLinks.css ├── global.css ├── greenManga.css ├── noAWC.css ├── notifications.css ├── rightToLeft.css ├── smallScreens.css ├── termsFeed.css └── verticalNav.css ├── data ├── AnimePlanet_anthologies.json ├── AnimePlanet_mappings_anime.json ├── AnimePlanet_mappings_manga.json ├── badDomains.json ├── commonUnfinishedManga.json ├── data contribution.txt ├── inlineSVG.json ├── languages │ ├── English.json │ ├── English_CA.json │ ├── English_US.json │ ├── English_short.json │ ├── French.json │ ├── German.json │ ├── Italian.json │ ├── JSON field details.txt │ ├── Japanese.json │ ├── Norwegian.json │ ├── Portuguese.json │ ├── README.txt │ ├── SouthernSami.json │ ├── Spanish.json │ ├── Swedish.json │ ├── Turkish.json │ ├── diff_tool.html │ ├── raw_keys.json │ └── special_keys.txt ├── legacyModuleDescriptions.json ├── review_scripts │ └── review.html ├── sequel_scripts │ ├── .eslintrc.json │ ├── api.mjs │ ├── fuzzyDateCompare.mjs │ ├── getAnimeSequels.mjs │ └── legacy │ │ ├── anime.html │ │ └── manga.html ├── sequel_special_cases.txt ├── sequels.json ├── sequels_manga.json ├── shortRomaji.json ├── studios.json └── titlecaseRomaji.json ├── graphql.js ├── localisation.js ├── makefile ├── manifest.json ├── modules ├── ALbuttonReload.js ├── accessTokenWarning.js ├── addActivityLinks.js ├── addActivityTimeline.js ├── addBrowseFilters.js ├── addComparisionPage.js ├── addCompletedScores.js ├── addCustomCSS.js ├── addDblclickZoom.js ├── addEntryScore.js ├── addFeedFilters.js ├── addFeedFilters_user.js ├── addFollowCount.js ├── addForumMedia.js ├── addForumMediaNoAWC.js ├── addForumMediaTitle.js ├── addImageFallback.js ├── addMALscore.js ├── addMediaReviewConfidence.js ├── addMoreStats.js ├── addMyThreadsLink.js ├── addProgressBar.js ├── addRelationStatusDot.js ├── addReviewConfidence.js ├── addSocialThemeSwitch.js ├── addStudioBrowseSwitch.js ├── addSubTitleInfo.js ├── additionalTranslation.js ├── altBanner.js ├── anisongs.js ├── autoLogin.js ├── betterListPreview.js ├── betterReviewRatings.js ├── browseSubmenu.js ├── cencorMediaPage.js ├── character.js ├── characterBrowse.js ├── clickableActivityHistory.js ├── directEditorAccess.js ├── documentTitleManager.js ├── dubMarker.js ├── durationTooltip.js ├── embedHentai.js ├── enumerateSubmissionStaff.js ├── expandDescriptions.js ├── expandFeedFilters.js ├── expandRight.js ├── expandedListNotes.js ├── extraDefaultSorts.js ├── extraFavs.js ├── feedListLikes.js ├── filterStaffTabs.js ├── forumLikes.js ├── forumRecent.js ├── forumVisualLikes.js ├── hideGlobalFeed.js ├── hideScores.js ├── hollowHearts.js ├── imageFreeEditor.js ├── infoTable.js ├── interestingRecs.js ├── keepAlive.js ├── mangaBrowse.js ├── mangaGuess.js ├── markdownHelp.js ├── meanScoreBack.js ├── mediaList.js ├── mediaTranslation.js ├── middleClickLinkFixer.js ├── mobileAdjustments.js ├── mobileTags.js ├── moreImports.js ├── navbarDroptext.js ├── newChapters.js ├── noAutoplay.js ├── noScrollPosts.js ├── noSequel.js ├── nonJapaneseVoiceDefaults.js ├── nonJumpScroll.js ├── notificationCake.js ├── notifications.js ├── oldDarkTheme.js ├── possibleBlocked.js ├── profileBackground.js ├── randomButtons.js ├── rangeSetter.js ├── recommendationsFade.js ├── reinaDark.js ├── relations.js ├── replaceStaffRoles.js ├── rightSideNavbar.js ├── scoreOverviewFixer.js ├── selectMyThreads.js ├── selfInsert.js ├── settingsPage.js ├── showMarkdown.js ├── singleActivityReplyLikes.js ├── slimNav.js ├── socialTab.js ├── socialTabFeed.js ├── staff.js ├── staffBrowse.js ├── studio.js ├── submenu.js ├── termsFeed.js ├── tweets.js ├── twoColumnFeed.js ├── unicodifier.js ├── videoMimeTypeFixer.js ├── viewAdvancedScores.js ├── webmResize.js ├── yearStepper.js └── youtubeFullscreen.js ├── polyfills.js ├── purify.js ├── queries ├── BroomCat.js ├── autorecs.js ├── blockedNumber.js ├── compatibility.js ├── datingMess.js ├── datingMessDanger.js ├── findBlocked.js ├── findMessage.js ├── findStatus.js ├── firstActivity.js ├── mediaStatistics.js ├── messageSpy.js ├── mostLikedStatus.js ├── notecleaner.js ├── popularFavourites.js ├── queries.js ├── rank.js ├── relatedAnime.js ├── relatedManga.js ├── reviews.js ├── seasonalStats.js └── submissionStats.js ├── settings.js ├── utilities.js └── utilities ├── colourPicker.js ├── displayBox.js ├── levDist.js ├── localforage.js ├── lz-string.js ├── modification.txt ├── parseListJSON.js ├── saveAs.js └── showdown.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | # Browsers that support ES9 (2018) 2 | Edge >= 79 3 | Firefox >= 78 4 | Chrome >= 64 5 | Safari >= 12 6 | Opera >= 51 7 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Third-party code 2 | /src/utilities/levDist.js 3 | /src/utilities/localforage.js 4 | /src/utilities/lz-string.js 5 | /src/utilities/showdown.js 6 | /src/purify.js 7 | 8 | # Code fragments 9 | /src/queries/ 10 | 11 | # Build artifacts 12 | /src/build/ 13 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "es2018": true, 6 | "greasemonkey": true 7 | }, 8 | "extends": ["eslint:recommended", "plugin:compat/recommended"], 9 | "rules": { 10 | "no-unused-vars": "off", 11 | "no-undef": "off", 12 | "no-extra-semi": "warn", 13 | "no-useless-escape": "warn", 14 | "array-callback-return": "warn", 15 | "no-await-in-loop": "warn", 16 | "no-constructor-return": "warn", 17 | "no-promise-executor-return": "warn", 18 | "no-self-compare": "warn", 19 | "no-template-curly-in-string": "warn", 20 | "no-unmodified-loop-condition": "warn", 21 | "no-unreachable-loop": "warn", 22 | "no-use-before-define": "warn", 23 | "no-caller": "warn", 24 | "no-eval": "warn", 25 | "no-implied-eval": "warn", 26 | "no-extend-native": "warn", 27 | "no-extra-bind": "warn", 28 | "no-floating-decimal": "warn" 29 | }, 30 | "globals": { 31 | "DOMPurify": "readonly", 32 | "localforage": "readonly", 33 | "showdown": "readonly" 34 | }, 35 | "settings": { 36 | "polyfills": [ 37 | "String.prototype.includes", 38 | "BroadcastChannel", 39 | "Array.prototype.flat" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '44 0 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v4 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v3 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v3 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v3 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | src/build/ 2 | automail.zip 3 | node_modules 4 | package-lock.json 5 | yarn.lock 6 | pnpm-lock.yaml 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Automail 2 | Extra parts for anilist.co 3 | 4 | When installed, a list of options in https://anilist.co/settings/apps can be used to configure the behaviour of the website 5 | 6 | Automail primarily deals with: 7 | - Notifications 8 | - Statistics 9 | - Styling 10 | - Navigation 11 | - UI translation (Japanese, Spanish, Portuguese, German, Turkish, Norwegian, Southern Sami, Italian, French) 12 | 13 | ## Available releases 14 | 15 | As a userscript: https://greasyfork.org/en/scripts/370473-automail ([How to use userscripts](https://greasyfork.org/en/help/installing-user-scripts)) 16 | As a Firefox addon: https://github.com/hohMiyazawa/Automail/releases 17 | 18 | ## Build from source 19 | 20 | "src/" contains a makefile, run "make" there. 21 | Requires make, m4 and basic shell utilities 22 | 23 | Will build the userscript and a Firefox addon in src/build/ 24 | 25 | If you have an archived version of this repo, updated code can be found at 26 | https://github.com/hohMiyazawa/Automail 27 | 28 | ## Copyright 29 | 30 | Copyright (C) 2019-2023 hoh and the Automail contributors 31 | 32 | This program is free software: you can redistribute it and/or modify 33 | it under the terms of the GNU General Public License as published by 34 | the Free Software Foundation, either version 3 of the License, or 35 | (at your option) any later version. 36 | 37 | This program is distributed in the hope that it will be useful, 38 | but WITHOUT ANY WARRANTY; without even the implied warranty of 39 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 40 | GNU General Public License for more details. 41 | 42 | You should have received a copy of the GNU General Public License 43 | along with this program. If not, see . 44 | -------------------------------------------------------------------------------- /doc/browser support.txt: -------------------------------------------------------------------------------- 1 | SHOULD WORK: Browsers Automail should theoretically work for: 2 | - Firefox 52+ 3 | - Chrome 55+ 4 | - Chromium 5 | - Edge 14+ 6 | - Opera 32+ 7 | - Safari 10.1+ 8 | 9 | DOESN'T WORK: Browsers Automail should theoretically NOT work for: 10 | - All versions of Internet Explorer [no support planned] 11 | - Dillo [no support planned] 12 | 13 | --- 14 | 15 | Versions confirmed to work with Automail: 16 | - Firefox 76.0.1, 96.0, 105.0, 108.0.2 17 | - Chromium 81.0.4044.138 18 | 19 | Browsers confirmed to NOT work with Automail: 20 | - Dillo [no support planned] 21 | 22 | Platforms with dedicated testers: 23 | - Firefox on Linux: hoh 24 | 25 | 26 | 27 | Both positive and negative confirmations can be posted as issues on github: 28 | "Automail does not work in " 29 | in body, include details about what doesn't work 30 | "Automail works in " 31 | (optionally) in body, include the steps used to install Automail 32 | -------------------------------------------------------------------------------- /doc/build on windows.txt: -------------------------------------------------------------------------------- 1 | See https://github.com/hohMiyazawa/Automail/issues/5 2 | for one possible way found by Reinachan 3 | -------------------------------------------------------------------------------- /doc/codingStyle.txt: -------------------------------------------------------------------------------- 1 | 0. Just do your thing. I can deal with code submissions pretty quickly. 2 | 3 | --- 4 | 5 | There's also an eslint config. 6 | 7 | --- 8 | 9 | As with any project, follow its established coding style. 10 | 11 | Your contributions may still be accepted if you don't but there will be less of a cleanup job for me. 12 | 13 | 1. Indent with tabs, not spaces 14 | 15 | 2. Opening curly { are on the same line as control flow statements 16 | 17 | 3. There's no space between control flow statements and the opening curly { 18 | 19 | 4. If statements should always have curly brackets {}, even when trivial. Also always () 20 | 21 | 5. Omit semicolons when possible 22 | 23 | 6. Conditionals can be split over multiple lines for clarity 24 | 25 | --- example if statement, showing 1-6: 26 | 27 | if( 28 | someTest() 29 | && ( 30 | useScripts.someModule 31 | || useScripts.someOtherModule 32 | ) 33 | ){ 34 | counter++ 35 | } 36 | 37 | --- 38 | 39 | 7. Use double quotes, not single quotes. (single quotes may be used for single characters, like in c) 40 | 41 | 8. Make HTML elements with the "create" function. (see /src/utilities.js) 42 | 43 | 9. All usage of "innerHTML" should be avoided if possible (innerText, textContent), and if that's unavoidable, you MUST use DOMPurify.sanitize 44 | 45 | 10. If you need some caching, use localForage 46 | 47 | 11. Format your module using the "exportModule" syntax if possible (see src/HOWTO.js) 48 | 49 | 12. Don't import any external libraries. (see if you usecase can be covered by purify.js, localforage.js, anything in utilities.js, or src/utitilites/). Write it yourself if you really need it. 50 | 51 | 13. Avoid calling anilist code. 52 | (permitted, for opening the list editor: document.getElementById("app").__vue__.$store.dispatch("medialistEditor/open",mediaID) ) 53 | (permitted, for loading other parts of the site from memory: document.getElementById("app").__vue__._router.push() BUT MAKE SURE IT FALLS BACK TO A REGULAR LINK!) 54 | 55 | 14. All communication with the Anilist API must be done via the interfaces defined in src/graphql.js 56 | 57 | 15. Joke comments are permitted. 58 | 59 | 16. m4 and make are the only permitted pre-processing tools (Python3 scripts, using the standard library only, can be permitted) 60 | -------------------------------------------------------------------------------- /doc/create a CSS module.txt: -------------------------------------------------------------------------------- 1 | --- new way: 2 | 3 | Follow the template in src/HOWTO.css, insert your css in the css field, and save in /src/modules/yourModule.js 4 | 5 | --- old way: 6 | 7 | 1. Add a boolean in useScripts, in settings.js 8 | 9 | 2. Link your boolean in useScriptsDefinitions, in controller.js 10 | 11 | 3. Inline you insertion in conditionalStyles.js 12 | 13 | 4. (optional) split it into its own file in the css directory, linking it by m4 from conditionalStyles.js 14 | -------------------------------------------------------------------------------- /doc/eslint.txt: -------------------------------------------------------------------------------- 1 | Eslint is a tool to detect style and code mistakes. 2 | This is optional, and neither required for building nor contributing to Automail. (It can however save you some time) 3 | 4 | How to use: 5 | 6 | Install Node.js (https://nodejs.org/en/) LTS for your platform 7 | Open terminal and enter the Automail directory 8 | Run "npm install" 9 | Run "npm run lint" to lint all JS project files 10 | Or "npm run lint-build" to lint just the compiled userscript 11 | 12 | Installing globally (alternative): 13 | Follow the same steps as above, except replace step 3 with: 14 | 15 | Run "npm install -g eslint@8 eslint-plugin-compat@4" 16 | 17 | Optional: 18 | 19 | Install an integration (https://eslint.org/docs/user-guide/integrations#editors) for your code editor. 20 | will apply linting as you edit open files instead of needing to run the terminal command 21 | 22 | 23 | Added in https://github.com/hohMiyazawa/Automail/pull/138 24 | -------------------------------------------------------------------------------- /doc/greasyfork_description.txt: -------------------------------------------------------------------------------- 1 | Repository at https://github.com/hohMiyazawa/Automail 2 | 3 | A collection of enhancements for Anilist. 4 | Main: 5 | - New notification system 6 | - More stats (including full lists instead of just 30 items) 7 | - Add scores to completion activities in the feed 8 | - Custom tags 9 | - Find old media activities 10 | Minor: 11 | - Social tab average score, notes and progress 12 | - Various pieces of optional page styling 13 | - Title aliases 14 | - Themes (including high contrast dark) 15 | - Following/follower counts 16 | - Custom profile backgrounds and CSS 17 | - MAL score, recs and serialisation info 18 | - Feed filtering and blocking 19 | - Anime-Planet list importer 20 | - A list exporter 21 | - Accessibility options 22 | - Staff page sorting and filtering 23 | - Translation (Français, Português, Italiano, Español, 日本語, Norsk, Türkçe, Deutsch) 24 | 25 | These can be turned on and off individually in settings > apps 26 | -------------------------------------------------------------------------------- /doc/importantInterfaces.txt: -------------------------------------------------------------------------------- 1 | The "create" function 2 | Used to create new DOM elements 3 | documented in /src/utilities.js 4 | 5 | --- 6 | 7 | graphql.js contains some important functions for interacting with the Anilist API 8 | 9 | --- 10 | 11 | The automailAPI 12 | 13 | Behind a setting (turned off by default), users can enable API to controll Automail. 14 | 15 | This is in the form of an object that belongs to the document object. 16 | 17 | document.automailAPI 18 | 19 | This exposes some functions of Automail to any other script. The goal being to either simplify writing of third-party script, or make it easier for such scripts to adapt to the changes Automail does. 20 | 21 | This is not a security vulnerability, as these permissions are available in any case to scripts a user decides to run. It just makes it easier for those scripts. 22 | 23 | Current interfaces: 24 | 25 | automailAPI.automailAPI 26 | -- an object containing various info about the version of Automail running. Can be found in src/scriptInfo.js 27 | automailAPI.api 28 | -- the new Automail internal Graphql API 29 | automailAPI.generalAPIcall 30 | automailAPI.authAPIcall 31 | automailAPI.queryPacker 32 | -- implements an interface to the Anilist Graphql API. 33 | -- their implementation can be found in src/graphql.js 34 | automailAPI.settings 35 | -- an object containing the user's script settings 36 | automailAPI.logOut 37 | -- a function to make Automail forget its access token 38 | -- the access token is still valid, and must be retracted from the apps permission page if you want to disable it entirely 39 | 40 | --- 41 | 42 | All raw HTML (avoid if possible) must be sanitised. 43 | Do: 44 | DOMPurify.sanitize(yourHTML) 45 | -------------------------------------------------------------------------------- /doc/listJSON.txt: -------------------------------------------------------------------------------- 1 | A somewhat unimportant system that's implemented in the script, which allows for making adjustments to the stats generated. 2 | 3 | It works by including a JSON object to the list notes of an entry, surounded by $-signs 4 | 5 | Implements the following keys: 6 | 7 | "adjust": takes a number, representing the number of episodes to add/remove for the show's duration. 8 | "more": takes an array of episode numbers, or a string specifying extra episodes watched. 9 | "skip": same as more, except it represents filler 10 | String syntax: 11 | comma separated, each enty bein either: 12 | a number, representing an episode 13 | number dash number, representing a range (like 4-8) 14 | both episode numbers and ranges can have an "x" multiplier after them 15 | 16 | valid eamples: 17 | [1,3,"1-4"] 18 | "1,2,3" 19 | "1-3" 20 | "4,7x5,1-3x4" 21 | 22 | 23 | Implemented in the function "parseListJSON" 24 | 25 | 26 | Other list note stuff: 27 | 28 | #tag <- this is a custom tag, which shows up in your "more stats" section, and in an index on your list 29 | 30 | ##STRICT <- a special tag, used to turn off various functions fixing "sloppy" tag formatting and casing errors 31 | -------------------------------------------------------------------------------- /doc/to a future maintainer.txt: -------------------------------------------------------------------------------- 1 | videoMimeTypeFixer: 2 | check if Anilist still serves all video as "video/webm". 3 | if not, this module can be removed. 4 | purify.js: 5 | check for new versions of this every six months or so, to keep up with the xss meta 6 | 7 | "remind hoh to update the commonUnfinishedManga list" 8 | /src/data/commonUnfinishedManga.json contains a mini-database of unfinished manga. 9 | edit the timestamp in /src/utilities.js to make it stop nagging 10 | 11 | How do I publish the firefox addon? 12 | You have to register a firefox account, and publish it separately. 13 | The existing firefox addon is currently not kept up to date 14 | 15 | sequels.json, sequels_manga.json 16 | These are also databases, and must manually be kept up to date. 17 | /src/data/sequel_scripts has some code, were you run the calli() function with page numbers (wait some time, or get throttled) until the entire database is scanned. 18 | Then merge this somehow. Be aware of special cases in sequels_special_cases.txt 19 | 20 | If hoh can't be reached, ask synthtech about the architecture of the script 21 | -------------------------------------------------------------------------------- /doc/using Automail without persistent storage.txt: -------------------------------------------------------------------------------- 1 | Automail has configurable settings. 2 | If those settings are to be kept, they have to be stored somewhere. 3 | 4 | Since Automail DOES NOT make use of a central server, they have to be saved locally on your computer. 5 | Browsers provide several APIs for this, generally known as cookies. 6 | 7 | 8 | 9 | Automail still mostly works without persistant storage, but in case you want to re-enable it anyway, it should be sufficient to only enable it for the anilist.co domain. 10 | 11 | 12 | 13 | 14 | Case 1. All storage disabled 15 | 16 | Automail will always have the default settings, and you will not be able to sign in to it, making certain modules unavailable 17 | 18 | 19 | Case 2. Storage is not kept between sessions. 20 | 21 | You can change the settings and sign in, but your settings will be gone and you will be signed out if you close and re-open your browser 22 | 23 | 24 | 25 | Patching default settings: 26 | 27 | 28 | src/settings.js contains an object called "useScripts" with the default settings. 29 | (some modules are not listed there. But you can still add their keys there, and it will work. For instance, if you look at the file "/src/modules/hollowHearts.js", you can take the id "hollowHearts", and place it in the usescript settings as "hollowHearts: true") 30 | 31 | Edit this to your taste, and build the script. (you can also save your access token here, which should be in the URL when you click the sign in button on the settings page) 32 | 33 | (if you are editing a compiled build, the relevant code can be located by searching for "let useScripts =") 34 | -------------------------------------------------------------------------------- /icons/automail-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hohMiyazawa/Automail/79a88dde51ceb40ac56a9d547afc98efa7952c38/icons/automail-128.png -------------------------------------------------------------------------------- /icons/automail-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hohMiyazawa/Automail/79a88dde51ceb40ac56a9d547afc98efa7952c38/icons/automail-16.png -------------------------------------------------------------------------------- /icons/automail-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hohMiyazawa/Automail/79a88dde51ceb40ac56a9d547afc98efa7952c38/icons/automail-2.png -------------------------------------------------------------------------------- /icons/automail-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hohMiyazawa/Automail/79a88dde51ceb40ac56a9d547afc98efa7952c38/icons/automail-3.png -------------------------------------------------------------------------------- /icons/automail-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hohMiyazawa/Automail/79a88dde51ceb40ac56a9d547afc98efa7952c38/icons/automail-32.png -------------------------------------------------------------------------------- /icons/automail-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hohMiyazawa/Automail/79a88dde51ceb40ac56a9d547afc98efa7952c38/icons/automail-48.png -------------------------------------------------------------------------------- /icons/automail-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hohMiyazawa/Automail/79a88dde51ceb40ac56a9d547afc98efa7952c38/icons/automail-8.png -------------------------------------------------------------------------------- /icons/automail-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hohMiyazawa/Automail/79a88dde51ceb40ac56a9d547afc98efa7952c38/icons/automail-96.png -------------------------------------------------------------------------------- /icons/automail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "automail", 3 | "version": "10.4.6", 4 | "description": "An enhancement collection for anilist.co", 5 | "author": "hoh", 6 | "license": "GPL-3.0-or-later", 7 | "private": true, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/hohMiyazawa/Automail.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/hohMiyazawa/Automail/issues" 14 | }, 15 | "homepage": "https://github.com/hohMiyazawa/Automail", 16 | "scripts": { 17 | "build": "make -C src", 18 | "build-wsl": "wsl make -C src", 19 | "dev": "http-server src/build -s -c5 -o automail.user.js", 20 | "lint": "eslint \"**/*.js\"", 21 | "lint-build": "eslint --rule \"no-unused-vars: warn\" --rule \"no-undef: warn\" --no-ignore src/build/automail.user.js", 22 | "update-anime-sequels": "node src/data/sequel_scripts/getAnimeSequels.mjs" 23 | }, 24 | "devDependencies": { 25 | "eslint": "^8.33.0", 26 | "eslint-plugin-compat": "^4.1.1", 27 | "http-server": "^14.1.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/HOWTO.css: -------------------------------------------------------------------------------- 1 | //create your own CSS module 2 | //make a javascript file, called yourModule.js, in the directory "modules" (even when it is a CSS-only module) 3 | //worked example: /src/modules/noScrollPosts.js 4 | //include the following code: 5 | 6 | exportModule({ 7 | id: "howto",//an unique identified for your module 8 | description: "what your module does", 9 | extendedDescription: ` 10 | A more detailed description of what your module does. (optional) 11 | 12 | This appears when people click the "more info" icon (🛈) on the settings page. 13 | `, 14 | isDefault: false, 15 | importance: 0,//a number, which determines the order of the settings page. Higher numbers are more important. Leave it as 0 if unsure. 16 | categories: [],//what categories your module belongs in 17 | //Notifications, Feeds, Forum, Lists, Profiles, Stats, Media, Navigation, Browse, Script, Login, Newly Added 18 | visible: false,//if the module should be visible in the settings (REMEMBER TO CHANGE THIS TO TRUE!) 19 | css: ""//your CSS here, use `` instead for multi line 20 | 21 | //your module can also have extra code and utility functions 22 | -------------------------------------------------------------------------------- /src/HOWTO.js: -------------------------------------------------------------------------------- 1 | //create your own module 2 | //make a javascript file, called yourModule.js, in the directory "modules" 3 | //include the following code: 4 | 5 | exportModule({ 6 | id: "howto",//an unique identified for your module 7 | description: "what your module does", 8 | extendedDescription: ` 9 | A more detailed description of what your module does. (optional) 10 | 11 | This appears when people click the "more info" icon (🛈) on the settings page. 12 | `, 13 | isDefault: false, 14 | importance: 0,//a number, which determines the order of the settings page. Higher numbers are more important. Leave it as 0 if unsure. 15 | categories: [],//what categories your module belongs in 16 | //Notifications, Feeds, Forum, Lists, Profiles, Stats, Media, Navigation, Browse, Script, Login, Newly Added 17 | visible: false,//if the module should be visible in the settings (REMEMBER TO CHANGE THIS TO TRUE!) 18 | urlMatch: function(url,oldUrl){//a function that returns true when on the parts of the site you want it to run. url is the current url, oldUrl is the previous page 19 | //example: return url === "https://anilist.co/reviews" 20 | return false; 21 | }, 22 | code: function(){ 23 | //your code goes here 24 | }, 25 | css: ""//css rules you need 26 | }) 27 | 28 | //your module can also have extra code and utility functions 29 | -------------------------------------------------------------------------------- /src/README.txt: -------------------------------------------------------------------------------- 1 | Run "make" to build Automail 2 | A userscript and a firefox addon will be built in the build directory 3 | 4 | Read "HOWTO.js" for instructions about how to write your own module 5 | -------------------------------------------------------------------------------- /src/alias.js: -------------------------------------------------------------------------------- 1 | //begin "alias.js" 2 | const moreStyle = create("style"); 3 | moreStyle.id = "conditional-" + script_type.toLowerCase() + "-styles"; 4 | moreStyle.type = "text/css"; 5 | 6 | let createAlias = function(alias){ 7 | if(alias[0] === "css/"){ 8 | moreStyle.textContent += alias[1] 9 | } 10 | else{ 11 | const dataSelect = `[href^="${alias[0]}"]`; 12 | const targetName = alias[1].substring(0,Math.min(100,alias[1].length)); 13 | moreStyle.textContent += ` 14 | .title > a${dataSelect} 15 | ,a.title${dataSelect} 16 | ,.overlay > a.title${dataSelect} 17 | ,.media-preview-card a.title${dataSelect} 18 | ,.quick-search-results .el-select-dropdown__item a${dataSelect}> span 19 | ,.media-embed${dataSelect} .title 20 | ,.status > a.title${dataSelect} 21 | ,.role-card a.content${dataSelect} > .name{ 22 | visibility: hidden; 23 | line-height: 0px; 24 | } 25 | .results.media a.title${dataSelect} 26 | ,.home .status > a.title${dataSelect}{ 27 | font-size: 2%; 28 | } 29 | 30 | a.title${dataSelect}::before 31 | ,.quick-search-results .el-select-dropdown__item a${dataSelect} > span::before 32 | ,.role-card a.content${dataSelect} > .name::before 33 | ,.home .status > a.title${dataSelect}::before 34 | ,.media-embed${dataSelect} .title::before 35 | ,.overlay > a.title${dataSelect}::before 36 | ,.media-preview-card a.title${dataSelect}::before 37 | ,.title > a${dataSelect}::before{ 38 | content:"${targetName}"; 39 | visibility: visible; 40 | }`; 41 | } 42 | } 43 | 44 | const shortRomaji = (useScripts.titlecaseRomaji ? m4_include(data/titlecaseRomaji.json) : []).concat( 45 | (useScripts.shortRomaji ? m4_include(data/shortRomaji.json) : []) 46 | ); 47 | //end "alias.js" 48 | -------------------------------------------------------------------------------- /src/css/CSSfavs.js: -------------------------------------------------------------------------------- 1 | if(useScripts.CSSfavs){ 2 | /*adds a logo to most favourite studio entries. Add more if needed */ 3 | const favStudios = m4_include(data/studios.json) 4 | let favStudioString = ""; 5 | if(useScripts.CSSfavs){ 6 | favStudioString += ` 7 | .overview .favourites > .favourites-wrap > div, 8 | .overview .favourites > .favourites-wrap > a{ 9 | /*make the spaces in the grid even*/ 10 | margin-bottom: 0px!important; 11 | margin-right: 0px!important; 12 | column-gap: 10px!important; 13 | } 14 | .user .overview{ 15 | grid-template-columns: 470px auto!important; 16 | } 17 | .overview .favourites > .favourites-wrap{ 18 | display: grid!important; 19 | padding: 0px!important; 20 | display: grid; 21 | grid-gap: 10px; 22 | column-gap: 10px!important; 23 | grid-template-columns: repeat(auto-fill,85px); 24 | grid-template-rows: repeat(auto-fill,115px); 25 | background: rgb(0,0,0,0) !important; 26 | width: 470px; 27 | } 28 | .genre-overview .genre{ 29 | display: inline!important; 30 | } 31 | .genre-overview .genre .name{ 32 | font-size: small; 33 | } 34 | .overview .favourite.studio{ 35 | cursor: pointer; 36 | min-height: 115px; 37 | font-size: 15px; 38 | display: grid; 39 | grid-gap: 10px; 40 | padding: 2px!important; 41 | padding-top: 8px!important; 42 | background-color: rgba(var(--color-foreground))!important; 43 | text-align: center; 44 | align-content: center; 45 | } 46 | .site-theme-dark .overview .favourite.studio{ 47 | background-color: rgb(49,56,68)!important; 48 | } 49 | .preview .favourite.media, 50 | .preview .favourite.staff, 51 | .preview .favourite.character{ 52 | background-color: rgb(var(--color-foreground)); 53 | } 54 | .overview .favourite.studio::after{ 55 | display: inline-block; 56 | background-repeat: no-repeat; 57 | content:""; 58 | margin-left:5px; 59 | background-size: 76px 19px; 60 | width: 76px; 61 | height: 19px; 62 | }`; 63 | favStudios.forEach(studio => { 64 | if(studio[2] !== ""){ 65 | favStudioString += `.favourite.studio[href="/studio/${studio[0]}/${studio[1]}"]::after{background-image: url("${studio[2]}");`; 66 | if(studio.length === 5){ 67 | favStudioString += `background-size: ${studio[3]}px ${studio[4]}px;width: ${studio[3]}px;height: ${studio[4]}px;`; 68 | } 69 | favStudioString += "}"; 70 | } 71 | }); 72 | } 73 | moreStyle.textContent += favStudioString; 74 | } 75 | -------------------------------------------------------------------------------- /src/css/compactBrowse.css: -------------------------------------------------------------------------------- 1 | .results > .studio{ 2 | counter-reset: ranking; 3 | } 4 | .studio .media-card.isMain{ 5 | border-bottom: rgb(var(--color-blue)); 6 | border-bottom-width: 1px; 7 | border-bottom-style: solid; 8 | } 9 | .search .results.cover .media-card, 10 | .search .results .staff-card, 11 | .search-landing .results:not(.table) .media-card{ 12 | width: 150px; 13 | } 14 | .search .results.cover, 15 | .search-landing .results:not(.table){ 16 | grid-template-columns: repeat(auto-fill,150px); 17 | grid-gap: 25px 20px; 18 | } 19 | .search .results.table{ 20 | grid-gap: 12px; 21 | } 22 | .search .results.table .cover{ 23 | height: 81px; 24 | width: 54px; 25 | } 26 | .search .results.table .media-card{ 27 | padding: 0px; 28 | } 29 | .search .results.cover .media-card .cover, 30 | .search-landing .results:not(.table) .media-card .cover, 31 | .search .results .staff-card .cover{ 32 | height: 225px; 33 | } 34 | .search:not(.other-type) .landing-section:not(.top) .link{ 35 | max-width: 1000px; 36 | } 37 | -------------------------------------------------------------------------------- /src/css/displayBox.css: -------------------------------------------------------------------------------- 1 | .hohDisplayBox{ 2 | position: fixed; 3 | top: 80px; 4 | left: 200px; 5 | z-index: 9999; 6 | padding: 20px; 7 | background-color: rgb(var(--color-foreground)); 8 | border: solid 1px; 9 | border-radius: 4px; 10 | box-shadow: black 2px 2px 20px; 11 | overflow: hidden; 12 | filter: brightness(110%); 13 | } 14 | .hohDisplayBox .scrollableContent{ 15 | overflow: auto; 16 | height: 100%; 17 | scrollbar-width: thin; 18 | margin-top: 5px; 19 | padding: 30px; 20 | padding-top: 35px; 21 | padding-left: 15px; 22 | } 23 | .hohDisplayBoxClose{ 24 | position: absolute; 25 | right: 15px; 26 | top: 15px; 27 | cursor: pointer; 28 | background-color: red; 29 | border: solid; 30 | border-width: 1px; 31 | border-radius: 2px; 32 | color: white; 33 | border-color: rgb(var(--color-text)); 34 | filter: drop-shadow(0 0 0.2rem crimson); 35 | z-index: 20; 36 | } 37 | } 38 | .hohDisplayBoxClose:hover{ 39 | filter: drop-shadow(0 0 0.75rem crimson); 40 | } 41 | .hohNewChapter .hohDisplayBoxClose{ 42 | display: none; 43 | top: 7px; 44 | } 45 | .hohNewChapter:hover .hohDisplayBoxClose{ 46 | display: inline; 47 | } 48 | .hohDisplayBoxTitle{ 49 | position: absolute; 50 | top: 5px; 51 | left: 5px; 52 | font-weight: bold; 53 | font-size: 1.2em; 54 | } 55 | -------------------------------------------------------------------------------- /src/css/expandFeedFilters.css: -------------------------------------------------------------------------------- 1 | .home .activity-feed-wrap .section-header .el-dropdown-menu, 2 | .user .activity-feed-wrap .section-header .el-dropdown-menu{ 3 | background: none; 4 | position: static; 5 | display: inline !important; 6 | margin-right: 15px; 7 | box-shadow: none !important; 8 | } 9 | .home .activity-feed-wrap .section-header .el-dropdown-menu__item, 10 | .user .activity-feed-wrap .section-header .el-dropdown-menu__item{ 11 | font-weight: normal; 12 | color: rgb(var(--color-text-lighter)); 13 | margin-left: -2px !important; 14 | display: inline; 15 | font-size: 1.2rem; 16 | padding: 4px 15px 5px 15px; 17 | border-radius: 3px; 18 | transition: .2s; 19 | background: none; 20 | } 21 | .home .activity-feed-wrap .section-header .el-dropdown-menu__item.active, 22 | .user .activity-feed-wrap .section-header .el-dropdown-menu__item.active{ 23 | background: none!important; 24 | color: rgb(var(--color-blue)); 25 | } 26 | .home .activity-feed-wrap .section-header .el-dropdown-menu__item:hover, 27 | .user .activity-feed-wrap .section-header .el-dropdown-menu__item:hover{ 28 | background: none!important; 29 | color: rgb(var(--color-blue)); 30 | } 31 | .home .feed-select .feed-filter, 32 | .user .section-header > .el-dropdown > .el-dropdown-selfdefine{ 33 | display: none; 34 | } 35 | -------------------------------------------------------------------------------- /src/css/greenManga.css: -------------------------------------------------------------------------------- 1 | .review-card:hover .banner[data-src*="/media/manga/"] + .content > .header{ 2 | color: rgb(var(--color-green)); 3 | } 4 | .review-card:hover .banner[data-src*="/media/anime/"] + .content > .header{ 5 | color: rgb(var(--color-blue)); 6 | } 7 | .user .review-card:hover .banner[data-src*="/media/anime/"] + .content > .header{ 8 | color: rgb(61,180,242); 9 | } 10 | .activity-markdown a[href^="https://anilist.co/manga/"], 11 | .activity-markdown a[href^="https://anilist.co/search/manga"], 12 | .activity-markdown a[href^="/manga/"], 13 | .reply-markdown a[href^="https://anilist.co/manga/"], 14 | .reply-markdown a[href^="https://anilist.co/search/manga"], 15 | .reply-markdown a[href^="/manga/"]{ 16 | color: rgba(var(--color-green)); 17 | } 18 | .hohDataChange a[href^="/manga/"]{ 19 | color: rgba(var(--color-green)); 20 | } 21 | .activity-manga_list > div > div > div > div > .title, 22 | .hohPinned .list .title[href^="/manga/"]{ 23 | color: rgba(var(--color-green))!important; 24 | } 25 | .media .relations .cover[href^="/manga/"] + div div{ 26 | color: rgba(var(--color-green)); 27 | } 28 | .media .relations .cover[href^="/anime/"] + div div{ 29 | color: rgba(var(--color-blue)); 30 | } 31 | .media .relations .cover[href^="/manga/"]{ 32 | border-bottom-style: solid; 33 | border-bottom-color: rgba(var(--color-green)); 34 | border-bottom-width: 2px; 35 | } 36 | .character-wrap .role-card:hover .title[href^="/anime/"]{ 37 | color: rgb(var(--color-blue)) !important; 38 | } 39 | .character-wrap .role-card .title[href^="/manga/"], 40 | .character-wrap .role-card:hover .title[href^="/manga/"], 41 | .media-roles .media .content:hover[href^="/manga/"] .name{ 42 | color: rgb(var(--color-green)) !important; 43 | } 44 | .media .relations.small .cover[href^="/manga/"]::after{ 45 | position:absolute; 46 | left:1px; 47 | bottom:3px; 48 | content:""; 49 | border-style: solid; 50 | border-color: rgba(var(--color-green)); 51 | border-width: 2px; 52 | } 53 | .media .relations .cover[href^="/anime/"]{ 54 | border-bottom-style: solid; 55 | border-bottom-color: rgba(var(--color-blue)); 56 | border-bottom-width: 2px; 57 | } 58 | .media .relations .cover div.image-text{ 59 | margin-bottom: 2px!important; 60 | border-radius: 0px!important; 61 | padding-bottom: 8px!important; 62 | padding-top: 8px!important; 63 | font-weight: 500!important; 64 | } 65 | .media-embed[data-media-type="manga"] .title{ 66 | color: rgba(var(--color-green)); 67 | } 68 | .media-manga .actions .list{ 69 | background: rgba(var(--color-green)); 70 | } 71 | .media-manga .sidebar .review.button{ 72 | background: rgba(var(--color-green)); 73 | } 74 | .media-manga .container .content .nav .link{ 75 | color: rgba(var(--color-green)); 76 | } 77 | .hover-manga:hover{ 78 | color: rgba(var(--color-green))!important; 79 | } 80 | .home .recent-reviews + div .cover[href^="/manga/"] + .content .info-header{ 81 | color: rgba(var(--color-green)); 82 | } 83 | .recommendations-wrap .recommendation-pair-card a[href^="/manga/"]:hover .title{ 84 | color: rgba(var(--color-green)); 85 | } 86 | -------------------------------------------------------------------------------- /src/css/noAWC.css: -------------------------------------------------------------------------------- 1 | .hohNoAWC .thread-card.small{ 2 | margin-bottom: 15px; 3 | background: rgb(var(--color-foreground)); 4 | border-radius: 3px; 5 | padding: 18px; 6 | position: relative; 7 | } 8 | .hohNoAWC .title{ 9 | font-size: 1.4rem; 10 | display: block; 11 | margin-bottom: 12px; 12 | margin-right: 110px; 13 | } 14 | .hohNoAWC .footer{ 15 | align-items: center; 16 | display: flex; 17 | flex-direction: row; 18 | } 19 | .hohNoAWC .avatar{ 20 | background-position: 50%; 21 | background-repeat: no-repeat; 22 | background-size: cover; 23 | border-radius: 3px; 24 | display: inline-block; 25 | height: 25px; 26 | vertical-align: text-top; 27 | width: 25px; 28 | } 29 | .hohNoAWC .name{ 30 | display: inline-block; 31 | font-size: 1.3rem; 32 | padding-left: 10px; 33 | } 34 | .hohNoAWC .name span{ 35 | color: rgb(var(--color-blue)); 36 | } 37 | .hohNoAWC .categories{ 38 | margin-left: auto; 39 | white-space: nowrap; 40 | max-width: 310px; 41 | } 42 | .hohNoAWC .category{ 43 | border-radius: 100px; 44 | color: #fff; 45 | display: inline-block; 46 | font-size: 1.1rem; 47 | margin-left: 10px; 48 | padding: 4px 8px; 49 | } 50 | .hohNoAWC .category.default{ 51 | text-transform: lowercase; 52 | } 53 | .hohNoAWC .category:hover{ 54 | color: rgba(26,27,28,.6); 55 | } 56 | .hohNoAWC .info{ 57 | color: rgb(var(--color-text-lighter)); 58 | font-size: 1.2rem; 59 | position: absolute; 60 | right: 12px; 61 | top: 12px; 62 | } 63 | .hohNoAWC .info span{ 64 | padding-left: 10px; 65 | } 66 | -------------------------------------------------------------------------------- /src/css/rightToLeft.css: -------------------------------------------------------------------------------- 1 | .favourites-wrap.anime, 2 | .favourites-wrap.manga, 3 | .favourites-wrap.staff, 4 | .favourites-wrap.characters, 5 | .favourites-wrap.studios{ 6 | direction: rtl; 7 | } 8 | .genre-overview .genres{ 9 | direction: rtl; 10 | } 11 | .genre-overview .percentage-bar{ 12 | direction: rtl; 13 | } 14 | .milestones{ 15 | direction: rtl; 16 | } 17 | .milestones + .progress{ 18 | transform: scale(-1); 19 | } 20 | .list-preview{ 21 | direction: rtl; 22 | } 23 | #hohListPreview .list-preview{ 24 | width: 100%; 25 | } 26 | .media-preview-card .hohFallback, 27 | .media-preview-card .content{ 28 | direction: ltr; 29 | } 30 | .media-preview-card .content meter{ 31 | direction: rtl; 32 | } 33 | .banner-content{ 34 | direction: rtl; 35 | } 36 | .banner-content .actions{ 37 | margin-right: auto; 38 | margin-left: inherit!important; 39 | } 40 | #hohListPreview .info-left .content { 41 | border-radius: 3px 0 0 3px; 42 | left: auto !important; 43 | right: 100%; 44 | text-align: right; 45 | } 46 | #app .home{ 47 | grid-template-columns: 470px auto !important; 48 | } 49 | .home > .activity-feed-wrap + div{ 50 | grid-row: 1; 51 | } 52 | .recent-reviews .review-wrap{ 53 | direction: rtl; 54 | } 55 | .recent-reviews .review-card{ 56 | direction: ltr; 57 | } 58 | .recent-reviews + div .media-preview{ 59 | direction: rtl; 60 | } 61 | #app > .progress{ 62 | transform: scale(-1); 63 | } 64 | .hohColourPicker{ 65 | margin-right: 20px; 66 | } 67 | .home .activity-feed-wrap .section-header{ 68 | direction: rtl; 69 | } 70 | .home .activity-feed-wrap .section-header .feed-select{ 71 | margin-right: auto; 72 | margin-left: inherit; 73 | } 74 | .home .activity-feed-wrap .section-header .feed-select .el-dropdown{ 75 | direction: ltr; 76 | margin-left: 20px; 77 | } 78 | .hohSubMenuLink{ 79 | text-align: right; 80 | } 81 | .quick-search .input{ 82 | direction: rtl; 83 | } 84 | .quick-search .results{ 85 | direction: rtl; 86 | } 87 | .quick-search .results .result{ 88 | direction: ltr; 89 | } 90 | .quick-search .results .result-col h3.title{ 91 | right: 0px; 92 | } 93 | .home .section-header{ 94 | text-align: right; 95 | } 96 | .home .list-previews .section-header{ 97 | direction: rtl; 98 | } 99 | .user .nav-wrap{ 100 | direction: rtl; 101 | } 102 | .user .medialist{ 103 | direction: rtl; 104 | } 105 | .medialist .filters .filter-group:first-child > span .count{ 106 | left: 0px; 107 | right: inherit; 108 | } 109 | .medialist.table .entry .title a{ 110 | margin-left: auto; 111 | margin-right: 10px; 112 | direction: ltr; 113 | } 114 | .medialist .lists > .actions{ 115 | left: 0px; 116 | right: inherit; 117 | } 118 | .list-editor-wrap .header .content{ 119 | direction: rtl; 120 | } 121 | -------------------------------------------------------------------------------- /src/data/AnimePlanet_anthologies.json: -------------------------------------------------------------------------------- 1 | { 2 | "The Dragon Dentist": 20947, 3 | "Hill Climb Girl": 20947, 4 | "20min Walk From Nishi-Ogikubo Station": 20947, 5 | "Collection of Key Animation Films": 20947, 6 | "(Making of) Evangelion: Another Impact": 20947, 7 | "Sex and Violence with Mach Speed": 20947, 8 | "Memoirs of Amorous Gentlemen": 20947, 9 | "Denkou Choujin Gridman: boys invent great hero": 20947, 10 | "Evangelion: Another Impact": 20947, 11 | "Bureau of Proto Society": 20947, 12 | "Cassette Girl": 20947, 13 | "Bubu & Bubulina": 20947, 14 | "I can Friday by day!": 20947, 15 | "Three Fallen Witnesses": 20947, 16 | "Robot on the Road": 20947, 17 | "Comedy Skit 1989": 20947, 18 | "Power Plant No.33": 20947, 19 | "Me! Me! Me! Chronic": 20947, 20 | "Endless Night": 20947, 21 | "Neon Genesis IMPACTS": 20947, 22 | "Obake-chan": 20947, 23 | "Hammerhead": 20947, 24 | "Girl": 20947, 25 | "Yamadeloid": 20947, 26 | "Me! Me! Me!": 20947, 27 | "Ibuseki Yoruni": 20947, 28 | "Rapid Rouge": 20947, 29 | "Tomorrow from there": 20947, 30 | "The Diary of Ochibi": 20947, 31 | "until You come to me.": 20947, 32 | "Tsukikage no Tokio": 20947, 33 | "Carnage": 20947, 34 | "Iconic Field": 20947, 35 | "The Ultraman (2015)": 20947, 36 | "Kanoun": 20947, 37 | "Ragnarok": 20947, 38 | "Death Note Rewrite 1: Visions of a God": 2994, 39 | "Death Note Rewrite 2: L's Successors": 2994 40 | } 41 | -------------------------------------------------------------------------------- /src/data/AnimePlanet_mappings_anime.json: -------------------------------------------------------------------------------- 1 | { 2 | "Rebuild of Evangelion: Final": 3786, 3 | "KonoSuba – God’s blessing on this wonderful world!! Movie: Legend of Crimson": 102976, 4 | "Puella Magi Madoka Magica: Magica Quartet x Nisioisin": 20891, 5 | "Kanye West: Good Morning": 8626, 6 | "Patlabor 2: The Movie": 1096, 7 | "She and Her Cat": 1004, 8 | "Star Blazers: Space Battleship Yamato 2199": 12029, 9 | "Digimon Season 3: Tamers": 874, 10 | "The Anthem of the Heart": 20968, 11 | "Digimon Movie 1: Digimon Adventure": 2961, 12 | "Love, Chunibyo & Other Delusions!: Sparkling... Slapstick Noel": 16934, 13 | "The Labyrinth of Grisaia Special": 21312, 14 | "Candy Boy EX01": 5116, 15 | "Candy Boy EX02": 6479, 16 | "Attack on Titan 3rd Season": 99147, 17 | "Attack on Titan 2nd Season": 20958, 18 | "Nichijou - My Ordinary Life: Episode 0": 8857, 19 | "March Comes in like a Lion 2nd Season": 98478, 20 | "KonoSuba – God’s blessing on this wonderful world!! 2 OVA": 97996, 21 | "KonoSuba – God’s blessing on this wonderful world!! OVA": 21574, 22 | "Laid-Back Camp Specials": 101206, 23 | "Spice and Wolf II OVA": 6007, 24 | "Mob Psycho 100 Specials": 102449 25 | } 26 | -------------------------------------------------------------------------------- /src/data/AnimePlanet_mappings_manga.json: -------------------------------------------------------------------------------- 1 | { 2 | "GATE: Where the JSDF Fought": 71733, 3 | "Emanon: Memories of Emamon": 47465 4 | } 5 | -------------------------------------------------------------------------------- /src/data/badDomains.json: -------------------------------------------------------------------------------- 1 | [556415734,1724824539,-779421562,-1111399772,-93654449,1120312799,-781704176,-1550515495,3396395,567115318,-307082983,1954992241,-307211474,-307390044,1222804306,-795095039,-1014860289,403785740,547002932,128627683] 2 | -------------------------------------------------------------------------------- /src/data/commonUnfinishedManga.json: -------------------------------------------------------------------------------- 1 | { 2 | "30002":{ 3 | "chapters":376, 4 | "volumes":40, 5 | "comment":"berserk" 6 | }, 7 | "30013":{ 8 | "chapters":1118, 9 | "volumes":101, 10 | "comment":"one piece" 11 | }, 12 | "85486":{ 13 | "chapters":375, 14 | "volumes":32, 15 | "comment":"mha" 16 | }, 17 | "74347":{ 18 | "chapters":152, 19 | "volumes":24, 20 | "comment":"opm" 21 | }, 22 | "30026":{ 23 | "chapters":390, 24 | "volumes":36, 25 | "comment":"HxH" 26 | }, 27 | "30656":{ 28 | "chapters":327, 29 | "volumes":37, 30 | "comment":"vagabond" 31 | }, 32 | "30105":{ 33 | "chapters":106, 34 | "volumes":14, 35 | "comment":"yotsuba&" 36 | }, 37 | "105398":{ 38 | "chapters":173, 39 | "volumes":4, 40 | "comment":"solo leveling" 41 | }, 42 | "101517":{ 43 | "chapters":167, 44 | "volumes":17, 45 | "comment":"juju" 46 | }, 47 | "97852":{ 48 | "chapters":383, 49 | "volumes":23, 50 | "comment":"komi" 51 | }, 52 | "102988":{ 53 | "chapters":233, 54 | "volumes":24, 55 | "comment":"revengers" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/data/data contribution.txt: -------------------------------------------------------------------------------- 1 | --- badDomains.json 2 | 3 | This file contains an array of hashes for illegal manga/anime aggregators, for link filtering (it's currently only used by a few modules) 4 | 5 | Use the simple java-hash on the domain name, without the top level domain (so "wikipedia.org" would be just "wikipedia", which would be 1558992055 (and not a domain we want to block!)) 6 | 7 | 8 | function hashCode(string){ 9 | var hash = 0, i, chr; 10 | if(string.length === 0){ 11 | return hash 12 | } 13 | for(i = 0; i < string.length; i++) { 14 | chr = string.charCodeAt(i); 15 | hash = ((hash << 5) - hash) + chr; 16 | hash |= 0; 17 | } 18 | return hash 19 | } 20 | 21 | 22 | It's not supposed to be cryptocraphically secure, the purpouse is just to not dump a huge catalogue of such sites to anyone taking a glance. 23 | 24 | If you submit a new domain, your pull request must mention in plaintext the domain it blocks. 25 | 26 | --- inlineSVG.json 27 | 28 | Use this if your module needs a new icon, as Automail must be delivered in a single file 29 | 30 | 31 | --- studios.json 32 | 33 | Format: 34 | 35 | [ 36 | studio ID, 37 | studio name (as stringified in the URL), 38 | direct link to logo, 39 | [optional width], 40 | [optional height] 41 | ] 42 | 43 | Example: 44 | 45 | [ 46 | 309, 47 | "GoHands", 48 | "https://i.stack.imgur.com/pScIZ.jpg", 49 | ] 50 | 51 | --- shortRomaji.json 52 | 53 | Alternate romaji titles for stuff that's too long. 54 | 55 | --- legacyModuleDescriptions.json 56 | 57 | Do not use, see HOWTO.js instead. 58 | 59 | --- languages/translation 60 | 61 | See https://github.com/hohMiyazawa/Automail/issues/69 62 | -------------------------------------------------------------------------------- /src/data/languages/English_CA.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "language": "English (CA)", 4 | "language_english": "English (CA)", 5 | "variation_of": "English", 6 | "locale": "en-CA", 7 | "fallback": ["English"], 8 | "maintainer": "Zarin", 9 | "maintainer_link": "https://github.com/kazzarin", 10 | "discussion_link": "", 11 | "notes": "There's in general no need to translate all the keys, since the default fallback is English." 12 | }, 13 | "keys": { 14 | "$MAL_serialization": "Serialization", 15 | "$compare_normalizeRatings": "Normalize ratings:", 16 | "$setting_MALserial": "Add MAL serialization info to manga", 17 | "$forumCategory_14": "List Customization" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/data/languages/English_US.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "language": "English (US)", 4 | "language_english": "English (US)", 5 | "variation_of": "English", 6 | "locale": "en-US", 7 | "fallback": ["English"], 8 | "maintainer": "hoh", 9 | "maintainer_link": "https://anilist.co/user/hoh/", 10 | "discussion_link": "", 11 | "notes": "There's in general no need to translate all the keys, since the default fallback is English." 12 | }, 13 | "keys": { 14 | "$setting_CSSsmileyScore": "Give smiley ratings distinct colors", 15 | "$profileBackground_help2": "Tip: Use a color with transparancy, to work better with both light and dark themes. Example:", 16 | "$compare_colourCell": "Color entire cell:", 17 | "$adjustColours_title": "Adjust Colors", 18 | "$settings_notificationDotColour": "Notification Dot Colors", 19 | "$statusBorder_description": "Color code the right border of activities by media status", 20 | "$submenu_favourites": "Favorites", 21 | "$characterBrowseTooltip": "Favorites", 22 | "$MAL_serialization": "Serialization", 23 | "$compare_normalizeRatings": "Normalize ratings:", 24 | "$setting_MALserial": "Add MAL serialization info to manga", 25 | "$forumCategory_14": "List Customization", 26 | "$CSSoldDarkTheme_description":"Use the old dark theme colors", 27 | "$dataSet_favorites": "Favorites" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/data/languages/JSON field details.txt: -------------------------------------------------------------------------------- 1 | The main JSON object has two field: "info", containing the metadata, and "keys", containing the translations. 2 | 3 | In "info": 4 | "language": The name of your language, in your language. Add parenthesis if you need to distinguish it as a variant of another language. 5 | "language_english": The name of your language, in English. Copy the value of "language" if your language doesn't have a name in English. 6 | (Optional) "variation_of": If you are adding a dialect of an existing translation, this should be the name of that translation. 7 | "fallback": A list, containing in descending order what languages the script should try to search for missing translations until it finds one. The last one should be "English". 8 | "maintainer": The name you wish to be addressed as. Does not have to be your real name. Your Anilist name or Github name are good candidates, but it doesn't have to be one of those. 9 | "maintainer_link": Were people can contact you. An anilist user page or a Github page are good candidates. But you can also use a nick on some chat platform if that's your preference. Can be left blank. Add "I do not wish to be contacted" + the same in your language if you really do not wish people to contact you. 10 | "discussion_link": Where people can discuss this translation. Use "https://github.com/hohMiyazawa/Automail/issues/69" if you don't have a dedicated place for discussion. 11 | (Optional) "notes": Anything. You can also create your own text file in the /src/data/languages directory if you have more detailed notes 12 | (Optional) "translators": A list of translators, if multiple people have contributed. But there can only be one maintainer. 13 | (Optional) "locale": a BCP 47 code identifying the language. Mostly the same as the ISO 639 two and three letter language codes. 14 | 15 | You are allowed to add additional fields in "info". 16 | 17 | In "keys": 18 | language keys, all starting with "$". You do not have to translate all the keys as the script will fall back to English or the dedicated fallback languages. 19 | 20 | 21 | Example: 22 | 23 | { 24 | "info": { 25 | "language": "Norsk", 26 | "language_english": "Norwegian", 27 | "locale": "nn-NO", 28 | "fallback": ["Svenska","English"], 29 | "maintainer": "hoh", 30 | "maintainer_link": "https://anilist.co/user/hoh/", 31 | "discussion_link": "https://github.com/hohMiyazawa/Automail/issues/69", 32 | "notes": "", 33 | "translators": ["hoh"] 34 | }, 35 | "keys": { 36 | "$logo_home": "Heim" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/data/languages/README.txt: -------------------------------------------------------------------------------- 1 | Q: How do I add a translation? 2 | 3 | A: You create a file named "yourlanguage.json" in this directory. Copy one of the existing translations to get the right structure. (or see "JSON field details.txt") 4 | To get it added to Automail I prefer pull requests (https://github.com/hohMiyazawa/Automail/), but just sending me a translation file is fine. 5 | 6 | 7 | Q: How do I activate my translation in the Automail code? 8 | 9 | A: I will happily do this for you, but if you want to do this yourself, you must add an entry near the top of the file in "/src/localisation.js", and a settings option near the bottom of "/src/data/legacyModuleDescriptions.json". 10 | 11 | 12 | Q: What tools do I need? 13 | 14 | A: Just a simple text editor, that is, something without formatting. (think notepad) 15 | 16 | 17 | Q: Do I have to translate all the keys? 18 | 19 | A: No. Any key you leave out will have the default English text, or text from one of the fallback languages. 20 | 21 | 22 | Q: How can I find out what keys I'm missing? 23 | 24 | A: There's a tool called "diff_tool.html" in this directory, which you can open in your browser to compare translation files. 25 | 26 | 27 | Q: How do fallback languages work? 28 | 29 | A: If your translation file is missing a key, the script will go through the languages listed in the "fallback" field in order. 30 | If one of those translations has the key, it will use that. If none of those have it, it will use the default English text. 31 | Fallbacks are useful if an existing translation is close to your language. 32 | 33 | 34 | Q: Do I have to be a native speaker? 35 | 36 | A: You do not. 37 | 38 | 39 | Q: What can I do if I only know English? 40 | 41 | A: You can proofread the English language file ("/src/data/English.json), as I'm not a native speaker and likely made many mistakes. 42 | 43 | 44 | Q: is not a real language! It's a dialect/version of ! 45 | 46 | A: That's not a problem. It's up to the individual translators if making the translation is worth their time. 47 | 48 | 49 | Q: Should I translate keys that are exactly the same in my language (Like "Format" in English is "Format" in my language) 50 | 51 | A: Yes. 52 | 53 | 54 | Q: In what order are languages ordered? 55 | 56 | A: The order is determined by the list at the bottom of /src/data/legacyModuleDescriptions.json 57 | The order should be the following 58 | 1. English, the default Anilist language 59 | 2. Translations to other languages 60 | - As a main rule, languages are ordered by the *completeness* of the translation, most complete first 61 | - A possible measure of "completeness" is the number of keys translated. 62 | - If multiple languages are complete, the complete translations should be ordered as Japanese first, then alphabetically by English name. 63 | 3. English variations 64 | 4. $raw_keys translator mode file 65 | -------------------------------------------------------------------------------- /src/data/languages/SouthernSami.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "language": "Åarjelsaemie", 4 | "language_english": "Southern Sami", 5 | "locale": "sma", 6 | "fallback": ["Norsk","Svenska","English"], 7 | "maintainer": "hoh", 8 | "maintainer_link": "https://anilist.co/user/hoh/", 9 | "discussion_link": "", 10 | "notes": "" 11 | }, 12 | "keys": { 13 | "$button_search": "Ohtsh", 14 | "$settings_experimental_suffix": "[PRYÖVENASSE]", 15 | "$settings_partialLocalisationLanguage_description": "Automailen gïele", 16 | "$stats_moreStats_title": "Vielie Deahpadimmieh", 17 | "$stats_siteStats_title": "Sijjien Deahpadimmieh", 18 | "$stats_longestTime": "{1} {0}% lea", 19 | "$stats_mostCommonScore": "Sïejhmemes: ", 20 | "$stats_name": "Nomme", 21 | "$settings_title": "Automailen bïjre", 22 | "$settings_version": "Versjovne: ", 23 | "$settings_homepage": "Gaskeviermesne: ", 24 | "$settings_category_Feeds": "Galkijh", 25 | "$settings_category_Newly Added": "Orre", 26 | "$notification_likeActivity_1person_1activity": " dov aatem lyjhki.", 27 | "$notification_likeActivity_1person_Mactivity": " dov aath lyjhki.", 28 | "$notification_likeActivity_2person_1activity": " dov aatem lyjhkigan.", 29 | "$notification_likeActivity_2person_Mactivity": " dov aath lyjhkigan.", 30 | "$notification_likeActivity_Mperson_1activity": " dov aatem lyjhkin.", 31 | "$notification_likeActivity_Mperson_Mactivity": " dov aath lyjhkin.", 32 | "$notification_message": " prieviem seedti.", 33 | "$menu_home": "gåetie", 34 | "$menu_profile": "manne", 35 | "$menu_animelist": "animelæstoe", 36 | "$menu_mangalist": "mangalæstoe", 37 | "$menu_browse": "ohtsh", 38 | "$menu_forum": "digkie", 39 | "$filters_year": "Jaepie", 40 | "$markdown_help_images_header": "Guvvieh", 41 | "$markdown_help_infixOr": "jallh", 42 | "$preview_animeSection_title": "Dov Anime", 43 | "$preview_mangaSection_title": "Dov Manga", 44 | "$preview_airingSection_title": "Saadtegh", 45 | "$profile_title": "{0}en sæjroe", 46 | "$recs_forYou": "Dutnjien", 47 | "$colour_transparent": "Tjaetsie", 48 | "$colour_blue": "Plaave", 49 | "$colour_white": "Veelkes", 50 | "$colour_black": "Tjeehpes", 51 | "$colour_red": "Rööpses", 52 | "$colour_peach": "Peersika", 53 | "$colour_orange": "Rööps-viskes", 54 | "$colour_yellow": "Viskes", 55 | "$colour_green": "Kruana", 56 | "$mediaFormat_ONE_SHOT" : "Oktegh" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/data/languages/Swedish.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "language": "Svenska", 4 | "language_english": "Swedish", 5 | "locale": "sv-SE", 6 | "fallback": ["Norsk","English"], 7 | "maintainer": "no maintainer", 8 | "maintainer_link": "", 9 | "discussion_link": "", 10 | "notes": "This translation is not maintained. Want to contribute?" 11 | }, 12 | "keys": { 13 | "$time_1second": "1 sekund sedan", 14 | "$time_Msecond": "{0} sekund sedan", 15 | "$time_1minute": "1 minut sedan", 16 | "$time_Mminute": "{0} minut sedan", 17 | "$time_1hour": "1 timme sedan", 18 | "$time_Mhour": "{0} timmar sedan", 19 | "$time_1day": "1 dag sedan", 20 | "$time_Mday": "{0} dagar sedan", 21 | "$time_1week": "1 vecka sida", 22 | "$time_Mweek": "{0} veckor sedan", 23 | "$time_1month": "1 månad sedan", 24 | "$time_Mmonth": "{0} månadar sedan", 25 | "$time_1year": "1 år sedan", 26 | "$time_Myear": "{0} år sedan", 27 | "$settings_homepage": "Hemesida: ", 28 | "$settings_repository": "Källkod: ", 29 | "$button_search": "Sök", 30 | "$mediaStatus_dropped": "droppad", 31 | "$language_English": "engelska", 32 | "$language_German": "tyska", 33 | "$language_Italian": "italienska", 34 | "$language_Spanish": "spanska", 35 | "$language_French": "franska", 36 | "$language_Korean": "koreanska", 37 | "$language_Portuguese": "portugisiska", 38 | "$language_Hebrew": "hebreiska", 39 | "$language_Hungarian": "ungerska", 40 | "$language_Chinese": "kinesiska", 41 | "$language_Japanese": "japanska", 42 | "$language_Arabic": "arabiska", 43 | "$language_Filipino": "filipino", 44 | "$language_Catalan": "katalanska", 45 | "$language_Polish": "polska", 46 | "$language_Norwegian": "norska", 47 | "$mediaFormat_NOVEL": "Lättroman" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/data/languages/diff_tool.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Translation diff tool

8 |

Load two translation files to find untranslated keys

9 | 10 | 11 |

12 | 		
59 | 	
60 | 
61 | 


--------------------------------------------------------------------------------
/src/data/languages/raw_keys.json:
--------------------------------------------------------------------------------
 1 | {
 2 | 	"info": {
 3 | 		"language": "$raw_keys",
 4 | 		"language_english": "$raw_translator_keys",
 5 | 		"locale": "",
 6 | 		"fallback": [],
 7 | 		"maintainer": "(translator mode)",
 8 | 		"maintainer_link": "",
 9 | 		"discussion_link": "https://github.com/hohMiyazawa/Automail/issues/69",
10 | 		"notes": "This is a special language file, used to see raw keys in the UI. DO NOT USE THIS FILE AS A TEMPLATE FOR YOUR OWN TRANSLATION FILE (use English.json, or Norwegian.json, or read the documentation)",
11 | 		"translators": ["(translator mode)"]
12 | 	},
13 | 	"keys": {}
14 | }
15 | 


--------------------------------------------------------------------------------
/src/data/languages/special_keys.txt:
--------------------------------------------------------------------------------
1 | The English language file does not contain keys of the form "$role_something".
2 | These are used on staff pages to translate roles, but since those are already in English, there's no need to translate them into English.
3 | But if you want those roles translated into your language, you need to add keys like "$role_Story & Art" and "$role_Director".
4 | There's a ton of those roles, so just adding the most common ones is good enough
5 | 


--------------------------------------------------------------------------------
/src/data/sequel_scripts/.eslintrc.json:
--------------------------------------------------------------------------------
 1 | {
 2 |     "env": {
 3 |         "node": true
 4 |     },
 5 |     "parserOptions": {
 6 |         "ecmaVersion": "latest",
 7 |         "sourceType": "module"
 8 |     }
 9 | }
10 | 


--------------------------------------------------------------------------------
/src/data/sequel_scripts/api.mjs:
--------------------------------------------------------------------------------
 1 | let apiResetLimit;
 2 | 
 3 | /**
 4 |  * Constructs and sends a request to the AniList GraphQL API
 5 |  * @param {string} query - A GraphQL query string.
 6 |  * @param {object} [variables] - GraphQL variables.
 7 |  * @returns {Promise} Response data from the API.
 8 |  */
 9 | async function anilistAPI(query, variables){
10 | 	if(apiResetLimit){
11 | 		if(Date.now() < apiResetLimit*1000){
12 | 			return {
13 | 				"data": null,
14 | 				"errors": [{"message": "Too Many Requests.","status": 429}]
15 | 			};
16 | 		}
17 | 		apiResetLimit = undefined;
18 | 	}
19 | 	//const req = new Request(");
20 | 	try{
21 | 		const res = await fetch("https://graphql.anilist.co", {
22 | 			method: "POST",
23 | 			headers: {"Content-Type": "application/json"},
24 | 			body: JSON.stringify({query, variables})
25 | 		});
26 | 		const data = await res.json();
27 | 		if(res.status === 429){
28 | 			apiResetLimit = res.headers.has("x-ratelimit-reset") ? res.headers.get("x-ratelimit-reset") : (Date.now()+60*1000)/1000;
29 | 		}
30 | 		return data;
31 | 	}
32 | 	catch(e){
33 | 		console.error(e)
34 | 		if(e.message === "NetworkError when attempting to fetch resource."){
35 | 			apiResetLimit = (Date.now()+60*1000)/1000;
36 | 		}
37 | 		return {
38 | 			"data": null,
39 | 			"errors": [{"message": e,"status": null}]
40 | 		};
41 | 	}
42 | }
43 | 
44 | export { anilistAPI, apiResetLimit }
45 | 


--------------------------------------------------------------------------------
/src/data/sequel_scripts/fuzzyDateCompare.mjs:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Returns an INDEX, not to be used for sorting. That is, "-1" means they are equal.
 3 |  * @param {object} first
 4 |  * @param {object} second
 5 |  * @returns {number}
 6 |  */
 7 | export function fuzzyDateCompare(first,second){
 8 | 	if(!first.year || !second.year){
 9 | 		return -1
10 | 	}
11 | 	if(first.year > second.year){
12 | 		return 0
13 | 	}
14 | 	else if(first.year < second.year){
15 | 		return 1
16 | 	}
17 | 	if(!first.month || !second.month){
18 | 		return -1
19 | 	}
20 | 	if(first.month > second.month){
21 | 		return 0
22 | 	}
23 | 	else if(first.month < second.month){
24 | 		return 1
25 | 	}
26 | 	if(!first.day || !second.day){
27 | 		return -1
28 | 	}
29 | 	if(first.day > second.day){
30 | 		return 0
31 | 	}
32 | 	else if(first.day < second.day){
33 | 		return 1
34 | 	}
35 | 	return -1
36 | }
37 | 


--------------------------------------------------------------------------------
/src/data/sequel_scripts/getAnimeSequels.mjs:
--------------------------------------------------------------------------------
 1 | import { anilistAPI, apiResetLimit } from "./api.mjs";
 2 | import { fuzzyDateCompare } from "./fuzzyDateCompare.mjs";
 3 | import { readFile, writeFile } from "fs/promises";
 4 | import { fileURLToPath } from "url";
 5 | import { dirname, join } from "path";
 6 | 
 7 | const __dirname = dirname(fileURLToPath(import.meta.url));
 8 | const args = process.argv;
 9 | const maxPage = args[2] || null;
10 | const sequels = new Set();
11 | const animeQuery = `
12 | query($page: Int){
13 | 	Page(page: $page){
14 | 		pageInfo{
15 | 			currentPage
16 | 			hasNextPage
17 | 		}
18 | 		media(type: ANIME, sort: START_DATE_DESC){
19 | 			id
20 | 			startDate{year month day}
21 | 			relations{
22 | 				edges{
23 | 					relationType(version:2)
24 | 					node{
25 | 						type
26 | 						startDate{year month day}
27 | 					}
28 | 				}
29 | 			}
30 | 		}
31 | 	}
32 | }`;
33 | 
34 | async function getAnime(){
35 | 	let nextPage = true;
36 | 	let page = 1;
37 | 	const pageProgress = setInterval(() => {
38 | 		process.stdout.write(`Page processed: ${page}\r`);
39 | 	}, 1000);
40 | 	console.log("Starting API search")
41 | 	/* eslint-disable no-await-in-loop */
42 | 	do{
43 | 		const data = await anilistAPI(animeQuery, {page});
44 | 		if(data.errors){
45 | 			if(data.errors.some(thing => thing.status === 429)){
46 | 				const wait = Math.max(1000, Date.now() - apiResetLimit*1000);
47 | 				await new Promise(res => {setTimeout(res, wait)})
48 | 			}
49 | 			else{
50 | 				console.error(errors)
51 | 			}
52 | 		}
53 | 		else{
54 | 			data.data.Page.media.forEach(entry => {
55 | 				const isSequel = entry.relations.edges.some(rel => {
56 | 					if(rel.node.type === "MANGA"){
57 | 						return false
58 | 					}
59 | 					let sequel = rel.relationType === "PREQUEL" || rel.relationType === "PARENT";
60 | 					const compare = fuzzyDateCompare(entry.startDate,rel.node.startDate);
61 | 					if(compare === 1){
62 | 						sequel = false;
63 | 					}
64 | 					return sequel;
65 | 				});
66 | 				if(isSequel && ![101517,69625,30336,37776,104271,95,82,96].includes(entry.id)){
67 | 					sequels.add(entry.id)
68 | 				}
69 | 			})
70 | 			nextPage = data.data.Page.pageInfo.hasNextPage === true && !maxPage || data.data.Page.pageInfo.currentPage < maxPage;
71 | 			if(nextPage){
72 | 				page = data.data.Page.pageInfo.currentPage + 1;
73 | 				await new Promise(res => {setTimeout(res, (Math.floor(Math.random()*3)+1)*1000)})
74 | 			}
75 | 		}
76 | 	}
77 | 	while(nextPage)
78 | 	/* eslint-enable no-await-in-loop */
79 | 	clearInterval(pageProgress)
80 | 	console.log("Completed API search")
81 | }
82 | 
83 | async function init(){
84 | 	await getAnime()
85 | 	const filePath = join(__dirname, "../sequels.json");
86 | 	const sequelFile = await readFile(filePath, "utf8");
87 | 	console.log("Reading anime sequels file");
88 | 	const fileSet = new Set(JSON.parse(sequelFile));
89 | 	//const diff =Array.from(sequels).filter(seq => !fileSet.has(seq)));
90 | 	//await writeFile("diff.json", JSON.stringify(diff), "utf8");
91 | 	const combinedSet = new Set([...fileSet, ...sequels]);
92 | 	await writeFile(filePath, JSON.stringify(Array.from(combinedSet)), "utf8");
93 | 	console.log("Updated anime sequels file")
94 | }
95 | init()
96 | 


--------------------------------------------------------------------------------
/src/data/sequel_special_cases.txt:
--------------------------------------------------------------------------------
 1 | Added:
 2 | 	9260 kizu movie 1.
 3 | 	131573 Jujutsu kaisen 0
 4 | 	114129 Gintama out of order
 5 | 	21875 prequel movie
 6 | 	21220 out of order release
 7 | 	4107 tengen topp
 8 | 	393 escaflowne movie
 9 | 	1089 macross movie
10 | 	1096 patlabor 2
11 | 	44 prequel OVA
12 | 	15689 monogatari
13 | 	114317 prequel movie
14 | 	21158 free prequel
15 | 	4059 clannad side entry
16 | 	985 dbz
17 | 	893 db
18 | 	502 db
19 | 	984 dbz
20 | 	3371 logh
21 | 	1170 slayers
22 | 	1010 ranma
23 | 	1379 kino
24 | 	441 utena movie
25 | 	20159 pokemon
26 | 	16916 special
27 | 	13411 special
28 | 	20811 aot
29 | 	21624 steinsgate
30 | 	6351 clannad
31 | 	21386 opm
32 | 	21326 tokyo ghoul prequel
33 | 	11701 another
34 | 	20779 prequel
35 | 	13851 ova
36 | 	5252 one piece
37 | 	101695 ova
38 | 	7472 gintama
39 | 	20593 hanamonogatari
40 | 	20918 tsukimonogatari
41 | 	112300 raisha
42 | 	21509 danganropa
43 | 	21825 danganropa
44 | 	105596 gundam recap
45 | 	102622 gundam prologue
46 | 	114841 gundam commercial
47 | 	5051 macross commercial
48 | 	129549 macross rec
49 | 	4454 macross alt
50 | 	5310 macross mov
51 | 	4939 macross fluff
52 | 	101126 macross mov
53 | 
54 | Removed
55 | 	101517 jujutsu kaisen 0
56 | 	69625 kagerou days
57 | 	30336 GTO
58 | 	37776 railgun
59 | 	104271 abimusang
60 | 	95 turn a gundam
61 | 	82 war in the pocket
62 | 	96 g gundam
63 | 


--------------------------------------------------------------------------------
/src/data/sequels_manga.json:
--------------------------------------------------------------------------------
1 | [85199,103255,85522,137620,55515,87178,52651,97842,99549,114768,112673,103884,35157,30743,127231,86508,107267,91203,115166,48200,85617,87217,33008,31630,33006,30872,85216,52519,30081,85666,53751,86640,108817,102490,30092,30070,33403,30029,85151,47915,34941,30932,66903,118730,85611,31706,33009,104203,45242,103192,51151,111406,101863,103586,102423,98777,31260,112518,96626,33573,43277,98232,137278,69565,81857,118991,119767,31709,87296,81855,64053,85814,33574,71865,120796,86276,86569,30648,77363,39547,103252,49423,30027,103029,51499,99886,53900,54875,87232,87259,133104,114328,44115,138072,37760,116180,86637,124283,46081,74111,101864,114329,126445,95821,117802,61853,33526,103180,85604,85457,31258,86056,51498,86691,100774,46144,44531,30770,53661,129117,104896,109455,66131,126408,74227,30704,86652,31262,114528,98030,135129,143701,143726]
2 | 


--------------------------------------------------------------------------------
/src/data/titlecaseRomaji.json:
--------------------------------------------------------------------------------
 1 | [
 2 | ["/anime/1535/","Death Note"],
 3 | ["/anime/11061/","Hunter×Hunter (2011)"],
 4 | ["/anime/20/","Naruto"],
 5 | ["/anime/1735/","Naruto: Shippuuden"],
 6 | ["/anime/21/","One Piece"],
 7 | ["/anime/2167/","Clannad"],
 8 | ["/anime/4059/","Clannad: Mou Hitotsu no Sekai, Tomoyo-hen"],
 9 | ["/anime/6351/","Clannad: After Story - Mou Hitotsu no Sekai, Kyou-hen"],
10 | ["/anime/4181/","Clannad: After Story"],
11 | ["/anime/105333/","Dr. Stone"],
12 | ["/anime/6702/","Fairy Tail"],
13 | ["/anime/8074/","Highschool of the Dead"],
14 | ["/anime/9515/","Highschool of the Dead - Drifters of the Dead"],
15 | ["/anime/10793/","Guilty Crown"],
16 | ["/anime/13411/","Guilty Crown: Lost Christmas"],
17 | ["/anime/13561/","Guilty Crown: 4-koma Gekijou"],
18 | ["/anime/12419/","Guilty Crown Kiseki: Reassortment"],
19 | ["/anime/13601/","Psycho-Pass"],
20 | ["/anime/20513/","Psycho-Pass 2"],
21 | ["/anime/100388/","Banana Fish"],
22 | ["/anime/107660/","Beastars"],
23 | ["/anime/114194/","Beastars 2"],
24 | ["/anime/136880/","Beastars 3"],
25 | ["/anime/19/","Monster"],
26 | ["/anime/32/","Shin Seiki Evangelion: The End of Evangelion"],
27 | ["/anime/97980/","Re:Creators"],
28 | ["/anime/889/","Black Lagoon"],
29 | ["/anime/110349/","Great Pretender"],
30 | ["/anime/20812/","Shirobako"],
31 | ["/anime/20938/","Shirobako Specials"],
32 | ["/anime/101574/","Shirobako Movie"],
33 | ["/anime/16870/","The Last: Naruto the Movie"],
34 | ["/anime/21123/","Drifters"],
35 | ["/anime/97988/","Drifters OVA"],
36 | ["/anime/20607/","Ping Pong the Animation"],
37 | ["/anime/110350/","ID: Invaded"],
38 | ["/anime/140960/","Spy×Family"],
39 | ["/anime/142838/","Spy×Family Part 2"],
40 | ["/anime/101348/","Vinland Saga"],
41 | ["/anime/136430/","Vinland Saga Season 2"],
42 | ["/anime/6/","Trigun"],
43 | ["/anime/151040/","Trigun Stampede"],
44 | ["/manga/30124/","Aqua"],
45 | ["/manga/30081/","Aria"],
46 | ["/anime/477/","Aria the Animation"],
47 | ["/anime/962/","Aria the Natural"],
48 | ["/anime/5244/","Aria the Natural: Sono Futatabi Deaeru Kiseki ni..."],
49 | ["/anime/2563/","Aria the OVA: Arietta"],
50 | ["/anime/3297/","Aria the Origination"],
51 | ["/anime/5196/","Aria the Origination Picture Drama"],
52 | ["/anime/4772/","Aria the Origination: Sono Choppiri Himitsu no Basho ni..."],
53 | ["/anime/21043/","Aria the Avvenire"],
54 | ["/anime/117556/","Aria the Crepuscolo"],
55 | ["/anime/130558/","Aria the Benedizione"],
56 | ["/anime/320/","Kite"],
57 | ["/anime/47/","Akira"],
58 | ["/manga/30001/","Monster"],
59 | ["/manga/30011/","Naruto"],
60 | ["/manga/30012/","Bleach"],
61 | ["/manga/30013/","One Piece"],
62 | ["/manga/30021/","Death Note"],
63 | ["/manga/30026/","Hunter×Hunter"],
64 | ["/manga/30149/","Blame!"],
65 | ["/manga/30598/","Fairy Tail"],
66 | ["/manga/30664/","Akira"],
67 | ["/manga/30745/","Pluto"],
68 | ["/manga/98587/","Beastars"],
69 | ["/manga/98416/","Dr. Stone"],
70 | ["/manga/108556/","Spy×Family"],
71 | ["/manga/114960/","Mashle"],
72 | ["/manga/85603/","Psycho-Pass"]
73 | ]
74 | 


--------------------------------------------------------------------------------
/src/localisation.js:
--------------------------------------------------------------------------------
 1 | //begin "localisation.js"
 2 | const languageFiles = {//key: language name in language ("日本語"), filename: language name in English ("Japanese.json")
 3 | 	"English": m4_include(data/languages/English.json),
 4 | 	"Français": m4_include(data/languages/French.json),
 5 | 	"Português": m4_include(data/languages/Portuguese.json),
 6 | 	"Deutsch": m4_include(data/languages/German.json),
 7 | 	"日本語": m4_include(data/languages/Japanese.json),
 8 | 	"Italiano": m4_include(data/languages/Italian.json),
 9 | 	"Türkçe": m4_include(data/languages/Turkish.json),
10 | 	"Åarjelsaemie": m4_include(data/languages/SouthernSami.json),
11 | 	"Norsk": m4_include(data/languages/Norwegian.json),
12 | 	"Svenska": m4_include(data/languages/Swedish.json),
13 | 	"English (US)": m4_include(data/languages/English_US.json),
14 | 	"English (CA)": m4_include(data/languages/English_CA.json),
15 | 	"English (short)": m4_include(data/languages/English_short.json),
16 | 	"Español": m4_include(data/languages/Spanish.json),
17 | 	"$raw_keys": m4_include(data/languages/raw_keys.json)
18 | }
19 | Object.keys(languageFiles["English"].keys).forEach(key => {
20 | 	languageFiles["$raw_keys"].keys[key] = key
21 | })
22 | 
23 | function translate(key,subs,fallback){
24 | 	if(key[0] !== "$"){
25 | 		return key
26 | 	}
27 | 	let immediate = languageFiles[useScripts.partialLocalisationLanguage].keys[key];
28 | 	if(!immediate){
29 | 		for(let i=0;i {
51 | 				immediate = immediate.replace("{" + i + "}",sub)
52 | 			})
53 | 		}
54 | 		else{
55 | 			immediate = immediate.replace("{0}",subs)
56 | 		}
57 | 	}
58 | 	return immediate
59 | }
60 | 
61 | if(!languageFiles[useScripts.partialLocalisationLanguage]){
62 | 	let candidates = []
63 | 	Object.keys(languageFiles).forEach(key => {
64 | 		if(key.includes(useScripts.partialLocalisationLanguage)){
65 | 			candidates.push(key)
66 | 		}
67 | 	})
68 | 	if(candidates.length){
69 | 		alert("No \"" + useScripts.partialLocalisationLanguage +"\" language file for " + script_type + " found. Setting language to \"English\"\nPossible candidates: " + candidates.map(a => "\"" + a + "\"").join(",") +"\nYou can change this in the settings")
70 | 	}
71 | 	else{
72 | 		alert("No \"" + useScripts.partialLocalisationLanguage +"\" language file for " + script_type + " found. Setting language to \"English\"\nYou can change this in the settings")
73 | 	}
74 | 	useScripts.partialLocalisationLanguage = "English";
75 | 	useScripts.save()
76 | }
77 | //end "localisation.js"
78 | 


--------------------------------------------------------------------------------
/src/makefile:
--------------------------------------------------------------------------------
 1 | SHELL := /bin/bash
 2 | EXECUTABLES = sed rm mkdir date zip m4 cp cat
 3 | K := $(foreach exec,$(EXECUTABLES),\
 4 | 	$(if $(shell which $(exec)),some string,$(error "Could not find the dependency '$(exec)'. If you are unable to install it, there are complete builds of Automail at https://github.com/hohMiyazawa/Automail/releases or https://greasyfork.org/en/scripts/370473-automail")))
 5 | 
 6 | all: pre-build build build/automail.user.js build/firefox_addon.zip post-build
 7 | 
 8 | pre-build:
 9 | 	rm -f build/userModules.js
10 | 
11 | build/automail.user.js: automail.m4 settings.js alias.js css/global.css conditionalStyles.js utilities.js purify.js graphql.js localisation.js controller.js build/userModules.js HOWTO.js
12 | 	m4 --prefix-builtins automail.m4 > build/automail.user.js
13 | 	date +"%s" | sed 's_^_//Automail built at _' >> build/automail.user.js
14 | 
15 | # build/boneless.js: boneless.m4 settings.js alias.js css/global.css conditionalStyles.js utilities.js purify.js graphql.js localisation.js controller.js build/userModules.js HOWTO.js
16 | #	m4 --prefix-builtins boneless.m4 > build/boneless.js
17 | #	sed -i 's/Automail/Boneless/' build/boneless.js
18 | #	sed -i 's/automail/boneless/' build/boneless.js
19 | #	date +"%s" | sed 's_^_//Boneless built at _' >> build/boneless.js
20 | 
21 | build/userModules.js: modules
22 | 	for module in modules/*; do\
23 | 	    echo "//begin $$module";\
24 | 	    cat "$$module";\
25 | 	    echo "//end $$module";\
26 | 	done > build/userModules.js
27 | 
28 | build:
29 | 	mkdir build
30 | 
31 | build/firefox_addon.zip: build/automail.user.js ../icons manifest.json
32 | 	$(info )
33 | 	$(info Creating Firefox addon)
34 | 	cd build && cp -rp ../../icons/ icons/ && cp -p ../manifest.json . && cp -p automail.user.js automail.js && \
35 | 	zip -FS firefox_addon.zip -r automail.js icons/ manifest.json && rm -fr icons manifest.json automail.js
36 | 
37 | post-build:
38 | 	$(info )
39 | 	$(info Automail build completed)
40 | 	$(info The compiled script is in /src/build/)
41 | 


--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
 1 | {
 2 | 	"manifest_version": 2,
 3 | 	"name": "Automail",
 4 | 	"version": "10.4.6",
 5 | 
 6 | 	"description": "Extra parts for Anilist.co",
 7 | 
 8 | 	"author": "hoh",
 9 | 
10 | 	"homepage_url": "https://github.com/hohMiyazawa/Automail",
11 | 
12 | 	"icons": {
13 | 		"2": "icons/automail-2.png",
14 | 		"3": "icons/automail-3.png",
15 | 		"8": "icons/automail-8.png",
16 | 		"16": "icons/automail-16.png",
17 | 		"48": "icons/automail-48.png",
18 | 		"96": "icons/automail-96.png",
19 | 		"128": "icons/automail-128.png",
20 | 		"256": "icons/automail.svg"
21 | 	},
22 | 
23 | 	"content_scripts": [
24 | 		{
25 | 			"matches": ["https://anilist.co/*"],
26 | 			"js": ["automail.js"]
27 | 		}
28 | 	],
29 | 	"permissions": [
30 | 		"*://graphql.anilist.co/*",
31 | 		"*://myanimelist.net/*",
32 | 		"webRequest"
33 | 	]
34 | }
35 | 


--------------------------------------------------------------------------------
/src/modules/ALbuttonReload.js:
--------------------------------------------------------------------------------
 1 | if(useScripts.ALbuttonReload){
 2 | 	let logo = document.querySelector("#nav .logo");
 3 | 	if(logo){
 4 | 		logo.onclick = function(){
 5 | 			if(/\/home\/?$/.test(location.pathname)){//we only want this behaviour here
 6 | 				window.location.reload(false);//reload page, but use cache if possible
 7 | 			}
 8 | 		}
 9 | 	}
10 | }
11 | 
12 | exportModule({
13 | 	id: "ALbuttonReload",
14 | 	description: "$ALbuttonReload_description",
15 | 	isDefault: true,
16 | 	categories: ["Navigation"],
17 | 	visible: true
18 | })
19 | 


--------------------------------------------------------------------------------
/src/modules/accessTokenWarning.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "accessTokenWarning",
 3 | 	description: "$accessTokenWarning_description",
 4 | 	isDefault: false,
 5 | 	importance: 0,
 6 | 	categories: ["Login","Script"],
 7 | 	visible: true
 8 | })
 9 | 
10 | if(useScripts.accessTokenWarning && !useScripts.accessToken){
11 | 	accessTokenRetractedInfo()
12 | }
13 | 


--------------------------------------------------------------------------------
/src/modules/addBrowseFilters.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "browseFilters",
 3 | 	description: "$browseFilters_description",
 4 | 	isDefault: true,
 5 | 	categories: ["Browse"],
 6 | 	urlMatch: function(url){
 7 | 		return /^https:\/\/anilist\.co\/search\/(anime|manga)/.test(url);
 8 | 	},
 9 | 	code: function(){
10 | 		const customSorts = {
11 | 			TITLE_ROMAJI: "Title ↑",
12 | 			TITLE_ROMAJI_DESC: "Title ↓",
13 | 			POPULARITY_DESC: "Popularity ↓",
14 | 			POPULARITY: "Popularity ↑",
15 | 			SCORE_DESC: "Average Score ↓",
16 | 			SCORE: "Average Score ↑",
17 | 			TRENDING_DESC: "Trending",
18 | 			FAVOURITES_DESC: "Favorites",
19 | 			ID_DESC: "Date Added",
20 | 			START_DATE_DESC: "Release Date ↓",
21 | 			START_DATE: "Release Date ↑"
22 | 		};
23 | 		const customAnime = {EPISODES_DESC: "Episodes ↓", EPISODES: "Episodes ↑", DURATION_DESC: "Duration ↓", DURATION: "Duration ↑"};
24 | 		const customManga = {CHAPTERS_DESC: "Chapters ↓", CHAPTERS: "Chapters ↑", VOLUMES_DESC: "Volumes ↓", VOLUMES: "Volumes ↑"};
25 | 		const sorts = document.querySelector(".sort-wrap.sort-select");
26 | 		function addSorts(){
27 | 			const type = location.pathname.match(/^\/search\/(anime|manga)/)[1];
28 | 			Object.keys(sorts.__vue__.sortOptions).forEach(key => delete sorts.__vue__.sortOptions[key])
29 | 			Object.assign(sorts.__vue__.sortOptions, customSorts, type === "anime" ? customAnime : customManga)
30 | 		}
31 | 		setTimeout(addSorts,200);
32 | 	}
33 | })
34 | 


--------------------------------------------------------------------------------
/src/modules/addDblclickZoom.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "dblclickZoom",
 3 | 	description: "$dblclickZoom_description",
 4 | 	extendedDescription: "$dblclickZoom_extendedDescription",
 5 | 	isDefault: false,
 6 | 	importance: -1,
 7 | 	categories: ["Feeds"],
 8 | 	visible: true,
 9 | 	urlMatch: function(url,oldUrl){
10 | 		return location.pathname.match(/^\/home\/?$/)
11 | 	},
12 | 	code: function(){
13 | 		function addDblclickZoom(){
14 | 			if(!location.pathname.match(/^\/home\/?$/)){
15 | 				return
16 | 			}
17 | 			let activityFeedWrap = document.querySelector(".activity-feed-wrap");
18 | 			if(!activityFeedWrap){
19 | 				setTimeout(addDblclickZoom,200);
20 | 				return
21 | 			}
22 | 			activityFeedWrap.addEventListener("dblclick",function(e){
23 | 				e = e || window.event;
24 | 				let target = e.target || e.srcElement;
25 | 				while(target.classList){
26 | 					if(target.classList.contains("activity-entry")){
27 | 						target.classList.toggle("hohZoom");
28 | 						break
29 | 					}
30 | 					target = target.parentNode
31 | 				}  
32 | 			},false)
33 | 		}
34 | 	},
35 | 	css: `
36 | .hohZoom{
37 | 	transform: scale(1.5);
38 | 	transform-origin: 0 0;
39 | 	transition: transform 0.4s;
40 | 	z-index: 200;
41 | 	box-shadow: 5px 5px 5px black;
42 | }
43 | .hohZoom .reply-wrap{
44 | 	background: rgb(var(--color-background));
45 | }`
46 | })
47 | 


--------------------------------------------------------------------------------
/src/modules/addFeedFilters_user.js:
--------------------------------------------------------------------------------
 1 | function addFeedFilters_user(){
 2 | 	if(!/^https:\/\/anilist\.co\/user/.test(document.URL)){
 3 | 		return
 4 | 	}
 5 | 	let activityFeed = document.querySelector(".activity-feed");
 6 | 	if(!activityFeed){
 7 | 		setTimeout(addFeedFilters_user,100);
 8 | 		return
 9 | 	}
10 | 	if(activityFeed.classList.contains("hohTranslated")){
11 | 		return
12 | 	}
13 | 	activityFeed.classList.add("hohTranslated");
14 | 	let postTranslator = function(){
15 | 		Array.from(activityFeed.children).forEach(activity => {
16 | 			try{
17 | 				let timeElement = activity.querySelector(".time time");
18 | 				if(timeElement && !timeElement.classList.contains("hohTimeGeneric")){
19 | 					let seconds = new Date(timeElement.dateTime).valueOf()/1000;
20 | 					let replacement = nativeTimeElement(seconds);
21 | 					timeElement.style.display = "none";
22 | 					replacement.style.position = "relative";
23 | 					replacement.style.right = "unset";
24 | 					replacement.style.top = "unset";
25 | 					timeElement.parentNode.insertBefore(replacement, timeElement)
26 | 				}
27 | 			}
28 | 			catch(e){
29 | 				console.warn("time element translation is broken")
30 | 			}
31 | 		})
32 | 	}
33 | 	let mutationConfig = {
34 | 		attributes: false,
35 | 		childList: true,
36 | 		subtree: false
37 | 	};
38 | 	let observer = new MutationObserver(function(){
39 | 		if(useScripts.additionalTranslation && useScripts.partialLocalisationLanguage !== "English"){
40 | 			postTranslator()
41 | 		}
42 | 	});
43 | 	observer.observe(activityFeed,mutationConfig);
44 | 	let observerObserver = new MutationObserver(function(){
45 | 		activityFeed = document.querySelector(".activity-feed");
46 | 		if(activityFeed){
47 | 			observer.disconnect();
48 | 			observer = new MutationObserver(function(){
49 | 				postRemover();
50 | 				if(useScripts.additionalTranslation && useScripts.partialLocalisationLanguage !== "English"){
51 | 					postTranslator()
52 | 				}
53 | 			});
54 | 			observer.observe(activityFeed,mutationConfig);
55 | 		}
56 | 	});
57 | 	observerObserver.observe(activityFeed,mutationConfig);
58 | 	if(useScripts.additionalTranslation && useScripts.partialLocalisationLanguage !== "English"){
59 | 		postTranslator()
60 | 	}
61 | }
62 | 


--------------------------------------------------------------------------------
/src/modules/addFollowCount.js:
--------------------------------------------------------------------------------
 1 | async function addFollowCount(){
 2 | 	let URLstuff = location.pathname.match(/^\/user\/(.*)\/social/)
 3 | 	if(!URLstuff){
 4 | 		return
 5 | 	}
 6 | 	const userData = await anilistAPI("query($name:String){User(name:$name){id}}", {
 7 | 		variables: {name: decodeURIComponent(URLstuff[1])},
 8 | 		cacheKey: "hohIDlookup" + decodeURIComponent(URLstuff[1]).toLowerCase(),
 9 | 		duration: 5*60*1000
10 | 	});
11 | 	if(userData.errors){
12 | 		return
13 | 	}
14 | 	//these two must be separate calls, because they are allowed to fail individually (too many followers)
15 | 	const followerData = await anilistAPI("query($id:Int!){Page(perPage:1){pageInfo{total} followers(userId:$id){id}}}", {
16 | 		variables: {id:userData.data.User.id}
17 | 	});
18 | 	const followingData = await anilistAPI("query($id:Int!){Page(perPage:1){pageInfo{total} following(userId:$id){id}}}", {
19 | 		variables: {id:userData.data.User.id}
20 | 	});
21 | 	const insertCount = function(data, id, pos){
22 | 		const target = document.querySelector(".filter-group");
23 | 		if(target){
24 | 			target.style.position = "relative";
25 | 			let followCount = "65536+";
26 | 			if(!data.errors){
27 | 				followCount = data.data.Page.pageInfo.total
28 | 			}
29 | 			create("span",[id,"hohCount"],followCount,target.children[pos]);
30 | 		}
31 | 	}
32 | 	insertCount(followerData, "#hohFollowersCount", 2)
33 | 	insertCount(followingData, "#hohFollowingCount", 1)
34 | 	return
35 | }
36 | 


--------------------------------------------------------------------------------
/src/modules/addForumMedia.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "addForumMedia",
 3 | 	description: "$forumMedia_backlink",
 4 | 	isDefault: true,
 5 | 	importance: -1,
 6 | 	categories: ["Forum","Navigation"],
 7 | 	visible: false,
 8 | 	urlMatch: function(url,oldUrl){
 9 | 		return url.includes("https://anilist.co/forum/recent?media=")
10 | 	},
11 | 	code: async function(){
12 | 		let id = parseInt(document.URL.match(/\d+$/)[0]);
13 | 		let adder = function(data){
14 | 			if(!document.URL.includes(id) || !data){
15 | 				return
16 | 			}
17 | 			let feed = document.querySelector(".feed");
18 | 			if(!feed){
19 | 				setTimeout(function(){adder(data)},200);
20 | 				return
21 | 			}
22 | 			data.data.Media.id = id;
23 | 			let mediaLink = create("a",false,titlePicker(data.data.Media),false,"padding:10px;display:block;");
24 | 			mediaLink.href = data.data.Media.siteUrl;
25 | 			cheapReload(mediaLink,{path: mediaLink.pathname})
26 | 			if(data.data.Media.siteUrl.includes("manga") && useScripts.CSSgreenManga){
27 | 				mediaLink.style.color = "rgb(var(--color-green))"
28 | 			}
29 | 			else{
30 | 				mediaLink.style.color = "rgb(var(--color-blue))"
31 | 			}
32 | 			feed.insertBefore(mediaLink,feed.firstChild);
33 | 		}
34 | 		const data = await anilistAPI("query($id:Int){Media(id:$id){title{native english romaji} siteUrl}}", {
35 | 			variables: {id},
36 | 			cacheKey: "hohMediaLookup" + id,
37 | 			duration: 30*60*1000
38 | 		})
39 | 		if(data.errors){
40 | 			return
41 | 		}
42 | 		adder(data)
43 | 		return
44 | 	}
45 | })
46 | 


--------------------------------------------------------------------------------
/src/modules/addForumMediaTitle.js:
--------------------------------------------------------------------------------
 1 | async function addForumMediaTitle(){
 2 | 	if(location.pathname !== "/home"){
 3 | 		return
 4 | 	}
 5 | 	// Forum previews may contain multiple categories but only show the first one
 6 | 	let forumThreads = Array.from(document.querySelectorAll(".home .forum-wrap .thread-card .categories span:first-child .category"));
 7 | 	if(!forumThreads.length){
 8 | 		setTimeout(addForumMediaTitle,200);
 9 | 		return;
10 | 	}
11 | 	if(forumThreads.some(
12 | 		thread => thread && ["anime","manga"].includes(thread.innerText.toLowerCase())
13 | 	)){
14 | 		const {data, errors} = await anilistAPI("query{Page(perPage:3){threads(sort:REPLIED_AT_DESC){title mediaCategories{id title{romaji native english}}}}}");
15 | 		if(errors){
16 | 			return
17 | 		}
18 | 		if(location.pathname !== "/home"){
19 | 			return
20 | 		}
21 | 		data.Page.threads.forEach((thread,index) => {
22 | 			if(thread.mediaCategories.length && ["anime","manga"].includes(forumThreads[index].innerText.toLowerCase())){
23 | 				let title = titlePicker(thread.mediaCategories[0]);
24 | 				if(title.length > 40){
25 | 					forumThreads[index].title = title;
26 | 					title = title.slice(0,35) + "…";
27 | 				}
28 | 				forumThreads[index].innerText = title;
29 | 			}
30 | 		})
31 | 	}
32 | 	return
33 | }
34 | 


--------------------------------------------------------------------------------
/src/modules/addImageFallback.js:
--------------------------------------------------------------------------------
 1 | function addImageFallback(){
 2 | 	if(!document.URL.match(/(\/home|\/user\/)/)){
 3 | 		return
 4 | 	}
 5 | 	setTimeout(addImageFallback,1000);
 6 | 	let mediaImages = document.querySelectorAll(".media-preview-card:not(.hohFallback) .content .title");
 7 | 	mediaImages.forEach(cover => {
 8 | 		cover.parentNode.parentNode.classList.add("hohFallback");
 9 | 		if(cover.parentNode.parentNode.querySelector(".hohFallback")){
10 | 			return
11 | 		}
12 | 		let fallback = create("span","hohFallback",cover.textContent,cover.parentNode.parentNode);
13 | 		if(useScripts.titleLanguage === "ROMAJI"){
14 | 			fallback.textContent = cover.textContent;
15 | 		}
16 | 	})
17 | }
18 | 


--------------------------------------------------------------------------------
/src/modules/addMediaReviewConfidence.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "addMediaReviewConfidence",
 3 | 	description: "$addMediaReviewConfidence_description",
 4 | 	isDefault: true,
 5 | 	categories: ["Media"],
 6 | 	visible: true,
 7 | 	urlMatch: function(url){
 8 | 		return /^https:\/\/anilist\.co\/(anime|manga)\/[0-9]+\/(.*\/)?reviews/.test(url)
 9 | 	},
10 | 	code: function(){
11 | 		const [,id] = location.pathname.match(/^\/(?:anime|manga)\/([0-9]+)\/(.*\/)?reviews/)
12 | 		const query = `
13 | query media($id: Int, $page: Int) {
14 | 	Media(id: $id) {
15 | 		reviews(page: $page, sort: [RATING_DESC, ID]) {
16 | 			pageInfo {
17 | 				total
18 | 				perPage
19 | 				hasNextPage
20 | 			}
21 | 			nodes {
22 | 				id
23 | 				rating
24 | 				ratingAmount
25 | 			}
26 | 		}
27 | 	}
28 | }
29 | `
30 | 		let pageCount = 0;
31 | 		let reviewCount = 0;
32 | 
33 | 		const addConfidence = async function(){
34 | 			pageCount++
35 | 			const {data, errors} = await anilistAPI(query, {
36 | 				variables: {id, page: pageCount},
37 | 				cacheKey: "recentMediaReviews" + id + "Page" + pageCount,
38 | 				duration: 30*60*1000
39 | 			})
40 | 			if(errors){
41 | 				return;
42 | 			}
43 | 			const adder = function(){
44 | 				const reviewWrap = document.querySelector(".media-reviews .review-wrap");
45 | 				if(!reviewWrap){
46 | 					setTimeout(adder,200);
47 | 					return;
48 | 				}
49 | 				data.Media.reviews.nodes.forEach(review => {
50 | 					reviewCount++
51 | 					const wilsonLowerBound = wilson(review.rating,review.ratingAmount).left
52 | 					const extraScore = create("span",false,"~" + Math.round(100*wilsonLowerBound));
53 | 					extraScore.style.color = "hsl(" + wilsonLowerBound*120 + ",100%,50%)";
54 | 					extraScore.style.marginRight = "3px";
55 | 					const findParent = function(){
56 | 						const parent = reviewWrap.querySelector('[href="/review/' + review.id + '"] .votes');
57 | 						if(!parent){
58 | 							setTimeout(findParent,200);
59 | 							return;
60 | 						}
61 | 						parent.insertBefore(extraScore,parent.firstChild);
62 | 						if(wilsonLowerBound < 0.05){
63 | 							reviewWrap.children[reviewCount - 1].style.opacity = "0.5"
64 | 						}
65 | 					}; findParent();
66 | 				})
67 | 				return;
68 | 			};adder();
69 | 		}
70 | 		addConfidence()
71 | 
72 | 		const checkMore = function(){
73 | 			const loadMore = document.querySelector(".media-reviews .load-more");
74 | 			if(!loadMore){
75 | 				setTimeout(checkMore,200);
76 | 				return;
77 | 			}
78 | 			loadMore.addEventListener("click", addConfidence)
79 | 		};checkMore();
80 | 	},
81 | 	css: `
82 | 	.media-reviews .review-wrap .review-card .summary {
83 | 		margin-bottom: 15px;
84 | 	}
85 | 	`
86 | })
87 | 


--------------------------------------------------------------------------------
/src/modules/addMyThreadsLink.js:
--------------------------------------------------------------------------------
 1 | function addMyThreadsLink(){
 2 | 	if(!document.URL.match(/^https:\/\/anilist\.co\/forum\/?(overview|search\?.*|recent|new|subscribed)?$/)){
 3 | 		return
 4 | 	}
 5 | 	if(document.querySelector(".hohMyThreads")){
 6 | 		return
 7 | 	}
 8 | 	let target = document.querySelector(".filters");
 9 | 	if(!target){
10 | 		setTimeout(addMyThreadsLink,100)
11 | 	}
12 | 	else{
13 | 		create("a",["hohMyThreads","link"],translate("$myThreads_link"),target)
14 | 			.href = "https://anilist.co/user/" + whoAmI + "/social#my-threads"
15 | 	}
16 | }
17 | 


--------------------------------------------------------------------------------
/src/modules/addProgressBar.js:
--------------------------------------------------------------------------------
 1 | function addProgressBar(){
 2 | 	if(location.pathname !== "/home"){
 3 | 		return
 4 | 	}
 5 | 	let mediaCards = document.querySelectorAll(".media-preview-card .content .info:not(.hasMeter) > div");
 6 | 	if(!mediaCards.length){
 7 | 		setTimeout(function(){
 8 | 			addProgressBar()
 9 | 		},200);//may take some time to load
10 | 		return
11 | 	}
12 | 	mediaCards.forEach(card => {
13 | 		const progressInformation = card.innerText.match(/Progress: (\d+)\/(\d+)/);
14 | 		if(progressInformation){
15 | 			let pBar = create("meter");
16 | 			pBar.value = progressInformation[1];
17 | 			pBar.min = 0;
18 | 			pBar.max = progressInformation[2];
19 | 			card.parentNode.insertBefore(pBar,card);
20 | 			card.parentNode.parentNode.parentNode.querySelector(".plus-progress").onclick = function(){
21 | 				pBar.value++;
22 | 				setTimeout(function(){
23 | 					pBar.value = card.innerText.match(/Progress: (\d+)\/(\d+)/)[1]
24 | 				},1000)
25 | 			}
26 | 		}
27 | 	});
28 | 	if(document.querySelector(".size-toggle")){
29 | 		document.querySelector(".size-toggle").onclick = function(){
30 | 			setTimeout(function(){
31 | 				addProgressBar()
32 | 			},200);
33 | 		}
34 | 	}
35 | }
36 | 


--------------------------------------------------------------------------------
/src/modules/addReviewConfidence.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "reviewConfidence",
 3 | 	description: "$reviewConfidence_description",
 4 | 	isDefault: true,
 5 | 	categories: ["Browse"],
 6 | 	visible: true,
 7 | 	urlMatch: function(url){
 8 | 		return /^https:\/\/anilist\.co\/reviews/.test(url)
 9 | 	},
10 | 	code: function(){
11 | 		let pageCount = 0;
12 | 		const adultContent = userObject ? userObject.options.displayAdultContent : false;
13 | 
14 | 		const addReviewConfidence = async function(){
15 | 			pageCount++
16 | 			const {data, errors} = await anilistAPI("query($page:Int){Page(page:$page,perPage:30){reviews(sort:ID_DESC){id rating ratingAmount}}}", {
17 | 				variables: {page: pageCount},
18 | 				cacheKey: "hohRecentReviewsPage" + pageCount,
19 | 				duration: 30*1000,
20 | 				auth: adultContent // api doesn't return reviews for adult content unless authed + have the option enabled
21 | 			})
22 | 			if(errors){
23 | 				return;
24 | 			}
25 | 			const locationForIt = document.querySelector(".recent-reviews");
26 | 			if(!locationForIt){
27 | 				return;
28 | 			}
29 | 			const reviewWrap = locationForIt.querySelector(".review-wrap") || await watchElem(".review-wrap", locationForIt);
30 | 			data.Page.reviews.forEach(async (review) => {
31 | 				const wilsonLowerBound = wilson(review.rating,review.ratingAmount).left
32 | 				const extraScore = create("span","wilson","~" + Math.round(100*wilsonLowerBound));
33 | 				extraScore.style.color = "hsl(" + wilsonLowerBound*120 + ",100%,50%)";
34 | 				extraScore.style.marginRight = "3px";
35 | 				const votes = `[href="/review/${review.id}"] .votes`;
36 | 				const parent = reviewWrap.querySelector(votes) || await watchElem(votes, reviewWrap);
37 | 				if(parent.querySelector(".wilson")){
38 | 					return;
39 | 				}
40 | 				parent.insertBefore(extraScore,parent.firstChild);
41 | 				if(wilsonLowerBound < 0.05){
42 | 					parent.parentNode.parentNode.style.opacity = "0.5" // dim review-card
43 | 				}
44 | 				return;
45 | 			})
46 | 			return;
47 | 		}
48 | 
49 | 		const checkMore = async function(){
50 | 			const container = document.querySelector(".recent-reviews");
51 | 			if(!container){
52 | 				return;
53 | 			}
54 | 			const loadMore = container.querySelector(".load-more") || await watchElem(".load-more", container);
55 | 			addReviewConfidence()
56 | 			loadMore.addEventListener("click", () => {
57 | 				addReviewConfidence()
58 | 				checkMore() // a different load more button is created, so the listener needs to be reattached
59 | 			})
60 | 			return;
61 | 		};checkMore();
62 | 	},
63 | 	css: `
64 | 	.recent-reviews .review-wrap .review-card .summary {
65 | 		margin-bottom: 15px;
66 | 	}
67 | 	`
68 | })
69 | 


--------------------------------------------------------------------------------
/src/modules/addSocialThemeSwitch.js:
--------------------------------------------------------------------------------
 1 | function addSocialThemeSwitch(){
 2 | 	let URLstuff = location.pathname.match(/^\/user\/(.*)\/social/)
 3 | 	if(!URLstuff){
 4 | 		return
 5 | 	}
 6 | 	if(document.querySelector(".filters .hohThemeSwitch")){
 7 | 		return
 8 | 	}
 9 | 	let target = document.querySelector(".filters");
10 | 	if(!target){
11 | 		setTimeout(addSocialThemeSwitch,100);
12 | 		return;
13 | 	}
14 | 	let themeSwitch = create("div",["theme-switch","hohThemeSwitch"],false,target,"width:70px;");
15 | 	let listView = create("span",false,false,themeSwitch);
16 | 	let cardView = create("span","active",false,themeSwitch);
17 | 	listView.appendChild(svgAssets2.listView.cloneNode(true));
18 | 	cardView.appendChild(svgAssets2.cardView.cloneNode(true));
19 | 	listView.onclick = function(){
20 | 		document.querySelector(".hohThemeSwitch .active").classList.remove("active");
21 | 		listView.classList.add("active");
22 | 		document.querySelector(".user-social").classList.add("listView");
23 | 	}
24 | 	cardView.onclick = function(){
25 | 		document.querySelector(".hohThemeSwitch .active").classList.remove("active");
26 | 		cardView.classList.add("active");
27 | 		document.querySelector(".user-social.listView").classList.remove("listView");
28 | 	}
29 | }
30 | 


--------------------------------------------------------------------------------
/src/modules/addStudioBrowseSwitch.js:
--------------------------------------------------------------------------------
 1 | function addStudioBrowseSwitch(){
 2 | 	let URLstuff = location.pathname.match(/^\/studio\//)
 3 | 	if(!URLstuff){
 4 | 		return
 5 | 	}
 6 | 	if(document.querySelector(".studio-page-unscoped .hohThemeSwitch")){
 7 | 		return
 8 | 	}
 9 | 	let target = document.querySelector(".studio-page-unscoped");
10 | 	if(!target){
11 | 		setTimeout(addStudioBrowseSwitch,100);
12 | 		return;
13 | 	}
14 | 	let themeSwitch = create("div",["theme-switch","hohThemeSwitch"],false,target);
15 | 	target.classList.add("cardView");
16 | 	let listView = create("span",false,false,themeSwitch);
17 | 	listView.title = "List View";
18 | 	let cardView = create("span","active",false,themeSwitch);
19 | 	cardView.title = "Card View";
20 | 	listView.appendChild(svgAssets2.bigListView.cloneNode(true));
21 | 	cardView.appendChild(svgAssets2.compactView.cloneNode(true));
22 | 	cardView.onclick = function(){
23 | 		document.querySelector(".hohThemeSwitch .active").classList.remove("active");
24 | 		cardView.classList.add("active");
25 | 		target.classList.add("cardView");
26 | 		target.classList.remove("listView");
27 | 	}
28 | 	listView.onclick = function(){
29 | 		document.querySelector(".hohThemeSwitch .active").classList.remove("active");
30 | 		listView.classList.add("active");
31 | 		target.classList.remove("cardView");
32 | 		target.classList.add("listView");
33 | 	}
34 | }
35 | 


--------------------------------------------------------------------------------
/src/modules/altBanner.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "altBanner",
 3 | 	description: "$altBanner_description",
 4 | 	extendedDescription: "$altBanner_extendedDescription",
 5 | 	isDefault: false,
 6 | 	importance: 0,
 7 | 	categories: ["Media","Newly Added"],
 8 | 	visible: true,
 9 | 	urlMatch: function(url){
10 | 		return /^https:\/\/anilist\.co\/(anime|manga)\/.*/.test(url)
11 | 	},
12 | 	code: function(){
13 | 		let adder = function(mutations,observer){
14 | 			let pNode = document.querySelector(".media .header-wrap");
15 | 			if(!pNode){
16 | 				setTimeout(adder,200);
17 | 				return
18 | 			}
19 | 			if(pNode.childNodes[0] && pNode.childNodes[0].nodeType === 8){
20 | 				return
21 | 			}
22 | 			let banner = pNode.querySelector(".banner");
23 | 			if(!banner && !observer){
24 | 				let mutationConfig = {
25 | 					attributes: false,
26 | 					childList: true,
27 | 					subtree: false
28 | 				}
29 | 				let observer = new MutationObserver(adder);
30 | 				observer.observe(pNode,mutationConfig);
31 | 				return
32 | 			}
33 | 			else if(!banner){
34 | 				return
35 | 			}
36 | 			observer && observer.disconnect();
37 | 			banner.classList.add("blur-filter");
38 | 			let bannerFull = document.querySelector(".altBanner") || create("img","altBanner",null,banner);
39 | 			bannerFull.height = "400";
40 | 			bannerFull.src = banner.style.backgroundImage.replace("url(","").replace(")","").replace('"',"").replace('"',"")
41 | 		}
42 | 		adder()
43 | 	},
44 | 	css: `
45 | 	.media .header-wrap .banner{
46 | 		margin-top: 0px !important;
47 | 		position: relative;
48 | 		z-index: -2;
49 | 	}
50 | 	.blur-filter::after{
51 | 		backdrop-filter: blur(10px);
52 | 		content: "";
53 | 		display: block;
54 | 		position: absolute;
55 | 		width: 100%;
56 | 		height: 100%;
57 | 		top: 0;
58 | 		z-index: -2;
59 | 	}
60 | 	.altBanner{
61 | 		position: absolute;
62 | 		top: 0;
63 | 		left: 50%;
64 | 		transform: translate(-50%);
65 | 		z-index: -1;
66 | 	}
67 | 	`
68 | })
69 | 


--------------------------------------------------------------------------------
/src/modules/autoLogin.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	boneless_disable: true,
 3 | 	id: "autoLogin",
 4 | 	description: "$autoLogin_description",
 5 | 	extendedDescription: `
 6 | Normally, ${script_type} will stay signed in even if you close your browser.
 7 | 
 8 | However, if you have all persistant storage turned off, that's not possible.
 9 | To use features that requires an Anilist login, you will normally have to click the "sign in" link on the settings page each time.
10 | 
11 | In those cases, this module tries to automatically sign in when first visiting the page, potentially saving you a few clicks.
12 | 
13 | IMPORTANT DETAILS FOR THIS MODULE TO WORK!
14 | 
15 | This module is off by default. In some cases of non-persistance storage, ${script_type} will always load at default settings, thus checking this checkbox will do absolutely nothing.
16 | To change the defaults:
17 | 
18 | Option a) When building from source edit "src/modules/autoLogin.js" so "isDefault" is set to true
19 | 
20 | Option b) Manually add your access token in the file "src/settings.js". It's a field in the "useScripts" object.
21 | 
22 | Option c) If you just have the compiled JS file "${script_type.toLowerCase()}.js", search for "I EAT PANCAKES" in the code, and change "isDefault" line below to true
23 | `,
24 | 	isDefault: false,
25 | 	categories: ["Script"],
26 | 	visible: true
27 | })
28 | 


--------------------------------------------------------------------------------
/src/modules/betterReviewRatings.js:
--------------------------------------------------------------------------------
 1 | function betterReviewRatings(){
 2 | 	if(!location.pathname.match(/\/home/)){
 3 | 		return
 4 | 	}
 5 | 	let reviews = document.querySelectorAll(".review-card .el-tooltip.votes");
 6 | 	if(!reviews.length){
 7 | 		setTimeout(betterReviewRatings,500);
 8 | 		return;
 9 | 	}
10 | 	// Basic idea: read the rating info from the tooltips to avoid an API call.
11 | 	document.body.classList.add("TMPreviewScore");//add a temporary class, which makes all tooltips
12 | 	reviews.forEach(likeElement => {//trigger creation of the tooltips (they don't exist before hover)
13 | 		likeElement.dispatchEvent(new Event("mouseenter"));
14 | 		likeElement.dispatchEvent(new Event("mouseleave"));
15 | 		//bonus: add some alias and localisation
16 | 		let showId;
17 | 		if(likeElement.parentNode.previousElementSibling && likeElement.parentNode.previousElementSibling.classList.contains("banner")){//unreliable: they load separately. But better than nothing
18 | 			let possibleRefId = likeElement.parentNode.previousElementSibling.style.backgroundImage.match(/banner\/n?(\d+)-/);
19 | 			if(possibleRefId){
20 | 				showId = parseInt(possibleRefId[1])
21 | 			}
22 | 		}
23 | 		if(useScripts.partialLocalisationLanguage !== "English" || aliases.has(showId)){
24 | 			let elements = likeElement.previousElementSibling.previousElementSibling.textContent.match(/Review of (.+) by (.+)$/);
25 | 			if(elements){
26 | 				likeElement.previousElementSibling.previousElementSibling.childNodes[0].textContent
27 | 					= translate(
28 | 						"$review_reviewTitle",[
29 | 							titlePicker({id: showId, title: {romaji: elements[1]}}),
30 | 							elements[2]
31 | 						]
32 | 					)
33 | 			}
34 | 		}
35 | 	});
36 | 	setTimeout(function(){//give anilist some time to generate them
37 | 		reviews.forEach(likeElement => {
38 | 			let likeExtra = document.getElementById(likeElement.attributes["aria-describedby"].value);
39 | 			if(likeExtra){
40 | 				let matches = likeExtra.innerText.match(/(\d+) out of (\d+)/);
41 | 				if(matches){
42 | 					likeElement.childNodes[1].textContent += "/" + matches[2];
43 | 					if(useScripts.additionalTranslation){
44 | 						likeExtra.childNodes[0].textContent = translate("$reviewLike_tooltip",[matches[1],matches[2]])
45 | 					}
46 | 				}
47 | 			}
48 | 			likeElement.style.bottom = "4px";
49 | 			likeElement.style.right = "7px";
50 | 		})
51 | 		document.body.classList.remove("TMPreviewScore");//make tooltips visible again
52 | 	},200);
53 | }
54 | 


--------------------------------------------------------------------------------
/src/modules/browseSubmenu.js:
--------------------------------------------------------------------------------
 1 | if(useScripts.browseSubmenu && useScripts.CSSverticalNav && whoAmI && !useScripts.mobileFriendly){
 2 | 	let addMouseover = function(){
 3 | 		let navThingy = document.querySelector(`.nav .links .browse-wrap`);
 4 | 		if(navThingy){
 5 | 			navThingy.classList.add("subMenuContainer");
 6 | 			let subMenu = create("div","hohSubMenu",false,navThingy);
 7 | 
 8 | 			[
 9 | 				{
10 | 					text: "$submenu_anime",
11 | 					href: "/search/anime",
12 | 					vue: { name: 'Search', params: {type:'anime'}}
13 | 				},
14 | 				{
15 | 					text: "$submenu_manga",
16 | 					href: "/search/manga",
17 | 					vue: { name: 'Search', params: {type:'manga'}}
18 | 				},
19 | 				{
20 | 					text: "$submenu_staff",
21 | 					href: "/search/staff",
22 | 					vue: { name: 'Search', params: {type:'staff'}}
23 | 				},
24 | 				{
25 | 					text: "$submenu_characters",
26 | 					href: "/search/characters",
27 | 					vue: { name: 'Search', params: {type:'characters'}}
28 | 				},
29 | 				{
30 | 					text: "$submenu_reviews",
31 | 					href: "/reviews",
32 | 					vue: { name: 'Reviews'}
33 | 				},
34 | 				{
35 | 					text: "$submenu_recommendations",
36 | 					href: "/recommendations",
37 | 					vue: { name: 'Recommendations'}
38 | 				}
39 | 			].forEach(link => {
40 | 				let element = create("a","hohSubMenuLink",translate(link.text),subMenu);
41 | 				element.href = link.href;
42 | 				if(link.vue){
43 | 					element.onclick = function(){
44 | 						try{
45 | 							document.getElementById('app').__vue__._router.push(link.vue);
46 | 							return false
47 | 						}
48 | 						catch(e){
49 | 							console.warn("vue routes are outdated!")
50 | 						}
51 | 					}
52 | 				}
53 | 			})
54 | 			navThingy.onmouseenter = function(){
55 | 				subMenu.style.display = "inline"
56 | 			}
57 | 			navThingy.onmouseleave = function(){
58 | 				subMenu.style.display = "none"
59 | 			}
60 | 		}
61 | 		else{
62 | 			setTimeout(addMouseover,500)
63 | 		}
64 | 	};addMouseover()
65 | }
66 | 


--------------------------------------------------------------------------------
/src/modules/cencorMediaPage.js:
--------------------------------------------------------------------------------
 1 | function cencorMediaPage(id){
 2 | 	if(!location.pathname.match(/^\/(anime|manga)/)){
 3 | 		return
 4 | 	}
 5 | 	let possibleLocation = document.querySelectorAll(".tags .tag .name");
 6 | 	if(possibleLocation.length){
 7 | 		if(possibleLocation.some(
 8 | 			tag => badTags.some(
 9 | 				bad => tag.innerText.toLowerCase().includes(bad)
10 | 			)
11 | 		)){
12 | 			let content = document.querySelector(".page-content");
13 | 			if(content){
14 | 				content.classList.add("hohCencor")
15 | 			}
16 | 		}
17 | 	}
18 | 	else{
19 | 		setTimeout(() => {cencorMediaPage(id)},200)
20 | 	}
21 | }
22 | 


--------------------------------------------------------------------------------
/src/modules/character.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "characterFavouriteCount",
 3 | 	description: "Add an exact favourite count to character pages",
 4 | 	isDefault: true,
 5 | 	categories: ["Media"],
 6 | 	visible: false,
 7 | 	urlMatch: function(url){
 8 | 		return /^https:\/\/anilist\.co\/character(\/.*)?/.test(url)
 9 | 	},
10 | 	code: async function(){
11 | 		const charWrap = document.querySelector(".character");
12 | 		const favWrap = charWrap.querySelector(".favourite") || await watchElem(".favourite", charWrap);
13 | 		const favCount = favWrap.querySelector(".count") || await watchElem(".count", favWrap);
14 | 		if(!favCount){
15 | 			return;
16 | 		}
17 | 		if(!isNaN(favCount.textContent)){
18 | 			return; // abort early since the site already displays exact fav count if under 1000
19 | 		}
20 | 		const favCallback = function(data){
21 | 			favWrap.onclick = function(){
22 | 				if(favWrap.classList.contains("isFavourite")){
23 | 					favCount.textContent = parseInt(favCount.textContent) - 1;
24 | 				}
25 | 				else{
26 | 					favCount.textContent = parseInt(favCount.textContent) + 1;
27 | 				}
28 | 			};
29 | 			if(data.Character.favourites){
30 | 				favCount.textContent = data.Character.favourites;
31 | 			}
32 | 		};
33 | 		const query = `query($id: Int!){
34 | 			Character(id: $id){
35 | 				favourites
36 | 			}
37 | 		}`;
38 | 		const variables = {id: parseInt(location.pathname.match(/\/character\/(\d+)\/?/)[1])};
39 | 		const {data, errors} = await anilistAPI(query, {
40 | 			variables,
41 | 			cacheKey: "hohCharacterFavs" + variables.id,
42 | 			duration: 60*60*1000
43 | 		});
44 | 		if(errors){
45 | 			return;
46 | 		}
47 | 		return favCallback(data);
48 | 	}
49 | })
50 | 


--------------------------------------------------------------------------------
/src/modules/characterBrowse.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "characterBrowseFavouriteCount",
 3 | 	description: "Add favourite counts to character browse pages",
 4 | 	isDefault: true,
 5 | 	categories: ["Browse"],
 6 | 	visible: false,
 7 | 	urlMatch: function(url){
 8 | 		return /^https:\/\/anilist\.co\/search\/characters\/?(favorites)?$/.test(url)
 9 | 	},
10 | 	code: function(){
11 | 		let pageCount = 0;
12 | 		let perPage = 30;
13 | 		const query = `
14 | query($page: Int!,$perPage: Int!){
15 | 	Page(page: $page,perPage: $perPage){
16 | 		characters(sort: [FAVOURITES_DESC]){
17 | 			id
18 | 			favourites
19 | 		}
20 | 	}
21 | }`;
22 | 		const results = document.querySelector(".landing-section.characters > .results, .results.cover");
23 | 		let charCount = results.childElementCount;
24 | 
25 | 		const insertFavs = function(data){
26 | 			const chars = data.Page.characters;
27 | 			chars.forEach((character,index) => create(
28 | 				"span",
29 | 				"hohFavCountBrowse",
30 | 				character.favourites,
31 | 				results.children[(pageCount - 1)*chars.length + index]
32 | 			).title = translate("$characterBrowseTooltip"));
33 | 		}
34 | 
35 | 		const getFavs = async function(){
36 | 			pageCount++
37 | 			const {data, errors} = await anilistAPI(query, {
38 | 				variables: {page: pageCount, perPage}
39 | 			})
40 | 			if(errors){
41 | 				return;
42 | 			}
43 | 			return insertFavs(data);
44 | 		}
45 | 
46 | 		if(!/\/search\/characters\/?$/.test(location.pathname)){ // full favorites page
47 | 			perPage = 20;
48 | 			new MutationObserver((_mutations) => {
49 | 				if(results.childElementCount !== charCount && results.childElementCount % 20 === 0){
50 | 					charCount = results.childElementCount;
51 | 					getFavs();
52 | 				}
53 | 			}).observe(results, { subtree: true, childList: true })
54 | 		}
55 | 
56 | 		getFavs();
57 | 	},
58 | 	css: `
59 | .hohFavCountBrowse{
60 | 	color: rgb(var(--color-text-lighter));
61 | 	position: absolute;
62 | 	right: 2px;
63 | 	font-size: 60%;
64 | 	opacity: 0.7;
65 | 	top: -10px;
66 | }`
67 | })
68 | 


--------------------------------------------------------------------------------
/src/modules/clickableActivityHistory.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "clickableActivityHistory",
 3 | 	description: "Displays activities for an entry in the activity history",
 4 | 	isDefault: true,
 5 | 	categories: ["Navigation","Profiles"],
 6 | 	visible: false,
 7 | 	urlMatch: function(url,oldUrl){
 8 | 		return url.match(/\/user\/[^/]+\/?$/);
 9 | 	},
10 | 	code: function(){
11 | 		if(!useScripts.termsFeed){
12 | 			return
13 | 		}
14 | 		let waiter = function(){
15 | 			let activityHistory = document.querySelector(".activity-history");
16 | 			if(!activityHistory){
17 | 				setTimeout(waiter,1000);
18 | 				return
19 | 			}
20 | 			activityHistory.onclick = function(event){
21 | 				let target = event.target;
22 | 				if(target && target.classList.contains("history-day")){
23 | 					if(target.classList.contains("lv-0")){
24 | 						return
25 | 					}
26 | 					let offset = 1;
27 | 					while(target.nextSibling){
28 | 						offset++;
29 | 						target = target.nextSibling
30 | 					}
31 | 					let presentDayPresentTime = (new Date()).valueOf();
32 | 					presentDayPresentTime = new Date(presentDayPresentTime.valueOf() - offset * 24*60*60*1000);
33 | 					let year = presentDayPresentTime.getUTCFullYear();
34 | 					let month = presentDayPresentTime.getUTCMonth() + 1;
35 | 					let day = presentDayPresentTime.getUTCDate();
36 | 					let hour = presentDayPresentTime.getUTCHours();
37 | 					if(hour + 9 > 23){
38 | 						day++
39 | 					}
40 | 					window.location.href = "https://anilist.co/terms?user=" + encodeURIComponent(document.querySelector("h1.name").innerText) + "&date=" + year + "-" + month + "-" + day
41 | 				}
42 | 			}
43 | 		};waiter()
44 | 	}
45 | })
46 | 


--------------------------------------------------------------------------------
/src/modules/directEditorAccess.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "directListAccess",
 3 | 	description: "$directListAccess_description",
 4 | 	extendedDescription: "$directListAccess_extendedDescription",
 5 | 	isDefault: false,
 6 | 	importance: 0,
 7 | 	categories: ["Feeds"],
 8 | 	visible: true,
 9 | 	urlMatch: function(url,oldUrl){
10 | 		return url === "https://anilist.co/home" || url.match(/^https:\/\/anilist\.co\/user\/(.*)\/$/)
11 | 	},
12 | 	code: function(){
13 | 		let adder = function(){
14 | 			if(document.querySelector(".activity-feed")){
15 | 				document.querySelector(".activity-feed").addEventListener("click",function(e){
16 | 					let tmp_target = e.target;
17 | 					if(!tmp_target.classList.contains("el-dropdown-menu__item--divided")){
18 | 						for(let i=0;i<4;i++){
19 | 							if(tmp_target.classList.contains("entry-dropdown")){
20 | 								let item = document.getElementById(tmp_target.children[0].getAttribute("aria-controls"));
21 | 								if(item){
22 | 									item.querySelector(".el-dropdown-menu__item--divided").click();
23 | 									item.hidden = true
24 | 								}
25 | 								break
26 | 							}
27 | 							else{
28 | 								tmp_target = tmp_target.parentNode
29 | 							}
30 | 						}
31 | 					}
32 | 				})
33 | 			}
34 | 			else{
35 | 				setTimeout(adder,2000)
36 | 			}
37 | 		};
38 | 		adder()
39 | 	}
40 | })
41 | 


--------------------------------------------------------------------------------
/src/modules/documentTitleManager.js:
--------------------------------------------------------------------------------
 1 | let mutated = false;
 2 | 
 3 | let titleObserver = new MutationObserver(mutations => {
 4 | 	if(mutated){
 5 | 		mutated = false;
 6 | 		return
 7 | 	}
 8 | 	let title = document.querySelector("head > title").textContent;
 9 | 	let titleMatch = title.match(/(.*)\s\((\d+)\)\s\((.*)\s\(\2\)\)(.*)/);//ugly nested paranthesis like "Tetsuwan Atom (1980) (Astro Boy (1980)) · AniList"
10 | 	if(titleMatch){
11 | 		//change to the form "Tetsuwan Atom (Astro Boy 1980) · AniList"
12 | 		document.title = titleMatch[1] + " (" + titleMatch[3] + " " + titleMatch[2] + ")" + titleMatch[4];
13 | 		mutated = true
14 | 	}
15 | 	let badApostropheMatch = title.match(/^(\S+?s)'s\sprofile(.*)/);
16 | 	if(badApostropheMatch){
17 | 		document.title = badApostropheMatch[1] + "' profile" + badApostropheMatch[2];
18 | 		mutated = true
19 | 	}
20 | 	let name = title.match(/^(\S+?)'s\sprofile(.*)/);
21 | 	if(name && useScripts.partialLocalisationLanguage !== "English" && translate("$profile_title","") !== "'s profile"){
22 | 		document.title = translate("$profile_title",name[1]);
23 | 		mutated = true
24 | 	}
25 | 	if(useScripts.additionalTranslation){
26 | 		[
27 | ["Home · AniList","$documentTitle_home"],
28 | ["Notifications · AniList","$documentTitle_notifications"],
29 | ["Forum - Anime & Manga Discussion · AniList","$documentTitle_forum"],
30 | ["App Settings · AniList","$documentTitle_appSettings"]
31 | 		].forEach(pair => {
32 | 			if(title === pair[0]){
33 | 				let translation = translate(pair[1]);
34 | 				if(translation !== pair[0]){
35 | 					document.title = translation
36 | 				}
37 | 			}
38 | 		})
39 | 	}
40 | 	if(useScripts.SFWmode && title !== "Table of Contents"){//innocent looking
41 | 		document.title = "Table of Contents";
42 | 		mutated = true
43 | 	}
44 | });
45 | if(document.title){
46 | 	titleObserver.observe(document.querySelector("head > title"),{subtree: true, characterData: true, childList: true })
47 | }
48 | 


--------------------------------------------------------------------------------
/src/modules/dubMarker.js:
--------------------------------------------------------------------------------
 1 | function dubMarker(){
 2 | 	if(!document.URL.match(/^https:\/\/anilist\.co\/anime\/.*/)){
 3 | 		return
 4 | 	}
 5 | 	if(document.getElementById("dubNotice")){
 6 | 		return
 7 | 	}
 8 | 	const variables = {
 9 | 		id: document.URL.match(/\/anime\/(\d+)\//)[1],
10 | 		page: 1,
11 | 		language: useScripts.dubMarkerLanguage.toUpperCase()
12 | 	};
13 | 	const query = `
14 | query($id: Int!, $type: MediaType, $page: Int = 1, $language: StaffLanguage){
15 | 	Media(id: $id, type: $type){
16 | 		characters(page: $page, sort: [ROLE], role: MAIN){
17 | 			edges {
18 | 				node{id}
19 | 				voiceActors(language: $language){language}
20 | 			}
21 | 		}
22 | 	}
23 | }`;
24 | 	let dubCallback = function(data){
25 | 		if(!document.URL.match(/^https:\/\/anilist\.co\/anime\/.*/)){
26 | 			return
27 | 		}
28 | 		let dubNoticeLocation = document.querySelector(".sidebar");
29 | 		if(!dubNoticeLocation){
30 | 			setTimeout(function(){
31 | 				dubCallback(data)
32 | 			},200);
33 | 			return
34 | 		}
35 | 		if(data.data.Media.characters.edges.reduce(
36 | 			(actors,a) => actors + a.voiceActors.length,0
37 | 		)){//any voice actors for this language?
38 | 			if(document.getElementById("dubNotice")){
39 | 				return
40 | 			}
41 | 			let dubNotice = create("p","#dubNotice",
42 | 				translate("$dubMarker_notice",translate("$language_" + useScripts.dubMarkerLanguage))
43 | 			);
44 | 			dubNoticeLocation.insertBefore(dubNotice,dubNoticeLocation.firstChild)
45 | 		}
46 | 	};
47 | 	generalAPIcall(query,variables,dubCallback,"hohDubInfo" + variables.id + variables.language)
48 | }
49 | 


--------------------------------------------------------------------------------
/src/modules/durationTooltip.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "durationTooltip",
 3 | 	description: "Adds media duration as a tooltip",
 4 | 	isDefault: true,
 5 | 	importance: -2,
 6 | 	categories: ["Media"],//what categories your module belongs in
 7 | 	visible: false,//trivial, can be turned on
 8 | 	urlMatch: function(url,oldUrl){
 9 | 		let urlStuff = url.match(/\/anime\/(\d+)\//);
10 | 		if(urlStuff){
11 | 			let urlStuff2 = oldUrl.match(/\/anime\/(\d+)\//);
12 | 			if(urlStuff2 && urlStuff[1] === urlStuff2[1]){
13 | 				return false
14 | 			}
15 | 			return true
16 | 		}
17 | 		return false
18 | 	},
19 | 	code: function(){
20 | 		let specials = {
21 | 			"721": "total 10 hours 38 minutes (13x25min, 24x12min + 1x25min)"//tutu
22 | 		};
23 | 		let waiter = function(){
24 | 			let urlStuff = document.URL.match(/\/anime\/(\d+)\//);
25 | 			if(!urlStuff){
26 | 				return
27 | 			}
28 | 			let side = document.querySelector(".sidebar > .data");
29 | 			if(!side){
30 | 				setTimeout(waiter,1000);
31 | 				return
32 | 			}
33 | 			let eps = null;
34 | 			let dur = null;
35 | 			let anchor = null;
36 | 			if(document.querySelector(".hohHasDurationTooltip")){
37 | 				document.querySelector(".hohHasDurationTooltip").title = ""
38 | 			}
39 | 			try{
40 | 				let found = false
41 | 				Array.from(side.children).forEach(child => {
42 | 					if(child.querySelector(".type")){
43 | 						if(["Episodes",translate("$dataSet_episodes")].includes(child.querySelector(".type").innerText)){
44 | 							eps = parseInt(child.querySelector(".value").innerText)
45 | 						}
46 | 						else if(["Duration","Episode Duration",translate("$dataSet_episodeDuration"),translate("$dataSet_duration")].includes(child.querySelector(".type").innerText)){
47 | 							anchor = child.querySelector(".value");
48 | 							found = true;
49 | 							let hours = parseInt((anchor.innerText.match(/(\d+) hours?/) || [null,"0"])[1]);
50 | 							let minutes = parseInt((anchor.innerText.match(/(\d+) mins?/) || [null,"0"])[1]);
51 | 							dur = hours * 60 + minutes
52 | 						}
53 | 					}
54 | 				})
55 | 				if(!found){
56 | 					setTimeout(waiter,1000);
57 | 					return
58 | 				}
59 | 				if(anchor && eps && dur){
60 | 					if(specials[urlStuff[1]]){
61 | 						anchor.title = specials[urlStuff[1]];
62 | 					}
63 | 					else{
64 | 						anchor.title = "total " + formatTime(eps*dur*60,"twoPart");
65 | 					}
66 | 					anchor.classList.add("hohHasDurationTooltip")
67 | 				}
68 | 			}
69 | 			catch(e){
70 | 				console.warn("failed to parse duration info")
71 | 			}
72 | 		};waiter()
73 | 	},
74 | })
75 | 


--------------------------------------------------------------------------------
/src/modules/embedHentai.js:
--------------------------------------------------------------------------------
 1 | function embedHentai(){
 2 | 	if(!document.URL.match(/^https:\/\/anilist\.co\/(home|user|forum|activity)/)){
 3 | 		return
 4 | 	}
 5 | 	if(useScripts.SFWmode){//saved you there
 6 | 		return
 7 | 	}
 8 | 	setTimeout(embedHentai,1000);
 9 | 	let mediaEmbeds = document.querySelectorAll(".media-embed");
10 | 	let bigQuery = [];//collects all on a page first so we only have to send 1 API query.
11 | 	mediaEmbeds.forEach(function(embed){
12 | 		if(embed.children.length === 0 && !embed.classList.contains("hohMediaEmbed")){//if( "not-rendered-natively" && "not-rendered-by-this sript" )
13 | 			embed.classList.add("hohMediaEmbed");
14 | 			let createEmbed = function(data){
15 | 				if(!data){
16 | 					return
17 | 				}
18 | 				embed.innerText = "";
19 | 				let eContainer = create("div",false,false,embed);
20 | 				let eEmbed = create("div","embed",false,eContainer);
21 | 				let eCover = create("div","cover",false,eEmbed);
22 | 				if(data.data.Media.coverImage.color){
23 | 					eCover.style.backgroundColor = data.data.Media.coverImage.color
24 | 				}
25 | 				eCover.style.backgroundImage = "url(" + data.data.Media.coverImage.large + ")";
26 | 				let eWrap = create("div","wrap",false,eEmbed);
27 | 				let mediaTitle = titlePicker(data.data.Media);
28 | 				let eTitle = create("div","title",mediaTitle,eWrap);
29 | 				let eInfo = create("div","info",false,eWrap);
30 | 				let eGenres = create("div","genres",false,eInfo);
31 | 				data.data.Media.genres.forEach((genre,index) => {
32 | 					let eGenre = create("span",false,genre,eGenres);
33 | 					let comma = create("span",false,", ",eGenre);
34 | 					if(index === data.data.Media.genres.length - 1){
35 | 						comma.style.display = "none"
36 | 					}
37 | 				});
38 | 				create("span",false,distributionFormats[data.data.Media.format],eInfo);
39 | 				create("span",false," · " + distributionStatus[data.data.Media.status],eInfo);
40 | 				if(data.data.Media.season){
41 | 					create("span",false,
42 | 						" · " + capitalize(data.data.Media.season.toLowerCase()) + " " + data.data.Media.startDate.year,
43 | 						eInfo
44 | 					)
45 | 				}
46 | 				else if(data.data.Media.startDate.year){
47 | 					create("span",false,
48 | 						" · " + data.data.Media.startDate.year,
49 | 						eInfo
50 | 					)
51 | 				}
52 | 				if(data.data.Media.averageScore){
53 | 					create("span",false," · " + data.data.Media.averageScore + "%",eInfo)
54 | 				}
55 | 				else if(data.data.Media.meanScore){//fallback if it's not popular enough, better than nothing
56 | 					create("span",false," · " + data.data.Media.meanScore + "%",eInfo)
57 | 				}
58 | 			}
59 | 			bigQuery.push({
60 | 				query: "query($mediaId:Int,$type:MediaType){Media(id:$mediaId,type:$type){id title{romaji native english} coverImage{large color} genres format status season meanScore averageScore startDate{year}}}",
61 | 				variables: {
62 | 					mediaId: +embed.dataset.mediaId,
63 | 					type: embed.dataset.mediaType.toUpperCase()
64 | 				},
65 | 				callback: createEmbed,
66 | 				cacheKey: "hohMedia" + embed.dataset.mediaId
67 | 			})
68 | 		}
69 | 	});
70 | 	queryPacker(bigQuery);
71 | }
72 | 


--------------------------------------------------------------------------------
/src/modules/enumerateSubmissionStaff.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "enumerateSubmissionStaff",
 3 | 	description: "$enumerateSubmissionStaff_description",
 4 | 	isDefault: true,
 5 | 	categories: [/*"Submissions",*/"Profiles"],
 6 | 	visible: true,
 7 | 	urlMatch: function(url,oldUrl){
 8 | 		return url.match(/^https:\/\/anilist\.co\/edit/)
 9 | 	},
10 | 	code: function enumerateSubmissionStaff(){
11 | 		if(!location.pathname.match(/^\/edit/)){
12 | 			return
13 | 		}
14 | 		setTimeout(enumerateSubmissionStaff,500);
15 | 		let staffFound = [];
16 | 		let staffEntries = document.querySelectorAll(".staff-row .col > .image");
17 | 		Array.from(staffEntries).forEach(function(staff){
18 | 			let enumerate = staffFound.filter(a => a === staff.href).length;
19 | 			if(enumerate === 1){
20 | 				let firstStaff = document.querySelector(".staff-row .col > .image[href=\"" + staff.href.replace("https://anilist.co","") + "\"]");
21 | 				if(!firstStaff.previousSibling){
22 | 					firstStaff.parentNode.insertBefore(
23 | 						create("span","hohEnumerateStaff",1),
24 | 						firstStaff
25 | 					)
26 | 				}
27 | 			}
28 | 			if(enumerate > 0){
29 | 				if(staff.previousSibling){
30 | 					staff.previousSibling.innerText = enumerate + 1;
31 | 				}
32 | 				else{
33 | 					staff.parentNode.insertBefore(
34 | 						create("span","hohEnumerateStaff",(enumerate + 1)),
35 | 						staff
36 | 					)
37 | 				}
38 | 			}
39 | 			staffFound.push(staff.href);
40 | 		})
41 | 	}
42 | })
43 | 


--------------------------------------------------------------------------------
/src/modules/expandDescriptions.js:
--------------------------------------------------------------------------------
1 | exportModule({
2 | 	id: "expandDescriptions",
3 | 	description: "$expandDescriptions_description",
4 | 	isDefault: true,
5 | 	categories: ["Media"],
6 | 	visible: true
7 | })
8 | 


--------------------------------------------------------------------------------
/src/modules/expandFeedFilters.js:
--------------------------------------------------------------------------------
1 | exportModule({
2 | 	id: "CSSexpandFeedFilters",
3 | 	description: "$CSSexpandFeedFilters_description",
4 | 	isDefault: true,
5 | 	categories: ["Feeds"],
6 | 	visible: true
7 | })
8 | 


--------------------------------------------------------------------------------
/src/modules/expandRight.js:
--------------------------------------------------------------------------------
 1 | function expandRight(){
 2 | 	if(!location.pathname.match(/^\/home\/?$/)){
 3 | 		return
 4 | 	}
 5 | 	let possibleFullWidth = document.querySelector(".home.full-width");
 6 | 	if(possibleFullWidth){
 7 | 		let homeContainer = possibleFullWidth.parentNode;
 8 | 		let sideBar = document.querySelector(".activity-feed-wrap")
 9 | 		if(!sideBar){
10 | 			setTimeout(expandRight,100);
11 | 			return;
12 | 		}
13 | 		sideBar = sideBar.nextElementSibling;
14 | 		sideBar.insertBefore(possibleFullWidth,sideBar.firstChild);
15 | 		let setSemantics = function(){
16 | 			let toggle = document.querySelector(".size-toggle.fa-compress");
17 | 			if(toggle){
18 | 				toggle.onclick = function(){
19 | 					homeContainer.insertBefore(possibleFullWidth,homeContainer.firstChild)
20 | 				}
21 | 			}
22 | 			else{
23 | 				setTimeout(setSemantics,200)
24 | 			}
25 | 		};setSemantics();
26 | 	}
27 | }
28 | 


--------------------------------------------------------------------------------
/src/modules/feedListLikes.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "feedListLikes",
 3 | 	description: "Add a full list of likes to feed posts",
 4 | 	isDefault: true,
 5 | 	categories: ["Feeds"],
 6 | 	visible: false
 7 | })
 8 | 
 9 | let likeLoop = setInterval(function(){
10 | 	document.querySelectorAll(
11 | 		".activity-entry > .wrap > .actions .action.likes:not(.hohHandledLike)"
12 | 	).forEach(thingy => {
13 | 		thingy.classList.add("hohHandledLike");
14 | 		thingy.onmouseover = function(){
15 | 			if(!thingy.querySelector(".count")){
16 | 				return
17 | 			}
18 | 			let likeCount = parseInt(thingy.querySelector(".count").innerText) || 0;
19 | 			if(likeCount <= 5){
20 | 				return
21 | 			}
22 | 			if(thingy.classList.contains("hohLoadedLikes")){
23 | 				let dataSetCache = parseInt(thingy.dataset.cacheLikeCount);
24 | 				if(isNaN(dataSetCache)){//API query already in progress
25 | 					return
26 | 				}
27 | 				if(dataSetCache === likeCount){//nothing changed
28 | 					return
29 | 					//in theory, someone *could* have retracted a like, and someone else been added, but it doesn't really happen all that often.
30 | 					//at least, this is better than what was previously done, namely never refetching the data at all, even if the count changed
31 | 				}
32 | 			}
33 | 			thingy.classList.add("hohLoadedLikes");
34 | 			const id = parseInt(thingy.parentNode.parentNode.querySelector(`[href^="/activity/"`).href.match(/\d+/));
35 | 			generalAPIcall(`
36 | query($id: Int){
37 | 	Activity(id: $id){
38 | 		... on TextActivity{
39 | 			likes{name}
40 | 		}
41 | 		... on MessageActivity{
42 | 			likes{name}
43 | 		}
44 | 		... on ListActivity{
45 | 			likes{name}
46 | 		}
47 | 	}
48 | }`,
49 | 				{id: id},
50 | 				data => {
51 | 					thingy.title = data.data.Activity.likes.map(like => like.name).join("\n");
52 | 					thingy.dataset.cacheLikeCount = data.data.Activity.likes.length
53 | 				}
54 | 			)
55 | 		}
56 | 	});
57 | },400);
58 | 


--------------------------------------------------------------------------------
/src/modules/filterStaffTabs.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "filterStaffTabs",
 3 | 	description: "$filterStaffTabs_description",
 4 | 	isDefault: true,
 5 | 	categories: ["Media"],
 6 | 	visible: true,
 7 | 	urlMatch: function(url,oldUrl){
 8 | 		return url.match(/^https:\/\/anilist\.co\/(anime|manga)\/\d+\/.*\/staff/)
 9 | 	},
10 | 	code: async function(){
11 | 		const mediaStaff = document.querySelector(".media-staff") || await watchElem(".media-staff");
12 | 		const staffGrid = mediaStaff.querySelector(".grid-wrap") || await watchElem(".grid-wrap",mediaStaff);
13 | 		if(staffGrid.children.length > 9){
14 | 			let filterBoxContainer = create("div","#hohStaffTabFilter");
15 | 			mediaStaff.prepend(filterBoxContainer);
16 | 			let filterRemover = create("span","#hohFilterRemover",svgAssets.cross,filterBoxContainer)
17 | 			let filterBox = create("input",false,false,filterBoxContainer);
18 | 			filterBox.placeholder = translate("$mediaStaff_filter");
19 | 			filterBox.setAttribute("list","staffRoles");
20 | 			let filterer = function(){
21 | 				let val = filterBox.value;
22 | 				Array.from(staffGrid.children).forEach(card => {
23 | 					if(
24 | 						looseMatcher(card.querySelector(".name").innerText,val)
25 | 						|| looseMatcher(card.querySelector(".role").innerText,val)
26 | 					){
27 | 						card.style.display = "inline-grid"
28 | 					}
29 | 					else{
30 | 						card.style.display = "none"
31 | 					}
32 | 				});
33 | 				if(val === ""){
34 | 					filterRemover.style.display = "none"
35 | 				}
36 | 				else{
37 | 					filterRemover.style.display = "inline"
38 | 				}
39 | 			}
40 | 			filterRemover.onclick = function(){
41 | 				filterBox.value = "";
42 | 				filterer()
43 | 			}
44 | 			filterBox.oninput = filterer;
45 | 			let dataList = create("datalist","#staffRoles",false,filterBoxContainer);
46 | 			let buildStaffRoles = function(){
47 | 				let autocomplete = new Set();
48 | 				Array.from(staffGrid.children).forEach(card => {
49 | 					autocomplete.add(card.querySelector(".name").innerText);
50 | 					autocomplete.add(card.querySelector(".role").innerText.replace(/\s*\(.*\)\.?\s*/,""));
51 | 					if(card.querySelector(".role").innerText.includes("OP")){
52 | 						autocomplete.add("OP")
53 | 					}
54 | 					if(card.querySelector(".role").innerText.includes("ED")){
55 | 						autocomplete.add("ED")
56 | 					}
57 | 				})
58 | 				removeChildren(dataList);
59 | 				autocomplete.forEach(
60 | 					value => create("option",false,false,dataList).value = value
61 | 				)
62 | 			};buildStaffRoles();
63 | 			let mutationConfig = {
64 | 				attributes: false,
65 | 				childList: true,
66 | 				subtree: false
67 | 			};
68 | 			let observer = new MutationObserver(function(){
69 | 				filterer();
70 | 				buildStaffRoles()
71 | 			});
72 | 			observer.observe(staffGrid,mutationConfig)
73 | 		}
74 | 	}
75 | })
76 | 


--------------------------------------------------------------------------------
/src/modules/forumLikes.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "forumLikes",
 3 | 	description: "$forumLikes_description",
 4 | 	isDefault: true,
 5 | 	categories: ["Forum"],
 6 | 	visible: false,
 7 | 	urlMatch: function(url,oldUrl){
 8 | 		return /^https:\/\/anilist\.co\/forum\/thread\/.*/.test(url)
 9 | 	},
10 | 	code: function(){
11 | 		let URLstuff = location.pathname.match(/^\/forum\/thread\/(\d+)/);
12 | 		if(!URLstuff){
13 | 			return
14 | 		}
15 | 		let adder = function(data){
16 | 			if((!data) || (!location.pathname.includes("forum/thread/" + URLstuff[1]))){
17 | 				return
18 | 			}
19 | 			let button = document.querySelector(".footer .actions .like-wrap .button");
20 | 			if(!button){
21 | 				setTimeout(function(){adder(data)},200);
22 | 				return;
23 | 			}
24 | 			button.title = data.data.Thread.likes.map(like => like.name).join("\n");
25 | 		}
26 | 		generalAPIcall(`
27 | 			query($id: Int){
28 | 				Thread(id: $id){
29 | 					likes{name}
30 | 				}
31 | 			}`,
32 | 			{id: parseInt(URLstuff[1])},
33 | 			adder
34 | 		)
35 | 	}
36 | })
37 | 


--------------------------------------------------------------------------------
/src/modules/forumRecent.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "forumRecent",
 3 | 	description: "$forumRecent_description",
 4 | 	isDefault: false,
 5 | 	categories: ["Forum","Navigation"],
 6 | 	visible: true,
 7 | 	urlMatch: function(url,oldUrl){
 8 | 		return false
 9 | 	}
10 | })
11 | 
12 | if(useScripts.forumRecent){
13 | 	let finder = function(){
14 | 		let navLinks = document.querySelector(`#nav .links .link[href="/forum/overview"]`);
15 | 		if(navLinks){
16 | 			navLinks.href = "/forum/recent";
17 | 			navLinks.onclick = function(){
18 | 				try{
19 | 					document.getElementById("app").__vue__._router.push({ name: "ForumFeed", params: {type: "recent"}});
20 | 					return false
21 | 				}
22 | 				catch(e){
23 | 					let forumRecentLink = navLinks.cloneNode(true);//copying and pasting the node should remove all event references to it
24 | 					navLinks.parentNode.replaceChild(forumRecentLink,navLinks);
25 | 				}
26 | 			}
27 | 		}
28 | 		else{
29 | 			setTimeout(finder,1000)
30 | 		}
31 | 	}
32 | 	finder()
33 | }
34 | 


--------------------------------------------------------------------------------
/src/modules/hideGlobalFeed.js:
--------------------------------------------------------------------------------
 1 | function hideGlobalFeed(){
 2 | 	if(!location.pathname.match(/^\/home/)){
 3 | 		return
 4 | 	}
 5 | 	let toggle = document.querySelector(".feed-type-toggle");
 6 | 	if(!toggle){
 7 | 		setTimeout(hideGlobalFeed,100);
 8 | 		return
 9 | 	}
10 | 	toggle.children[1].style.display = "none";
11 | 	if(toggle.children[1].classList.contains("active")){
12 | 		toggle.children[0].click()
13 | 	}
14 | }
15 | 


--------------------------------------------------------------------------------
/src/modules/imageFreeEditor.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "imageFreeEditor",
 3 | 	description: "$imageFreeEditor_description",
 4 | 	isDefault: false,
 5 | 	importance: -2,
 6 | 	categories: ["Media","Lists"],
 7 | 	visible: true,
 8 | 	css: `
 9 | .list-editor-wrap .cover{
10 | 	display: none;
11 | }
12 | .list-editor-wrap .header{
13 | 	background-image: none!important;
14 | 	height: auto;
15 | 	box-shadow: none;
16 | 	background: rgb(var(--color-foreground));
17 | }
18 | .list-editor-wrap .header::after{
19 | 	background: none;
20 | }
21 | .list-editor-wrap .header .content{
22 | 	align-items: center;
23 | }
24 | .list-editor-wrap .header .title{
25 | 	padding: 0;
26 | }
27 | .list-editor-wrap .header .favourite{
28 | 	margin-bottom: 0;
29 | }
30 | .list-editor-wrap .header .save-btn{
31 | 	margin-bottom: 0;
32 | }
33 | .list-editor-wrap .list-editor .body{
34 | 	padding-top: 20px;
35 | }
36 | @media (max-width: 760px){
37 | 	.list-editor-wrap .header .content{
38 | 		padding-top: 60px;
39 | 	}
40 | }
41 | 	`
42 | })
43 | 


--------------------------------------------------------------------------------
/src/modules/infoTable.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "infoTable",
 3 | 	description: "$setting_infoTable",
 4 | 	isDefault: false,
 5 | 	importance: 1,
 6 | 	categories: ["Media"],
 7 | 	visible: true,
 8 | 	css: `
 9 | .media-page-unscoped > .content.container{
10 | 	grid-template-columns: 215px auto;
11 | }
12 | .media-page-unscoped .sidebar > .data{
13 | 	padding: 15px;
14 | }
15 | .media-page-unscoped .data-set,
16 | .media-page-unscoped .data-set #hohMALserialization{
17 | 	display: inline-block;
18 | 	width: 100%;
19 | 	padding-bottom: 9px!important;
20 | 	padding-top: 4px;
21 | }
22 | .media-page-unscoped .data-set ~ .data-set{
23 | 	border-top-style: solid;
24 | 	border-top-width: 1px;
25 | 	border-top-color: rgb(var(--color-background));
26 | }
27 | .media-page-unscoped .data-set .type{
28 | 	display: inline;
29 | }
30 | .media-page-unscoped .data-set .value{
31 | 	display: inline;
32 | 	float: right;
33 | 	margin-top: 2px;
34 | }`
35 | })
36 | 


--------------------------------------------------------------------------------
/src/modules/keepAlive.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	boneless_disable: true,
 3 | 	id: "keepAlive",
 4 | 	description: translate("$keepAlive_description") + " " + translate("$settings_experimental_suffix"),
 5 | 	extendedDescription: "$keepAlive_extendedDescription",
 6 | 	isDefault: false,
 7 | 	importance: 0,
 8 | 	categories: ["Script"],
 9 | 	visible: true
10 | })
11 | 
12 | new MutationObserver(function(){
13 | 	let messages = Array.from(document.querySelectorAll(".el-message--error.is-closable"));
14 | 	if(messages.some(message => message.textContent === "Session expired, please refresh")){
15 | 		fetch("index.html").then(function(response){
16 | 			return response.text()
17 | 		}).then(function(html){
18 | 			let token = html.match(/window\.al_token = "([a-zA-Z0-9]+)";/);
19 | 			console.log("token",token);
20 | 			if(!token){
21 | 				return//idk, stuff changed, better do nothing
22 | 			}
23 | 			window.al_token = token;
24 | 			//alert the other tabs so they don't have to do the same
25 | 			try{
26 | 				aniCast.postMessage({type:"sessionToken",value:token});
27 | 			}
28 | 			catch(e){
29 | 				aniCastFailure(e)
30 | 			}
31 | 			//fetch the alert list again, they may have piled up while fetching
32 | 			Array.from(document.querySelectorAll(".el-message--error.is-closable")).forEach(message => {
33 | 				if(message.textContent === "Session expired, please refresh"){
34 | 					message.querySelector(".el-message__closeBtn").click()
35 | 				}
36 | 			});
37 | 		}).catch(function(){})//fail silently, trust Anilist to do the right thing by default
38 | 	}
39 | }).observe(
40 | 	document.body,
41 | 	{attributes: false, childList: true, subtree: false}
42 | )
43 | 


--------------------------------------------------------------------------------
/src/modules/mangaBrowse.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "mangaBrowse",
 3 | 	description: "$mangaBrowse_description",
 4 | 	isDefault: false,
 5 | 	categories: ["Browse","Navigation"],
 6 | 	visible: true,
 7 | 	urlMatch: function(url,oldUrl){
 8 | 		return false
 9 | 	}
10 | })
11 | 
12 | if(useScripts.mangaBrowse){
13 | 	let finder = function(){
14 | 		let navLinks = document.querySelector(`#nav .links .link[href="/search/anime"]`);
15 | 		if(navLinks){
16 | 			navLinks.href = "/search/manga";
17 | 			navLinks.onclick = function(){
18 | 				try{
19 | 					document.getElementById('app').__vue__._router.push({ name: 'Search', params: {type:'manga'}});
20 | 					return false
21 | 				}
22 | 				catch(e){
23 | 					let mangaBrowseLink = navLinks.cloneNode(true);//copying and pasting the node should remove all event references to it
24 | 					navLinks.parentNode.replaceChild(mangaBrowseLink,navLinks);
25 | 				}
26 | 			}
27 | 		}
28 | 		else{
29 | 			setTimeout(finder,1000)
30 | 		}
31 | 	}
32 | 	finder()
33 | }
34 | 


--------------------------------------------------------------------------------
/src/modules/markdownHelp.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "markdownHelp",
 3 | 	description: "$markdown_help_description",
 4 | 	isDefault: false,
 5 | 	categories: ["Navigation"],
 6 | 	visible: true,
 7 | 	urlMatch: function(url,oldUrl){
 8 | 		return true
 9 | 	},
10 | 	code: function(){
11 | 		let markdownHelper = document.getElementById("hohMarkdownHelper");
12 | 		if(markdownHelper){
13 | 			return
14 | 		}
15 | 		markdownHelper = create("span","#hohMarkdownHelper","?",document.getElementById("app"));
16 | 		markdownHelper.title = translate("$markdown_help_title");
17 | 		markdownHelper.onclick = function(){
18 | 			let existing = document.querySelector(".hohDisplayBox");
19 | 			if(existing){
20 | 				existing.remove()
21 | 			}
22 | 			else{
23 | 				let disp = createDisplayBox("height: 600px;",translate("$markdown_help_title"));
24 | 				create("h3","hohGuideHeading",translate("$markdown_help_images_header"),disp);
25 | 				create("pre","hohCode","img(your link here)",disp);
26 | 				create("pre","hohCode","img(https://i.stack.imgur.com/Wlvkk.jpg)",disp);
27 | 				create("p",false,translate("$markdown_help_imageUpload"),disp);
28 | 				create("p",false,translate("$markdown_help_imageSize"),disp);
29 | 				create("pre","hohCode","img300(your link here)",disp);
30 | 				create("p",false,translate("$markdown_help_infixOr"),disp);
31 | 				create("pre","hohCode","img40%(your link here)",disp);
32 | 				create("h3","hohGuideHeading",translate("$markdown_help_links_header"),disp);
33 | 				create("pre","hohCode","[link text](URL)",disp);
34 | 				create("pre","hohCode","[cool show](https://en.wikipedia.org/wiki/Urusei_Yatsura)",disp);
35 | 				create("p",false,"To get a media preview card, just put the Anilist URL of the show:",disp);
36 | 				create("pre","hohCode","https://anilist.co/anime/1293/Urusei-Yatsura/",disp);
37 | 				create("p",false,"To make an image a link, put the image markdown inside the link markdown, with a space on both sides",disp);
38 | 				create("pre","hohCode","[ img(image URL) ](link URL)",disp);
39 | 				create("h3","hohGuideHeading",translate("$markdown_help_formatting_header"),disp);
40 | 				create("h1",false,"headline",disp);
41 | 				create("pre","hohCode","# headline",disp);
42 | 				create("i",false,"italics",disp);
43 | 				create("pre","hohCode","*italics* or _italics_",disp);
44 | 				create("b",false,"bold",disp);
45 | 				create("pre","hohCode","**bold** or __bold__",disp);
46 | 				create("del",false,"strikethrough",disp);
47 | 				create("pre","hohCode","~~strikethrough~~",disp);
48 | 				create("span",false,"Use a backslash \\ to undo special meaning of formatting symols like * ~ # _ \\",disp);
49 | 				create("pre","hohCode","Use a backslash \\\\ to undo special meaning of formatting symols like \\* \\~ \\# \\_ \\\\",disp);
50 | 				create("a",["link","hohGuideHeading"],"Full guide",disp).href = "https://anilist.co/forum/thread/6125";
51 | 				create("span",false," ◆ ",disp);
52 | 				create("a",["link","hohGuideHeading"],"Make emojis work",disp).href = "https://files.kiniro.uk/unicodifier.html";
53 | 			}
54 | 		}
55 | 	}
56 | })
57 | 


--------------------------------------------------------------------------------
/src/modules/meanScoreBack.js:
--------------------------------------------------------------------------------
 1 | //rename?
 2 | function meanScoreBack(){
 3 | 	const userRegex = /^\/user\/([^/]+)\/?$/;
 4 | 	let URLstuff = location.pathname.match(userRegex);
 5 | 	if(!URLstuff){
 6 | 		return
 7 | 	}
 8 | 	const query = `
 9 | 	query($userName: String) {
10 | 		User(name: $userName){
11 | 			statistics{
12 | 				anime{
13 | 					episodesWatched
14 | 					meanScore
15 | 				}
16 | 				manga{
17 | 					volumesRead
18 | 					meanScore
19 | 				}
20 | 			}
21 | 		}
22 | 	}`;
23 | 	let variables = {
24 | 		userName: decodeURIComponent(URLstuff[1])
25 | 	}
26 | 	generalAPIcall(query,variables,function(data){
27 | 		if(!data){
28 | 			return;
29 | 		}
30 | 		let adder = function(){
31 | 			if(
32 | 				!userRegex.test(location.pathname)
33 | 				|| location.pathname.match(userRegex)[1] !== URLstuff[1]
34 | 			){
35 | 				return
36 | 			}
37 | 			let possibleStatsWrap = document.querySelectorAll(".stats-wrap .stats-wrap");
38 | 			if(possibleStatsWrap.length){
39 | 				if(possibleStatsWrap.length === 2 && possibleStatsWrap[0].childElementCount === 3){
40 | 					if(data.data.User.statistics.anime.meanScore){
41 | 						let statAnime = create("div","stat",false,possibleStatsWrap[0]);
42 | 						create("div","value",data.data.User.statistics.anime.episodesWatched,statAnime);
43 | 						create("div","label",translate("$milestones_totalEpisodes"),statAnime);
44 | 						let totalDays = possibleStatsWrap[0].children[1].children[0].innerText;
45 | 						possibleStatsWrap[0].children[1].remove();
46 | 						possibleStatsWrap[0].parentNode.querySelector(".milestone:nth-child(2)").innerText = translate("$milestones_daysWatched",totalDays);
47 | 						possibleStatsWrap[0].parentNode.classList.add("hohMilestones")
48 | 					}
49 | 					if(data.data.User.statistics.manga.meanScore){
50 | 						let statManga = create("div","stat",false,possibleStatsWrap[1]);
51 | 						create("div","value",data.data.User.statistics.manga.volumesRead,statManga);
52 | 						create("div","label",translate("$milestones_totalVolumes"),statManga);
53 | 						let totalChapters = possibleStatsWrap[1].children[1].children[0].innerText;
54 | 						possibleStatsWrap[1].children[1].remove();
55 | 						possibleStatsWrap[1].parentNode.querySelector(".milestone:nth-child(2)").innerText = translate("$milestones_chaptersRead",totalChapters);
56 | 						possibleStatsWrap[1].parentNode.classList.add("hohMilestones")
57 | 					}
58 | 				}
59 | 				else if(possibleStatsWrap[0].innerText.includes("Total Manga")){
60 | 					if(data.data.User.statistics.manga.meanScore){
61 | 						let statManga = create("div","stat",false,possibleStatsWrap[0]);
62 | 						create("div","value",data.data.User.statistics.manga.volumesRead,statManga);
63 | 						create("div","label",translate("$milestones_totalVolumes"),statManga);
64 | 						let totalChapters = possibleStatsWrap[0].children[1].children[0].innerText;
65 | 						possibleStatsWrap[0].children[1].remove();
66 | 						possibleStatsWrap[0].parentNode.querySelector(".milestone:nth-child(2)").innerText = translate("$milestones_chaptersRead",totalChapters);
67 | 						possibleStatsWrap[0].parentNode.classList.add("hohMilestones")
68 | 					}
69 | 				}
70 | 			}
71 | 			else{
72 | 				setTimeout(adder,200)
73 | 			}
74 | 		};adder();
75 | 	},"hohMeanScoreBack" + variables.userName,60*1000)
76 | }
77 | 


--------------------------------------------------------------------------------
/src/modules/mediaTranslation.js:
--------------------------------------------------------------------------------
1 | exportModule({
2 | 	id: "mediaTranslation",
3 | 	description: "$mediaTranslation_description",
4 | 	isDefault: false,
5 | 	importance: 0,
6 | 	categories: ["Media","Newly Added"],
7 | 	visible: true
8 | })
9 | 


--------------------------------------------------------------------------------
/src/modules/middleClickLinkFixer.js:
--------------------------------------------------------------------------------
 1 | function linkFixer(){
 2 | 	if(location.pathname !== "/home"){
 3 | 		return
 4 | 	}
 5 | 	let recentReviews = document.querySelector(".recent-reviews h2.section-header");
 6 | 	let recentThreads = document.querySelector(".recent-threads h2.section-header");
 7 | 	if(recentReviews && recentThreads){
 8 | 		recentReviews.innerText = "";
 9 | 		create("a",false,translate("$home_reviewLink"),recentReviews)
10 | 			.href = "/reviews";
11 | 		recentThreads.innerText = "";
12 | 		create("a",false,translate("$home_forumLink"),recentThreads)
13 | 			.href = "/forum/overview";
14 | 		let sectionHeaders = document.querySelectorAll(".section-header");
15 | 		Array.from(sectionHeaders).forEach(header => {
16 | 			if(header.innerText.match("Trending")){
17 | 				header.innerText = "";
18 | 				create("a",false,translate("$home_trendingAnime"),header)
19 | 					.href = "https://anilist.co/search/anime/trending";
20 | 				create("a","hover-manga",translate("$home_trendingManga"),header)
21 | 					.href = "https://anilist.co/search/manga/trending"
22 | 			}
23 | 			else if(header.innerText.match("Newly Added Anime")){
24 | 				header.innerText = "";
25 | 				create("a",false,translate("$home_newAnime"),header)
26 | 					.href = "https://anilist.co/search/anime/new"
27 | 			}
28 | 			else if(header.innerText.match("Newly Added Manga")){
29 | 				header.innerText = "";
30 | 				create("a","hover-manga",translate("$home_newManga"),header)
31 | 					.href = "https://anilist.co/search/manga/new"
32 | 			}
33 | 		})
34 | 	}
35 | 	else{
36 | 		if(useScripts.additionalTranslation){
37 | 			setTimeout(linkFixer,1000)
38 | 		}
39 | 		else{
40 | 			setTimeout(linkFixer,2000)//invisible change, does not take priority
41 | 		}
42 | 	}
43 | }
44 | 


--------------------------------------------------------------------------------
/src/modules/mobileAdjustments.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "mobileFriendly",
 3 | 	description: "$mobileFriendly_description",
 4 | 	isDefault: false,
 5 | 	importance: 7,
 6 | 	categories: ["Navigation","Script"],
 7 | 	visible: true
 8 | })
 9 | 
10 | if(useScripts.mobileFriendly){
11 | 	let addReviewLink = function(){
12 | 		let footerPlace = document.querySelector(".footer .links section:last-child");
13 | 		if(footerPlace){
14 | 			let revLink = create("a",false,"Reviews",footerPlace,"display:block;padding:6px;");
15 | 			revLink.href = "/reviews/";
16 | 		}
17 | 		else{
18 | 			setTimeout(addReviewLink,500)
19 | 		}
20 | 	};addReviewLink();
21 | }
22 | 


--------------------------------------------------------------------------------
/src/modules/mobileTags.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "CSSmobileTags",
 3 | 	description: "$setting_CSSmobileTags",
 4 | 	isDefault: true,
 5 | 	importance: 0,
 6 | 	categories: ["Media"],
 7 | 	visible: true,
 8 | 	css: `
 9 | @media(max-width: 760px){
10 | 	.media .sidebar .tags{
11 | 		display: inline;
12 | 	}
13 | 	.media .sidebar .tags .tag{
14 | 		display: inline-block;
15 | 		margin-right: 2px;
16 | 	}
17 | 	.media .sidebar .tags .tag .rank{
18 | 		display: inline;
19 | 	}
20 | 	.media .overview .tags .tag .vote-dropdown .el-dropdown-link{
21 | 		opacity: 1;
22 | 		display: inline!important;
23 | 	}
24 | 	.media .overview .tags .add-icon{
25 | 		opacity: 1;
26 | 		display: inline!important;
27 | 	}
28 | 	.media-page-unscoped .review.button{
29 | 		display: inline-block;
30 | 		width: 48%;
31 | 	}
32 | 	.media-page-unscoped .sidebar + .overview{
33 | 		margin-top: 20px;
34 | 	}
35 | }`
36 | })
37 | 


--------------------------------------------------------------------------------
/src/modules/navbarDroptext.js:
--------------------------------------------------------------------------------
 1 | if(useScripts.navbarDroptext){
 2 | 	let addDrop = function(){
 3 | 		let navThingy = document.querySelector(".nav");
 4 | 		if(navThingy){
 5 | 			navThingy.ondragover = function(event){
 6 | 				event.preventDefault()
 7 | 			}
 8 | 			navThingy.ondrop = function(event){
 9 | 				event.preventDefault();
10 | 				let data = event.dataTransfer.getData("text");
11 | 				if(data.length && data.length < 1000){//avoid performance issues if someone accidentally drops the lord of the rings script into the navbar or something
12 | 					document.querySelector(".nav .wrap .search").click();
13 | 					let observer = new MutationObserver(function(){
14 | 						let inputElement = document.querySelector(".nav .quick-search .input input");
15 | 						inputElement.value = data;
16 | 						inputElement.dispatchEvent(new Event("input"));
17 | 						observer.disconnect()
18 | 					});
19 | 					observer.observe(document.querySelector(".nav .quick-search"),{
20 | 						attributes: true,
21 | 						childList: false,
22 | 						subtree: false
23 | 					})
24 | 				}
25 | 			}
26 | 		}
27 | 		else{
28 | 			setTimeout(addDrop,500)
29 | 		}
30 | 	};addDrop()
31 | }
32 | 


--------------------------------------------------------------------------------
/src/modules/noAutoplay.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "noAutoplay",
 3 | 	description: "$noAutoplay_description",
 4 | 	extendedDescription: "$noAutoplay_extendedDescription",
 5 | 	isDefault: false,
 6 | 	categories: ["Feeds"],
 7 | 	visible: true
 8 | })
 9 | 
10 | if(useScripts.noAutoplay){
11 | 	setInterval(function(){
12 | 		document.querySelectorAll("video").forEach(video => {
13 | 			if(video.hasAttribute("autoplay")){
14 | 				if(!(video.querySelector("source") && video.querySelector("source").src.match(/#image$/))){
15 | 					video.removeAttribute("autoplay");
16 | 					video.load()
17 | 				}
18 | 				else{
19 | 					video.removeAttribute("controls")
20 | 				}
21 | 			}
22 | 		})
23 | 	},500)
24 | }
25 | 


--------------------------------------------------------------------------------
/src/modules/noScrollPosts.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "noScrollPosts",
 3 | 	description: "$noScrollPosts_description",
 4 | 	isDefault: false,
 5 | 	importance: -2,
 6 | 	categories: ["Feeds"],
 7 | 	visible: true,
 8 | 	css: ".activity-text .text .markdown{max-height: unset}"
 9 | })
10 | 


--------------------------------------------------------------------------------
/src/modules/noSequel.js:
--------------------------------------------------------------------------------
 1 | const sequelList = new Set(m4_include(data/sequels.json))
 2 | const sequelList_manga = new Set(m4_include(data/sequels_manga.json))
 3 | 
 4 | exportModule({
 5 | 	id: "noSequel",
 6 | 	description: "$noSequel_description",
 7 | 	extendedDescription: "$noSequel_extendedDescription",
 8 | 	isDefault: true,
 9 | 	importance: 1,
10 | 	categories: ["Browse","Newly Added"],
11 | 	visible: true,
12 | 	urlMatch: function(){
13 | 		return /^\/search\/anime/.test(location.pathname) || /^\/search\/manga/.test(location.pathname)
14 | 	},
15 | 	code: function(){
16 | 		let optionInserter = function(){
17 | 			if(!(/^\/search\/anime/.test(location.pathname) || /^\/search\/manga/.test(location.pathname))){
18 | 				return
19 | 			}
20 | 			let place = document.querySelector(".primary-filters .filters");
21 | 			if(!place){
22 | 				setTimeout(optionInserter,500);
23 | 				return
24 | 			}
25 | 			place.style.position = "relative";
26 | 			if(document.querySelector(".hohNoSequelSetting")){
27 | 				return
28 | 			}
29 | 			let setting = create("span","hohNoSequelSetting",false,place);
30 | 			let input = createCheckbox(setting);
31 | 			input.classList.add("hohNoSequelSetting_input");
32 | 			input.checked = useScripts.noSequel_value;
33 | 			input.onchange = function(){
34 | 				useScripts.noSequel_value = this.checked;
35 | 				useScripts.save();
36 | 			}
37 | 			create("span",false,translate("$hideSequels"),setting);
38 | 			let remover = setInterval(function(){
39 | 				if(!(/^\/search\/anime/.test(location.pathname) || /^\/search\/manga/.test(location.pathname))){
40 | 					clearInterval(remover);
41 | 					return
42 | 				}
43 | 				let input = document.querySelector(".hohNoSequelSetting_input");
44 | 				if(!input){
45 | 					clearInterval(remover);
46 | 					return
47 | 				}
48 | 				Array.from(document.querySelectorAll(".media-card")).forEach(hit => {
49 | 					const cover = hit.querySelector(".cover");
50 | 					if(!cover) return
51 | 					let link = "";
52 | 					if(cover.href) link = cover.href;
53 | 					else{
54 | 						let img = cover.querySelector(".image-link");
55 | 						if(img && img.href) link = img.href;
56 | 						else return
57 | 					}
58 | 					let id = link.match(/(anime|manga)\/(\d+)\//);
59 | 					if(id && id[2]){
60 | 						id = parseInt(id[2]);
61 | 						if((sequelList.has(id) || sequelList_manga.has(id) || link.match(/2nd|3rd|season-2|season-3/i)) && input.checked){
62 | 							hit.classList.add("hohHiddenSequel")
63 | 						}
64 | 						else{
65 | 							hit.classList.remove("hohHiddenSequel")
66 | 						}
67 | 					}
68 | 				})
69 | 			},500)
70 | 		};
71 | 		optionInserter()
72 | 	},
73 | 	css: ".hohHiddenSequel{display: none!important}"
74 | })
75 | 


--------------------------------------------------------------------------------
/src/modules/nonJapaneseVoiceDefaults.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "nonJapaneseVoiceDefaults",
 3 | 	description: "defaults to Chinese and Korean voice actors for non-Japanese shows",
 4 | 	isDefault: true,
 5 | 	categories: ["Media"],
 6 | 	visible: false,
 7 | 	urlMatch: function(url,oldUrl){
 8 | 		return url.match(/\/anime\/.*\/characters\/?$/)
 9 | 	},
10 | 	code: function(){
11 | 		let checker = function(){
12 | 			if(!document.URL.match(/\/anime\/.*\/characters\/?$/)){
13 | 				return
14 | 			}
15 | 			let sidebarInfo = document.querySelector(".sidebar .data-set .value");
16 | 			if(!sidebarInfo){
17 | 				setTimeout(checker,500);
18 | 				return
19 | 			}
20 | 			let country = sidebarInfo.innerText.match(/Chinese|South Korean|Taiwanese/);
21 | 			if(!country){
22 | 				return
23 | 			}
24 | 			let selector = document.querySelector('.language-select input[placeholder="Language"]');
25 | 			if(!selector){
26 | 				setTimeout(checker,500);
27 | 				return
28 | 			}
29 | 			//opens the dropdown, spawning the alternate options
30 | 			selector.click();
31 | 			let selection = function(){
32 | 				if(!document.URL.match(/\/anime\/.*\/characters\/?$/)){
33 | 					return
34 | 				}
35 | 				let dropdown = document.querySelector(".el-select-dropdown");
36 | 				if(!dropdown){
37 | 					setTimeout(selection,100);
38 | 					return
39 | 				}
40 | 				let options = Array.from(dropdown.querySelectorAll(".el-select-dropdown__item span"));
41 | 				if(options.length === 0){
42 | 					selector.click()
43 | 				}
44 | 				options.forEach(option => {
45 | 					if(
46 | 						(option.innerText === "Chinese" && (country[0] === "Chinese" || country[0] === "Taiwanese"))
47 | 						|| (option.innerText === "Korean" && country[0] === "South Korean")
48 | 					){
49 | 						option.click()
50 | 					}
51 | 				})
52 | 			};selection()
53 | 		};checker()
54 | 	}
55 | })
56 | 


--------------------------------------------------------------------------------
/src/modules/nonJumpScroll.js:
--------------------------------------------------------------------------------
 1 | // SPDX-FileCopyrightText: 2021 Reina
 2 | // SPDX-License-Identifier: MIT
 3 | /*
 4 | Copyright (c) 2021 Reina
 5 | 
 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 7 | 
 8 | The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
 9 | 
10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 | //updated code here: https://github.com/Reinachan/AniList-High-Contrast-Dark-Theme
13 | exportModule({
14 | 	id: "nonJumpScroll",
15 | 	description: "$nonJumpScroll_description",
16 | 	isDefault: true,
17 | 	importance: 1,
18 | 	categories: ["Feeds"],
19 | 	visible: true,
20 | 	css: `
21 | /* Scrollbar */
22 | * {
23 | 	scrollbar-color: rgb(var(--color-blue)) rgba(0, 0, 0, 0.2);
24 | 	scrollbar-width: thin;
25 | }
26 | ::-webkit-scrollbar {
27 | 	width: 4px;
28 | 	height: 8px;
29 | }
30 | ::-webkit-scrollbar-button {
31 | 	display: none;
32 | }
33 | ::-webkit-scrollbar-track {
34 | 	background-color: #1110;
35 | 	width: 0px;
36 | }
37 | ::-webkit-scrollbar-track-piece {
38 | 	display: none;
39 | }
40 | ::-webkit-scrollbar-thumb {
41 | 	background-color: rgb(var(--color-blue));
42 | }
43 | .activity-markdown .markdown {
44 | 	overflow-y: scroll !important;
45 | 	scrollbar-color: rgba(0, 0, 0, 0) rgba(0, 0, 0, 0);
46 | }
47 | .activity-markdown .markdown:hover {
48 | 	scrollbar-color: rgb(var(--color-blue)) rgba(0, 0, 0, 0);
49 | }
50 | .activity-markdown .markdown::-webkit-scrollbar-thumb,
51 | .activity-markdown .markdown .about .content-wrap::-webkit-scrollbar-thumb {
52 | 	background-color: rgba(0, 0, 0, 0);
53 | }
54 | .activity-markdown .markdown:hover::-webkit-scrollbar-thumb,
55 | .activity-markdown .markdown .about .content-wrap:hover::-webkit-scrollbar-thumb {
56 | 	background-color: rgb(var(--color-blue));
57 | }
58 | ::-webkit-scrollbar-corner {
59 | 	display: none;
60 | }
61 | /*::-webkit-resizer {
62 | 	display: none;
63 | }*/
64 | .about .content-wrap {
65 | 	overflow-y: scroll !important;
66 | 	scrollbar-color: rgba(0, 0, 0, 0) rgba(0, 0, 0, 0);
67 | }
68 | .about .content-wrap .markdown {
69 | 	overflow: hidden !important;
70 | }
71 | .about .content-wrap:hover {
72 | 	overflow-y: scroll !important;
73 | 	scrollbar-color: rgb(var(--color-blue)) rgba(0, 0, 0, 0);
74 | }
75 | .about .content-wrap .markdown::after {
76 | 	content: '';
77 | 	display: block;
78 | 	height: 10px;
79 | 	width: 10px;
80 | }
81 | .list-editor .custom-lists {
82 | 	overflow-y: auto;
83 | }
84 | .list-editor .custom-lists:hover {
85 | 	margin-right: 0;
86 | }
87 | `
88 | })
89 | 


--------------------------------------------------------------------------------
/src/modules/oldDarkTheme.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "CSSoldDarkTheme",
 3 | 	description: "$CSSoldDarkTheme_description",
 4 | 	isDefault: false,
 5 | 	importance: -3,
 6 | 	categories: [],
 7 | 	visible: true,
 8 | 	css: `
 9 | .site-theme-dark{
10 | 	--color-background:39,44,56;
11 | 	--color-foreground:31,35,45;
12 | 	--color-foreground-grey:25,29,38;
13 | 	--color-foreground-grey-dark:16,20,25;
14 | 	--color-foreground-blue:25,29,38;
15 | 	--color-foreground-blue-dark:19,23,29;
16 | 	--color-background-blue-dark:31,35,45;
17 | 	--color-overlay:34,28,22;
18 | 	--color-shadow:49,54,68;
19 | 	--color-shadow-dark:6,13,34;
20 | 	--color-shadow-blue:103,132,187;
21 | 	--color-text:159,173,189;
22 | 	--color-text-light:129,140,153;
23 | 	--color-text-lighter:133,150,165;
24 | 	--color-text-bright:237,241,245;
25 | }
26 | .site-theme-dark .nav-unscoped.transparent{
27 | 	background: rgba(31, 38, 49, .5);
28 | 	color: rgb(var(--color-text));
29 | }
30 | 
31 | .site-theme-dark .nav-unscoped,
32 | .site-theme-dark .nav-unscoped.transparent:hover{
33 | 	background: rgb(var(--color-foreground));
34 | }`
35 | })
36 | 


--------------------------------------------------------------------------------
/src/modules/possibleBlocked.js:
--------------------------------------------------------------------------------
 1 | function possibleBlocked(oldURL){
 2 | 	let URLstuff = oldURL.match(/\/user\/(.*?)\/?$/);
 3 | 	if(URLstuff){
 4 | 		let name = decodeURIComponent(URLstuff[1]);
 5 | 		const query = `
 6 | 		query($userName: String) {
 7 | 			User(name: $userName){
 8 | 				id
 9 | 			}
10 | 		}`;
11 | 		let variables = {
12 | 			userName: name
13 | 		}
14 | 		if(name !== whoAmI){
15 | 			generalAPIcall(query,variables,data => {
16 | 				let notFound = document.querySelector(".not-found");
17 | 				name = name.split("/")[0];
18 | 				if(notFound){
19 | 					if(name.includes("submissions")){
20 | 						notFound.innerText = "This submission was probably denied"
21 | 					}
22 | 					else if(data){
23 | 						notFound.innerText = translate("$404_blocked",name)
24 | 					}
25 | 					else if(name === "ModChan"){
26 | 						notFound.innerText = "Nope."
27 | 					}
28 | 					else{
29 | 						notFound.innerText = translate("$404_private_or_noUser",name);
30 | 						generalAPIcall(
31 | `
32 | query($name: String){
33 | 	MediaList(userName: $name,mediaId: 1){
34 | 		id
35 | 	}
36 | }
37 | `,
38 | 							{name: name},
39 | 							function(data,variables,errors){
40 | 								if(errors){
41 | 									if(errors.errors[0].message === "Private User"){
42 | 										notFound.innerText = translate("$404_private",name)
43 | 									}
44 | 									else{
45 | 										notFound.innerText = translate("$404_noUser",name)
46 | 									}
47 | 								}
48 | 							}
49 | 						)
50 | 					}
51 | 					notFound.style.paddingTop = "200px";
52 | 					notFound.style.fontSize = "2rem"
53 | 				}
54 | 			})
55 | 		}
56 | 		return
57 | 	}
58 | 	URLstuff = oldURL.match(/\/(anime|manga)\/(\d+)/);
59 | 	if(URLstuff){
60 | 		let type = URLstuff[1];
61 | 		let id = parseInt(URLstuff[2]);
62 | 		const query = `
63 | 		query($id: Int,$type: MediaType) {
64 | 			Media(id: $id,type: $type){
65 | 				genres
66 | 			}
67 | 		}`;
68 | 		let variables = {
69 | 			type: type.toUpperCase(),
70 | 			id: id
71 | 		}
72 | 		generalAPIcall(query,variables,data => {
73 | 			if(data.data.Media.genres.some(genre => genre === "Hentai")){
74 | 				let notFound = document.querySelector(".not-found");
75 | 				if(notFound){
76 | 					if(id === 320){
77 | 						notFound.innerText = `Kite isn't *really* hentai, but it kinda is too, and it's a bit complicated.
78 | 
79 | (You can enable 18+ content in settings > Anime & Manga)`
80 | 					}
81 | 					else{
82 | 						notFound.innerText = `That's one of them hentais.
83 | 
84 | (You can enable 18+ content in settings > Anime & Manga)`
85 | 					}
86 | 					notFound.style.paddingTop = "200px";
87 | 					notFound.style.fontSize = "2rem"
88 | 				}
89 | 			}
90 | 		})
91 | 	}
92 | }
93 | 


--------------------------------------------------------------------------------
/src/modules/profileBackground.js:
--------------------------------------------------------------------------------
 1 | function profileBackground(){
 2 | 	if(useScripts.SFWmode){//clearly not safe, users can upload anything
 3 | 		return
 4 | 	}
 5 | 	const userRegex = /^\/user\/([^/]+)(\/.*)?$/;
 6 | 	let URLstuff = location.pathname.match(userRegex);
 7 | 	const query = `
 8 | 	query($userName: String) {
 9 | 		User(name: $userName){
10 | 			about
11 | 		}
12 | 	}`;
13 | 	let variables = {
14 | 		userName: decodeURIComponent(URLstuff[1])
15 | 	}
16 | 	generalAPIcall(query,variables,data => {
17 | 		if(!data){
18 | 			return;
19 | 		}
20 | 		let jsonMatch = (data.data.User.about || "").match(/^\[\]\(json([A-Za-z0-9+/=]+)\)/);
21 | 		if(!jsonMatch){
22 | 			let target = document.querySelector(".user-page-unscoped");
23 | 			if(target){
24 | 				target.style.background = "unset"
25 | 			}
26 | 			return;
27 | 		}
28 | 		try{
29 | 			let jsonData;
30 | 			try{
31 | 				jsonData = JSON.parse(atob(jsonMatch[1]))
32 | 			}
33 | 			catch(e){
34 | 				jsonData = JSON.parse(LZString.decompressFromBase64(jsonMatch[1]))
35 | 			}
36 | 			let adder = function(){
37 | 				if(!userRegex.test(location.pathname)){
38 | 					return
39 | 				}
40 | 				let target = document.querySelector(".user-page-unscoped");
41 | 				if(target){
42 | 					target.style.background = jsonData.background || "none";
43 | 				}
44 | 				else{
45 | 					setTimeout(adder,200);
46 | 				}
47 | 			};adder();
48 | 		}
49 | 		catch(e){
50 | 			console.warn("Invalid profile JSON for " + variables.userName + ". Aborting.");
51 | 			console.log(atob(jsonMatch[1]));
52 | 		}
53 | 	},"hohProfileBackground" + variables.userName,30*1000);
54 | }
55 | 


--------------------------------------------------------------------------------
/src/modules/recommendationsFade.js:
--------------------------------------------------------------------------------
1 | exportModule({
2 | 	id: "recommendationsFade",
3 | 	description: "$recommendationsFade_description",
4 | 	isDefault: false,
5 | 	importance: 0,
6 | 	categories: ["Media","Newly Added"],
7 | 	visible: true,
8 | 	css: ".recommendation-card .cover:has(.hohStatusDot):not(:hover){opacity: 0.3 !important;}"
9 | })


--------------------------------------------------------------------------------
/src/modules/rightSideNavbar.js:
--------------------------------------------------------------------------------
1 | exportModule({
2 | 	id: "rightSideNavbar",
3 | 	description: "$rightSideNavbar_description",
4 | 	isDefault: false,
5 | 	categories: ["Navigation"],
6 | 	visible: true
7 | })
8 | 


--------------------------------------------------------------------------------
/src/modules/scoreOverviewFixer.js:
--------------------------------------------------------------------------------
 1 | function scoreOverviewFixer(){
 2 | 	if(!document.URL.match(/^https:\/\/anilist\.co\/(anime|manga)\//)){
 3 | 		return;
 4 | 	}
 5 | 	let overview = document.querySelector(".media .overview");
 6 | 	if(!overview){
 7 | 		setTimeout(scoreOverviewFixer,300);
 8 | 		return;
 9 | 	}
10 | 	let follows = overview.querySelectorAll(".follow");
11 | 	if(follows.length){
12 | 		follows.forEach(el => {
13 | 			scoreColors(el);
14 | 		});
15 | 	}
16 | 	else{
17 | 		setTimeout(scoreOverviewFixer,300);
18 | 	}
19 | }
20 | 


--------------------------------------------------------------------------------
/src/modules/selectMyThreads.js:
--------------------------------------------------------------------------------
 1 | function selectMyThreads(){
 2 | 	if(document.URL !== "https://anilist.co/user/" + whoAmI + "/social#my-threads"){
 3 | 		return
 4 | 	}
 5 | 	let target = document.querySelector(".filter-group span:nth-child(4)");
 6 | 	if(!target){
 7 | 		setTimeout(selectMyThreads,100)
 8 | 	}
 9 | 	else{
10 | 		target.click()
11 | 	}
12 | }
13 | 


--------------------------------------------------------------------------------
/src/modules/selfInsert.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	boneless_disable: true,
 3 | 	id: "selfInsert",
 4 | 	description: "add " + script_type + " to the apps page",
 5 | 	isDefault: true,
 6 | 	categories: ["Script"],
 7 | 	visible: false,
 8 | 	urlMatch: function(url,oldUrl){
 9 | 		return url.match("https://anilist.co/apps")
10 | 	},
11 | 	code: function(){
12 | 		let waiter = function(){
13 | 			if(!document.URL.match("https://anilist.co/apps")){
14 | 				return
15 | 			}
16 | 			if(document.querySelector(".app.hohscript")){
17 | 				return
18 | 			}
19 | 			let location = document.querySelector("[href=\"https://www.animouto.moe/\"]");
20 | 			if(!location){
21 | 				setTimeout(waiter,500)
22 | 				return
23 | 			}
24 | 			if(
25 | 				location.parentNode.childNodes.length % 3 !== 0
26 | 				&& location.parentNode.childNodes.length % 2 !== 0
27 | 			){//two or three per row, so only fill the gap if we can make the symmetry pleasing
28 | 				let app = location.cloneNode(true);
29 | 				app.classList.add("hohscript");
30 | 				app.href = scriptInfo.link;
31 | 				app.children[0].src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAABkCAMAAAAL3/3yAAAA1VBMVEUfJjKXHyngDxb6AwXAGCH/AADBAAByAAA4AAAWAAAFAABzAADCAAAeJTEVGiMEBQkAAAAEBgkYHigFBwsFBwwUGSITGCEPExsUGiMZH5kLD+ECA/oAAP8AAMAAYgAAgAACfQULcRYZTykTGMIAAHEAOgATYiEAADcAHQAAABUADAAAAAYABAACA/kCfQYAADgAHgAAAHMAOwAAAMIAYwAVGiQFCAwYHikTGSLDwwB0dAA6OgAXFwAGBgB1dQDExAD//wD5+QX5+Qbg4RaXlynAwSH6+gXAOZK3AAABlUlEQVR42u3ZA4IsMRRG4b8tpW0+m2Nz/0t6rB7mZszzLeFUxQIA4K7E4olEPCaEKZn6KykExVL/xYSQeBQrLoQkolgJISQ1J4QQi1jEIhaxiAViEYtYxCIWsUAsYhGLWMQiFohFLGIRi1jEQjqTzeXzuWyhKJhK5Yqbq5ZLglet7k5o1ASPpjujJZyr7c7RFs7RcpHQv4Wa86gJp5QazqNeEk7qOK+ycFLFeVX1DHV7/YHP0BlGY5/JdKYn6cXA8NIZXo0Nr/UEdQeWN87wdmyZ6enpDSzvnOH92DLV09MfWD44w8exZaKn59Ntxfqsi2MYPrsJ/oszfLUn+Ge3dfjmDN/NrcPz25T+cIaf5qb0Gapy3Lm4svNaEE4qLd7AFQ2Xf0vCRa+Vl4U7fLDgKQy1hjthcUnwKi1U3Vx1oSSYVlbX1jc21tc2t4SQ7TkhhFjEIhaxiEUsEItYxCIWsYgFYhGLWMQiFrFALGIRi1jEIhZ2ola7QsheFGtPCNmPYu0LQQf/Wh1ICNvfOzzc2xcAAHfkF2ouxpBwdu2dAAAAAElFTkSuQmCC";
32 | 				app.children[1].textContent = script_type;
33 | 				location.parentNode.appendChild(app)
34 | 			}
35 | 		};
36 | 		waiter()
37 | 	}
38 | })
39 | 


--------------------------------------------------------------------------------
/src/modules/showMarkdown.js:
--------------------------------------------------------------------------------
 1 | function showMarkdown(id){
 2 | 	if(!location.pathname.match(id)){
 3 | 		return
 4 | 	}
 5 | 	if(document.querySelector(".hohGetMarkdown")){
 6 | 		return
 7 | 	}
 8 | 	let timeContainer = document.querySelector(".activity-text .time,.activity-message .time");
 9 | 	if(!timeContainer){
10 | 		setTimeout(function(){showMarkdown(id)},200);
11 | 		return
12 | 	}
13 | 	if(!useScripts.accessToken && document.querySelector(".private-badge")){
14 | 		return//can't fetch private messages without privileges
15 | 	}
16 | 	let codeLink = create("span",["action","hohGetMarkdown"],"",false,"font-weight:bolder;");
17 | 	timeContainer.insertBefore(codeLink,timeContainer.firstChild);
18 | 	codeLink.onclick = function(){
19 | 		let activityMarkdown = document.querySelector(".activity-markdown");
20 | 		if(activityMarkdown.style.display === "none"){
21 | 			let markdownSource = document.querySelector(".hohMarkdownSource");
22 | 			if(markdownSource){
23 | 				markdownSource.style.display = "none"
24 | 			}
25 | 			activityMarkdown.style.display = "initial"
26 | 		}
27 | 		else{
28 | 			activityMarkdown.style.display = "none";
29 | 			let markdownSource = document.querySelector(".hohMarkdownSource");
30 | 			if(markdownSource){
31 | 				markdownSource.style.display = "initial"
32 | 			}
33 | 			else{
34 | 				const caller = (document.querySelector(".private-badge") ? authAPIcall : generalAPIcall);
35 | 				caller("query($id:Int){Activity(id:$id){...on MessageActivity{text:message}...on TextActivity{text}}}",{id:id},function(data){
36 | 					if(!location.pathname.match(id)){
37 | 						return
38 | 					}
39 | 					if(!data){
40 | 						markdownSource = create("div",["activity-markdown","hohMarkdownSource","hohError"],translate("$error_markdown"),activityMarkdown.parentNode);
41 | 						return
42 | 					}
43 | 					markdownSource = create("div",["activity-markdown","hohMarkdownSource"],data.data.Activity.text,activityMarkdown.parentNode);
44 | 				},"hohGetMarkdown" + id,20*1000)
45 | 			}
46 | 		}
47 | 	}
48 | }
49 | 


--------------------------------------------------------------------------------
/src/modules/singleActivityReplyLikes.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "singleActivityReplyLikes",
 3 | 	description: "Add like tooltips to all replies when viewing a single activity",
 4 | 	isDefault: true,
 5 | 	categories: ["Feeds"],
 6 | 	visible: false,
 7 | 	urlMatch: function(url,oldUrl){
 8 | 		return url.match(/^https:\/\/anilist\.co\/activity\/(\d+)/)
 9 | 	},
10 | 	code: function singleActivityReplyLikes(){
11 | 		let id = parseInt(document.URL.match(/^https:\/\/anilist\.co\/activity\/(\d+)/)[1])
12 | 		let adder = function(data){
13 | 			if(!data){
14 | 				return//private actitivites, mostly. Doesn't matter as there aren't many people there.
15 | 			}
16 | 			if(!document.URL.includes("activity/" + id || !data)){
17 | 				return
18 | 			}
19 | 			let post = document.querySelector(".activity-entry > .wrap > .actions .action.likes");
20 | 			if(!post){
21 | 				setTimeout(function(){adder(data)},200);
22 | 				return
23 | 			}
24 | 			post.classList.add("hohLoadedLikes");
25 | 			post.classList.add("hohHandledLike");
26 | 			if(post.querySelector(".count") && !(parseInt(post.querySelector(".count").innerText) <= 5)){
27 | 				post.title = data.data.Activity.likes.map(like => like.name).join("\n")
28 | 			}
29 | 			let smallAdder = function(){
30 | 				if(!document.URL.includes("activity/" + id)){
31 | 					return
32 | 				}
33 | 				let actionLikes = document.querySelectorAll(".activity-replies .action.likes");
34 | 				if(!actionLikes.length){
35 | 					setTimeout(smallAdder,200);
36 | 					return
37 | 				}
38 | 				actionLikes.forEach((node,index) => {
39 | 					if(node.querySelector(".count") && !(parseInt(node.querySelector(".count").innerText) <= 5)){
40 | 						node.title = data.data.Activity.replies[index].likes.map(like => like.name).join("\n")
41 | 					}
42 | 				});
43 | 			};
44 | 			if(data.data.Activity.replies.length){
45 | 				smallAdder()
46 | 			}
47 | 		}
48 | 		generalAPIcall(`
49 | 	query($id: Int){
50 | 		Activity(id: $id){
51 | 			... on TextActivity{
52 | 				likes{name}
53 | 				replies{likes{name}}
54 | 			}
55 | 			... on MessageActivity{
56 | 				likes{name}
57 | 				replies{likes{name}}
58 | 			}
59 | 			... on ListActivity{
60 | 				likes{name}
61 | 				replies{likes{name}}
62 | 			}
63 | 		}
64 | 	}`,
65 | 			{id: id},
66 | 			adder
67 | 		)
68 | 	}
69 | })
70 | 


--------------------------------------------------------------------------------
/src/modules/slimNav.js:
--------------------------------------------------------------------------------
1 | exportModule({
2 | 	id: "slimNav",
3 | 	description: "$slimNav_description",
4 | 	isDefault: false,
5 | 	importance: -2,
6 | 	categories: ["Navigation"],
7 | 	visible: true
8 | })
9 | 


--------------------------------------------------------------------------------
/src/modules/staff.js:
--------------------------------------------------------------------------------
 1 | function enhanceStaff(){
 2 | 	if(!document.URL.match(/^https:\/\/anilist\.co\/staff\/.*/)){
 3 | 		return
 4 | 	}
 5 | 	if(document.querySelector(".hohFavCount")){
 6 | 		return
 7 | 	}
 8 | 	const variables = {id: document.URL.match(/\/staff\/(\d+)\/?/)[1]};
 9 | 	const query = "query($id: Int!){Staff(id: $id){favourites}}";
10 | 	let favCallback = function(data){
11 | 		if(!document.URL.match(/^https:\/\/anilist\.co\/staff\/.*/)){
12 | 			return
13 | 		}
14 | 		let favCount = document.querySelector(".favourite .count");
15 | 		if(favCount){
16 | 			favCount.parentNode.onclick = function(){
17 | 				if(favCount.parentNode.classList.contains("isFavourite")){
18 | 					favCount.innerText = Math.max(parseInt(favCount.innerText) - 1,0)//0 or above, just to avoid looking silly
19 | 				}
20 | 				else{
21 | 					favCount.innerText = parseInt(favCount.innerText) + 1
22 | 				}
23 | 			};
24 | 			if(data.data.Staff.favourites === 0 && favCount[0].classList.contains("isFavourite")){//safe to assume
25 | 				favCount.innerText = data.data.Staff.favourites + 1
26 | 			}
27 | 			else{
28 | 				favCount.innerText = data.data.Staff.favourites
29 | 			}
30 | 		}
31 | 		else{
32 | 			setTimeout(function(){favCallback(data)},200)
33 | 		}
34 | 	};
35 | 	generalAPIcall(query,variables,favCallback,"hohStaffFavs" + variables.id,60*60*1000)
36 | }
37 | 


--------------------------------------------------------------------------------
/src/modules/staffBrowse.js:
--------------------------------------------------------------------------------
 1 | function enhanceStaffBrowse(){
 2 | 	if(!document.URL.match(/\/search\/staff\/?(favorites)?$/)){
 3 | 		return
 4 | 	}
 5 | 	const query = `
 6 | query($page: Int!){
 7 | 	Page(page: $page,perPage: 30){
 8 | 		staff(sort: [FAVOURITES_DESC]){
 9 | 			id
10 | 			favourites
11 | 			anime:staffMedia(type:ANIME){
12 | 				pageInfo{
13 | 					total
14 | 				}
15 | 			}
16 | 			manga:staffMedia(type:MANGA){
17 | 				pageInfo{
18 | 					total
19 | 				}
20 | 			}
21 | 			characters{
22 | 				pageInfo{
23 | 					total
24 | 				}
25 | 			}
26 | 		}
27 | 	}
28 | }`;
29 | 	let favCallback = function(data,page){
30 | 		if(!document.URL.match(/\/search\/staff\/?(favorites)?$/)){
31 | 			return
32 | 		}
33 | 		let resultsToTag = document.querySelectorAll(".results.cover .staff-card,.landing-section.staff .staff-card");
34 | 		if(resultsToTag.length < page*data.data.Page.staff.length){
35 | 			setTimeout(function(){
36 | 				favCallback(data,page)
37 | 			},200);//may take some time to load
38 | 			return
39 | 		}
40 | 		data = data.data.Page.staff;
41 | 		data.forEach(function(staff,index){
42 | 			create("span","hohFavCountBrowse",staff.favourites,resultsToTag[(page - 1)*data.length + index]).title = "Favourites";
43 | 			if(staff.anime.pageInfo.total + staff.manga.pageInfo.total > staff.characters.pageInfo.total){
44 | 				let roleLine = create("div","hohRoleLine",false,resultsToTag[(page - 1)*data.length + index]);
45 | 				roleLine.style.backgroundImage =
46 | 				"linear-gradient(to right,hsla(" + Math.round(
47 | 					120*(1 + staff.anime.pageInfo.total/(staff.anime.pageInfo.total + staff.manga.pageInfo.total))
48 | 				) + ",100%,50%,0.8),rgba(var(--color-overlay),0.8))";
49 | 				let animePercentage = Math.round(100*staff.anime.pageInfo.total/(staff.anime.pageInfo.total + staff.manga.pageInfo.total));
50 | 				if(animePercentage === 100){
51 | 					roleLine.title = "100% anime"
52 | 				}
53 | 				else if(animePercentage === 0){
54 | 					roleLine.title = "100% manga"
55 | 				}
56 | 				else if(animePercentage >= 50){
57 | 					roleLine.title = animePercentage + "% anime, " + (100 - animePercentage) + "% manga"
58 | 				}
59 | 				else{
60 | 					roleLine.title = (100 - animePercentage) + "% manga, " + animePercentage + "% anime"
61 | 				}
62 | 			}
63 | 		});
64 | 		generalAPIcall(query,{page:page+1},data => favCallback(data,page+1))
65 | 	};
66 | 	generalAPIcall(query,{page:1},data => favCallback(data,1))
67 | }
68 | 


--------------------------------------------------------------------------------
/src/modules/studio.js:
--------------------------------------------------------------------------------
 1 | function enhanceStudio(){//adds a favourite count to every studio page
 2 | 	if(!location.pathname.match(/^\/studio(\/.*)?/)){
 3 | 		return
 4 | 	}
 5 | 	let filterGroup = document.querySelector(".container.header");
 6 | 	if(!filterGroup){
 7 | 		setTimeout(enhanceStudio,200);//may take some time to load
 8 | 		return;
 9 | 	}
10 | 	let favCallback = function(data){
11 | 		if(!document.URL.match(/^https:\/\/anilist\.co\/studio\/.*/)){
12 | 			return
13 | 		}
14 | 		let favCount = document.querySelector(".favourite .count");
15 | 		if(favCount){
16 | 			favCount.parentNode.onclick = function(){
17 | 				if(favCount.parentNode.classList.contains("isFavourite")){
18 | 					favCount.innerText = Math.max(parseInt(favCount.innerText) - 1,0)//0 or above, just to avoid looking silly
19 | 				}
20 | 				else{
21 | 					favCount.innerText = parseInt(favCount.innerText) + 1
22 | 				}
23 | 			};
24 | 			if(data.data.Studio.favourites === 0 && favCount[0].classList.contains("isFavourite")){//safe to assume
25 | 				favCount.innerText = data.data.Studio.favourites + 1
26 | 			}
27 | 			else{
28 | 				favCount.innerText = data.data.Studio.favourites
29 | 			}
30 | 		}
31 | 		else{
32 | 			setTimeout(function(){favCallback(data)},200);
33 | 		}
34 | 	};
35 | 	const variables = {id: location.pathname.match(/\/studio\/(\d+)\/?/)[1]};
36 | 	generalAPIcall(
37 | 		`
38 | query($id: Int!){
39 | 	Studio(id: $id){
40 | 		favourites
41 | 	}
42 | }`,
43 | 		variables,favCallback,"hohStudioFavs" + variables.id,60*60*1000
44 | 	);
45 | }
46 | 


--------------------------------------------------------------------------------
/src/modules/submenu.js:
--------------------------------------------------------------------------------
 1 | if(useScripts.CSSverticalNav && whoAmI && !useScripts.mobileFriendly){
 2 | 	let addMouseover = function(){
 3 | 		let navThingy = document.querySelector(`.nav .links .link[href^="/user/"]`);
 4 | 		if(navThingy){
 5 | 			navThingy.style.position = "relative";
 6 | 			let hackContainer = create("div","subMenuContainer",false,false,"position:relative;width:100%;min-height:50px;z-index:134;display:inline-flex;");
 7 | 			navThingy.parentNode.insertBefore(hackContainer,navThingy);
 8 | 			hackContainer.appendChild(navThingy);
 9 | 			let subMenu = create("div","hohSubMenu",false,hackContainer);
10 | 			let linkStats = create("a","hohSubMenuLink",translate("$submenu_stats"),subMenu);
11 | 			if(useScripts.mangaBrowse){
12 | 				linkStats.href = "/user/" + whoAmI + "/stats/manga/overview";
13 | 				cheapReload(linkStats,{path: "/user/" + whoAmI + "/stats/manga/overview"});
14 | 			}
15 | 			else{
16 | 				linkStats.href = "/user/" + whoAmI + "/stats/anime/overview";
17 | 				cheapReload(linkStats,{path: "/user/" + whoAmI + "/stats/anime/overview"});
18 | 			}
19 | 			[
20 | 				{
21 | 					text: "$submenu_social",
22 | 					href: "/user/" + whoAmI + "/social",
23 | 					vue: {path: "/user/" + whoAmI + "/social"}
24 | 				},
25 | 				{
26 | 					text: "$submenu_reviews",
27 | 					href: "/user/" + whoAmI + "/reviews",
28 | 					vue: {path: "/user/" + whoAmI + "/reviews"}
29 | 				},
30 | 				{
31 | 					text: "$submenu_favourites",
32 | 					href: "/user/" + whoAmI + "/favorites",
33 | 					vue: {path: "/user/" + whoAmI + "/favorites"}
34 | 				},
35 | 				{
36 | 					text: "$submenu_submissions",
37 | 					href: "/user/" + whoAmI + "/submissions",
38 | 					vue: {path: "/user/" + whoAmI + "/submissions"}
39 | 				}
40 | 			].forEach(link => {
41 | 				let element = create("a","hohSubMenuLink",translate(link.text),subMenu);
42 | 				element.href = link.href;
43 | 				if(link.vue){
44 | 					cheapReload(element,link.vue)
45 | 				}
46 | 			})
47 | 			hackContainer.onmouseenter = function(){
48 | 				subMenu.style.display = "inline"
49 | 			}
50 | 			hackContainer.onmouseleave = function(){
51 | 				subMenu.style.display = "none"
52 | 			}
53 | 		}
54 | 		else{
55 | 			setTimeout(addMouseover,500)
56 | 		}
57 | 	};addMouseover()
58 | }
59 | 


--------------------------------------------------------------------------------
/src/modules/tweets.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "tweets",
 3 | 	description: "$setting_tweets",
 4 | 	extendedDescription: `
 5 | This works by running Twitter's official embedding script. Be advised that Twitter embeds display NSFW content no differently than other content.
 6 | 	`,
 7 | 	isDefault: false,
 8 | 	categories: ["Feeds"],
 9 | 	visible: true,
10 | 	boneless_disabled: true
11 | })
12 | 
13 | const isPublishedMozillaAddon = false;
14 | let tweetLoop;
15 | if(useScripts.tweets && !isPublishedMozillaAddon){
16 | 	tweetLoop = setInterval(function(){
17 | 		document.querySelectorAll(
18 | 			`.markdown a[href^="https://twitter.com/"][href*="/status/"]`
19 | 		).forEach(tweet => {
20 | 			if(tweet.classList.contains("hohEmbedded")){
21 | 				return
22 | 			}
23 | 			let tweetMatch = tweet.href.match(/^https:\/\/twitter\.com\/(.+?)\/status\/\d+/)
24 | 			if(!tweetMatch || tweet.href !== tweet.innerText){
25 | 				return
26 | 			}
27 | 			tweet.classList.add("hohEmbedded");
28 | 			let tweetBlockQuote = create("blockquote",false,false,tweet);
29 | 			tweetBlockQuote.classList.add("twitter-tweet");
30 | 			if(document.body.classList.contains("site-theme-dark")){
31 | 				tweetBlockQuote.setAttribute("data-theme","dark")
32 | 			}
33 | 			let tweetBlockQuoteInner = create("p",false,false,tweetBlockQuote);
34 | 			tweetBlockQuoteInner.setAttribute("lang","en");
35 | 			tweetBlockQuoteInner.setAttribute("dir","ltr");
36 | 			let tweetBlockQuoteInnerInner = create("a","hohEmbedded","Loading tweet by " + tweetMatch[1] + "...",tweetBlockQuoteInner)
37 | 				.href = tweet.href;
38 | 			if(document.getElementById("hohTwitterEmbed") && window.twttr){
39 | 				window.twttr.widgets.load(tweet)
40 | 			}
41 | 			else{
42 | 				let script = document.createElement("script");
43 | 				script.setAttribute("src","https://platform.twitter.com/widgets.js");
44 | 				script.setAttribute("async","");
45 | 				script.id = "hohTwitterEmbed";
46 | 				document.head.appendChild(script)
47 | 			}
48 | 		})
49 | 	},400);
50 | }
51 | 


--------------------------------------------------------------------------------
/src/modules/twoColumnFeed.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "twoColumnFeed",
 3 | 	description: "$twoColumnFeed_description",
 4 | 	isDefault: false,
 5 | 	importance: 0,
 6 | 	categories: ["Feeds"],
 7 | 	visible: true,
 8 | 	css: `
 9 | .home .activity-feed{
10 | 	grid-template-columns: repeat(2,1fr);
11 | 	display: grid;
12 | 	grid-column-gap: 15px;
13 | }
14 | .home .activity-feed .activity-entry.activity-text{
15 | 	grid-column: 1/3;
16 | }
17 | .home .activity-feed .activity-entry{
18 | 	margin-bottom: 15px;
19 | }
20 | `
21 | })
22 | 
23 | if(useScripts.twoColumnFeed && !useScripts.CSSverticalNav){
24 | 	moreStyle.textContent += `
25 | .home{
26 | 	margin-left: -15px;
27 | 	margin-right: -15px;
28 | }
29 | @media(min-width: 1540px){
30 | 	.home{
31 | 		margin-left: -95px;
32 | 		margin-right: -95px;
33 | 	}
34 | }
35 | @media(min-width:1040px) and (max-width:1540px){
36 | 	.home{
37 | 		margin-left: -45px;
38 | 		margin-right: -45px;
39 | 	}
40 | }
41 | @media(min-width:760px) and (max-width:1040px){
42 | 	.home{
43 | 		margin-left: -25px;
44 | 		margin-right: -25px;
45 | 	}
46 | }
47 | @media(max-width: 1040px){
48 | 	.home .activity-anime_list .details,.home .activity-manga_list .details{
49 | 		padding-right: 15px;
50 | 	}
51 | }
52 | @media(max-width: 760px){
53 | 	.home .activity-anime_list .details,.home .activity-manga_list .details{
54 | 		padding-top: 35px;
55 | 	}
56 | }
57 | @media(max-width: 500px){
58 | 	.home .activity-anime_list .cover,.home .activity-manga_list .cover{
59 | 		padding-top: 35px;
60 | 		max-height: 120px;
61 | 	}
62 | 	.home .activity-entry > .wrap > .actions{
63 | 		width: calc(100% - 25px);
64 | 		bottom: 7px;
65 | 		display: flex;
66 | 	}
67 | 	.home .activity-feed{
68 | 		grid-column-gap: 10px;
69 | 	}
70 | }
71 | `
72 | }
73 | 


--------------------------------------------------------------------------------
/src/modules/unicodifier.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "unicodifier",
 3 | 	description: "$module_unicodifier_description",
 4 | 	extendedDescription: "$module_unicodifier_extendedDescription",
 5 | 	isDefault: true,
 6 | 	importance: 0,
 7 | 	categories: ["Feeds","Forum"],
 8 | 	visible: true
 9 | })
10 | 
11 | setInterval(function(){
12 | 	Array.from(document.querySelectorAll(".activity-edit textarea.el-textarea__inner,.editor textarea.el-textarea__inner")).forEach(editor => {
13 | 		if(editor.value){
14 | 			editor.value = emojiSanitize(editor.value);
15 | 			editor.dispatchEvent(new Event("input",{bubbles: false}))
16 | 		}
17 | 	})
18 | },2000)
19 | 


--------------------------------------------------------------------------------
/src/modules/videoMimeTypeFixer.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "videoMimeTypeFixer",
 3 | 	description: "$videoMimeTypeFixer_description",
 4 | 	extendedDescription: `
 5 | Anilist by default serves all video as "video/webm".
 6 | However, it's common to use non-webm video, as brower support is common.
 7 | But some browsers don't autodetect the proper mime type. This module adds a mime type based on the file extension, which may help if the video won't play otherwise.
 8 | 	`,
 9 | 	isDefault: false,
10 | 	categories: ["Feeds"],
11 | 	visible: true
12 | })
13 | 
14 | if(useScripts.videoMimeTypeFixer){
15 | 	setInterval(function(){
16 | 		document.querySelectorAll('source[src$=".av1"][type="video/webm"]').forEach(video => {
17 | 			video.setAttribute("type","video/av1")
18 | 		})
19 | 		document.querySelectorAll('source[src$=".mp4"][type="video/webm"]').forEach(video => {
20 | 			video.setAttribute("type","video/mp4")
21 | 		})
22 | 		document.querySelectorAll('source[src$=".avi"][type="video/webm"]').forEach(video => {
23 | 			video.setAttribute("type","video/x-msvideo")
24 | 		})
25 | 		document.querySelectorAll('source[src$=".mpeg"][type="video/webm"]').forEach(video => {
26 | 			video.setAttribute("type","video/mpeg")
27 | 		})
28 | 		document.querySelectorAll('source[src$=".ogg"][type="video/webm"]').forEach(video => {
29 | 			video.setAttribute("type","video/ogv")
30 | 		})
31 | 		document.querySelectorAll('source[src$=".ts"][type="video/webm"]').forEach(video => {
32 | 			video.setAttribute("type","video/mp2t")
33 | 		})
34 | 	},2000)
35 | }
36 | 


--------------------------------------------------------------------------------
/src/modules/viewAdvancedScores.js:
--------------------------------------------------------------------------------
 1 | function viewAdvancedScores(url){
 2 | 	let URLstuff = url.match(/^https:\/\/anilist\.co\/user\/(.+)\/(anime|manga)list\/?/);
 3 | 	let name = decodeURIComponent(URLstuff[1]);
 4 | 	generalAPIcall(
 5 | 		`query($name:String!){
 6 | 			User(name:$name){
 7 | 				mediaListOptions{
 8 | 					animeList{advancedScoringEnabled}
 9 | 					mangaList{advancedScoringEnabled}
10 | 				}
11 | 			}
12 | 		}`,
13 | 		{name: name},function(data){
14 | 		if(
15 | 			!(
16 | 				(URLstuff[2] === "anime" && data.data.User.mediaListOptions.animeList.advancedScoringEnabled)
17 | 				|| (URLstuff[2] === "manga" && data.data.User.mediaListOptions.mangaList.advancedScoringEnabled)
18 | 			)
19 | 		){
20 | 			return
21 | 		}
22 | 		generalAPIcall(
23 | 			`query($name:String!,$listType:MediaType){
24 | 				MediaListCollection(userName:$name,type:$listType){
25 | 					lists{
26 | 						entries{mediaId advancedScores}
27 | 					}
28 | 				}
29 | 			}`,
30 | 			{name: name,listType: URLstuff[2].toUpperCase()},
31 | 			function(data2){
32 | 				let list = new Map(returnList(data2,true).map(a => [a.mediaId,a.advancedScores]));
33 | 				let finder = function(){
34 | 					if(!document.URL.match(/^https:\/\/anilist\.co\/user\/(.+)\/(anime|manga)list\/?/)){
35 | 						return
36 | 					}
37 | 					document.querySelectorAll(
38 | 						".list-entries .entry .title > a:not(.hohAdvanced)"
39 | 					).forEach(function(entry){
40 | 						entry.classList.add("hohAdvanced");
41 | 						let key = parseInt(entry.href.match(/\/(\d+)\//)[1]);
42 | 						let dollar = create("span",["hohAdvancedDollar","noselect"],"$",entry.parentNode);
43 | 						let advanced = list.get(key);
44 | 						let reasonable = Object.keys(advanced).map(
45 | 							key => [key,advanced[key]]
46 | 						).filter(
47 | 							a => a[1]
48 | 						);
49 | 						dollar.dataset.tooltip = reasonable.map(
50 | 							a => a[0] + ": " + a[1]
51 | 						).join("\n");
52 | 						if(!reasonable.length){
53 | 							dollar.style.display = "none"
54 | 						}
55 | 					});
56 | 					setTimeout(finder,1000);
57 | 				};finder();
58 | 			}
59 | 		)
60 | 	})
61 | }
62 | 


--------------------------------------------------------------------------------
/src/modules/webmResize.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "webmResize",
 3 | 	description: "$webmResize_description",
 4 | 	isDefault: true,
 5 | 	categories: ["Feeds"],
 6 | 	visible: true
 7 | })
 8 | 
 9 | if(useScripts.webmResize){
10 | 	setInterval(function(){
11 | 		document.querySelectorAll("source").forEach(video => {
12 | 			let hashMatch = (video.src || "").match(/#(image)?(\d+(\.\d+)?%?)$/);
13 | 			if(hashMatch && !video.parentNode.width){
14 | 				video.parentNode.setAttribute("width",hashMatch[2])
15 | 			}
16 | 			if(video.src.match(/#image\d*(\.\d+)?%?$/)){
17 | 				video.parentNode.removeAttribute("controls")
18 | 			}
19 | 		})
20 | 	},500)
21 | }
22 | 


--------------------------------------------------------------------------------
/src/modules/yearStepper.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "yearStepper",
 3 | 	description: "$yearStepper_description",
 4 | 	isDefault: true,
 5 | 	importance: 0,
 6 | 	categories: ["Lists"],
 7 | 	visible: true,
 8 | 	urlMatch: function(url,oldUrl){
 9 | 		return url.match(/\/user\/.*\/(anime|manga)list/)
10 | 	},
11 | 	code: function(){
12 | 		let yearStepper = function(){
13 | 			if(!location.pathname.match(/\/user\/.*\/(anime|manga)list/)){
14 | 				return
15 | 			}
16 | 			let slider = document.querySelector(".el-slider");
17 | 			if(!slider){
18 | 				setTimeout(yearStepper,200);
19 | 				return
20 | 			}
21 | 			const maxYear = parseInt(slider.getAttribute("aria-valuemax"));
22 | 			const minYear = parseInt(slider.getAttribute("aria-valuemin"));
23 | 			const yearRange = maxYear - minYear;
24 | 			let clickSlider = function(year){//thanks, mator!
25 | 				let runway = slider.children[0];
26 | 				let r = runway.getBoundingClientRect();
27 | 				const x = r.left + r.width * ((year - minYear) / yearRange);
28 | 				const y = r.top + r.height / 2;
29 | 				runway.dispatchEvent(new MouseEvent("click",{
30 | 					clientX: x,
31 | 					clientY: y
32 | 				}))
33 | 			};
34 | 			let adjuster = function(delta){
35 | 				let heading = slider.previousElementSibling;
36 | 				if(heading.children.length === 0){
37 | 					if(delta === -1){
38 | 						clickSlider(maxYear)
39 | 					}
40 | 					else{
41 | 						clickSlider(minYear + 1)
42 | 					}
43 | 				}
44 | 				else{
45 | 					let current = parseInt(heading.children[0].innerText);
46 | 					clickSlider(current + delta)
47 | 				}
48 | 			};
49 | 			if(document.querySelector(".hohStepper")){
50 | 				return
51 | 			}
52 | 			slider.style.position = "relative";
53 | 			let decButton = create("span",["hohStepper","noselect"],"<",slider,"left:-27px;font-size:200%;top:0px;");
54 | 			let incButton = create("span",["hohStepper","noselect"],">",slider,"right:-27px;font-size:200%;top:0px;");
55 | 			decButton.onclick = function(){
56 | 				adjuster(-1)
57 | 			};
58 | 			incButton.onclick = function(){
59 | 				adjuster(1)
60 | 			}
61 | 		};yearStepper()
62 | 	},
63 | 	css: `
64 | .hohStepper{
65 | 	cursor: pointer;
66 | 	position: absolute;
67 | 	opacity: 0.5;
68 | }
69 | .el-slider:hover .hohStepper{
70 | 	opacity: 1;
71 | }`
72 | })
73 | 


--------------------------------------------------------------------------------
/src/modules/youtubeFullscreen.js:
--------------------------------------------------------------------------------
 1 | exportModule({
 2 | 	id: "youtubeFullscreen",
 3 | 	description: "$youtubeFullscreen_description",
 4 | 	isDefault: false,
 5 | 	categories: ["Feeds"],
 6 | 	visible: true
 7 | })
 8 | 
 9 | if(useScripts.youtubeFullscreen){
10 | 	setInterval(function(){
11 | 		document.querySelectorAll(".youtube iframe").forEach(video => {
12 | 			if(!video.hasAttribute("allowfullscreen")){
13 | 				video.setAttribute("allowfullscreen","allowfullscreen");
14 | 				video.setAttribute("frameborder","0");
15 | 				video.setAttribute("src",video.getAttribute("src").replace("autohide=1","autohide=0"))
16 | 			}
17 | 		})
18 | 	},1000)
19 | }
20 | 


--------------------------------------------------------------------------------
/src/queries/blockedNumber.js:
--------------------------------------------------------------------------------
 1 | {name: "How many people have blocked you",code: function(){
 2 | 	if(!useScripts.accessToken){
 3 | 		miscResults.innerText = loginMessage;
 4 | 		return
 5 | 	}
 6 | 	authAPIcall("query{Page{pageInfo{total}users{id}}}",{},function(data){
 7 | 		generalAPIcall("query{Page{pageInfo{total}users{id}}}",{},function(data2){
 8 | 			miscResults.innerText = "This only applies to you, regardless of what stats page you ran this query from.";
 9 | 			if(data.data.Page.pageInfo.total === data2.data.Page.pageInfo.total){
10 | 				create("p",false,"No users have blocked you",miscResults)
11 | 			}
12 | 			else if((data2.data.Page.pageInfo.total - data.data.Page.pageInfo.total) < 0){
13 | 				create("p",false,"Error: The elevated privileges of moderators makes this query fail",miscResults)
14 | 			}
15 | 			else{
16 | 				create("p",false,(data2.data.Page.pageInfo.total - data.data.Page.pageInfo.total) + " users have blocked you",miscResults)
17 | 			}
18 | 		})
19 | 	})
20 | }},
21 | 


--------------------------------------------------------------------------------
/src/queries/findBlocked.js:
--------------------------------------------------------------------------------
 1 | {name: "Find people you have blocked/are blocked by",code: function(){
 2 | 	if(!useScripts.accessToken){
 3 | 		miscResults.innerText = loginMessage;
 4 | 		return
 5 | 	}
 6 | 	miscResults.innerText = `This only applies to you, regardless of what stats page you ran this query from. Furthermore, it probably won't find everyone.
 7 | Use the other query if you just want the number.`;
 8 | 	let flag = true;
 9 | 	let stopButton = create("button",["button","hohButton"],"Stop",miscResults,"display:block");
10 | 	let progress = create("p",false,false,miscResults);
11 | 	stopButton.onclick = function(){
12 | 		flag = false
13 | 	};
14 | 	let blocks = new Set();
15 | 	progress.innerText = "1 try..."
16 | 	let caller = function(page,page2){
17 | 		generalAPIcall(`
18 | query($page: Int){
19 | 	Page(page: $page){
20 | 		activities(sort: ID_DESC,type: TEXT){
21 | 			... on TextActivity{
22 | 				id
23 | 				user{name}
24 | 			}
25 | 		}
26 | 	}
27 | }`,
28 | 		{page: page},function(data){
29 | 			progress.innerText = (page + 1) + " tries...";
30 | 			authAPIcall(`
31 | query($page: Int){
32 | 	Page(page: $page){
33 | 		activities(sort: ID_DESC,type: TEXT){
34 | 			... on TextActivity{
35 | 				id
36 | 			}
37 | 		}
38 | 	}
39 | }`,						{page: page2},function(data2){
40 | 				let offset = 0;
41 | 				while(data2.data.Page.activities[offset].id > data.data.Page.activities[0].id){
42 | 					offset++
43 | 				};
44 | 				while(data2.data.Page.activities[0].id < data.data.Page.activities[-offset].id){
45 | 					offset--
46 | 				};
47 | 				for(var k=Math.max(-offset,0);k {
10 | 			if(act.text.match(new RegExp(searchQuery,"i")) || act.text.includes(searchQuery)){
11 | 				let newDate = create("p",false,false,results,"font-family:monospace;margin-right:10px;");
12 | 				let newPage = create("a","newTab",act.siteUrl,newDate,"color:rgb(var(--color-blue));");
13 | 				newPage.href = act.siteUrl;
14 | 				newDate.innerHTML += DOMPurify.sanitize(act.text);//reason for innerHTML: preparsed sanitized HTML from the Anilist API
15 | 				create("hr",false,false,results)
16 | 			}
17 | 		})
18 | 	}
19 | 	else{
20 | 		generalAPIcall("query($name:String){User(name:$name){id}}",{name: user},function(data){
21 | 			const query = `
22 | 			query($userId: Int,$page: Int){
23 | 				Page(page: $page){
24 | 					pageInfo{
25 | 						currentPage
26 | 						total
27 | 						lastPage
28 | 					}
29 | 					activities (userId: $userId, sort: ID_DESC, type: TEXT){
30 | 						... on TextActivity{
31 | 							siteUrl
32 | 							text(asHtml: true)
33 | 						}
34 | 					}
35 | 				}
36 | 			}`;
37 | 			miscResults.innerText = "";
38 | 			let results = create("p",false,false,miscResults);
39 | 			let posts = 0;
40 | 			let progress = create("p",false,false,miscResults);
41 | 			let userId = data.data.User.id;
42 | 			let addNewUserData = function(data){
43 | 				console.log(data);
44 | 				if(!data){
45 | 					return
46 | 				}
47 | 				if(data.data.Page.pageInfo.currentPage === 1){
48 | 					for(var i=2;i<=data.data.Page.pageInfo.lastPage && i < ANILIST_QUERY_LIMIT;i++){
49 | 						generalAPIcall(query,{userId: userId,page: i},addNewUserData)
50 | 					}
51 | 				};
52 | 				posts += data.data.Page.activities.length;
53 | 				progress.innerText = "Searching status post " + posts + "/" + data.data.Page.pageInfo.total;
54 | 				data.data.Page.activities.forEach(act => {
55 | 					if(act.text.match(new RegExp(searchQuery,"i")) || act.text.includes(searchQuery)){
56 | 						let newDate = create("p",false,false,results,"font-family:monospace;margin-right:10px;");
57 | 						let newPage = create("a","newTab",act.siteUrl,newDate,"color:rgb(var(--color-blue));");
58 | 						newPage.href = act.siteUrl;
59 | 						newDate.innerHTML += DOMPurify.sanitize(act.text);//reason for innerHTML: preparsed sanitized HTML from the Anilist API
60 | 						create("hr",false,false,results)
61 | 					}
62 | 					statusSearchCache.push(act)
63 | 				})
64 | 			};
65 | 			generalAPIcall(query,{userId: userId,page: 1},addNewUserData);
66 | 		},"hohIDlookup" + user.toLowerCase())
67 | 	}
68 | }},
69 | 


--------------------------------------------------------------------------------
/src/queries/firstActivity.js:
--------------------------------------------------------------------------------
 1 | {name: translate("$query_firstActivity"),code: function(){
 2 | 	generalAPIcall("query($name:String){User(name:$name){id}}",{name: user},function(data){
 3 | 		let userId = data.data.User.id;
 4 | 		let userFirstQuery =
 5 | 		`query ($userId: Int) {
 6 | 			Activity(sort: ID,userId: $userId){
 7 | 				... on MessageActivity {
 8 | 					id
 9 | 					createdAt
10 | 				}
11 | 				... on TextActivity {
12 | 					id
13 | 					createdAt
14 | 				}
15 | 				... on ListActivity {
16 | 					id
17 | 					createdAt
18 | 				}
19 | 			}
20 | 		}`;
21 | 		generalAPIcall(userFirstQuery,{userId: userId},function(data){
22 | 			miscResults.innerText = "";
23 | 			let newPage = create("a",false,"https://anilist.co/activity/" + data.data.Activity.id,miscResults,"color:rgb(var(--color-blue));padding-right:30px;");
24 | 			newPage.href = "/activity/" + data.data.Activity.id;
25 | 			let createdAt = data.data.Activity.createdAt;
26 | 			create("span",false," " + (new Date(createdAt*1000)),miscResults);
27 | 			let possibleOlder = create("p",false,false,miscResults);
28 | 			for(var i=1;i<=15;i++){
29 | 				generalAPIcall(userFirstQuery,{userId: userId + i},function(data){
30 | 					if(!data){return};
31 | 					if(data.data.Activity.createdAt < createdAt){
32 | 						createdAt = data.data.Activity.createdAt;
33 | 						possibleOlder.innerText = "But the account is known to exist already at " + (new Date(createdAt * 1000));
34 | 					}
35 | 				})
36 | 			}
37 | 		},"hohFirstActivity" + data.data.User.id,60*1000);
38 | 	},"hohIDlookup" + user.toLowerCase());
39 | }},
40 | 


--------------------------------------------------------------------------------
/src/queries/messageSpy.js:
--------------------------------------------------------------------------------
 1 | {name: "Message spy",code: function(){
 2 | 	miscResults.innerText = "";
 3 | 	let page = 1;
 4 | 	let results = create("div",false,false,miscResults);
 5 | 	let moreButton = create("button",["button","hohButton"],"Load more",miscResults);
 6 | 	let getPage = function(page){
 7 | 		generalAPIcall(`
 8 | query($page: Int){
 9 | 	Page(page: $page){
10 | 		activities(type: MESSAGE,sort: ID_DESC){
11 | 			... on MessageActivity{
12 | 				id
13 | 				recipient{name}
14 | 				message(asHtml: true)
15 | 				pure:message(asHtml: false)
16 | 				createdAt
17 | 				messenger{name}
18 | 			}
19 | 		}
20 | 	}
21 | }`,
22 | 			{page: page},
23 | 			data => {
24 | 				data.data.Page.activities.forEach(function(message){
25 | 					if(
26 | 						message.pure.includes("AWC")
27 | 						|| message.pure.match(/^.{0,8}(thanks|tha?n?x|thank|ty).*follow.{0,10}(http.*(jpg|png|gif))?.{0,10}$/i)
28 | 						|| message.pure.match(/for( the)? follow/i)
29 | 					){
30 | 						return
31 | 					};
32 | 					let time = new Date(message.createdAt*1000);
33 | 					let newElem = create("div","message",false,results);
34 | 					create("span","time",time.toISOString().match(/^(.*)\.000Z$/)[1] + " ",newElem);
35 | 					let user = create("a",["link","newTab"],message.messenger.name,newElem,"color:rgb(var(--color-blue))");
36 | 					user.href = "/user/" + message.messenger.name;
37 | 					create("span",false," sent a message to ",newElem);
38 | 					let user2 = create("a",["link","newTab"],message.recipient.name,newElem,"color:rgb(var(--color-blue))");
39 | 					user2.href = "/user/" + message.recipient.name;
40 | 					let link = create("a",["link","newTab"]," Link",newElem);
41 | 					link.href = "/activity/" + message.id;
42 | 					newElem.innerHTML += DOMPurify.sanitize(message.message);//reason for innerHTML: preparsed sanitized HTML from the Anilist API
43 | 					create("hr",false,false,results);
44 | 				})
45 | 			}
46 | 		);
47 | 	};getPage(page);
48 | 	moreButton.onclick = function(){
49 | 		page++;
50 | 		getPage(page);
51 | 	}
52 | }},
53 | 


--------------------------------------------------------------------------------
/src/queries/mostLikedStatus.js:
--------------------------------------------------------------------------------
 1 | {name: "Most liked status posts",code: function(){
 2 | 	generalAPIcall("query($name:String){User(name:$name){id}}",{name: user},function(data){
 3 | 		let userId = data.data.User.id;
 4 | 		let list = [];
 5 | 		miscResults.innerText = "";
 6 | 		let progress = create("p",false,false,miscResults);
 7 | 		let results = create("p",false,false,miscResults);
 8 | 		const query = `
 9 | 		query($userId: Int,$page: Int){
10 | 			Page(page: $page){
11 | 				pageInfo{
12 | 					currentPage
13 | 					total
14 | 					lastPage
15 | 				}
16 | 				activities (userId: $userId, sort: ID_DESC, type: TEXT){
17 | 					... on TextActivity{
18 | 						siteUrl
19 | 						likes{id}
20 | 					}
21 | 				}
22 | 			}
23 | 		}`;
24 | 		let addNewUserData = function(data){
25 | 			list = list.concat(data.data.Page.activities);
26 | 			if(data.data.Page.pageInfo.currentPage === 1){
27 | 				for(var i=2;i<=Math.min(data.data.Page.pageInfo.lastPage,50);i++){//FIXME temporary workaround to prevent crashes until anilist fixes the page API. Limits to 2500 posts
28 | 					generalAPIcall(query,{userId: userId,page: i},addNewUserData);
29 | 				};
30 | 			};
31 | 			list.sort(function(b,a){return a.likes.length - b.likes.length});
32 | 			progress.innerText = "Searching status post " + list.length + "/" + data.data.Page.pageInfo.total + " [total is incorrect until an Anilist bug is fixed. Query limited to 2500]";
33 | 			removeChildren(results)
34 | 			for(var i=0;i<20;i++){
35 | 				let newDate = create("p",false,list[i].likes.length + " likes ",results,"font-family:monospace;margin-right:10px;");
36 | 				let newPage = create("a","newTab",list[i].siteUrl,newDate,"color:rgb(var(--color-blue));");
37 | 				newPage.href = list[i].siteUrl;
38 | 			};
39 | 		};
40 | 		generalAPIcall(query,{userId: userId,page: 1},addNewUserData);
41 | 	},"hohIDlookup" + user.toLowerCase());
42 | }},
43 | 


--------------------------------------------------------------------------------
/src/queries/notecleaner.js:
--------------------------------------------------------------------------------
 1 | {name: "Note cleaner",
 2 | setup: function(){
 3 | 	if(!useScripts.accessToken){
 4 | 		miscResults.innerText = loginMessage;
 5 | 		return
 6 | 	};
 7 | 	if(user.toLowerCase() !== whoAmI.toLowerCase()){
 8 | 		miscResults.innerText = "This is the profile of\"" + user + "\", but currently signed in as \"" + whoAmI + "\". Are you sure this is right?";
 9 | 		return
10 | 	};
11 | 	let warning = create("b",false,"Clicking on the red button means changes to your data!",miscResults);
12 | 	let description = create("p",false,"When run, this will remove all your list notes. You can not get them back",miscResults);
13 | 	create("hr",false,false,miscResults);
14 | 	let select = create("select","#typeSelect",false,miscOptions);
15 | 	let animeOption = create("option",false,"Anime",select);
16 | 	let mangaOption = create("option",false,"Manga",select);
17 | 	animeOption.value = "ANIME";
18 | 	mangaOption.value = "MANGA";
19 | 	let fullRun = create("button",["button","hohButton","danger"],"RUN",miscResults);
20 | 	create("hr",false,false,miscResults);
21 | 	let changeLog = create("div",false,false,miscResults);
22 | 
23 | 	let runner = function(){
24 | 		authAPIcall("query($name:String){User(name:$name){id}}",{name: user},function(iddata,error){
25 | 			if(!iddata){
26 | 				alert("ID lookup failed!");
27 | 				console.log(iddata,error);
28 | 				return
29 | 			}
30 | 			authAPIcall(` 
31 | query ($type: MediaType $userId: Int) {
32 | 	MediaListCollection (type: $type userId: $userId ) {
33 | 		user {
34 | 			name
35 | 		}
36 | 		lists {
37 | 			entries {
38 | 				id
39 | 				notes
40 | 			}
41 | 		}
42 | 	}
43 | }`,
44 | 			{type: select.value,userId: iddata.data.User.id},
45 | 			function(data){
46 | 				if(!data){
47 | 					alert("loading list failed!");
48 | 					return
49 | 				};
50 |                    		let t = {};
51 | 				let mediaEntries = [];
52 | 
53 | 				// Go through all lists
54 | 				data.data.MediaListCollection.lists.forEach((list) => {
55 | 					// Go through all entries of each list
56 | 					list.entries.forEach((entry) => {
57 | 						// If entry has notes, add it to the array
58 | 						if(entry.notes){
59 | 							mediaEntries.push(entry.id);
60 | 							t[entry.id] = entry.notes;
61 | 						}
62 | 					})
63 | 				});
64 | 
65 | 				// Remove duplicates from the array
66 | 				mediaEntries = [... new Set(mediaEntries)];
67 | 				changeLog.innerText = JSON.stringify(t, null, 2);
68 | 				authAPIcall(
69 | `mutation ($ids: [Int]) {
70 | 	UpdateMediaListEntries (ids: $ids notes: "") {
71 | 		id
72 | 		notes
73 | 	}
74 | }`,
75 | 					{ids: mediaEntries},
76 | 					function(data){
77 | 						if(!data){
78 | 							alert("deleting notes failed!");
79 | 						}
80 | 						else{
81 | 							alert("notes deleted!");
82 | 						}
83 | 					}
84 | 				)
85 | 			})
86 | 		},"hohIDlookup" + user.toLowerCase())
87 | 	};
88 | 	fullRun.onclick = function(){
89 | 		runner()
90 | 	}
91 | },code: function(){
92 | 	alert("Read the description first!")
93 | }},
94 | 


--------------------------------------------------------------------------------
/src/queries/queries.js:
--------------------------------------------------------------------------------
 1 | m4_include(queries/firstActivity.js)
 2 | m4_include(queries/rank.js)
 3 | m4_include(queries/relatedAnime.js)
 4 | m4_include(queries/relatedManga.js)
 5 | m4_include(queries/compatibility.js)
 6 | m4_include(queries/messageSpy.js)
 7 | m4_include(queries/mediaStatistics.js)
 8 | m4_include(queries/popularFavourites.js)
 9 | m4_include(queries/datingMess.js)
10 | m4_include(queries/datingMessDanger.js)
11 | m4_include(queries/notecleaner.js)
12 | m4_include(queries/reviews.js)
13 | m4_include(queries/blockedNumber.js)
14 | m4_include(queries/findBlocked.js)
15 | m4_include(queries/BroomCat.js)
16 | m4_include(queries/autorecs.js)
17 | m4_include(queries/findStatus.js)
18 | m4_include(queries/findMessage.js)
19 | m4_include(queries/mostLikedStatus.js)
20 | m4_include(queries/seasonalStats.js)
21 | m4_include(queries/submissionStats.js)
22 | 


--------------------------------------------------------------------------------
/src/queries/rank.js:
--------------------------------------------------------------------------------
 1 | {name: "Rank",code: function(){
 2 | 	generalAPIcall(
 3 | 		"query($name:String){User(name:$name){name stats{watchedTime chaptersRead}}}",
 4 | 		{name: user},
 5 | 		function(data){
 6 | 			miscResults.innerText = "";
 7 | 			create("p",false,"NOTE: Due to an unfixed bug in the Anilist API, these results are increasingly out of date. This query is just kept here in case future changes allows it to work properly again.",miscResults);
 8 | 			create("p",false,"Time watched: " + (data.data.User.stats.watchedTime/(60*24)).roundPlaces(1) + " days",miscResults);
 9 | 			create("p",false,"Chapters read: " + data.data.User.stats.chaptersRead,miscResults);
10 | 			let ranks = {
11 | 				"anime": create("p",false,false,miscResults),
12 | 				"manga": create("p",false,false,miscResults)
13 | 			};
14 | 			let recursiveCall = function(userName,amount,currentPage,minPage,maxPage,type){
15 | 				ranks[type].innerText = capitalize(type) + " rank: [calculating...] range " + ((minPage - 1)*50 + 1) + " - " + (maxPage ? maxPage*50 : "");
16 | 				generalAPIcall(
17 | 					`
18 | query($page:Int){
19 | 	Page(page:$page){
20 | 		pageInfo{lastPage}
21 | 			users(sort:${type === "anime" ? "WATCHED_TIME_DESC" : "CHAPTERS_READ_DESC"}){
22 | 			stats{${type === "anime" ? "watchedTime" : "chaptersRead"}}
23 | 		}
24 | 	}
25 | }`,
26 | 					{page: currentPage},
27 | 					function(data){
28 | 						if(!maxPage){
29 | 							maxPage = data.data.Page.pageInfo.lastPage
30 | 						}
31 | 						let block = (
32 | 							type === "anime"
33 | 							? Array.from(data.data.Page.users,(a) => a.stats.watchedTime)
34 | 							: Array.from(data.data.Page.users,(a) => a.stats.chaptersRead)
35 | 						);
36 | 						if(block[block.length - 1] > amount){
37 | 							recursiveCall(userName,amount,Math.floor((currentPage + 1 + maxPage)/2),currentPage + 1,maxPage,type);
38 | 							return;
39 | 						}
40 | 						else if(block[0] > amount){
41 | 							block.forEach(function(item,index){
42 | 								if(amount === item){
43 | 									ranks[type].innerText = capitalize(type) + " rank: " + ((currentPage - 1)*50 + index + 1);
44 | 									return;
45 | 								}
46 | 							})
47 | 						}
48 | 						else if(block[0] === amount){
49 | 							if(minPage === currentPage){
50 | 								ranks[type].innerText = capitalize(type) + " rank: " + ((currentPage-1)*50 + 1)
51 | 							}
52 | 							else{
53 | 								recursiveCall(userName,amount,Math.floor((minPage + currentPage)/2),minPage,currentPage,type)
54 | 							};
55 | 							return;
56 | 						}
57 | 						else{
58 | 							recursiveCall(userName,amount,Math.floor((minPage + currentPage - 1)/2),minPage,currentPage - 1,type);
59 | 							return;
60 | 						};
61 | 					},"hohRank" + type + currentPage,60*60*1000
62 | 				);
63 | 			};
64 | 			recursiveCall(user,data.data.User.stats.watchedTime,1000,1,undefined,"anime");
65 | 			recursiveCall(user,data.data.User.stats.chaptersRead,500,1,undefined,"manga");
66 | 		},"hohRankStats" + user,2*60*1000
67 | 	);
68 | }},
69 | 


--------------------------------------------------------------------------------
/src/utilities/displayBox.js:
--------------------------------------------------------------------------------
 1 | function createDisplayBox(cssProperties,windowTitle){
 2 | 	let displayBox = create("div","hohDisplayBox",false,document.querySelector("#app") || document.querySelector(".termsFeed") || document.body,cssProperties);
 3 | 	if(windowTitle){
 4 | 		create("span","hohDisplayBoxTitle",windowTitle,displayBox)
 5 | 	}
 6 | 	let mousePosition;
 7 | 	let offset = [0,0];
 8 | 	let isDown = false;
 9 | 	let isDownResize = false;
10 | 	let displayBoxClose = create("span","hohDisplayBoxClose",svgAssets.cross,displayBox);
11 | 	displayBoxClose.onclick = function(){
12 | 		displayBox.remove();
13 | 	};
14 | 	let resizePearl = create("span","hohResizePearl",false,displayBox);
15 | 	displayBox.addEventListener("mousedown",function(e){
16 | 		let root = e.target;
17 | 		while(root.parentNode){//don't annoy people trying to copy-paste
18 | 			if(root.classList.contains("scrollableContent")){
19 | 				return
20 | 			}
21 | 			root = root.parentNode
22 | 		}
23 | 		isDown = true;
24 | 		offset = [
25 | 			displayBox.offsetLeft - e.clientX,
26 | 			displayBox.offsetTop - e.clientY
27 | 		];
28 | 	},true);
29 | 	resizePearl.addEventListener("mousedown",function(event){
30 | 		event.stopPropagation();
31 | 		event.preventDefault();
32 | 		isDownResize = true;
33 | 		offset = [
34 | 			displayBox.offsetLeft,
35 | 			displayBox.offsetTop
36 | 		];
37 | 	},true);
38 | 	document.addEventListener("mouseup",function(){
39 | 		isDown = false;
40 | 		isDownResize = false;
41 | 	},true);
42 | 	document.addEventListener("mousemove",function(event){
43 | 		if(isDownResize){
44 | 			mousePosition = {
45 | 				x : event.clientX,
46 | 				y : event.clientY
47 | 			};
48 | 			displayBox.style.width = (mousePosition.x - offset[0] + 5) + "px";
49 | 			displayBox.style.height = (mousePosition.y - offset[1] + 5) + "px";
50 | 			return;
51 | 		}
52 | 		if(isDown){
53 | 			mousePosition = {
54 | 				x : event.clientX,
55 | 				y : event.clientY
56 | 			};
57 | 			displayBox.style.left = (mousePosition.x + offset[0]) + "px";
58 | 			displayBox.style.top  = (mousePosition.y + offset[1]) + "px";
59 | 		}
60 | 	},true);
61 | 	let innerSpace = create("div","scrollableContent",false,displayBox);
62 | 	return innerSpace;
63 | }
64 | 


--------------------------------------------------------------------------------
/src/utilities/levDist.js:
--------------------------------------------------------------------------------
 1 | function levDist(s,t){//https://stackoverflow.com/a/11958496/5697837
 2 | 	// Step 1
 3 | 	s = s.replace("’", "'")
 4 | 	t = t.replace("’", "'")
 5 | 	let n = s.length;
 6 | 	let m = t.length;
 7 | 	if(!n){
 8 | 		return m
 9 | 	}
10 | 	if(!m){
11 | 		return n
12 | 	}
13 | 	let d = []; //2d matrix
14 | 	for(var i = n; i >= 0; i--) d[i] = [];
15 | 	// Step 2
16 | 	for(var i = n; i >= 0; i--) d[i][0] = i;
17 | 	for(var j = m; j >= 0; j--) d[0][j] = j;
18 | 	// Step 3
19 | 	for(var i = 1; i <= n; i++){
20 | 		let s_i = s.charAt(i - 1);
21 | 		// Step 4
22 | 		for(var j = 1; j <= m; j++){
23 | 			//Check the jagged ld total so far
24 | 			if(i === j && d[i][j] > 4){
25 | 				return n
26 | 			}
27 | 			let t_j = t.charAt(j - 1);
28 | 			let cost = (s_i === t_j) ? 0 : 1; // Step 5
29 | 			//Calculate the minimum
30 | 			let mi = d[i - 1][j] + 1;
31 | 			let b = d[i][j - 1] + 1;
32 | 			let c = d[i - 1][j - 1] + cost;
33 | 			if(b < mi){
34 | 				mi = b
35 | 			}
36 | 			if(c < mi){
37 | 				mi = c;
38 | 			}
39 | 			d[i][j] = mi; // Step 6
40 | 			//Damerau transposition
41 | 			/*if (i > 1 && j > 1 && s_i === t.charAt(j - 2) && s.charAt(i - 2) === t_j) {
42 | 				d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + cost);
43 | 			}*/
44 | 		}
45 | 	}
46 | 	return d[n][m]
47 | }
48 | 


--------------------------------------------------------------------------------
/src/utilities/modification.txt:
--------------------------------------------------------------------------------
 1 | showdown:
 2 | 
 3 | 	txt = txt.replace(/([\\*_~|`])/g, '\\$1');
 4 | 	// backport: escape escape characters!
 5 | 
 6 | 	var root = window;
 7 | 
 8 | 
 9 | purify:
10 | 	(function (global, factory) {
11 | 	  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
12 | 	  typeof define === 'function' && define.amd ? define(factory) :
13 | 	  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.DOMPurify = factory());
14 | 	})(window, (function () { 'use strict';
15 | 
16 | 
17 | 
18 | Remember to remove sourcemaps!
19 | 


--------------------------------------------------------------------------------
/src/utilities/parseListJSON.js:
--------------------------------------------------------------------------------
 1 | function parseListJSON(listNote){
 2 | 	if(!listNote){
 3 | 		return null
 4 | 	}
 5 | 	let commandMatches = listNote.match(/\$({.*})\$/);
 6 | 	if(commandMatches){
 7 | 		try{
 8 | 			let noteContent = JSON.parse(commandMatches[1]);
 9 | 			noteContent.adjustValue = noteContent.adjust || 0;
10 | 			let rangeParser = function(thing){
11 | 				if(typeof thing === "number"){
12 | 					return 1
13 | 				}
14 | 				else if(typeof thing === "string"){
15 | 					thing = thing.split(",").map(a => a.trim())
16 | 				}
17 | 				return thing.reduce(function(acc,item){
18 | 					if(typeof item === "number"){
19 | 						return acc + 1
20 | 					}
21 | 					let multiplierPresent = item.split("x").map(a => a.trim());
22 | 					let value = 1;
23 | 					let rangePresent = multiplierPresent[0].split("-").map(a => a.trim());
24 | 					if(rangePresent.length === 2){//range
25 | 						let minRange = parseFloat(rangePresent[0]);
26 | 						let maxRange = parseFloat(rangePresent[1]);
27 | 						if(minRange && maxRange){
28 | 							value = maxRange - minRange + 1
29 | 						}
30 | 					}
31 | 					if(multiplierPresent.length === 1){//no multiplier
32 | 						return acc + value
33 | 					}
34 | 					if(multiplierPresent.length === 2){//possible multiplier
35 | 						let multiplier = parseFloat(multiplierPresent[1]);
36 | 						if(multiplier || multiplier === 0){
37 | 							return acc + value*multiplier
38 | 						}
39 | 						else{
40 | 							return acc + 1
41 | 						}
42 | 					}
43 | 					else{//unparsable
44 | 						return acc + 1
45 | 					}
46 | 				},0);
47 | 			};
48 | 			if(noteContent.more){
49 | 				noteContent.adjustValue += rangeParser(noteContent.more)
50 | 			}
51 | 			if(noteContent.skip){
52 | 				noteContent.adjustValue -= rangeParser(noteContent.skip)
53 | 			}
54 | 			return noteContent;
55 | 		}
56 | 		catch(e){
57 | 			console.warn("Unable to parse JSON in list note",commandMatches)
58 | 		}
59 | 	}
60 | 	else{
61 | 		return null
62 | 	}
63 | }
64 | 


--------------------------------------------------------------------------------
/src/utilities/saveAs.js:
--------------------------------------------------------------------------------
 1 | function saveAs(data,fileName,pureText){
 2 | 	let link = create("a");
 3 | 	document.body.appendChild(link);
 4 | 	let json = pureText ? data : JSON.stringify(data);
 5 | 	let blob = new Blob([json],{type: "octet/stream"});
 6 | 	let url = window.URL.createObjectURL(blob);
 7 | 	link.href = url;
 8 | 	link.download = fileName || translate("$default_filename");
 9 | 	link.click();
10 | 	window.URL.revokeObjectURL(url);
11 | 	document.body.removeChild(link)
12 | }
13 | 


--------------------------------------------------------------------------------