├── .eslintrc.js ├── .github └── ISSUE_TEMPLATE │ ├── bug-report-template.md │ ├── bug反馈模板.md │ ├── feature_request.md │ └── 新功能建议.md ├── .gitignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── assets ├── 01.JPG ├── 01.png ├── 011.png ├── 012.png ├── 02.JPG ├── 02.png ├── 03.JPG ├── 03.png ├── 04.JPG ├── 04.png ├── 05.JPG ├── 05.png ├── 06.JPG ├── 06.png ├── 07.JPG ├── 07.png ├── 08.JPG ├── 08.png ├── 09.JPG ├── 09.png └── 13.png ├── index.js ├── stylesheet.css ├── synonym_dict_sample.json └── test.html /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | }, 7 | extends: ["eslint:recommended", "prettier", "eslint-config-prettier"], 8 | parserOptions: { 9 | ecmaVersion: 12, 10 | sourceType: "module", 11 | }, 12 | rules: { 13 | indent: ["error", 2, { SwitchCase: 1 }], 14 | semi: ["error", "always"], 15 | "no-undef": "off", 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report Template 3 | about: how to describe the bug 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **The Environments** 11 | The browser and script manager you are using 12 | eg: Chrome + Tampermonkey 13 | 14 | **To Reproduce** 15 | Steps to reproduce the behavior: 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. See error / not responding 19 | 20 | **Does it work before?** 21 | It helps me to determine where the bug locates 22 | 23 | **Error Message or Screenshots** 24 | Press F12 and there might be some error information 25 | If applicable, add screenshots to help explain your problem. 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug反馈模板.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug反馈模板 3 | about: 描述遇到的bug 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **运行环境** 11 | 使用的浏览器与脚本管理器 12 | eg: Chrome + Tampermonkey 13 | 14 | **如何复现** 15 | 描述触发bug的条件 16 | 1. 前往 '...' 17 | 2. 点击 '...' 18 | 3. 出现错误 / 无响应 19 | 20 | **此前是否能够正常工作** 21 | 可以帮助我更快定位问题所在 22 | 23 | **错误信息或截图** 24 | 按下F12,在控制台中可能会打印相关错误信息 25 | 如果允许,可以提交相关截图来更好地解释问题 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/新功能建议.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 新功能建议 3 | about: 帮助改进该项目 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **请描述您希望实现的功能** 11 | 清晰与详细地描述您希望实现的效果 12 | 13 | **是否有别的替代方案** 14 | 如果首选方案无法实现,是否有替代方案 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | /G_README.md 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.printWidth": 80, 3 | "prettier.tabWidth": 2, 4 | "prettier.useTabs": false, 5 | "prettier.semi": true, 6 | "prettier.singleQuote": false, 7 | "prettier.arrowParens": "avoid", 8 | "prettier.bracketSpacing": true, 9 | "prettier.endOfLine": "lf" 10 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ziqing19 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Label Pixiv Bookmarks 2 | 3 | 中文文档 4 | 5 | ## Automatically add existing tags for images in the bookmarks, and search them 6 | 7 | ## Please Use Tampermonkey as the script manager 8 | 9 | - The script is developed on Tampermonkey, whereas Greasemonkey has quite different API calls. 10 | 11 | ## Latest 12 | 13 | - Added functions to replace the tag-selection dialog, displaying tags alphabetically (Function Page) 14 | 15 | - Added functions to regard author name and uid as work tags (Label Page - Advanced) 16 | 17 | - Added functions to show user-labeled tags (in script manager menu) 18 | 19 | ## Intro 20 | 21 | - The script will compare **your existing bookmark tags** and tags of the image, then find the intersection and save 22 | 23 | - If there is no intersection, the script will skip the image. Or you can configure the script to add the first tag of the image by default. 24 | 25 | - You might also search your bookmarks by your custom tag 26 | 27 | - The script is implemented by Pixiv Web APIs. Please open a new issue at GitHub if it was outdated. 28 | 29 | ## For First-time User 30 | 31 | - This is a new account with all bookmarked images uncategorized. Let's start from here. 32 | 33 | ![step1](./assets/01.png) 34 | 35 | - There are several ways to build up your bookmarked tags pool from scratch before using the script 36 | 37 | 1. In the bookmarks page, hovering on the thumbnail and clicking the ***Edit tags*** button. you will find the ***Creat a tag*** button in the dialog. After saving all your changes, those created tags will be saved to your bookmarked tags. 38 | 39 | ![step2.1](./assets/011.png) 40 | 41 | 2. In the bookmark detail page, you can choose some tags from ***Tags for this work***, and then click ***Edit Bookmark*** to save 42 | 43 | ![step2.2](./assets/02.png) 44 | 45 | 3. Let the script grabs the first tag from the work to build up your bookmarked tags pool. In this case you should make sure that in the advanced settings of the script ***add the first tag*** option is set to ***Yes***. You are free to remove those unwanted tags later, and don't forget to reset the value to ***No*** after you tags pool has been settled. 46 | 47 | ![step2.3](./assets/012.png) 48 | 49 | 4. Use the synonym dictionary of the script. All the ***user tags*** (i.e. the target tag) will be added to your bookmarked tags pool. See below to find out how to use the synonym dict. 50 | 51 | ## Start Labeling 52 | 53 | - In the Bookmarks Page, click ***Label*** button to open the script page 54 | - For first time user, if you already have some bookmarked tags, just click ***Start*** to run 55 | - Or you might need to add some tags to your pool as aforementioned 56 | - Assume that we already have the tag [新世紀エヴァンゲリオン] being bookmarked 57 | 58 | ![step3](./assets/03.png) 59 | 60 | - Wait for the progress bar until it reaches the end. 61 | - Refresh the page, and we will find that all images with the tag [新世紀エヴァンゲリオン] have been categorized 62 | 63 | ![step3](./assets/04.png) 64 | 65 | ## Synonym Dictionary 66 | 67 | - Sometimes the author does not provide the so-called *official* tag for the artwork. This inconsistency makes it hard for us to do labeling. 68 | - Here we introduce the synonym dictionary that stores tag-alias pairs. All alias for a tag will be regarded as the same as the tag itself. 69 | - For example, the tags of the image contains [EVA] instead of [新世紀エヴァンゲリオン], so that it won't be categorized into [新世紀エヴァンゲリオン]. 70 | - You can find a sample file in Load Dict section which can be used to load for first-time users. 71 | 72 | ![step4](./assets/05.png) 73 | 74 | - Open the script page and we wil find the ***Edit Dict*** button. 75 | - The ***Target Tag*** is the one that you want it in your bookmark tags pool (eg: 新世紀エヴァンゲリオン); and the ***Alias*** are the tags that you want them to be regarded as the target tag (eg: eva, evangelion). 76 | - Note that all alias should be delimited by spaces, or line breakers 77 | - Click ***Update Tag*** to save the user tag and alias into the dictionary. If the alias is empty the user tag will be removed 78 | - You can export the dictionary to local disk as a backup 79 | - To modify the alias, enter the user tag value and click ***Load Tag*** to load the alias value from the dict (or use tab key), and click ***Update Tag*** to save 80 | 81 | ![step5](./assets/06.png) 82 | 83 | - After executing the script again, all uncategorized images with either the target tag, or the alias tag now have been categorized to your target tag. 84 | 85 | ![step6](./assets/07.png) 86 | 87 | - There are a lot of things you can do with this functionality. For example, most character in pixiv use the katakana (片仮名) as its official name, which makes the non-Japanese speakers hard to recognize the name. 88 | 89 | - Take Soryu Asuka Langley as an instance. She has at least four kinds of appellations like 惣流・アスカ・ラングレー/式波・アスカ・ラングレー/そうりゅう・アスカ・ラングレー/しきなみ・アスカ・ラングレー. Now we can set ***asuka*** as the target tag, and those Japanese names as the alias. 90 | 91 | ![step7](./assets/08.png) 92 | 93 | ![step7](./assets/09.png) 94 | 95 | ## Examples 96 | 97 | - Here are the example of my own synonym dictionary and user tags. You might refer it to set yours. 98 | 99 | ![dictionary](./assets/08.JPG) 100 | ![userTags](./assets/09.JPG) 101 | 102 | ## About Some Advanced Settings 103 | 104 | - Auto Labeling For 105 | - By default, the script does label for those uncategorized images 106 | - You may want to re-label all your favorite artworks when some newly-added tags were not applied to those former images. 107 | - For example, if you just add [ayanami_rei] to your user tags, you can choose [新世紀エヴァンゲリオン] and re-run the script. Therefore, all images with [新世紀エヴァンゲリオン] tag will be searched and labeled again. 108 | 109 | - Whether the first tag will be added if there is not any match 110 | - Design for a cold start 111 | - It works when the intersection of your existing bookmark tags and tags of the work is empty, then the first tag of the image will be added 112 | - When ignore tags are provided, the script will use the first tag that is not ignored 113 | 114 | - Whether ALL work tags will be added to your user tags 115 | - For people who just want to manage the bookmarks by the work tags 116 | - Work tags will be FIRST added 117 | - Synonym dict still works if you want to merge similar work tags 118 | 119 | - Publication Type for Labeling 120 | - Pixiv stores public and private bookmarks in different places, and they have independent tags 121 | - By default, the script only does label for public bookmarks 122 | 123 | - Whether NSFW works will be labeled as #R-18? 124 | - By default, the script will categorize those NSFW works into the R-18 tag 125 | - If you don't want the tag, set it as *No* before starting 126 | 127 | - Whether NSFW works will be labeled as #R-18? 128 | - By default, the script will not categorize those SFW works into the SFW tag 129 | - If you need to label the tag, set it as *Yes* before starting 130 | 131 | - Whether AI-generated works will be labeled as AI 132 | - By default, the script will not label AI-generated works 133 | - Set the config as *Yes* to distinguish them 134 | 135 | - Whether author name and uid will be regarded as part of work tags 136 | - Enable users to use author name and uid as the alias name of synonym dict 137 | 138 | - Whether the work tag and user tag need to be strictly match (both character name and work title) 139 | - Eg: work A, tagged as #Asuka #EVA; work B, tagged as #Asuka; work C, tagged as #Asuka(EVA). And you have #Asuka(EVA) in your user tags 140 | - When set to Yes, only work A and C will be tagged as #Asuka(EVA) 141 | - When set to No, all of them will be tagged as #Asuka(EVA) 142 | 143 | ## Remove All Tags from Selected Works 144 | 145 | - The script can help you remove all tags from selected artworks easily 146 | - This usually helps when you want to reset multiple images to Uncategorized 147 | - Click on the ***Edit Bookmarks*** button, and you will find an extra ***Remove Tags*** button below the tags section 148 | - The button is disabled until you select several images 149 | 150 | ## Remove a Bookmark Tag from Related Works 151 | 152 | - It is not hard for you to find that a new button named ***Delete Tag XXX*** comes out when you click on the Edit Bookmarks button 153 | - This will help you to remove this bookmark tag from ***ALL*** related images 154 | - This operation will not affect the bookmarked status, i.e. the images will become uncategorized if there is not any other tag remains 155 | 156 | ## Search Your Bookmarks 157 | 158 | - You can also search your bookmarks with this script 159 | - Click ***Search*** Button to open the search page, and search with the keyword [asuka], which is the target tag we have registered before 160 | - The script will return all images with tag [asuka] or [asuka]'s alias. 161 | 162 | ## Manually enable some functions 163 | 164 | - In order not to crowd to UI, some non-core functions need to be enabled manually by clicking corresponding button in the extension menu 165 | 166 | ![enableFunctions](./assets/13.png) 167 | 168 | ## Display Shuffled Images 169 | 170 | - This function is used to view random images of specified tag, or split a big tag into smaller ones 171 | - Set the tag to load and other configs (loading all works takes quite long time) and click on the ***Load*** button on the bottom right to start 172 | - Images of the tag will be split into N batches by the batch size. Click on the Load button will switch to the next batch 173 | - Click on the thumbnail to enter gallery mode. Use four arrow keys to manipulate. Click on the X on the top left to exit 174 | - Click on the ***Save to Tag*** button to save N batches into N sub-tags which is used for split a big tag into smaller ones and facilitates easier access to early bookmarks 175 | 176 | ## Additional Functions 177 | 178 | - Some scattered and independent functions are resided in this page 179 | - Tags 180 | - Click on ***Toggle Publication Type*** button to toggle the publication type of related works between public and private 181 | - Click on ***Delete This Tag*** button to the tag from all related works (select tag1, tag1,tag2 => tag2) 182 | - Click on ***Clear Work Tags*** button to set all related works to uncategorized (select tag1, tag1,tag2 => uncategorized) 183 | - Enter a new tag name (cannot duplicate) and click on ***Rename Tag*** to update the tag name, as well as the name in synonym dictionary and all related works 184 | - Bookmarks 185 | - Still in development. Meant to back up the whole bookmarks and retrieve information when necessary 186 | - When you find some works got deleted/private, and you have a previous version of backup, Click on ***Lookup Invalid Works*** and choose the backup to make a comparison. 187 | - The lookup function relies on the order of your bookmarks. By indexing and locating its previous work id, the script would find the information of the invalid work. 188 | 189 | ## Show User-label Tags 190 | 191 | - User-labeled tags can be displayed under the work image 192 | - Enable it in the script manager menu (where you enable/disable this script) 193 | 194 | ## FAQ 195 | 196 | - Some function fails? 197 | - Except internal bugs, there are other reasons lead to the function failure 198 | - Pixiv UI updates 199 | - Browser and script manager compatibility 200 | - Please open an issue at [Github](https://github.com/Ziqing19/LabelPixivBookmarks)提交issue 201 | - Please note the following information: 202 | - Browser and script manager used 203 | - How to reproduce the bug 204 | - Does it work before? 205 | - You might get some error information in the console by pressing F12 206 | 207 | - The ***Label*** button cannot be found on the website 208 | - Firstly make sure that you are at the correct place, and try to **refresh** before the next step 209 | - The new version of Pixiv UI uses React to update the page without refreshing, so the button might not be loaded 210 | - Generally, the correct path should be like https://www.pixiv.net/users/{YOUR UID}/bookmarks/artworks or https://www.pixiv.net/bookmark.php 211 | - If the path is correct, and the button is still nowhere to find, it is probably because Pixiv updates its UI. Inform me at GitHub by opening an issue 212 | 213 | - The script cannot work and alert a prompt 214 | - Please take down the prompt and open an issue at GitHub. The problem can be specific 215 | 216 | - The synonym dictionary got lost accidentally 217 | - Expand ***Load Dict*** and click the restore button the download the backup. The file could be imported to restore your dictionary. 218 | - If you think it is a bug, open an issue. 219 | 220 | - Whether Pixiv will ban my account for the script 221 | - The script is basically for self-use, and I have limited the speed of sending requests. It works properly on thousands of images. 222 | 223 | 224 | ## Copyright and contact 225 | 226 | The script complies with the MIT license. 227 | 228 | Please report bugs or new features expected at [GitHub](https://github.com/Ziqing19/LabelPixivBookmarks). 229 | 230 | 231 | 232 |

自动为Pixiv收藏夹内图片打上已有的标签

233 | 234 | ## 请使用Tampermonkey插件 235 | 236 | - 脚本基于Tampermonkey开发,Greasemonkey的API与本脚本不兼容,如果希望在Greasemonkey上使用请自行修改使用的API 237 | 238 | ## 最近更新 239 | 240 | - 新增替换标签选择对话框功能,将按照读音顺序展示标签(在其他功能中) 241 | 242 | - 新增识别作者名与uid用于自动标签功能(在添加标签-高级设置中) 243 | 244 | - 新增显示用户标签功能(在脚本管理器菜单中) 245 | 246 | ## 工作原理 247 | 248 | - 脚本会比对作品自带的标签,以及***用户已收藏的标签***,然后为作品打上匹配的标签 249 | 250 | - 如果已收藏标签与作品自带标签没有交集,将会跳过该作品(或可选地自动添加作品首个标签) 251 | 252 | - 脚本提供搜索收藏夹功能,可以对标签和标题进行搜索 253 | 254 | - 本脚本使用Pixiv的网页API进行操作,可能会出现API过时等情况,如果出现错误请在Github提交issue 255 | 256 | ## 第一次使用的用户 257 | 258 | - 这是一个收藏了部分图片,但是所有的图片都是未分类状态的新账户 259 | 260 | ![step1](./assets/01.png) 261 | 262 | - 在使用脚本前,我们有数种方法可以添加*用户已收藏的标签* 263 | 264 | 1. 在收藏夹页,悬停在图片缩略图上并点击左下角的***编辑标签***按钮,在对话框中可以找到***添加标签***按钮。在保存设置之后,所有创建的标签将被加入用户已收藏标签。 265 | 266 | ![step2.1](./assets/011.png) 267 | 268 | 2. 在作品收藏详情页,选择一些作品已有的标签,或手动输入需要的标签,保存结果 269 | 270 | ![step2](./assets/02.png) 271 | 272 | 3. 使用脚本自动添加标签。需要在高级设置中选择***自动添加首个标签***并设置为***是***。随后可以随意移除不需要的标签,但之后使用时请记得将此设置重置为***否***来避免增加过多不需要的标签。 273 | 274 | ![step2.3](./assets/012.png) 275 | 276 | 4. 使用脚本的同义词词典功能。词典中所有的目标标签(用户标签)将会被视为是用户已收藏的标签。关于词典的使用方法请见下文 277 | 278 | ## 开始使用 279 | 280 | - 在管理收藏页面,点击【添加标签】打开脚本页面 281 | - 如果在此前已经设置好用户收藏标签,直接点击开始即可使用 282 | - 否则需要按前文所述选择一种方式来添加一些用户收藏标签 283 | - 假设我们已经添加了【新世紀エヴァンゲリオン】标签 284 | 285 | ![step3](./assets/03.png) 286 | 287 | - 等待运行结束,刷新页面,可以看到所有未分类作品中带有【新世紀エヴァンゲリオン】标签的作品都被自动分类到该标签下 288 | 289 | ![step4](./assets/04.png) 290 | 291 | ## 同义词词典 292 | 293 | - 有些时候作者并没有为作品或人物提供所谓的***官方名称***,这就导致自动识别标签变得困难。如果我们使用一个同义词词典储存一个标签的全部同义词——或者叫别名,那么分类的结果将会更加整洁 294 | - 例如此作品下有【eva】标签,但没有【新世紀エヴァンゲリオン】标签,因此不会被自动分类到【新世紀エヴァンゲリオン】标签下 295 | - 在加载词典区域下,首次使用的用户可以尝试下载样例词典用于导入 296 | 297 | ![step4](./assets/05.png) 298 | 299 | - 在自动标签页面,点击***编辑词典***展开选项 300 | - 目标标签,指的是您希望保存在您收藏夹中的用户标签的名字,例如:新世紀エヴァンゲリオン。同义词则是那些您希望脚本将其识别为目标标签的作品本身提供的标签,例如:EVA 301 | - 所有的同义词之间使用空格或回车分隔 302 | - 点击***更新标签***将输入的内容加载到词典中,然后将会在下方的预览区域展示出来。如果您在同义词一栏空白的情况下更新,将会把目标标签从词典中删除 303 | - 在制作完词典后,可以导出词典到本地进行备份 304 | - 下次使用时,会自动记忆上次使用的词典,也可以从本地导入新的词典 305 | - ***加载标签***按钮用于从词典中载入标签对应的同义词,在***目标标签***一栏中输入标签名,点击***加载标签***即可,直接按Tab键也有同样的效果 306 | 307 | ![step5](./assets/06.png) 308 | 309 | - 再次点击开始。执行完脚本后,含有【EVA】标签的作品已经被分类到了【新世紀エヴァンゲリオン】下 310 | 311 | ![step6](./assets/07.png) 312 | 313 | - 利用此功能可以实现很多事情。例如Pixiv大部分角色都是用片假名作为官方名称,这对非日语母语的人来说识别起来非常痛苦。拿明日香做例子,明日香至少有4种常用称呼:惣流・アスカ・ラングレー/式波・アスカ・ラングレー/そうりゅう・アスカ・ラングレー/しきなみ・アスカ・ラングレー。我们现在就可以使用简单的***asuka***作为目标标签,将上述都做为同义词标签储存。 314 | - 注意自定义的目标标签中不能有空格,因为Pixiv使用空格作为标签间的分隔符 315 | 316 | ![step7](./assets/08.png) 317 | 318 | ## 示例 319 | 320 | - 下图为已经整理好的同义词词典,以及对应的用户收藏标签示例,可以作为参考 321 | 322 | ![dictionary](./assets/08.JPG) 323 | ![userTags](./assets/09.JPG) 324 | 325 | ## 可选设置说明 326 | 327 | - 以下为脚本提供的可选配置的说明 328 | 329 | - 自动标签范围 330 | - 脚本的工作范围,默认为对【未分类作品】进行自动标签 331 | - 可以使用下拉框选择其他的标签范围,使用场景例如下: 332 | - 用户收藏标签中新增了角色【绫波丽】,此时可以选择【新世紀エヴァンゲリオン】标签重新运行脚本,这样【新世紀エヴァンゲリオン】标签下所有含有【绫波丽】的图片都将被打上标签 333 | 334 | - 无匹配时是否自动添加首个标签 335 | - 用于没有任何***用户收藏标签***的账户进行冷启动 336 | - 作用为当该作品的标签与已收藏的标签***没有交集***时,默认添加该作品的第一个标签 337 | - 当设置了忽略标签范围时,会使用首个未被忽略的标签 338 | 339 | - 是否添加作品所有标签至用户标签 340 | - 为了部分希望使用作品自带标签管理收藏夹的用户设计 341 | - 作品自带标签将会被***优先***添加到用户标签 342 | - 作品自带标签中的相似标签同样可以用同义词词典进行合并 343 | 344 | - 作品公开类型 345 | - pixiv的公开和非公开作品使用两套不同的收藏体系,标签列表也是独立的 346 | - 默认为对公开收藏的作品进行自动标签 347 | 348 | - 是否为非全年龄作品标记#R-18标签 349 | - 默认会将非全年龄向作品归入#R-18标签 350 | - 如果不需要该标签可以设置为*忽略* 351 | 352 | - 是否为全年龄作品标记#SFW标签 353 | - 默认不会将全年龄向作品归入#SFW标签 354 | - 如果需要该标签可以设置为*标记* 355 | 356 | - 是否为AI生成的作品标记#AI标签 357 | - 默认不会标记 358 | - 如果需要该功能请选择*标记* 359 | 360 | - 是否将作者名与uid视为作品标签 361 | - 允许用户将作者名与uid作为同义词词典的别名 362 | 363 | - 是否作品标签与用户标签需要严格匹配(角色名与作品名) 364 | - 例:作品A,标签为#Asuka,#EVA;作品B,标签为#Asuka;作品C,标签为#Asuka(EVA)。此时你的用户词典中含有#Asuka(EVA) 365 | - 当设置为是时,仅有作品AC会被标记为#Asuka(EVA) 366 | - 当设置为否时,所有作品都会被标记为#Asuka(EVA) 367 | 368 | ## 清除作品的所有标签 369 | 370 | - 除了自动标签,这里还提供了能够批量清除作品标签的功能 371 | - 通常在需要将复数作品重置为***未分类***状态时使用 372 | - 点击***管理收藏***,在标签栏下方会显示一个新增的***清除标签***按钮 373 | - 当在下方选择了复数作品后,点击按钮清除作品标签 374 | 375 | ## 从所有关联作品中删除特定标签 376 | 377 | - 在点击***管理收藏***后,原位置会出现新的***删除标签 XXX***按钮 378 | - 这一功能将会从关联的所有作品中移除该标签 379 | - 这一操作并不会影响作品的收藏状态,该标签下的作品至多会被还原为未分类状态 380 | 381 | ## 搜索收藏夹标签 382 | 383 | - 除了自动标签以外,本脚本还提供搜索收藏夹功能,便于更快地在大量的收藏找到需要的图片。 384 | - 点击【搜索图片】打开搜索页,我们可以用刚刚设置的目标标签【asuka】进行搜索,脚本会搜索所有匹配同义词标签(此处为:明日香)的作品并返回。这样可以快速的从收藏夹中按照人物名或其他特征快速搜索到指定作品,而不需要继续细分该标签。 385 | - 搜索收藏夹时的标签匹配模式 386 | - 模糊匹配:作品标签部分匹配搜索内容即可 387 | - 精确匹配:作品的某个标签与搜索内容相同 388 | 389 | ![step7](./assets/09.png) 390 | 391 | ## 手动启用部分功能 392 | 393 | - 为了不让UI过于臃肿,部分非核心功能设定为在菜单中手动开启,点击对应按钮即可开启 394 | 395 | ![enableFunctions](./assets/13.png) 396 | 397 | ## 展示随机图片 398 | 399 | - 本功能可用于随机浏览特定标签下的图片,或用于切分较大的标签至数个子标签 400 | - 设定需要加载的标签(加载全部作品会需要较长时间)等设置后点击右下角的【加载】按钮即可 401 | - 该标签下的所有图片将按批量大小被切分为N批进行展示,点击【加载】将显示下一批 402 | - 点击缩略图进入画廊模式,使用上下左右方向键进行浏览,点击左上角X退出画廊模式 403 | - 点击【保存至标签】可以将切分好的N批保存至N个子标签,这一功能可用于切分较大的收藏标签以便于浏览较早的收藏 404 | 405 | ## 其他功能 406 | 407 | - 部分零散且独立的功能将会放置在这个页面 408 | - 标签相关 409 | - 点击【更改作品公开类型】将切换所有关联作品的公开类型 410 | - 点击【删除该标签】将从所有关联作品中移除该标签(选择Tag1,Tag1,Tag2 => Tag2) 411 | - 点击【清除作品标签】将从所有关联作品中移除所有标签(选择Tag1,Tag1,Tag2 => 未分类) 412 | - 输入新标签名(不可重复)并点击【更改标签名称】来重命名标签,同义词词典与关联作品都将被更新 413 | - 收藏夹相关 414 | - 开发中,预期使用备份的收藏夹数据在需要的时候取回部分信息 415 | - 点击【备份收藏夹】将整个收藏夹保存为JSON格式 416 | - 当用户发现部分作品失效,并持有较早时期的收藏夹备份时,点击【查询失效作品信息】并提供较早的备份后,脚本将会对比并显示作品失效前的相关信息 417 | - 脚本依靠收藏夹中作品的顺序进行比较,在收藏顺序发生较大变化时可能无法正确展示结果 418 | 419 | ## 显示用户标签 420 | 421 | - 该功能可以在作品图片下方展示用户标记的标签 422 | - 在脚本管理器的菜单中开启(即启用/禁用本脚本的位置) 423 | 424 | ## 常见问题 425 | 426 | - 遇到功能失效怎么办? 427 | - 除脚本本身Bug外,Pixiv网页UI更新,浏览器适配性,脚本管理器适配性都有可能导致问题 428 | - 请在[Github](https://github.com/Ziqing19/LabelPixivBookmarks)提交issue 429 | - 提交时请备注使用的浏览器、脚本管理器、Bug触发条件、此前是否成功运行过,可以点击F12在控制台检查错误信息并截图 430 | 431 | - 网页上找不到“自动添加标签”按钮 432 | - 请确认当前是否在个人主页或收藏夹页,网址通常为https://www.pixiv.net/users/{用户UID}/bookmarks/artworks或https://www.pixiv.net/bookmark.php 433 | - ***尝试刷新网页*** 434 | - 新版UI使用React在不重新加载的情况下更新页面内容,导致按钮可能没有被加载 435 | - 如果当前路径无误,刷新后依然无法找到按钮,可能为Pixiv更新了网页UI,请于Github提交issue 436 | 437 | - 无法正常运行,弹窗提示错误 438 | - 请记录下弹窗提示内容,并在Github提交issue,通常具体问题需要具体分析 439 | 440 | - 同义词词典意外丢失 441 | - 点击***加载词典***中的恢复按钮下载自动备份,备份可以直接再次导入脚本 442 | - 如果是bug导致的词典丢失,请在Github提交issue反馈 443 | 444 | - 电脑提示下载的词典文件有安全问题 445 | - 词典文件由浏览器生成,可能缺少一些我不太清楚的安全签名之类。因为是开源脚本,如果不放心可以检查一遍。如果能帮我解决掉这个问题更好了( 446 | 447 | - 我自己能编辑词典文件吗? 448 | - 词典用JSON格式储存,结构非常简单。如果不熟悉JSON格式,网上有很多在线编辑器可以使用,脚本只提供了最基本的增删改的功能 449 | 450 | - 使用该脚本是否会导致封号? 451 | - 该脚本为作者方便分类的自用脚本,并且限制了提交速度,在千数量级的工作量下暂时没有出现问题 452 | 453 | ## 版权与联络方式 454 | 455 | 本脚本使用MIT许可证,Bug与新功能需求请在[Github](https://github.com/Ziqing19/LabelPixivBookmarks)进行提交。 -------------------------------------------------------------------------------- /assets/01.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ziqing19/LabelPixivBookmarks/2398cb377ad012b47ab4ecdcdb24bc47e9aa06d4/assets/01.JPG -------------------------------------------------------------------------------- /assets/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ziqing19/LabelPixivBookmarks/2398cb377ad012b47ab4ecdcdb24bc47e9aa06d4/assets/01.png -------------------------------------------------------------------------------- /assets/011.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ziqing19/LabelPixivBookmarks/2398cb377ad012b47ab4ecdcdb24bc47e9aa06d4/assets/011.png -------------------------------------------------------------------------------- /assets/012.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ziqing19/LabelPixivBookmarks/2398cb377ad012b47ab4ecdcdb24bc47e9aa06d4/assets/012.png -------------------------------------------------------------------------------- /assets/02.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ziqing19/LabelPixivBookmarks/2398cb377ad012b47ab4ecdcdb24bc47e9aa06d4/assets/02.JPG -------------------------------------------------------------------------------- /assets/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ziqing19/LabelPixivBookmarks/2398cb377ad012b47ab4ecdcdb24bc47e9aa06d4/assets/02.png -------------------------------------------------------------------------------- /assets/03.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ziqing19/LabelPixivBookmarks/2398cb377ad012b47ab4ecdcdb24bc47e9aa06d4/assets/03.JPG -------------------------------------------------------------------------------- /assets/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ziqing19/LabelPixivBookmarks/2398cb377ad012b47ab4ecdcdb24bc47e9aa06d4/assets/03.png -------------------------------------------------------------------------------- /assets/04.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ziqing19/LabelPixivBookmarks/2398cb377ad012b47ab4ecdcdb24bc47e9aa06d4/assets/04.JPG -------------------------------------------------------------------------------- /assets/04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ziqing19/LabelPixivBookmarks/2398cb377ad012b47ab4ecdcdb24bc47e9aa06d4/assets/04.png -------------------------------------------------------------------------------- /assets/05.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ziqing19/LabelPixivBookmarks/2398cb377ad012b47ab4ecdcdb24bc47e9aa06d4/assets/05.JPG -------------------------------------------------------------------------------- /assets/05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ziqing19/LabelPixivBookmarks/2398cb377ad012b47ab4ecdcdb24bc47e9aa06d4/assets/05.png -------------------------------------------------------------------------------- /assets/06.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ziqing19/LabelPixivBookmarks/2398cb377ad012b47ab4ecdcdb24bc47e9aa06d4/assets/06.JPG -------------------------------------------------------------------------------- /assets/06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ziqing19/LabelPixivBookmarks/2398cb377ad012b47ab4ecdcdb24bc47e9aa06d4/assets/06.png -------------------------------------------------------------------------------- /assets/07.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ziqing19/LabelPixivBookmarks/2398cb377ad012b47ab4ecdcdb24bc47e9aa06d4/assets/07.JPG -------------------------------------------------------------------------------- /assets/07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ziqing19/LabelPixivBookmarks/2398cb377ad012b47ab4ecdcdb24bc47e9aa06d4/assets/07.png -------------------------------------------------------------------------------- /assets/08.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ziqing19/LabelPixivBookmarks/2398cb377ad012b47ab4ecdcdb24bc47e9aa06d4/assets/08.JPG -------------------------------------------------------------------------------- /assets/08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ziqing19/LabelPixivBookmarks/2398cb377ad012b47ab4ecdcdb24bc47e9aa06d4/assets/08.png -------------------------------------------------------------------------------- /assets/09.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ziqing19/LabelPixivBookmarks/2398cb377ad012b47ab4ecdcdb24bc47e9aa06d4/assets/09.JPG -------------------------------------------------------------------------------- /assets/09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ziqing19/LabelPixivBookmarks/2398cb377ad012b47ab4ecdcdb24bc47e9aa06d4/assets/09.png -------------------------------------------------------------------------------- /assets/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ziqing19/LabelPixivBookmarks/2398cb377ad012b47ab4ecdcdb24bc47e9aa06d4/assets/13.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Pixiv收藏夹自动标签 3 | // @name:en Label Pixiv Bookmarks 4 | // @namespace http://tampermonkey.net/ 5 | // @version 5.18 6 | // @description 自动为Pixiv收藏夹内图片打上已有的标签,并可以搜索收藏夹 7 | // @description:en Automatically add existing labels for images in the bookmarks, and users are able to search the bookmarks 8 | // @author philimao 9 | // @match https://www.pixiv.net/*users/* 10 | // @icon https://www.google.com/s2/favicons?domain=pixiv.net 11 | // @resource bootstrapCSS https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css 12 | // @resource bootstrapJS https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/js/bootstrap.bundle.min.js 13 | // @grant unsafeWindow 14 | // @grant GM_getResourceURL 15 | // @grant GM_getValue 16 | // @grant GM_setValue 17 | // @grant GM_addStyle 18 | // @grant GM_registerMenuCommand 19 | // @license MIT 20 | 21 | // ==/UserScript== 22 | 23 | const version = "5.18"; 24 | const latest = `♢ 处理Pixiv组件类名更新 25 | ♢ Update constants due to change of element class names 26 | ♢ 并非所有功能都已恢复,仅验证了设置标签功能 27 | ♢ Note that not all functions have been restored. Only labeling function has been validated.`; 28 | 29 | let uid, 30 | token, 31 | lang, 32 | userTags, 33 | userTagDict, 34 | synonymDict, 35 | pageInfo, 36 | theme, 37 | showWorkTags, 38 | generator, 39 | // workType, 40 | feature, 41 | turboMode, 42 | cachedBookmarks = {}, 43 | DEBUG; 44 | // noinspection TypeScriptUMDGlobal,JSUnresolvedVariable 45 | let unsafeWindow_ = unsafeWindow, 46 | GM_getValue_ = GM_getValue, 47 | GM_setValue_ = GM_setValue, 48 | GM_addStyle_ = GM_addStyle, 49 | GM_getResourceURL_ = GM_getResourceURL, 50 | GM_registerMenuCommand_ = GM_registerMenuCommand; 51 | 52 | // selectors 53 | const BANNER = ".sc-8bf48ebe-0"; 54 | const THEME_CONTAINER = "html"; 55 | const WORK_SECTION = "section.sc-3d8ed48f-0"; // 作品section,从works->pagination 56 | const WORK_CONTAINER = "ul.sc-7d21cb21-1.jELUak"; // 仅包含作品 57 | const PAGE_BODY = ".sc-2b45994f-0.cUskQy"; // 自主页、收藏起下方 58 | const EDIT_BUTTON_CONTAINER = ".sc-9d335d39-6.cfUrtF"; // 管理收藏按钮父容器,包含左侧作品文字 59 | const REMOVE_BOOKMARK_CONTAINER = ".sc-231887f1-4.kvBpUA"; 60 | const WORK_NUM = ".sc-b5e6ab10-0.hfQbJx"; 61 | const ADD_TAGS_MODAL_ENTRY = ".bbTNLI"; // 原生添加标签窗口中标签按钮 62 | const ALL_TAGS_BUTTON = ".jkGZFM"; // 标签切换窗口触发按钮 63 | const ALL_TAGS_CONTAINER = ".hpRxDJ"; // 标签按钮容器 64 | const ALL_TAGS_MODAL = ".ggMyQW"; // 原生标签切换窗口 65 | const ALL_TAGS_MODAL_CONTAINER = ".gOPhqx"; // 原生标签切换窗口中标签按钮容器 66 | 67 | function getCharacterName(tag) { 68 | return tag.split("(")[0]; 69 | } 70 | 71 | function getWorkTitle(tag) { 72 | return (tag.split("(")[1] || "").split(")")[0]; 73 | } 74 | 75 | function stringIncludes(s1, s2) { 76 | const isString = (s) => typeof s === "string" || s instanceof String; 77 | if (!isString(s1) || !isString(s2)) 78 | throw new Error("Argument is not a string"); 79 | return s1.includes(s2); 80 | } 81 | 82 | function arrayIncludes(array, element, func1, func2, fuzzy) { 83 | if (!Array.isArray(array)) 84 | throw new TypeError("First argument is not an array"); 85 | let array1 = func1 ? array.map(func1) : array; 86 | let array2 = Array.isArray(element) ? element : [element]; 87 | array2 = func2 ? array2.map(func2) : array2; 88 | const el = [...array1, ...array2].find((i) => !i.toUpperCase); 89 | if (el) { 90 | console.log(el, array, element); 91 | throw new TypeError( 92 | `Element ${el.toString()} does not have method toUpperCase`, 93 | ); 94 | } 95 | array1 = array1.map((i) => i.toUpperCase()); 96 | array2 = array2.map((i) => i.toUpperCase()); 97 | if (fuzzy) 98 | return array2.every((i2) => array1.some((i1) => stringIncludes(i1, i2))); 99 | else return array2.every((i) => array1.includes(i)); 100 | } 101 | 102 | function isEqualObject(obj1, obj2) { 103 | if (typeof obj1 !== "object") return obj1 === obj2; 104 | return ( 105 | typeof obj1 === typeof obj2 && 106 | Object.keys(obj1).every((key, i) => key === Object.keys(obj2)[i]) && 107 | Object.values(obj1).every((value, i) => 108 | isEqualObject(value, Object.values(obj2)[i]), 109 | ) 110 | ); 111 | } 112 | 113 | function delay(ms) { 114 | return new Promise((res) => setTimeout(res, ms)); 115 | } 116 | 117 | function chunkArray(arr, chunkSize) { 118 | return Array.from({ length: Math.ceil(arr.length / chunkSize) }, (_, index) => 119 | arr.slice(index * chunkSize, index * chunkSize + chunkSize), 120 | ); 121 | } 122 | 123 | function getValue(name, defaultValue) { 124 | return GM_getValue_(name, defaultValue); 125 | } 126 | 127 | function setValue(name, value) { 128 | if (name === "synonymDict" && (!value || !Object.keys(value).length)) return; 129 | GM_setValue_(name, value); 130 | // backup 131 | let valueArray = JSON.parse(window.localStorage.getItem(name)); 132 | if (!valueArray) valueArray = []; 133 | // save the dict by date 134 | if (name === "synonymDict") { 135 | const date = new Date().toLocaleDateString(); 136 | // not update if of same value 137 | if (valueArray.length) { 138 | if ( 139 | !valueArray.find( 140 | (el) => JSON.stringify(el.value) === JSON.stringify(value), 141 | ) 142 | ) { 143 | const lastElem = valueArray[valueArray.length - 1]; 144 | if (lastElem.date === date) { 145 | // append only 146 | for (let key of Object.keys(value)) { 147 | if (lastElem.value[key]) { 148 | // previous key 149 | lastElem.value[key] = Array.from( 150 | new Set(lastElem["value"][key].concat(value[key])), 151 | ); 152 | } else { 153 | // new key 154 | lastElem.value[key] = value[key]; 155 | } 156 | } 157 | valueArray.pop(); 158 | valueArray.push(lastElem); 159 | } else { 160 | if (valueArray.length > 30) valueArray.shift(); 161 | valueArray.push({ date, value }); 162 | } 163 | window.localStorage.setItem(name, JSON.stringify(valueArray)); 164 | } else { 165 | // same value, pass 166 | } 167 | } else { 168 | // empty array 169 | valueArray.push({ date, value }); 170 | window.localStorage.setItem(name, JSON.stringify(valueArray)); 171 | } 172 | } else { 173 | if (valueArray.length > 30) valueArray.shift(); 174 | valueArray.push(value); 175 | window.localStorage.setItem(name, JSON.stringify(valueArray)); 176 | } 177 | } 178 | 179 | function addStyle(style) { 180 | GM_addStyle_(style); 181 | } 182 | 183 | // merge all previous dict and return 184 | function restoreSynonymDict() { 185 | const value = window.localStorage.getItem("synonymDict"); 186 | if (!value) return {}; 187 | const dictArray = JSON.parse(value); 188 | const newDict = {}; 189 | for (let elem of dictArray) { 190 | const dict = elem.value; 191 | // merge all history value for the key 192 | Object.keys(dict).forEach((key) => { 193 | if (newDict[key]) 194 | newDict[key] = Array.from(new Set(newDict[key].concat(dict[key]))); 195 | else newDict[key] = dict[key]; 196 | }); 197 | } 198 | 199 | const a = document.createElement("a"); 200 | a.href = URL.createObjectURL( 201 | new Blob([JSON.stringify([newDict].concat(dictArray))], { 202 | type: "application/json", 203 | }), 204 | ); 205 | a.setAttribute( 206 | "download", 207 | `synonym_dict_restored_${new Date().toLocaleDateString()}.json`, 208 | ); 209 | a.click(); 210 | } 211 | 212 | function sortByParody(array) { 213 | const sortFunc = (a, b) => { 214 | let reg = /^[a-zA-Z0-9]/; 215 | if (reg.test(a) && !reg.test(b)) return -1; 216 | else if (!reg.test(a) && reg.test(b)) return 1; 217 | else return a.localeCompare(b, "zh"); 218 | }; 219 | const withParody = array.filter((key) => key.includes("(")); 220 | const withoutParody = array.filter((key) => !key.includes("(")); 221 | withoutParody.sort(sortFunc); 222 | withParody.sort(sortFunc); 223 | withParody.sort((a, b) => sortFunc(a.split("(")[1], b.split("(")[1])); 224 | return withoutParody.concat(withParody); 225 | } 226 | 227 | function loadResources() { 228 | function cssElement(url) { 229 | const link = document.createElement("link"); 230 | link.id = "bootstrapCSS"; 231 | link.href = url; 232 | link.rel = "stylesheet"; 233 | link.type = "text/css"; 234 | return link; 235 | } 236 | function jsElement(url) { 237 | const script = document.createElement("script"); 238 | script.id = "bootstrapJS"; 239 | script.src = url; 240 | return script; 241 | } 242 | 243 | document.head.appendChild(cssElement(GM_getResourceURL_("bootstrapCSS"))); 244 | document.head.appendChild(jsElement(GM_getResourceURL_("bootstrapJS"))); 245 | 246 | // overwrite bootstrap global box-sizing style 247 | const style = document.createElement("style"); 248 | style.id = "LB_overwrite"; 249 | style.innerHTML = 250 | "*,::after,::before { box-sizing: content-box; } .btn,.form-control,.form-select,.row>* { box-sizing: border-box; } body { background: initial; } a {color: inherit; text-decoration: none} .collapse.show {visibility: visible}"; 251 | document.head.appendChild(style); 252 | if (DEBUG) console.log("[Label Bookmarks] Stylesheet Loaded"); 253 | } 254 | 255 | const bookmarkBatchSize = 100; 256 | async function fetchBookmarks(uid, tagToQuery, offset, publicationType) { 257 | const bookmarksRaw = await fetch( 258 | `/ajax/user/${uid}` + 259 | `/illusts/bookmarks?tag=${tagToQuery}` + 260 | `&offset=${offset}&limit=${bookmarkBatchSize}&rest=${publicationType}`, 261 | ); 262 | if (!turboMode) await delay(500); 263 | const bookmarksRes = await bookmarksRaw.json(); 264 | if (!bookmarksRaw.ok || bookmarksRes.error === true) { 265 | return alert( 266 | `获取用户收藏夹列表失败\nFail to fetch user bookmarks\n` + 267 | decodeURI(bookmarksRes.message), 268 | ); 269 | } else return bookmarksRes.body; 270 | } 271 | 272 | async function fetchAllBookmarksByTag( 273 | tag, 274 | publicationType, 275 | progressBar, 276 | max = 100, 277 | ) { 278 | let total = 65535, 279 | offset = 0, 280 | totalWorks = []; 281 | try { 282 | while (offset < total && window.runFlag) { 283 | if (turboMode) { 284 | const fetchPromises = []; 285 | const bookmarksBatch = []; 286 | const batchSize = 10; 287 | for (let i = 0; i < batchSize && offset < total; i++) { 288 | bookmarksBatch.push( 289 | fetchBookmarks(uid, tag, offset, publicationType), 290 | ); 291 | offset += max; 292 | } 293 | const batchResults = await Promise.all(bookmarksBatch); 294 | for (const bookmarks of batchResults) { 295 | total = bookmarks.total; 296 | for (const work of bookmarks["works"]) { 297 | const fetchedWork = { 298 | ...work, 299 | associatedTags: 300 | bookmarks["bookmarkTags"][work["bookmarkData"]["id"]] || [], 301 | }; 302 | totalWorks.push(fetchedWork); 303 | fetchPromises.push(fetchedWork); 304 | } 305 | } 306 | await Promise.all(fetchPromises); 307 | await delay(500); 308 | } else { 309 | const bookmarks = await fetchBookmarks( 310 | uid, 311 | tag, 312 | offset, 313 | publicationType, 314 | ); 315 | total = bookmarks.total; 316 | const works = bookmarks["works"]; 317 | works.forEach( 318 | (w) => 319 | (w.associatedTags = 320 | bookmarks["bookmarkTags"][w["bookmarkData"]["id"]] || []), 321 | ); 322 | totalWorks.push(...works); 323 | offset = totalWorks.length; 324 | } 325 | if (progressBar) { 326 | progressBar.innerText = totalWorks.length + "/" + total; 327 | const ratio = ((totalWorks.length / total) * max).toFixed(2); 328 | progressBar.style.width = ratio + "%"; 329 | } 330 | } 331 | } catch (err) { 332 | window.alert( 333 | `获取收藏夹时发生错误,请截图到GitHub反馈\nAn error was caught during fetching bookmarks. You might report it on GitHub\n${err.name}: ${err.message}\n${err.stack}`, 334 | ); 335 | console.log(err); 336 | } finally { 337 | if (progressBar) { 338 | progressBar.innerText = total + "/" + total; 339 | progressBar.style.width = "100%"; 340 | } 341 | } 342 | return totalWorks; 343 | } 344 | 345 | async function addBookmark(illust_id, restrict, tags) { 346 | const resRaw = await fetch("/ajax/illusts/bookmarks/add", { 347 | headers: { 348 | accept: "application/json", 349 | "content-type": "application/json; charset=utf-8", 350 | "x-csrf-token": token, 351 | }, 352 | body: JSON.stringify({ 353 | illust_id, 354 | restrict, 355 | comment: "", 356 | tags, 357 | }), 358 | method: "POST", 359 | }); 360 | await delay(500); 361 | return resRaw; 362 | } 363 | 364 | async function removeBookmark(bookmarkIds, progressBar) { 365 | async function run(ids) { 366 | await fetch("/ajax/illusts/bookmarks/remove", { 367 | headers: { 368 | accept: "application/json", 369 | "content-type": "application/json; charset=utf-8", 370 | "x-csrf-token": token, 371 | }, 372 | body: JSON.stringify({ bookmarkIds: ids }), 373 | method: "POST", 374 | }); 375 | await delay(500); 376 | } 377 | const num = Math.ceil(bookmarkIds.length / bookmarkBatchSize); 378 | for (let i of [...Array(num).keys()]) { 379 | if (!window.runFlag) break; 380 | const ids = bookmarkIds.filter( 381 | (_, j) => j >= i * bookmarkBatchSize && j < (i + 1) * bookmarkBatchSize, 382 | ); 383 | await run(ids); 384 | if (progressBar) { 385 | const offset = i * bookmarkBatchSize; 386 | progressBar.innerText = offset + "/" + bookmarkIds.length; 387 | const ratio = ((offset / bookmarkIds.length) * 100).toFixed(2); 388 | progressBar.style.width = ratio + "%"; 389 | } 390 | } 391 | if (progressBar) { 392 | progressBar.innerText = bookmarkIds.length + "/" + bookmarkIds.length; 393 | progressBar.style.width = "100%"; 394 | } 395 | } 396 | 397 | async function updateBookmarkTags( 398 | bookmarkIds, 399 | addTags, 400 | removeTags, 401 | progressBar, 402 | ) { 403 | if (!bookmarkIds?.length) 404 | throw new TypeError("BookmarkIds is undefined or empty array"); 405 | if (!Array.isArray(addTags) && !Array.isArray(removeTags)) 406 | throw new TypeError("Either addTags or removeTags should be valid array"); 407 | 408 | async function fetchRequest(url, data) { 409 | return await fetch(url, { 410 | method: "POST", 411 | headers: { 412 | accept: "application/json", 413 | "content-type": "application/json; charset=utf-8", 414 | "x-csrf-token": token, 415 | }, 416 | body: JSON.stringify(data), 417 | }); 418 | } 419 | async function run(ids) { 420 | if (turboMode) { 421 | const requests = []; 422 | if (addTags && addTags.length) { 423 | const addTagsChunks = chunkArray(addTags, bookmarkBatchSize); 424 | for (const tagsChunk of addTagsChunks) { 425 | requests.push( 426 | fetchRequest("/ajax/illusts/bookmarks/add_tags", { 427 | tags: tagsChunk, 428 | bookmarkIds: ids, 429 | }), 430 | ); 431 | } 432 | } 433 | if (removeTags && removeTags.length) { 434 | const removeTagsChunks = chunkArray(removeTags, bookmarkBatchSize); 435 | for (const tagsChunk of removeTagsChunks) { 436 | requests.push( 437 | fetchRequest("/ajax/illusts/bookmarks/remove_tags", { 438 | removeTags: tagsChunk, 439 | bookmarkIds: ids, 440 | }), 441 | ); 442 | } 443 | } 444 | if (requests.length > 1) await Promise.all(requests); 445 | await delay(500); 446 | } else { 447 | if (addTags && addTags.length) { 448 | await fetchRequest("/ajax/illusts/bookmarks/add_tags", { 449 | tags: addTags, 450 | bookmarkIds: ids, 451 | }); 452 | await delay(500); 453 | } 454 | if (removeTags && removeTags.length) { 455 | await fetchRequest("/ajax/illusts/bookmarks/remove_tags", { 456 | removeTags, 457 | bookmarkIds: ids, 458 | }); 459 | await delay(500); 460 | } 461 | } 462 | } 463 | 464 | let i = 0; 465 | for (const ids of chunkArray(bookmarkIds, bookmarkBatchSize)) { 466 | if (!window.runFlag) break; 467 | await run(ids); 468 | if (progressBar) { 469 | i++; 470 | const offset = Math.min(i * bookmarkBatchSize, bookmarkIds.length); 471 | progressBar.innerText = offset + "/" + bookmarkIds.length; 472 | const ratio = ((offset / bookmarkIds.length) * 100).toFixed(2); 473 | progressBar.style.width = ratio + "%"; 474 | } 475 | } 476 | if (progressBar) { 477 | progressBar.innerText = bookmarkIds.length + "/" + bookmarkIds.length; 478 | progressBar.style.width = "100%"; 479 | } 480 | } 481 | 482 | async function updateBookmarkRestrict( 483 | bookmarkIds, 484 | bookmarkRestrict, 485 | progressBar, 486 | ) { 487 | if (!bookmarkIds?.length) 488 | throw new TypeError("BookmarkIds is undefined or empty array"); 489 | if (!["public", "private"].includes(bookmarkRestrict)) 490 | throw new TypeError("Bookmark restrict should be public or private"); 491 | async function run(ids) { 492 | await fetch("/ajax/illusts/bookmarks/edit_restrict", { 493 | headers: { 494 | accept: "application/json", 495 | "content-type": "application/json; charset=utf-8", 496 | "x-csrf-token": token, 497 | }, 498 | body: JSON.stringify({ bookmarkIds: ids, bookmarkRestrict }), 499 | method: "POST", 500 | }); 501 | await delay(500); 502 | } 503 | const num = Math.ceil(bookmarkIds.length / bookmarkBatchSize); 504 | for (let i of [...Array(num).keys()]) { 505 | if (!window.runFlag) break; 506 | const ids = bookmarkIds.filter( 507 | (_, j) => j >= i * bookmarkBatchSize && j < (i + 1) * bookmarkBatchSize, 508 | ); 509 | await run(ids); 510 | if (progressBar) { 511 | const offset = i * bookmarkBatchSize; 512 | progressBar.innerText = offset + "/" + bookmarkIds.length; 513 | const ratio = ((offset / bookmarkIds.length) * 100).toFixed(2); 514 | progressBar.style.width = ratio + "%"; 515 | } 516 | } 517 | if (progressBar) { 518 | progressBar.innerText = bookmarkIds.length + "/" + bookmarkIds.length; 519 | progressBar.style.width = "100%"; 520 | } 521 | } 522 | 523 | async function clearBookmarkTags(works) { 524 | if (!works?.length) { 525 | return alert( 526 | `没有获取到收藏夹内容,操作中断,请检查选项下是否有作品\nFetching bookmark information failed. Abort operation. Please check the existence of works with the configuration`, 527 | ); 528 | } 529 | if ( 530 | !window.confirm( 531 | `确定要删除所选作品的标签吗?(作品的收藏状态不会改变)\nThe tags of work(s) you've selected will be removed (become uncategorized). Is this okay?`, 532 | ) 533 | ) 534 | return; 535 | window.runFlag = true; 536 | 537 | const modal = document.querySelector("#progress_modal"); 538 | // noinspection TypeScriptUMDGlobal 539 | const bootstrap_ = bootstrap; 540 | let instance = bootstrap_.Modal.getInstance(modal); 541 | if (!instance) instance = new bootstrap_.Modal(modal); 542 | instance.show(); 543 | 544 | const prompt = document.querySelector("#progress_modal_prompt"); 545 | const progressBar = document.querySelector("#progress_modal_progress_bar"); 546 | 547 | const tagPool = Array.from( 548 | new Set(works.reduce((a, b) => [...a, ...b.associatedTags], [])), 549 | ); 550 | const workLength = works.length; 551 | const tagPoolSize = tagPool.length; 552 | if (DEBUG) console.log(works, tagPool); 553 | 554 | if (workLength > tagPoolSize) { 555 | for (let index = 1; index <= tagPoolSize; index++) { 556 | if (!window.runFlag) break; 557 | const tag = tagPool[index - 1]; 558 | const ids = works 559 | .filter((w) => w.associatedTags.includes(tag)) 560 | .map((w) => w.bookmarkId || w["bookmarkData"]["id"]); 561 | if (DEBUG) console.log("Clearing", tag, ids); 562 | 563 | progressBar.innerText = index + "/" + tagPoolSize; 564 | const ratio = ((index / tagPoolSize) * 100).toFixed(2); 565 | progressBar.style.width = ratio + "%"; 566 | prompt.innerText = `正在清除标签... / Clearing bookmark tags`; 567 | 568 | await updateBookmarkTags(ids, null, [tag]); 569 | } 570 | } else { 571 | for (let index = 1; index <= workLength; index++) { 572 | if (!window.runFlag) break; 573 | const work = works[index - 1]; 574 | const url = "https://www.pixiv.net/artworks/" + work.id; 575 | console.log(index, work.title, work.id, url); 576 | if (DEBUG) console.log(work); 577 | 578 | progressBar.innerText = index + "/" + workLength; 579 | const ratio = ((index / workLength) * 100).toFixed(2); 580 | progressBar.style.width = ratio + "%"; 581 | prompt.innerText = work.alt + "\n" + work.associatedTags.join(" "); 582 | 583 | await updateBookmarkTags( 584 | [work.bookmarkId || work["bookmarkData"]["id"]], 585 | undefined, 586 | work.associatedTags, 587 | ); 588 | } 589 | } 590 | 591 | if (window.runFlag) prompt.innerText = `标签删除完成!\nFinish Tag Clearing!`; 592 | else 593 | prompt.innerText = 594 | "检测到停止信号,程序已停止运行\nStop signal detected. Program exits."; 595 | setTimeout(() => { 596 | instance.hide(); 597 | if (window.runFlag && !DEBUG) window.location.reload(); 598 | }, 1000); 599 | } 600 | 601 | async function handleClearBookmarkTags(evt) { 602 | evt.preventDefault(); 603 | const selected = [ 604 | ...document.querySelectorAll("label>div[aria-disabled='true']"), 605 | ]; 606 | if (!selected.length) return; 607 | 608 | const works = selected 609 | .map((el) => { 610 | const middleChild = Object.values( 611 | el.parentNode.parentNode.parentNode.parentNode, 612 | )[0]["child"]; 613 | const work = middleChild["memoizedProps"]["work"]; 614 | work.associatedTags = 615 | middleChild["child"]["memoizedProps"]["associatedTags"] || []; 616 | work.bookmarkId = middleChild["memoizedProps"]["bookmarkId"]; 617 | return work; 618 | }) 619 | .filter((work) => work.associatedTags.length); 620 | await clearBookmarkTags(works); 621 | } 622 | 623 | async function deleteTag(tag, publicationType) { 624 | if (!tag) 625 | return alert( 626 | `请选择需要删除的标签\nPlease select the tag you would like to delete`, 627 | ); 628 | if ( 629 | tag === "未分類" || 630 | !window.confirm( 631 | `确定要删除所选的标签 ${tag} 吗?(作品的收藏状态不会改变)\nThe tag ${tag} will be removed and works of ${tag} will keep bookmarked. Is this okay?`, 632 | ) 633 | ) 634 | return; 635 | window.runFlag = true; 636 | const modal = document.querySelector("#progress_modal"); 637 | // noinspection TypeScriptUMDGlobal 638 | const bootstrap_ = bootstrap; 639 | let instance = bootstrap_.Modal.getInstance(modal); 640 | if (!instance) instance = new bootstrap_.Modal(modal); 641 | await instance.show(); 642 | 643 | const prompt = document.querySelector("#progress_modal_prompt"); 644 | const progressBar = document.querySelector("#progress_modal_progress_bar"); 645 | 646 | const totalBookmarks = await fetchAllBookmarksByTag( 647 | tag, 648 | publicationType, 649 | progressBar, 650 | 90, 651 | ); 652 | console.log(totalBookmarks); 653 | 654 | if (window.runFlag) { 655 | prompt.innerText = `标签${tag}删除中...\nDeleting Tag ${tag}`; 656 | progressBar.style.width = "90%"; 657 | } else { 658 | prompt.innerText = 659 | "检测到停止信号,程序已停止运行\nStop signal detected. Program exits."; 660 | progressBar.style.width = "100%"; 661 | return; 662 | } 663 | 664 | const ids = totalBookmarks.map((work) => work["bookmarkData"]["id"]); 665 | await updateBookmarkTags(ids, undefined, [tag]); 666 | 667 | progressBar.style.width = "100%"; 668 | if (window.runFlag) 669 | prompt.innerText = `标签${tag}删除完成!\nTag ${tag} Removed!`; 670 | else 671 | prompt.innerText = 672 | "检测到停止信号,程序已停止运行\nStop signal detected. Program exits."; 673 | setTimeout(() => { 674 | instance.hide(); 675 | if (window.runFlag && !DEBUG) 676 | window.location.href = `https://www.pixiv.net/users/${uid}/bookmarks/artworks?rest=${publicationType}`; 677 | }, 1000); 678 | } 679 | 680 | async function handleDeleteTag(evt) { 681 | evt.preventDefault(); 682 | const { tag, restrict } = await updateWorkInfo(); 683 | await deleteTag(tag, restrict ? "hide" : "show"); 684 | } 685 | 686 | async function handleLabel(evt) { 687 | evt.preventDefault(); 688 | 689 | const addFirst = document.querySelector("#label_add_first").value; 690 | const addAllTags = document.querySelector("#label_add_all_tags").value; 691 | const tagToQuery = document.querySelector("#label_tag_query").value; 692 | const publicationType = (await updateWorkInfo())["restrict"] 693 | ? "hide" 694 | : "show"; 695 | const labelR18 = document.querySelector("#label_r18").value; 696 | const labelSafe = document.querySelector("#label_safe").value; 697 | const labelAI = document.querySelector("#label_ai").value; 698 | const labelAuthor = document.querySelector("#label_author").value; 699 | const labelStrict = document.querySelector("#label_strict").value; 700 | const exclusion = document 701 | .querySelector("#label_exclusion") 702 | .value.split(/[\s\n]/) 703 | .filter((t) => t); 704 | 705 | console.log("Label Configuration:"); 706 | console.log( 707 | `addFirst: ${addFirst === "true"}; addAllTags: ${ 708 | addAllTags === "true" 709 | }; tagToQuery: ${tagToQuery}; labelR18: ${ 710 | labelR18 === "true" 711 | }; labelSafe: ${labelSafe}; labelAI: ${labelAI}; labelAuthor: ${ 712 | labelAuthor === "true" 713 | }; publicationType: ${publicationType}; exclusion: ${exclusion.join(",")}`, 714 | ); 715 | 716 | if ( 717 | addAllTags === "true" && 718 | !window.confirm(`作品自带的所有标签都会被优先加入用户标签,这将导致大量的标签被添加,是否确定? 719 | All tags that come with the work will be first added to your user tags, which can be large. Is this okay?`) 720 | ) 721 | return; 722 | 723 | window.runFlag = true; 724 | const promptBottom = document.querySelector("#label_prompt"); 725 | promptBottom.innerText = 726 | "处理中,请勿关闭窗口\nProcessing. Please do not close the window."; 727 | const objDiv = document.querySelector("#label_form"); 728 | objDiv.scrollTop = objDiv.scrollHeight; 729 | 730 | // fetch bookmarks 731 | let total, // total bookmarks of specific tag 732 | index = 0, // counter of do-while loop 733 | offset = 0; // as uncategorized ones will decrease, offset means num of images updated "successfully" 734 | // update progress bar 735 | const progressBar = document.querySelector("#progress_bar"); 736 | progressBar.style.width = "0"; 737 | const intervalId = setInterval(() => { 738 | if (total) { 739 | progressBar.innerText = index + "/" + total; 740 | const ratio = ((index / total) * 100).toFixed(2); 741 | progressBar.style.width = ratio + "%"; 742 | if (!window.runFlag || index === total) { 743 | console.log("Progress bar stops updating"); 744 | clearInterval(intervalId); 745 | } 746 | } 747 | }, 1000); 748 | do { 749 | const realOffset = tagToQuery === "未分類" ? offset : index; 750 | const bookmarks = await fetchBookmarks( 751 | uid, 752 | tagToQuery, 753 | realOffset, 754 | publicationType, 755 | ); 756 | if (DEBUG) console.log("Bookmarks", bookmarks); 757 | if (!total) total = bookmarks.total; 758 | for (let work of bookmarks["works"]) { 759 | const url = "https://www.pixiv.net/artworks/" + work.id; 760 | if (DEBUG) console.log(index, work.title, work.id, url); 761 | index++; 762 | // ---- means unavailable, hidden or deleted by author 763 | if (work.title === "-----") { 764 | offset++; 765 | continue; 766 | } 767 | const workTags = work["tags"]; 768 | let intersection = []; 769 | // add all work tags, and replace those which are defined in synonym dict 770 | if (addAllTags === "true") 771 | intersection = workTags.map( 772 | (workTag) => 773 | Object.keys(synonymDict).find((userTag) => 774 | synonymDict[userTag].includes(workTag), 775 | ) || workTag, 776 | ); 777 | if (labelAuthor === "true") 778 | workTags.push(work["userName"], work["userId"]); 779 | intersection = intersection.concat( 780 | [...userTags, ...Object.keys(synonymDict)].filter((userTag) => { 781 | // if work tags includes this user tag 782 | if ( 783 | arrayIncludes(workTags, userTag) || // full tag 784 | arrayIncludes(workTags, userTag, getWorkTitle) || // work title 785 | (arrayIncludes(workTags, userTag, null, getCharacterName) && // char name 786 | (!labelStrict || 787 | arrayIncludes(workTags, userTag, null, getWorkTitle))) // not strict or includes work title 788 | ) 789 | return true; 790 | // if work tags match a user alias (exact match) 791 | return ( 792 | synonymDict[userTag] && 793 | synonymDict[userTag].find( 794 | (alias) => 795 | arrayIncludes(workTags, alias) || 796 | arrayIncludes(workTags, alias, getWorkTitle) || // work title 797 | (arrayIncludes(workTags, alias, null, getCharacterName) && 798 | (!labelStrict || 799 | arrayIncludes(workTags, alias, null, getWorkTitle))), 800 | ) 801 | ); 802 | }), 803 | ); 804 | // if workTags match some alias, add it to the intersection (exact match, with or without work title) 805 | intersection = intersection.concat( 806 | Object.keys(synonymDict).filter((aliasName) => { 807 | if (!synonymDict[aliasName]) { 808 | console.log(aliasName, synonymDict[aliasName]); 809 | throw new Error("Empty value in synonym dictionary"); 810 | } 811 | if ( 812 | workTags.some( 813 | (workTag) => 814 | arrayIncludes( 815 | synonymDict[aliasName].concat(aliasName), 816 | workTag, 817 | null, 818 | getWorkTitle, 819 | ) || 820 | arrayIncludes( 821 | synonymDict[aliasName].concat(aliasName), 822 | workTag, 823 | null, 824 | getCharacterName, 825 | ), 826 | ) 827 | ) 828 | return true; 829 | }), 830 | ); 831 | if (work["xRestrict"] && labelR18 === "true") intersection.push("R-18"); 832 | if (!work["xRestrict"] && labelSafe === "true") intersection.push("SFW"); 833 | if (work["aiType"] === 2 && labelAI === "true") intersection.push("AI"); 834 | // remove duplicate and exclusion 835 | intersection = Array.from(new Set(intersection)).filter( 836 | (t) => !exclusion.includes(t), 837 | ); 838 | 839 | const bookmarkId = work["bookmarkData"]["id"]; 840 | const prevTags = bookmarks["bookmarkTags"][bookmarkId] || []; 841 | 842 | if (!intersection.length && !prevTags.length) { 843 | if (addFirst === "true") { 844 | const first = workTags 845 | .filter( 846 | (tag) => 847 | !exclusion.includes(tag) && 848 | tag.length <= 20 && 849 | !tag.includes("入り"), 850 | ) 851 | .slice(0, 1); // Can be changed if you want to add more than 1 tag from the same work 852 | if (first) { 853 | intersection.push(...first); 854 | userTags.push(...first); 855 | } 856 | } 857 | } 858 | 859 | const addTags = intersection.filter((tag) => !prevTags.includes(tag)); 860 | 861 | // for uncategorized 862 | if (!intersection.length) { 863 | offset++; 864 | } 865 | if (addTags.length) { 866 | if (!DEBUG) console.log(index, work.title, work.id, url); 867 | console.log("\tprevTags:", prevTags); 868 | console.log("\tintersection:", intersection); 869 | console.log("\taddTags:", addTags); 870 | } else continue; 871 | 872 | promptBottom.innerText = `处理中,请勿关闭窗口 / Processing. Please do not close the window.\n${work.alt}`; 873 | await updateBookmarkTags([bookmarkId], addTags); 874 | 875 | if (!window.runFlag) { 876 | promptBottom.innerText = 877 | "检测到停止信号,程序已停止运行\nStop signal detected. Program exits."; 878 | index = total; 879 | break; 880 | } 881 | } 882 | } while (index < total); 883 | if (total === 0) { 884 | promptBottom.innerText = `指定分类下暂无符合要求的作品,请关闭窗口 885 | Works needed to be labeled not found. Please close the window. 886 | `; 887 | } else if (window.runFlag) { 888 | promptBottom.innerText = `自动添加标签已完成,请关闭窗口并刷新网页 889 | Auto labeling finished successfully. Please close the window and refresh. 890 | `; 891 | } 892 | window.runFlag = false; 893 | } 894 | 895 | let prevSearch, searchBatch, searchResults, searchOffset, totalBookmarks; 896 | async function handleSearch(evt) { 897 | evt.preventDefault(); 898 | 899 | let searchMode = 0; 900 | let searchString = document.querySelector("#search_value")?.value; 901 | if ( 902 | document.querySelector("#basic_search_field").className.includes("d-none") 903 | ) { 904 | searchMode = 1; 905 | searchString = [...document.querySelectorAll(".advanced_search_field")] 906 | .map((el) => el.value.split(" ")[0]) 907 | .join(" "); 908 | } 909 | searchString = searchString.replace(/!/g, "!").trim(); 910 | const searchStringArray = searchString.split(" "); 911 | let searchConfigs = Array(searchStringArray.length).fill(Array(4).fill(true)); 912 | if (searchMode) { 913 | const advanced = document.querySelector("#advanced_search_fields"); 914 | const configContainers = [...advanced.querySelectorAll(".row")]; 915 | searchConfigs = configContainers.map((el) => 916 | [...el.querySelectorAll("input")].map((i) => i.checked), 917 | ); 918 | } 919 | 920 | const matchPattern = document.querySelector("#search_exact_match").value; 921 | const tagsLengthMatch = 922 | document.querySelector("#search_length_match").value === "true"; 923 | const tagToQuery = document.querySelector("#search_select_tag").value; 924 | const publicationType = document.querySelector("#search_publication").value; 925 | const newSearch = { 926 | searchString, 927 | searchConfigs, 928 | matchPattern, 929 | tagsLengthMatch, 930 | tagToQuery, 931 | publicationType, 932 | }; 933 | 934 | // initialize new search 935 | window.runFlag = true; 936 | const resultsDiv = document.querySelector("#search_results"); 937 | const noResult = document.querySelector("#no_result"); 938 | if (noResult) resultsDiv.removeChild(noResult); 939 | if (!prevSearch || !isEqualObject(prevSearch, newSearch)) { 940 | prevSearch = newSearch; 941 | searchResults = []; 942 | searchOffset = 0; 943 | totalBookmarks = 0; 944 | searchBatch = 200; 945 | document.querySelector("#search_prompt").innerText = ""; 946 | while (resultsDiv.firstChild) { 947 | resultsDiv.removeChild(resultsDiv.firstChild); 948 | } 949 | clearTimeout(timeout); 950 | document.querySelector("#search_suggestion").parentElement.style.display = 951 | "none"; 952 | } else { 953 | searchBatch += 200; 954 | } 955 | 956 | if (searchOffset && searchOffset === totalBookmarks) { 957 | window.runFlag = false; 958 | return alert(` 959 | 已经完成所选标签下所有收藏的搜索! 960 | All Bookmarks Of Selected Tag Have Been Searched! 961 | `); 962 | } 963 | const spinner = document.querySelector("#spinner"); 964 | spinner.style.display = "block"; 965 | 966 | // noinspection TypeScriptUMDGlobal 967 | const bootstrap_ = bootstrap; 968 | const collapseIns = bootstrap_.Collapse.getInstance( 969 | document.querySelector("#advanced_search"), 970 | ); 971 | if (collapseIns) collapseIns.hide(); 972 | 973 | let includeArray = searchStringArray.filter( 974 | (el) => el.length && !el.includes("!"), 975 | ); 976 | let excludeArray = searchStringArray 977 | .filter((el) => el.length && el.includes("!")) 978 | .map((el) => el.slice(1)); 979 | 980 | console.log("Search Configuration:", searchConfigs); 981 | console.log( 982 | `matchPattern: ${matchPattern}; tagsLengthMatch: ${tagsLengthMatch}; tagToQuery: ${tagToQuery}; publicationType: ${publicationType}`, 983 | ); 984 | console.log("includeArray:", includeArray, "excludeArray", excludeArray); 985 | 986 | const textColor = theme ? "rgba(0, 0, 0, 0.88)" : "rgba(255, 255, 255, 0.88)"; 987 | 988 | const searchPrompt = document.querySelector("#search_prompt"); 989 | let index = 0; // index for current search batch 990 | do { 991 | const bookmarks = await fetchBookmarks( 992 | uid, 993 | tagToQuery, 994 | searchOffset, 995 | publicationType, 996 | ); 997 | searchPrompt.innerText = ` 998 | 当前搜索进度 / Searched:${searchOffset} / ${totalBookmarks} 999 | `; 1000 | if (DEBUG) console.log(bookmarks); 1001 | if (!totalBookmarks) { 1002 | totalBookmarks = bookmarks.total; 1003 | } 1004 | for (let work of bookmarks["works"]) { 1005 | if (DEBUG) { 1006 | console.log(searchOffset, work.title, work.id); 1007 | console.log(work["tags"]); 1008 | } 1009 | index++; 1010 | searchOffset++; 1011 | 1012 | if (work.title === "-----") continue; 1013 | const bookmarkTags = 1014 | bookmarks["bookmarkTags"][work["bookmarkData"]["id"]] || []; // empty if uncategorized 1015 | work.bookmarkTags = bookmarkTags; 1016 | const workTags = work["tags"]; 1017 | 1018 | const ifInclude = (keyword) => { 1019 | // especially, R-18 tag is labelled in work 1020 | if (["R-18", "r-18", "R18", "r18"].includes(keyword)) 1021 | return work["xRestrict"]; 1022 | 1023 | const index = searchStringArray.findIndex((kw) => kw.includes(keyword)); 1024 | const config = searchConfigs[index]; 1025 | if (DEBUG) console.log(keyword, config); 1026 | 1027 | // convert input keyword to a user tag 1028 | // keywords from user input, alias from dict 1029 | // keyword: 新世纪福音战士 1030 | // alias: EVA eva 1031 | const el = Object.keys(synonymDict) 1032 | .map((key) => [key.split("(")[0], key]) // [char name, full key] 1033 | .find( 1034 | (el) => 1035 | stringIncludes(el[0], keyword) || // input match char name 1036 | stringIncludes(el[1], keyword) || // input match full name 1037 | arrayIncludes(synonymDict[el[1]], keyword) || // input match any alias 1038 | (matchPattern === "fuzzy" && 1039 | (stringIncludes(el[1], keyword) || 1040 | arrayIncludes( 1041 | synonymDict[el[1]], 1042 | keyword, 1043 | null, 1044 | null, 1045 | true, 1046 | ))), 1047 | ); 1048 | const keywordArray = [keyword]; 1049 | if (el) { 1050 | keywordArray.push(...el); 1051 | keywordArray.push(...synonymDict[el[1]]); 1052 | } 1053 | if ( 1054 | keywordArray.some( 1055 | (kw) => 1056 | (config[0] && stringIncludes(work.title, kw)) || 1057 | (config[1] && stringIncludes(work["userName"], kw)) || 1058 | (config[2] && arrayIncludes(workTags, kw)) || 1059 | (config[3] && arrayIncludes(bookmarkTags, kw)), 1060 | ) 1061 | ) 1062 | return true; 1063 | if (matchPattern === "exact") return false; 1064 | return keywordArray.some( 1065 | (kw) => 1066 | (config[2] && arrayIncludes(workTags, kw, null, null, true)) || 1067 | (config[3] && arrayIncludes(bookmarkTags, kw, null, null, true)), 1068 | ); 1069 | }; 1070 | 1071 | if ( 1072 | (!tagsLengthMatch || includeArray.length === bookmarkTags.length) && 1073 | includeArray.every(ifInclude) && 1074 | !excludeArray.some(ifInclude) 1075 | ) { 1076 | searchResults.push(work); 1077 | displayWork(work, resultsDiv, textColor); 1078 | } 1079 | } 1080 | } while (searchOffset < totalBookmarks && index < searchBatch); 1081 | if (totalBookmarks === 0) 1082 | document.querySelector("#search_prompt").innerText = "无结果 / No Result"; 1083 | else 1084 | document.querySelector("#search_prompt").innerText = ` 1085 | 当前搜索进度 / Searched:${searchOffset} / ${totalBookmarks} 1086 | `; 1087 | if (searchOffset < totalBookmarks) 1088 | document.querySelector("#search_more").style.display = "block"; 1089 | else document.querySelector("#search_more").style.display = "none"; 1090 | if (!searchResults.length) { 1091 | resultsDiv.innerHTML = ` 1092 |
1093 | 暂无结果 / No Result 1094 |
1095 | `; 1096 | } 1097 | spinner.style.display = "none"; 1098 | console.log(searchResults); 1099 | window.runFlag = false; 1100 | } 1101 | 1102 | function displayWork(work, resultsDiv, textColor) { 1103 | const tagsString = work.tags 1104 | .slice(0, 6) 1105 | .map((i) => "#" + i) 1106 | .join(" "); 1107 | const container = document.createElement("div"); 1108 | const profile = 1109 | work["profileImageUrl"] || 1110 | "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; 1111 | container.className = "col-4 col-lg-3 col-xl-2 p-1"; 1112 | container.innerHTML = ` 1113 |
1114 | square 1115 |
1116 |
1117 |
R-18
1118 |
1119 | 1120 | 1121 | 1122 | 1123 | 1124 | ${work["pageCount"]} 1125 |
1126 |
1127 |
1128 |
1129 |
1130 | ${tagsString} 1131 |
1132 |
1133 | 1136 | ${work.title} 1137 | 1138 |
1139 |
1140 | 1142 | profile 1146 | ${work["userName"]} 1147 | 1148 |
1149 | `; 1150 | if (work["xRestrict"]) 1151 | container.querySelector(".rate-icon").classList.remove("d-none"); 1152 | if (work["pageCount"] > 1) 1153 | container.querySelector(".page-icon").classList.remove("d-none"); 1154 | container.firstElementChild.addEventListener("click", (evt) => 1155 | galleryMode(evt, work), 1156 | ); 1157 | resultsDiv.appendChild(container); 1158 | } 1159 | 1160 | function galleryMode(evt, work) { 1161 | if (DEBUG) console.log(work); 1162 | const modal = evt.composedPath().find((el) => el.id.includes("modal")); 1163 | const scrollTop = modal.scrollTop; 1164 | const dialog = evt 1165 | .composedPath() 1166 | .find((el) => el.className.includes("modal-dialog")); 1167 | dialog.classList.add("modal-fullscreen"); 1168 | const title = dialog.querySelector(".modal-header"); 1169 | const body = dialog.querySelector(".modal-body"); 1170 | const footer = dialog.querySelector(".modal-footer"); 1171 | const gallery = modal.querySelector(".gallery"); 1172 | const works = modal.id === "search_modal" ? searchResults : generatedResults; 1173 | let index = works.findIndex((w) => w === work); 1174 | const host = "https://i.pximg.net/img-master"; 1175 | gallery.innerHTML = ` 1176 |
1177 |
1178 | 1179 | 1184 | 1185 | 1190 | 1191 | 1197 | 1203 |
1204 | `; 1205 | const imageContainer = gallery.querySelector(".images"); 1206 | const all = gallery.querySelector("#gallery_all"); 1207 | 1208 | let pageIndex = 0, 1209 | pageLoaded = false, 1210 | masterUrl; 1211 | function updateWork(work) { 1212 | masterUrl = work.url.includes("limit_unknown") 1213 | ? work.url 1214 | : host + 1215 | work.url 1216 | .match(/\/img\/.*/)[0] 1217 | .replace(/_(custom|square)1200/, "_master1200"); 1218 | imageContainer.innerHTML = ` 1219 |
1220 | master 1221 |
1222 | `; 1223 | gallery.querySelector("#gallery_link").href = "/artworks/" + work.id; 1224 | pageIndex = 0; 1225 | pageLoaded = false; 1226 | if (work["pageCount"] > 1) all.classList.remove("d-none"); 1227 | else all.classList.add("d-none"); 1228 | } 1229 | updateWork(work); 1230 | 1231 | function loadAll() { 1232 | const work = works[index]; 1233 | [...Array(work["pageCount"] - 1).keys()].forEach((i) => { 1234 | const p = i + 1; 1235 | const url = masterUrl.replace("_p0_", `_p${p}_`); 1236 | const div = document.createElement("div"); 1237 | div.className = "text-center"; 1238 | div.innerHTML = `page${p}`; 1239 | imageContainer.appendChild(div); 1240 | }); 1241 | all.classList.add("d-none"); 1242 | pageLoaded = true; 1243 | } 1244 | all.addEventListener("click", loadAll); 1245 | 1246 | gallery.querySelector("#gallery_exit").addEventListener("click", () => { 1247 | dialog.classList.remove("modal-fullscreen"); 1248 | gallery.classList.add("d-none"); 1249 | title.classList.remove("d-none"); 1250 | body.classList.remove("d-none"); 1251 | footer.classList.remove("d-none"); 1252 | modal.removeEventListener("keyup", fnKey); 1253 | modal.scrollTo({ top: scrollTop, behavior: "smooth" }); 1254 | }); 1255 | 1256 | function preFetch(work) { 1257 | const url = work.url.includes("limit_unknown") 1258 | ? work.url 1259 | : host + 1260 | work.url 1261 | .match(/\/img\/.*/)[0] 1262 | .replace(/_(custom|square)1200/, "_master1200"); 1263 | const img = new Image(); 1264 | img.src = url; 1265 | } 1266 | function prev() { 1267 | if (works[index - 1]) { 1268 | index--; 1269 | updateWork(works[index]); 1270 | if (works[index - 1]) preFetch(works[index - 1]); 1271 | } 1272 | } 1273 | function next() { 1274 | if (works[index + 1]) { 1275 | index++; 1276 | updateWork(works[index]); 1277 | if (works[index + 1]) preFetch(works[index + 1]); 1278 | } 1279 | } 1280 | function up() { 1281 | if (!pageLoaded) return; 1282 | const scrollY = gallery.scrollTop; 1283 | pageIndex = Math.max(0, pageIndex - 1); 1284 | const elemTop = 1285 | imageContainer.children[pageIndex].getBoundingClientRect().top; 1286 | gallery.scrollTo({ top: scrollY + elemTop }); 1287 | } 1288 | function down() { 1289 | if (!pageLoaded) return loadAll(); 1290 | const scrollY = gallery.scrollTop; 1291 | pageIndex = Math.min(pageIndex + 1, works[index]["pageCount"] - 1); 1292 | const elemTop = 1293 | imageContainer.children[pageIndex].getBoundingClientRect().top; 1294 | gallery.scrollTo({ top: scrollY + elemTop }); 1295 | } 1296 | function fnKey(evt) { 1297 | if (evt.key === "ArrowLeft") prev(); 1298 | else if (evt.key === "ArrowRight") next(); 1299 | else if (evt.key === "ArrowUp") up(); 1300 | else if (evt.key === "ArrowDown") down(); 1301 | } 1302 | gallery.querySelector("#gallery_left").addEventListener("click", prev); 1303 | gallery.querySelector("#gallery_right").addEventListener("click", next); 1304 | modal.addEventListener("keyup", fnKey); 1305 | 1306 | gallery.classList.remove("d-none"); 1307 | title.classList.add("d-none"); 1308 | body.classList.add("d-none"); 1309 | footer.classList.add("d-none"); 1310 | } 1311 | 1312 | let prevTag, 1313 | prevRestriction, 1314 | totalAvailable, 1315 | generatorBookmarks, 1316 | generatedResults, 1317 | generatorDisplayLimit, 1318 | generatorBatchNum; 1319 | async function handleGenerate(evt) { 1320 | evt.preventDefault(); 1321 | const tag = document.querySelector("#generator_select_tag").value; 1322 | const batchSize = Math.max( 1323 | 0, 1324 | parseInt(document.querySelector("#generator_form_num").value) || 100, 1325 | ); 1326 | const publicationType = document.querySelector( 1327 | "#generator_form_publication", 1328 | ).value; 1329 | const restriction = document.querySelector( 1330 | "#generator_form_restriction", 1331 | ).value; 1332 | console.log(tag, batchSize, publicationType, restriction); 1333 | if ( 1334 | !tag && 1335 | !confirm( 1336 | `加载全部收藏夹需要较长时间,是否确认操作?\nLoad the whole bookmark will take quite long time to process. Is this okay?`, 1337 | ) 1338 | ) 1339 | return; 1340 | 1341 | const resultsDiv = document.querySelector("#generator_results"); 1342 | while (resultsDiv.firstChild) { 1343 | resultsDiv.removeChild(resultsDiv.firstChild); 1344 | } 1345 | 1346 | const display = document.querySelector("#generator_display"); 1347 | display.classList.remove("d-none"); 1348 | const prompt = document.querySelector("#generator_save_tag_prompt"); 1349 | if (prevTag !== tag || !generatorBookmarks?.length) { 1350 | prevTag = tag; 1351 | prevRestriction = null; 1352 | generatorDisplayLimit = 12; 1353 | generatorBookmarks = []; 1354 | generatorBatchNum = -1; 1355 | let offset = 0, 1356 | total = 0; 1357 | window.runFlag = true; 1358 | prompt.classList.remove("d-none"); 1359 | prompt.innerText = 1360 | "正在加载收藏夹信息,点击停止可中断运行 / Loading bookmarks, Click stop to abort"; 1361 | do { 1362 | if (!window.runFlag) break; 1363 | const bookmarks = await fetchBookmarks(uid, tag, offset, publicationType); 1364 | if (!total) { 1365 | total = bookmarks.total; 1366 | prompt.innerText = `正在加载收藏夹信息(${total}),点击停止可中断运行 / Loading bookmarks (${total}), Click stop to abort`; 1367 | } 1368 | generatorBookmarks.push(...bookmarks["works"]); 1369 | offset = generatorBookmarks.length; 1370 | } while (offset < total); 1371 | prompt.classList.add("d-none"); 1372 | window.runFlag = false; 1373 | shuffle(generatorBookmarks); 1374 | } 1375 | if (prevRestriction !== restriction) { 1376 | prevRestriction = restriction; 1377 | generatorBatchNum = -1; 1378 | if (restriction !== "all") { 1379 | generatorBookmarks.forEach((w) => { 1380 | w.used = !!( 1381 | (restriction === "sfw" && w["xRestrict"]) || 1382 | (restriction === "nsfw" && !w["xRestrict"]) 1383 | ); 1384 | }); 1385 | } 1386 | totalAvailable = generatorBookmarks.filter((w) => !w.used).length; 1387 | document.querySelector("#generator_spinner").classList.add("d-none"); 1388 | console.log(generatorBookmarks); 1389 | } 1390 | if (!totalAvailable) { 1391 | display.classList.add("d-none"); 1392 | prompt.innerText = "图片加载失败 / Image Loading Failed"; 1393 | return; 1394 | } 1395 | document.querySelector("#generator_form_buttons").classList.remove("d-none"); 1396 | 1397 | let availableBookmarks = generatorBookmarks.filter((w) => !w.used); 1398 | if (generatorBookmarks.length && !availableBookmarks.length) { 1399 | generatorBatchNum = -1; 1400 | generatorBookmarks.forEach((w) => { 1401 | if ( 1402 | restriction === "all" || 1403 | (restriction === "sfw" && !w["xRestrict"]) || 1404 | (restriction === "nsfw" && w["xRestrict"]) 1405 | ) 1406 | w.used = false; 1407 | }); 1408 | availableBookmarks = generatorBookmarks.filter((w) => !w.used); 1409 | } 1410 | generatorBatchNum++; 1411 | 1412 | const textColor = theme ? "rgba(0, 0, 0, 0.88)" : "rgba(255, 255, 255, 0.88)"; 1413 | generatedResults = availableBookmarks.slice(0, batchSize); 1414 | generatedResults.forEach((w) => (w.used = true)); 1415 | generatedResults 1416 | .filter((_, i) => i < generatorDisplayLimit) 1417 | .forEach((w) => displayWork(w, resultsDiv, textColor)); 1418 | if (generatedResults.length > generatorDisplayLimit) { 1419 | document.querySelector("#generator_more").classList.remove("d-none"); 1420 | } 1421 | document.querySelector("#generator_prompt").innerText = 1422 | `当前批次 / Batch Num: ${generatorBatchNum} | 当前展示 / Display: ${generatedResults.length} / ${totalAvailable}`; 1423 | } 1424 | 1425 | function shuffle(array) { 1426 | let currentIndex = array.length, 1427 | randomIndex; 1428 | // while there remain elements to shuffle. 1429 | while (currentIndex !== 0) { 1430 | // pick a remaining element. 1431 | randomIndex = Math.floor(Math.random() * currentIndex); 1432 | currentIndex--; 1433 | // swap it with the current element. 1434 | [array[currentIndex], array[randomIndex]] = [ 1435 | array[randomIndex], 1436 | array[currentIndex], 1437 | ]; 1438 | } 1439 | return array; 1440 | } 1441 | 1442 | const hold = false; 1443 | function createModalElements() { 1444 | // noinspection TypeScriptUMDGlobal 1445 | const bootstrap_ = bootstrap; 1446 | const bgColor = theme ? "bg-white" : "bg-dark"; 1447 | const textColor = theme ? "text-lp-dark" : "text-lp-light"; 1448 | addStyle(` 1449 | .text-lp-dark { 1450 | color: rgb(31, 31, 31); 1451 | } 1452 | .text-lp-light { 1453 | color: rgb(245, 245, 245); 1454 | } 1455 | .label-button.text-lp-dark, .label-button.text-lp-light { 1456 | color: rgb(133, 133, 133); 1457 | } 1458 | .label-button.text-lp-dark:hover { 1459 | color: rgb(31, 31, 31); 1460 | } 1461 | .label-button.text-lp-light:hover { 1462 | color: rgb(245, 245, 245); 1463 | } 1464 | .icon-invert { 1465 | filter: invert(1); 1466 | } 1467 | .bg-dark button, .form-control, .form-control:focus, .form-select { 1468 | color: inherit; 1469 | background: inherit; 1470 | } 1471 | .modal::-webkit-scrollbar, .no-scroll::-webkit-scrollbar { 1472 | display: none; /* Chrome */ 1473 | } 1474 | .modal, .no-scroll { 1475 | -ms-overflow-style: none; /* IE and Edge */ 1476 | scrollbar-width: none; /* Firefox */ 1477 | } 1478 | .btn-close-empty { 1479 | background: none; 1480 | height: initial; 1481 | width: initial; 1482 | } 1483 | .gallery-image { 1484 | max-height: calc(100vh - 5rem); 1485 | } 1486 | .rate-icon { 1487 | padding: 0px 6px; 1488 | border-radius: 3px; 1489 | color: rgb(245, 245, 245); 1490 | background: rgb(255, 64, 96); 1491 | font-weight: bold; 1492 | font-size: 10px; 1493 | line-height: 16px; 1494 | user-select: none; 1495 | height: 16px; 1496 | } 1497 | .page-icon { 1498 | display: flex; 1499 | -webkit-box-pack: center; 1500 | justify-content: center; 1501 | -webkit-box-align: center; 1502 | align-items: center; 1503 | flex: 0 0 auto; 1504 | box-sizing: border-box; 1505 | height: 20px; 1506 | min-width: 20px; 1507 | color: rgb(245, 245, 245); 1508 | font-weight: bold; 1509 | padding: 0px 6px; 1510 | background: rgba(0, 0, 0, 0.32); 1511 | border-radius: 10px; 1512 | font-size: 10px; 1513 | line-height: 10px; 1514 | } 1515 | .page-icon:first-child { 1516 | display: inline-flex; 1517 | vertical-align: top; 1518 | -webkit-box-align: center; 1519 | align-items: center; 1520 | height: 10px; 1521 | } 1522 | #gallery_link:hover { 1523 | color: var(--bs-btn-hover-color); 1524 | } 1525 | `); 1526 | const backdropConfig = getValue("backdropConfig", "false") === "true"; 1527 | const svgPin = ` 1528 | 1529 | `; 1530 | const svgUnpin = ` 1531 | 1532 | `; 1533 | const svgClose = ` 1534 | 1535 | `; 1536 | const defaultPinConfig = backdropConfig ? svgPin : svgUnpin; 1537 | 1538 | const showLatest = getValue("version") !== version ? "show" : ""; 1539 | 1540 | const lastBackupDictTime = getValue("lastBackupDict", ""); 1541 | let lastBackupDict = ""; 1542 | if (lastBackupDictTime) { 1543 | if (lang.includes("zh")) { 1544 | lastBackupDict = `
最后备份:${new Date( 1545 | parseInt(lastBackupDictTime), 1546 | ).toLocaleDateString("zh-CN")}
`; 1547 | } else { 1548 | lastBackupDict = `
Last Backup: ${new Date( 1549 | parseInt(lastBackupDictTime), 1550 | ).toLocaleDateString("en-US")}
`; 1551 | } 1552 | } 1553 | 1554 | // label 1555 | const labelModal = document.createElement("div"); 1556 | labelModal.className = "modal fade"; 1557 | labelModal.id = "label_modal"; 1558 | labelModal.tabIndex = -1; 1559 | labelModal.innerHTML = ` 1560 | 1776 | `; 1777 | // backdrop pin 1778 | labelModal.setAttribute( 1779 | "data-bs-backdrop", 1780 | backdropConfig ? "static" : "true", 1781 | ); 1782 | const labelPinButton = labelModal.querySelector("#label_pin"); 1783 | labelPinButton.addEventListener("click", () => { 1784 | const ins = bootstrap_.Modal.getOrCreateInstance(labelModal); 1785 | const backdrop = ins["_config"]["backdrop"] === "static"; 1786 | if (backdrop) { 1787 | ins["_config"]["backdrop"] = true; 1788 | setValue("backdropConfig", "false"); 1789 | labelPinButton.innerHTML = svgUnpin; 1790 | } else { 1791 | ins["_config"]["backdrop"] = "static"; 1792 | setValue("backdropConfig", "true"); 1793 | labelPinButton.innerHTML = svgPin; 1794 | } 1795 | }); 1796 | // latest 1797 | labelModal 1798 | .querySelector("button#toggle_latest") 1799 | .addEventListener("click", () => { 1800 | setValue("version", version); 1801 | }); 1802 | 1803 | // search 1804 | const searchModal = document.createElement("div"); 1805 | searchModal.className = "modal fade"; 1806 | searchModal.id = "search_modal"; 1807 | searchModal.tabIndex = -1; 1808 | searchModal.innerHTML = ` 1809 | 1887 | `; 1888 | // backdrop pin 1889 | searchModal.setAttribute( 1890 | "data-bs-backdrop", 1891 | backdropConfig ? "static" : "true", 1892 | ); 1893 | const searchPinButton = searchModal.querySelector("#search_pin"); 1894 | searchPinButton.addEventListener("click", () => { 1895 | const ins = bootstrap_.Modal.getOrCreateInstance(searchModal); 1896 | const backdrop = ins["_config"]["backdrop"] === "static"; 1897 | if (backdrop) { 1898 | ins["_config"]["backdrop"] = true; 1899 | setValue("backdropConfig", "false"); 1900 | searchPinButton.innerHTML = svgUnpin; 1901 | } else { 1902 | ins["_config"]["backdrop"] = "static"; 1903 | setValue("backdropConfig", "true"); 1904 | searchPinButton.innerHTML = svgPin; 1905 | } 1906 | }); 1907 | 1908 | const generatorModal = document.createElement("div"); 1909 | generatorModal.className = "modal fade"; 1910 | generatorModal.id = "generator_modal"; 1911 | generatorModal.tabIndex = -1; 1912 | generatorModal.innerHTML = ` 1913 | 1986 | `; 1987 | generatorModal.setAttribute( 1988 | "data-bs-backdrop", 1989 | backdropConfig ? "static" : "true", 1990 | ); 1991 | const generatorPin = generatorModal.querySelector("#generator_pin"); 1992 | generatorPin.addEventListener("click", () => { 1993 | const ins = bootstrap_.Modal.getOrCreateInstance(generatorModal); 1994 | const backdrop = ins["_config"]["backdrop"] === "static"; 1995 | if (backdrop) { 1996 | ins["_config"]["backdrop"] = true; 1997 | setValue("backdropConfig", "false"); 1998 | generatorPin.innerHTML = svgUnpin; 1999 | } else { 2000 | ins["_config"]["backdrop"] = "static"; 2001 | setValue("backdropConfig", "true"); 2002 | generatorPin.innerHTML = svgPin; 2003 | } 2004 | }); 2005 | const generatorButtons = generatorModal 2006 | .querySelector("#generator_form_buttons") 2007 | .querySelectorAll("button"); 2008 | generatorButtons[0].addEventListener("click", () => { 2009 | generatedResults = null; 2010 | generatorBookmarks.forEach((w) => (w.used = true)); 2011 | generatorDisplayLimit = 12; 2012 | document.querySelector("#generator_form_buttons").classList.add("d-none"); 2013 | document.querySelector("#generator_display").classList.add("d-none"); 2014 | document.querySelector("#generator_spinner").classList.add("d-none"); 2015 | document.querySelector("#generator_more").classList.add("d-none"); 2016 | const resultsDiv = document.querySelector("#generator_results"); 2017 | while (resultsDiv.firstChild) { 2018 | resultsDiv.removeChild(resultsDiv.firstChild); 2019 | } 2020 | }); 2021 | generatorButtons[1].addEventListener("click", async () => { 2022 | window.runFlag = true; 2023 | const tag = document.querySelector("#generator_select_tag").value; 2024 | const restriction = document.querySelector( 2025 | "#generator_form_restriction", 2026 | ).value; 2027 | const availableBookmarks = generatorBookmarks.filter( 2028 | (w) => 2029 | restriction === "all" || 2030 | (restriction === "sfw" && !w["xRestrict"]) || 2031 | (restriction === "nsfw" && w["xRestrict"]), 2032 | ); 2033 | const batchSize = Math.max( 2034 | 0, 2035 | parseInt(document.querySelector("#generator_form_num").value) || 100, 2036 | ); 2037 | const batchNum = Math.ceil(availableBookmarks.length / batchSize); 2038 | const prompt = document.querySelector("#generator_save_tag_prompt"); 2039 | prompt.classList.remove("d-none"); 2040 | for (let index of [...Array(batchNum).keys()]) { 2041 | if (!window.runFlag) break; 2042 | const addTag = `S_${index}_${tag}`.slice(0, 30); 2043 | prompt.innerText = `正在保存至 ${addTag} / Saving to ${addTag}`; 2044 | const ids = availableBookmarks 2045 | .slice(index * batchSize, (index + 1) * batchSize) 2046 | .map((w) => w["bookmarkData"]["id"]); 2047 | // console.log(addTag, ids); 2048 | await updateBookmarkTags(ids, [addTag]); 2049 | } 2050 | window.runFlag = false; 2051 | prompt.classList.add("d-none"); 2052 | }); 2053 | generatorModal 2054 | .querySelector("#generator_footer_button") 2055 | .addEventListener("click", () => generatorButtons[2].click()); 2056 | generatorModal 2057 | .querySelector("#generator_footer_stop") 2058 | .addEventListener("click", () => { 2059 | window.runFlag = false; 2060 | document 2061 | .querySelector("#generator_save_tag_prompt") 2062 | .classList.add("d-none"); 2063 | }); 2064 | generatorModal 2065 | .querySelector("#generator_more") 2066 | .addEventListener("click", (evt) => { 2067 | const resultsDiv = document.querySelector("#generator_results"); 2068 | const s = resultsDiv.childElementCount; 2069 | const textColor = theme 2070 | ? "rgba(0, 0, 0, 0.88)" 2071 | : "rgba(255, 255, 255, 0.88)"; 2072 | generatorDisplayLimit += 108; 2073 | if (generatorDisplayLimit >= generatedResults.length) { 2074 | evt.target.classList.add("d-none"); 2075 | } 2076 | generatedResults 2077 | .filter((_, i) => i >= s && i < generatorDisplayLimit) 2078 | .forEach((w) => displayWork(w, resultsDiv, textColor)); 2079 | }); 2080 | 2081 | const tagSelectionDialog = getValue("tagSelectionDialog", "false") === "true"; 2082 | 2083 | /* eslint-disable indent */ 2084 | const featureModal = document.createElement("div"); 2085 | featureModal.className = "modal fade"; 2086 | featureModal.id = "feature_modal"; 2087 | featureModal.tabIndex = -1; 2088 | featureModal.innerHTML = ` 2089 | 2222 | `; 2223 | featureModal 2224 | .querySelector("#feature_footer_stop_button") 2225 | .addEventListener("click", () => (window.runFlag = false)); 2226 | /* eslint-disable indent */ 2227 | 2228 | const featurePrompt = featureModal.querySelector("#feature_prompt"); 2229 | const featureProgress = featureModal.querySelector("#feature_modal_progress"); 2230 | const featureProgressBar = featureModal.querySelector( 2231 | "#feature_modal_progress_bar", 2232 | ); 2233 | 2234 | // tag related 2235 | const featurePublicationType = featureModal.querySelector( 2236 | "#feature_form_publication", 2237 | ); 2238 | const featureTag = featureModal.querySelector("#feature_select_tag"); 2239 | const featureTagButtons = featureModal 2240 | .querySelector("#feature_tag_buttons") 2241 | .querySelectorAll("button"); 2242 | async function featureFetchWorks(tag, publicationType, progressBar) { 2243 | if (window.runFlag === false) return; 2244 | window.runFlag = true; 2245 | const tag_ = tag || featureTag.value; 2246 | const publicationType_ = publicationType || featurePublicationType.value; 2247 | if (tag_ === "" && cachedBookmarks[publicationType_]) 2248 | return cachedBookmarks[publicationType_]; 2249 | const progressBar_ = progressBar || featureProgressBar; 2250 | featurePrompt.innerText = 2251 | "正在获取收藏夹信息 / Fetching bookmark information"; 2252 | featurePrompt.classList.remove("d-none"); 2253 | if (!progressBar) featureProgress.classList.remove("d-none"); 2254 | const totalWorks = await fetchAllBookmarksByTag( 2255 | tag_, 2256 | publicationType_, 2257 | progressBar_, 2258 | ); 2259 | if (DEBUG) console.log(totalWorks); 2260 | if (tag_ === "" && totalWorks) 2261 | cachedBookmarks[publicationType_] = totalWorks; 2262 | return totalWorks || []; 2263 | } 2264 | // toggle publication type 2265 | featureTagButtons[0].addEventListener("click", () => 2266 | featureFetchWorks().then(async (works) => { 2267 | if (!works?.length) { 2268 | return alert( 2269 | `没有获取到收藏夹内容,操作中断,请检查选项下是否有作品\nFetching bookmark information failed. Abort operation. Please check the existence of works with the configuration`, 2270 | ); 2271 | } 2272 | const tag = featureTag.value; 2273 | const publicationType = featurePublicationType.value; 2274 | const restrict = publicationType === "show" ? "private" : "public"; 2275 | if ( 2276 | !window.confirm(`标签【${tag || "所有作品"}】下所有【${ 2277 | publicationType === "show" ? "公开" : "非公开" 2278 | }】作品(共${works.length}项)将会被移动至【${ 2279 | publicationType === "show" ? "非公开" : "公开" 2280 | }】类型,是否确认操作? 2281 | All works of tag ${tag || "All Works"} and type ${ 2282 | publicationType === "show" ? "PUBLIC" : "PRIVATE" 2283 | } (${ 2284 | works.length 2285 | } in total) will be set as ${restrict.toUpperCase()}. Is this Okay?`) 2286 | ) 2287 | return; 2288 | const instance = bootstrap_.Modal.getOrCreateInstance(progressModal); 2289 | instance.show(); 2290 | await updateBookmarkRestrict( 2291 | works.map((w) => w["bookmarkData"]["id"]), 2292 | restrict, 2293 | progressBar, 2294 | ); 2295 | setTimeout(() => { 2296 | instance.hide(); 2297 | if (window.runFlag && !hold) window.location.reload(); 2298 | }, 1000); 2299 | }), 2300 | ); 2301 | // delete tag 2302 | featureTagButtons[1].addEventListener("click", async () => { 2303 | const publicationType = featurePublicationType.value; 2304 | const tag = featureTag.value; 2305 | await deleteTag(tag, publicationType); 2306 | }); 2307 | // clear tag 2308 | featureTagButtons[2].addEventListener("click", () => 2309 | featureFetchWorks().then(clearBookmarkTags), 2310 | ); 2311 | // rename tag 2312 | featureTagButtons[3].addEventListener("click", async () => { 2313 | const tag = featureTag.value; 2314 | let newName = featureModal.querySelector( 2315 | "input#feature_new_tag_name", 2316 | ).value; 2317 | newName = newName.split(" ")[0].replace("(", "(").replace(")", ")"); 2318 | 2319 | if (!tag || tag === "未分類") 2320 | return window.alert(`无效的标签名\nInvalid tag name`); 2321 | if (!newName) 2322 | return window.alert(`新标签名不可以为空!\nEmpty New Tag Name!`); 2323 | const type = featurePublicationType.value === "show" ? "public" : "private"; 2324 | if (userTagDict[type].find((e) => e.tag === newName)) 2325 | if ( 2326 | !window.confirm( 2327 | `将会合并标签【${tag}】至【${newName}】,是否继续?\nWill merge tag ${tag} into ${newName}. Is this Okay?`, 2328 | ) 2329 | ) 2330 | return; 2331 | if ( 2332 | !window.confirm(`是否将标签【${tag}】重命名为【${newName}】?\n与之关联的作品标签将被更新,该操作将同时影响公开和非公开收藏 2333 | Tag ${tag} will be renamed to ${newName}.\n All related works (both public and private) will be updated. Is this okay?`) 2334 | ) 2335 | return; 2336 | const updateDict = featureModal.querySelector( 2337 | "#feature_tag_update_dict", 2338 | ).checked; 2339 | if (updateDict && synonymDict[tag]) { 2340 | const value = synonymDict[tag]; 2341 | delete synonymDict[tag]; 2342 | synonymDict[newName] = value; 2343 | const newDict = {}; 2344 | for (let key of sortByParody(Object.keys(synonymDict))) { 2345 | newDict[key] = synonymDict[key]; 2346 | } 2347 | synonymDict = newDict; 2348 | setValue("synonymDict", synonymDict); 2349 | } 2350 | const startTime = Date.now(); 2351 | featurePrompt.innerText = "更新中 / Updating"; 2352 | featurePrompt.classList.remove("d-none"); 2353 | featureProgress.classList.remove("d-none"); 2354 | const id = setInterval(() => { 2355 | fetch( 2356 | `https://www.pixiv.net/ajax/illusts/bookmarks/rename_tag_progress?lang=${lang}`, 2357 | ) 2358 | .then((resRaw) => resRaw.json()) 2359 | .then((res) => { 2360 | if (res.body["isInProgress"]) { 2361 | const estimate = res.body["estimatedSeconds"]; 2362 | const elapsed = (Date.now() - startTime) / 1000; 2363 | const ratio = 2364 | Math.min((elapsed / (elapsed + estimate)) * 100, 100).toFixed(2) + 2365 | "%"; 2366 | featureProgressBar.innerText = ratio; 2367 | featureProgressBar.style.width = ratio; 2368 | } else { 2369 | clearInterval(id); 2370 | featureProgressBar.innerText = "100%"; 2371 | featureProgressBar.style.width = "100%"; 2372 | featurePrompt.innerText = "更新成功 / Update Successfully"; 2373 | setTimeout(() => { 2374 | if (!hold) window.location.reload(); 2375 | }, 1000); 2376 | } 2377 | }); 2378 | }, 1000); 2379 | await fetch("https://www.pixiv.net/ajax/illusts/bookmarks/rename_tag", { 2380 | headers: { 2381 | accept: "application/json", 2382 | "content-type": "application/json; charset=utf-8", 2383 | "x-csrf-token": token, 2384 | }, 2385 | body: JSON.stringify({ newTagName: newName, oldTagName: tag }), 2386 | method: "POST", 2387 | }); 2388 | }); 2389 | // batch removing invalid bookmarks 2390 | const batchRemoveButton = featureModal 2391 | .querySelector("#feature_batch_remove_invalid_buttons") 2392 | .querySelector("button"); 2393 | batchRemoveButton.addEventListener("click", async () => { 2394 | const display = featureModal.querySelector( 2395 | "#feature_batch_remove_invalid_display", 2396 | ); 2397 | featureProgress.classList.remove("d-none"); 2398 | const invalidShow = ( 2399 | await featureFetchWorks("", "show", featureProgressBar) 2400 | ).filter((w) => w.title === "-----"); 2401 | if (DEBUG) console.log("invalidShow", invalidShow); 2402 | const invalidHide = ( 2403 | await featureFetchWorks("", "hide", featureProgressBar) 2404 | ).filter((w) => w.title === "-----"); 2405 | if (DEBUG) console.log("invalidHide", invalidHide); 2406 | if (window.runFlag) { 2407 | featurePrompt.classList.add("d-none"); 2408 | featureProgress.classList.add("d-none"); 2409 | if (invalidShow.length || invalidHide.length) { 2410 | display.innerHTML = 2411 | `
` + 2412 | [...invalidShow, ...invalidHide] 2413 | .map((w) => { 2414 | const { id, associatedTags, restrict, xRestrict } = w; 2415 | const l = lang.includes("zh") ? 0 : 1; 2416 | const info = [ 2417 | ["作品ID:", "ID: ", id], 2418 | [ 2419 | "用户标签:", 2420 | "User Tags: ", 2421 | (associatedTags || []).join(", "), 2422 | ], 2423 | [ 2424 | "公开类型:", 2425 | "Publication: ", 2426 | restrict ? ["非公开", "hide"][l] : ["公开", "show"][l], 2427 | ], 2428 | ["限制分类:", "Restrict: ", xRestrict ? "R-18" : "SFW"], 2429 | ]; 2430 | return `
${info 2431 | .map((i) => `${i[l] + i[2]}`) 2432 | .join("
")}
`; 2433 | }) 2434 | .join("") + 2435 | `
`; 2436 | const buttonContainer = document.createElement("div"); 2437 | buttonContainer.className = "d-flex mt-3"; 2438 | const labelButton = document.createElement("button"); 2439 | labelButton.className = "btn btn-outline-primary"; 2440 | labelButton.innerText = "标记失效 / Label As Invalid"; 2441 | labelButton.addEventListener("click", async (evt) => { 2442 | evt.preventDefault(); 2443 | if ( 2444 | !window.confirm( 2445 | `是否确认批量为失效作品添加"INVALID"标签\nInvalid works (deleted/private) will be labelled as INVALID. Is this okay?`, 2446 | ) 2447 | ) 2448 | return; 2449 | window.runFlag = true; 2450 | const bookmarkIds = [...invalidShow, ...invalidHide] 2451 | .filter((w) => !w.associatedTags.includes("INVALID")) 2452 | .map((w) => w["bookmarkData"]["id"]); 2453 | featureProgress.classList.remove("d-none"); 2454 | featurePrompt.classList.remove("d-none"); 2455 | featurePrompt.innerText = 2456 | "添加标签中,请稍后 / Labeling invalid bookmarks"; 2457 | await updateBookmarkTags( 2458 | bookmarkIds, 2459 | ["INVALID"], 2460 | null, 2461 | featureProgressBar, 2462 | ); 2463 | featurePrompt.innerText = 2464 | "标记完成,即将刷新页面 / Updated. The page is going to reload."; 2465 | setTimeout(() => { 2466 | if (window.runFlag && !hold) window.location.reload(); 2467 | }, 1000); 2468 | }); 2469 | const removeButton = document.createElement("button"); 2470 | removeButton.className = "btn btn-outline-danger ms-auto"; 2471 | removeButton.innerText = "确认删除 / Confirm Removing"; 2472 | removeButton.addEventListener("click", async (evt) => { 2473 | evt.preventDefault(); 2474 | if ( 2475 | !window.confirm( 2476 | `是否确认批量删除失效作品\nInvalid works (deleted/private) will be removed. Is this okay?`, 2477 | ) 2478 | ) 2479 | return; 2480 | window.runFlag = true; 2481 | const bookmarkIds = [...invalidShow, ...invalidHide].map( 2482 | (w) => w["bookmarkData"]["id"], 2483 | ); 2484 | featureProgress.classList.remove("d-none"); 2485 | featurePrompt.classList.remove("d-none"); 2486 | featurePrompt.innerText = 2487 | "删除中,请稍后 / Removing invalid bookmarks"; 2488 | await removeBookmark(bookmarkIds, featureProgressBar); 2489 | featurePrompt.innerText = 2490 | "已删除,即将刷新页面 / Removed. The page is going to reload."; 2491 | setTimeout(() => { 2492 | if (window.runFlag && !hold) window.location.reload(); 2493 | }, 1000); 2494 | }); 2495 | buttonContainer.appendChild(labelButton); 2496 | buttonContainer.appendChild(removeButton); 2497 | display.appendChild(buttonContainer); 2498 | } else { 2499 | display.innerText = "未检测到失效作品 / No invalid works detected"; 2500 | } 2501 | display.className = "mt-3"; 2502 | } else { 2503 | featurePrompt.innerText = "操作中断 / Operation Aborted"; 2504 | } 2505 | delete window.runFlag; 2506 | }); 2507 | // bookmarks related 2508 | const featureBookmarkButtons = featureModal 2509 | .querySelector("#feature_bookmark_buttons") 2510 | .querySelectorAll("button"); 2511 | // backup 2512 | featureBookmarkButtons[0].addEventListener("click", async () => { 2513 | featureProgress.classList.remove("d-none"); 2514 | const show = await featureFetchWorks("", "show", featureProgressBar); 2515 | const hide = await featureFetchWorks("", "hide", featureProgressBar); 2516 | if (window.runFlag) { 2517 | const bookmarks = { show, hide }; 2518 | const a = document.createElement("a"); 2519 | a.href = URL.createObjectURL( 2520 | new Blob([JSON.stringify(bookmarks)], { type: "application/json" }), 2521 | ); 2522 | a.setAttribute( 2523 | "download", 2524 | `label_pixiv_bookmarks_backup_${new Date().toLocaleDateString()}.json`, 2525 | ); 2526 | a.click(); 2527 | featurePrompt.innerText = "备份成功 / Backup successfully"; 2528 | featureProgress.classList.add("d-none"); 2529 | } else { 2530 | featurePrompt.innerText = "操作中断 / Operation Aborted"; 2531 | } 2532 | delete window.runFlag; 2533 | }); 2534 | // lookup invalid 2535 | const featureBookmarkDisplay = featureModal.querySelector( 2536 | "#feature_bookmark_display", 2537 | ); 2538 | featureBookmarkButtons[1].addEventListener("click", async () => { 2539 | const input = document.createElement("input"); 2540 | input.type = "file"; 2541 | input.accept = "application/json"; 2542 | input.addEventListener("change", async (evt) => { 2543 | const reader = new FileReader(); 2544 | reader.onload = async (evt) => { 2545 | let json = {}; 2546 | const invalidArray = []; 2547 | async function run(type) { 2548 | const col = await featureFetchWorks("", type, featureProgressBar); 2549 | if (!window.runFlag) return; 2550 | for (let work of col.filter((w) => w.title === "-----")) { 2551 | const jsonWork = json[type].find( 2552 | (w) => w.id.toString() === work.id.toString(), 2553 | ); 2554 | invalidArray.push(jsonWork || work); 2555 | if (DEBUG) console.log(jsonWork); 2556 | } 2557 | } 2558 | try { 2559 | eval("json = " + evt.target.result.toString()); 2560 | if (!json["show"]) 2561 | return alert( 2562 | "请检查是否加载了正确的收藏夹备份\nPlease check if the backup file is correct", 2563 | ); 2564 | if (DEBUG) console.log(json); 2565 | featureProgress.classList.remove("d-none"); 2566 | await run("show"); 2567 | await run("hide"); 2568 | if (invalidArray.length) { 2569 | featureBookmarkDisplay.innerHTML = 2570 | `
` + 2571 | invalidArray 2572 | .map((w) => { 2573 | const { 2574 | id, 2575 | title, 2576 | tags, 2577 | userId, 2578 | userName, 2579 | alt, 2580 | associatedTags, 2581 | restrict, 2582 | xRestrict, 2583 | } = w; 2584 | const l = lang.includes("zh") ? 0 : 1; 2585 | const info = [ 2586 | ["", "", alt], 2587 | ["作品ID:", "ID: ", id], 2588 | ["作品名称:", "Title: ", title], 2589 | ["用户名称:", "User: ", userName + " - " + userId], 2590 | ["作品标签:", "Tags: ", (tags || []).join(", ")], 2591 | [ 2592 | "用户标签:", 2593 | "User Tags: ", 2594 | (associatedTags || []).join(", "), 2595 | ], 2596 | [ 2597 | "公开类型:", 2598 | "Publication: ", 2599 | restrict ? ["非公开", "hide"][l] : ["公开", "show"][l], 2600 | ], 2601 | ["限制分类:", "Restrict: ", xRestrict ? "R-18" : "SFW"], 2602 | ]; 2603 | return `
${info 2604 | .map((i) => `${i[l] + i[2]}`) 2605 | .join("
")}
`; 2606 | }) 2607 | .join("") + 2608 | `
`; 2609 | } else { 2610 | featureBookmarkDisplay.innerText = 2611 | "未检测到失效作品 / No invalid works detected"; 2612 | } 2613 | featureBookmarkDisplay.className = "mt-3"; 2614 | } catch (err) { 2615 | alert("无法加载收藏夹 / Fail to load bookmarks\n" + err); 2616 | console.log(err); 2617 | } finally { 2618 | featurePrompt.classList.add("d-none"); 2619 | featureProgress.classList.add("d-none"); 2620 | delete window.runFlag; 2621 | } 2622 | }; 2623 | reader.readAsText(evt.target.files[0]); 2624 | }); 2625 | input.click(); 2626 | }); 2627 | // import bookmarks 2628 | const importBookmarkButtons = featureModal 2629 | .querySelector("#feature_import_bookmark") 2630 | .querySelectorAll("button"); 2631 | importBookmarkButtons[0].addEventListener("click", async () => { 2632 | const input = document.createElement("input"); 2633 | input.type = "file"; 2634 | input.accept = "application/json"; 2635 | input.addEventListener("change", (evt) => { 2636 | const reader = new FileReader(); 2637 | reader.onload = async (evt) => { 2638 | let json = {}; 2639 | try { 2640 | eval("json = " + evt.target.result.toString()); 2641 | if (!json["show"]) 2642 | return alert( 2643 | "请检查是否加载了正确的收藏夹备份\nPlease check if the backup file is correct", 2644 | ); 2645 | window.bookmarkImport = json; 2646 | const selectTag = featureModal.querySelector( 2647 | "#feature_import_bookmark_tag", 2648 | ); 2649 | while (selectTag.firstChild) { 2650 | selectTag.removeChild(selectTag.firstChild); 2651 | } 2652 | const tagShow = json["show"] 2653 | .map((w) => w.associatedTags || []) 2654 | .reduce((a, b) => [...new Set(a.concat(b))], []); 2655 | const tagHide = json["hide"] 2656 | .map((w) => w.associatedTags || []) 2657 | .reduce((a, b) => [...new Set(a.concat(b))], []); 2658 | console.log("tagShow", tagShow); 2659 | console.log("tagHide", tagHide); 2660 | const tagAll = sortByParody([...new Set(tagShow.concat(tagHide))]); 2661 | console.log("tagAll", tagAll); 2662 | const optionAll = document.createElement("option"); 2663 | optionAll.value = ""; 2664 | optionAll.innerText = `所有收藏 / All Works (${json["show"].length}, ${json["hide"].length})`; 2665 | const optionUncat = document.createElement("option"); 2666 | optionUncat.value = "未分類"; 2667 | const uncatS = json["show"].filter( 2668 | (w) => !(w.associatedTags || []).length, 2669 | ).length; 2670 | const uncatH = json["hide"].filter( 2671 | (w) => !(w.associatedTags || []).length, 2672 | ).length; 2673 | optionUncat.innerText = `未分类作品 / Uncategorized Works (${uncatS}, ${uncatH})`; 2674 | selectTag.appendChild(optionAll); 2675 | selectTag.appendChild(optionUncat); 2676 | tagAll.forEach((t) => { 2677 | const option = document.createElement("option"); 2678 | option.value = t; 2679 | const s = json["show"].filter((w) => 2680 | (w.associatedTags || []).includes(t), 2681 | ).length; 2682 | const h = json["hide"].filter((w) => 2683 | (w.associatedTags || []).includes(t), 2684 | ).length; 2685 | option.innerText = `${t} (${s}, ${h})`; 2686 | selectTag.appendChild(option); 2687 | }); 2688 | featureModal 2689 | .querySelector("#feature_import_bookmark_hide") 2690 | .classList.remove("d-none"); 2691 | } catch (err) { 2692 | alert("无法加载收藏夹 / Fail to load bookmarks\n" + err); 2693 | console.log(err); 2694 | } 2695 | }; 2696 | reader.readAsText(evt.target.files[0]); 2697 | }); 2698 | input.click(); 2699 | }); 2700 | importBookmarkButtons[1].addEventListener("click", async () => { 2701 | if (!window.bookmarkImport?.["show"]) 2702 | return alert("加载收藏夹备份失败!\nFail to load backup bookmarks"); 2703 | const json = window.bookmarkImport; 2704 | const pub = featureModal.querySelector( 2705 | "#feature_import_bookmark_publication", 2706 | ).value; 2707 | const tag = featureModal.querySelector( 2708 | "#feature_import_bookmark_tag", 2709 | ).value; 2710 | const mode = featureModal.querySelector( 2711 | "#feature_import_bookmark_mode", 2712 | ).value; 2713 | 2714 | const importWorks = json[pub].filter((w) => { 2715 | if (tag === "") return true; 2716 | else if (tag === "未分類") return !w.associatedTags?.length; 2717 | else return w.associatedTags?.includes(tag); 2718 | }); 2719 | importWorks.reverse(); 2720 | 2721 | featureProgress.classList.remove("d-none"); 2722 | const existWorks = await featureFetchWorks(tag, pub, featureProgressBar); 2723 | featurePrompt.classList.remove("d-none"); 2724 | 2725 | const errorList = []; 2726 | window.runFlag = true; 2727 | for (let i = 0; i < importWorks.length; i++) { 2728 | const w = importWorks[i]; 2729 | if (!window.runFlag) break; 2730 | let { id, title, restrict, associatedTags, alt } = w; 2731 | if (title === "-----") { 2732 | errorList.push({ 2733 | message: "The creator has limited who can view this content", 2734 | ...w, 2735 | }); 2736 | continue; 2737 | } 2738 | if (!associatedTags) associatedTags = []; 2739 | const ew = existWorks.find((ew) => ew.id === id); 2740 | if (ew) { 2741 | // note that when work does not have target tag but is in exist bookmarked works, skip will not take effect 2742 | if (mode === "skip") continue; 2743 | const diff = (ew.associatedTags || []).filter( 2744 | (t) => !associatedTags.includes(t), 2745 | ); 2746 | associatedTags = associatedTags.filter( 2747 | (t) => !(ew.associatedTags || []).includes(t), 2748 | ); 2749 | if (!associatedTags) continue; 2750 | if (mode === "merge") 2751 | await updateBookmarkTags([ew["bookmarkData"]["id"]], associatedTags); 2752 | else if (mode === "override") 2753 | await updateBookmarkTags( 2754 | [ew["bookmarkData"]["id"]], 2755 | associatedTags, 2756 | diff, 2757 | ); 2758 | } else { 2759 | const resRaw = await addBookmark(id, restrict, associatedTags); 2760 | if (!resRaw.ok) { 2761 | const res = await resRaw.json(); 2762 | errorList.push({ ...res, ...w }); 2763 | } 2764 | } 2765 | featurePrompt.innerText = alt; 2766 | featureProgressBar.innerText = i + "/" + importWorks.length; 2767 | const ratio = ((i / importWorks.length) * 100).toFixed(2); 2768 | featureProgressBar.style.width = ratio + "%"; 2769 | } 2770 | if (!window.runFlag) { 2771 | featurePrompt.innerText = "操作中断 / Operation Aborted"; 2772 | } else { 2773 | featurePrompt.innerText = "导入成功 / Import successfully"; 2774 | featureProgress.classList.add("d-none"); 2775 | } 2776 | if (errorList.length) { 2777 | console.log(errorList); 2778 | featurePrompt.innerText = "部分导入成功 / Import Partially Successful"; 2779 | featureBookmarkDisplay.classList.remove("d-none"); 2780 | featureBookmarkDisplay.innerText = errorList 2781 | .map((w) => { 2782 | const { 2783 | id, 2784 | title, 2785 | tags, 2786 | userId, 2787 | userName, 2788 | alt, 2789 | associatedTags, 2790 | xRestrict, 2791 | message, 2792 | } = w; 2793 | return `${alt}\ntitle: ${title}\nid: ${id}\nuser: ${userName} - ${userId}\ntags: ${( 2794 | tags || [] 2795 | ).join(", ")}\nuserTags: ${(associatedTags || []).join( 2796 | ", ", 2797 | )}\nrestrict: ${xRestrict ? "R-18" : "SFW"}\nmessage: ${message}`; 2798 | }) 2799 | .join("\n\n"); 2800 | } 2801 | }); 2802 | // switch dialog 2803 | const switchDialogButtons = featureModal 2804 | .querySelector("#feature_switch_tag_dialog") 2805 | .querySelectorAll("button"); 2806 | // dialog style 2807 | switchDialogButtons[0].addEventListener("click", () => { 2808 | const tagSelectionDialog = getValue("tagSelectionDialog", "false"); 2809 | if (tagSelectionDialog === "false") setValue("tagSelectionDialog", "true"); 2810 | else setValue("tagSelectionDialog", "false"); 2811 | window.location.reload(); 2812 | }); 2813 | // turbo mode 2814 | switchDialogButtons[1].addEventListener("click", () => { 2815 | if (turboMode) setValue("turboMode", "false"); 2816 | else setValue("turboMode", "true"); 2817 | window.location.reload(); 2818 | }); 2819 | 2820 | // all tags selection modal 2821 | const c_ = ALL_TAGS_CONTAINER.slice(1); 2822 | const allTagsModal = document.createElement("div"); 2823 | allTagsModal.className = "modal fade"; 2824 | allTagsModal.id = "all_tags_modal"; 2825 | allTagsModal.tabIndex = -1; 2826 | allTagsModal.innerHTML = ` 2827 | `; 2834 | const parodyContainer = allTagsModal.querySelector(ALL_TAGS_CONTAINER); 2835 | const characterContainer = [ 2836 | ...allTagsModal.querySelectorAll(ALL_TAGS_CONTAINER), 2837 | ][1]; 2838 | userTags.forEach((tag) => { 2839 | const d = document.createElement("div"); 2840 | d.className = "sc-1jxp5wn-2 cdeTmC"; 2841 | d.innerHTML = ` 2842 |
2843 |
2844 |
2845 |
#${tag}
2846 |
2847 |
2848 |
`; 2849 | d.addEventListener("click", async () => { 2850 | try { 2851 | const c0 = document.querySelector(ALL_TAGS_MODAL_CONTAINER); 2852 | const c1 = c0.lastElementChild; 2853 | let lastScrollTop = -1; 2854 | let targetDiv; 2855 | let i = 0; 2856 | while ( 2857 | c1.scrollTop !== lastScrollTop && 2858 | !targetDiv && 2859 | i < userTags.length 2860 | ) { 2861 | targetDiv = [...c1.firstElementChild.children].find((el) => 2862 | el.textContent.includes(tag), 2863 | ); 2864 | if (!targetDiv) { 2865 | c1.scrollTop = parseInt( 2866 | c1.firstElementChild.lastElementChild.style.top, 2867 | ); 2868 | if ("onscrollend" in window) 2869 | await new Promise((r) => 2870 | c1.addEventListener("scrollend", () => r(), { once: true }), 2871 | ); 2872 | else { 2873 | let j = 0, 2874 | lastText = c1.firstElementChild.lastElementChild.textContent; 2875 | while ( 2876 | j < 10 && 2877 | lastText === c1.firstElementChild.lastElementChild.textContent 2878 | ) { 2879 | console.log("wait"); 2880 | await new Promise((r) => setTimeout(r, 100)); 2881 | j++; 2882 | } 2883 | } 2884 | } 2885 | i++; 2886 | } 2887 | if (targetDiv) { 2888 | targetDiv.firstElementChild.click(); 2889 | allTagsModal.querySelector("button.btn-close").click(); 2890 | } 2891 | } catch (err) { 2892 | window.alert(`${err.name}: ${err.message}\n${err.stack}`); 2893 | } 2894 | }); 2895 | if (tag.includes("(")) characterContainer.appendChild(d); 2896 | else parodyContainer.appendChild(d); 2897 | }); 2898 | 2899 | const progressModal = document.createElement("div"); 2900 | progressModal.className = "modal fade"; 2901 | progressModal.id = "progress_modal"; 2902 | progressModal.setAttribute("data-bs-backdrop", "static"); 2903 | progressModal.tabIndex = -1; 2904 | progressModal.innerHTML = ` 2905 | 2920 | `; 2921 | const progressBar = progressModal.querySelector( 2922 | "#progress_modal_progress_bar", 2923 | ); 2924 | 2925 | const body = document.querySelector("body"); 2926 | body.appendChild(labelModal); 2927 | body.appendChild(searchModal); 2928 | body.appendChild(generatorModal); 2929 | body.appendChild(featureModal); 2930 | body.appendChild(progressModal); 2931 | body.appendChild(allTagsModal); 2932 | } 2933 | 2934 | async function fetchUserTags() { 2935 | const tagsRaw = await fetch( 2936 | `/ajax/user/${uid}/illusts/bookmark/tags?lang=${lang}`, 2937 | ); 2938 | const tagsObj = await tagsRaw.json(); 2939 | if (tagsObj.error === true) 2940 | return alert( 2941 | `获取tags失败 2942 | Fail to fetch user tags` + 2943 | "\n" + 2944 | decodeURI(tagsObj.message), 2945 | ); 2946 | userTagDict = tagsObj.body; 2947 | const userTagsSet = new Set(); 2948 | const addTag2Set = (tag) => { 2949 | try { 2950 | userTagsSet.add(decodeURI(tag)); 2951 | } catch (err) { 2952 | userTagsSet.add(tag); 2953 | if (err.message !== "URI malformed") { 2954 | console.log("[Label Pixiv] Error!"); 2955 | console.log(err.name, err.message); 2956 | console.log(err.stack); 2957 | window.alert(`加载标签%{tag}时出现错误,请按F12打开控制台截图错误信息并反馈至GitHub。 2958 | Error loading tag ${tag}. Please press F12 and take a screenshot of the error message in the console and report it to GitHub.`); 2959 | } 2960 | } 2961 | }; 2962 | for (let obj of userTagDict.public) { 2963 | addTag2Set(obj.tag); 2964 | } 2965 | for (let obj of userTagDict["private"]) { 2966 | addTag2Set(obj.tag); 2967 | } 2968 | userTagsSet.delete("未分類"); 2969 | return sortByParody(Array.from(userTagsSet)); 2970 | } 2971 | 2972 | async function fetchTokenPolyfill() { 2973 | // get token 2974 | const userRaw = await fetch( 2975 | "/bookmark_add.php?type=illust&illust_id=83540927", 2976 | ); 2977 | if (!userRaw.ok) { 2978 | console.log(`获取身份信息失败 2979 | Fail to fetch user information`); 2980 | throw new Error(); 2981 | } 2982 | const userRes = await userRaw.text(); 2983 | const tokenPos = userRes.indexOf("pixiv.context.token"); 2984 | const tokenEnd = userRes.indexOf(";", tokenPos); 2985 | return userRes.slice(tokenPos, tokenEnd).split('"')[1]; 2986 | } 2987 | 2988 | async function updateWorkInfo(bookmarkTags) { 2989 | const el = await waitForDom(WORK_SECTION); 2990 | let workInfo = {}; 2991 | for (let i = 0; i < 100; i++) { 2992 | workInfo = Object.values(el)[0]["memoizedProps"]["children"][2]["props"]; 2993 | if (Object.keys(workInfo).length) break; 2994 | else await delay(200); 2995 | } 2996 | if (bookmarkTags) { 2997 | [...el.querySelectorAll("li")].forEach((li, i) => { 2998 | workInfo["works"][i].associatedTags = 2999 | Object.values(li)[0].child.child["memoizedProps"].associatedTags; 3000 | }); 3001 | } 3002 | const page = window.location.search.match(/p=(\d+)/)?.[1] || 1; 3003 | workInfo.page = parseInt(page); 3004 | return workInfo; 3005 | } 3006 | 3007 | async function initializeVariables() { 3008 | async function polyfill() { 3009 | try { 3010 | const dataLayer = unsafeWindow_["dataLayer"][0]; 3011 | uid = dataLayer["user_id"]; 3012 | lang = dataLayer["lang"]; 3013 | token = await fetchTokenPolyfill(); 3014 | pageInfo.userId = window.location.href.match(/users\/(\d+)/)?.[1]; 3015 | pageInfo.client = { userId: uid, lang, token }; 3016 | } catch (err) { 3017 | console.log(err); 3018 | console.log("[Label Bookmarks] Initializing Failed"); 3019 | } 3020 | } 3021 | 3022 | try { 3023 | if (DEBUG) console.log("[Label Bookmarks] Initializing Variables"); 3024 | pageInfo = Object.values(document.querySelector(BANNER))[0]["return"][ 3025 | "return" 3026 | ]["memoizedProps"]; 3027 | if (DEBUG) console.log("[Label Bookmarks] Page Info", pageInfo); 3028 | uid = pageInfo["client"]["userId"]; 3029 | token = pageInfo["client"]["token"]; 3030 | lang = pageInfo["client"]["lang"]; 3031 | if (!uid || !token || !lang) await polyfill(); 3032 | } catch (err) { 3033 | console.log(err); 3034 | await polyfill(); 3035 | } 3036 | 3037 | userTags = await fetchUserTags(); 3038 | 3039 | // workType = Object.values(document.querySelector(".sc-1x9383j-0"))[0].child["memoizedProps"]["workType"]; 3040 | 3041 | // switch between default and dark theme 3042 | const themeDiv = document.querySelector(THEME_CONTAINER); 3043 | theme = themeDiv.getAttribute("data-theme") === "light"; 3044 | new MutationObserver(() => { 3045 | theme = themeDiv.getAttribute("data-theme") === "light"; 3046 | const prevBgColor = theme ? "bg-dark" : "bg-white"; 3047 | const bgColor = theme ? "bg-white" : "bg-dark"; 3048 | const prevTextColor = theme ? "text-lp-light" : "text-lp-dark"; 3049 | const textColor = theme ? "text-lp-dark" : "text-lp-light"; 3050 | [...document.querySelectorAll(".bg-dark, .bg-white")].forEach((el) => { 3051 | el.classList.replace(prevBgColor, bgColor); 3052 | }); 3053 | [...document.querySelectorAll(".text-lp-dark, .text-lp-light")].forEach( 3054 | (el) => { 3055 | el.classList.replace(prevTextColor, textColor); 3056 | }, 3057 | ); 3058 | const prevClearTag = theme ? "dydUg" : "jbzOgz"; 3059 | const clearTag = theme ? "jbzOgz" : "dydUg"; 3060 | const clearTagsButton = document.querySelector("#clear_tags_button"); 3061 | if (clearTagsButton) 3062 | clearTagsButton.children[0].classList.replace(prevClearTag, clearTag); 3063 | }).observe(themeDiv, { attributes: true }); 3064 | 3065 | synonymDict = getValue("synonymDict", {}); 3066 | if (Object.keys(synonymDict).length) { 3067 | // remove empty values on load, which could be caused by unexpected interruption 3068 | for (let key of Object.keys(synonymDict)) { 3069 | if (!synonymDict[key]) delete synonymDict[key]; 3070 | } 3071 | setValue("synonymDict", synonymDict); 3072 | } 3073 | if (DEBUG) console.log("[Label Bookmarks] Initialized"); 3074 | } 3075 | 3076 | const maxRetries = 100; 3077 | async function waitForDom(selector, container) { 3078 | let dom; 3079 | for (let i = 0; i < maxRetries; i++) { 3080 | dom = (container || document).querySelector(selector); 3081 | if (dom) return dom; 3082 | await delay(200); 3083 | } 3084 | throw new ReferenceError( 3085 | `[Label Bookmarks] Dom element ${selector} not loaded in given time`, 3086 | ); 3087 | } 3088 | 3089 | async function injectElements() { 3090 | if (DEBUG) console.log("[Label Bookmarks] Start Injecting"); 3091 | const textColor = theme ? "text-lp-dark" : "text-lp-light"; 3092 | const pageBody = document.querySelector(PAGE_BODY); 3093 | const root = document.querySelector("nav"); 3094 | if (!root) console.log("[Label Bookmarks] Navbar Not Found"); 3095 | root.classList.add("d-flex"); 3096 | const buttonContainer = document.createElement("span"); 3097 | buttonContainer.className = "flex-grow-1 justify-content-end d-flex"; 3098 | buttonContainer.id = "label_bookmarks_buttons"; 3099 | const gClass = generator ? "" : "d-none"; 3100 | const fClass = feature ? "" : "d-none"; 3101 | buttonContainer.innerHTML = ` 3102 |