├── .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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
92 |
93 | 
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 | 
100 | 
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 | 
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 | 
261 |
262 | - 在使用脚本前,我们有数种方法可以添加*用户已收藏的标签*
263 |
264 | 1. 在收藏夹页,悬停在图片缩略图上并点击左下角的***编辑标签***按钮,在对话框中可以找到***添加标签***按钮。在保存设置之后,所有创建的标签将被加入用户已收藏标签。
265 |
266 | 
267 |
268 | 2. 在作品收藏详情页,选择一些作品已有的标签,或手动输入需要的标签,保存结果
269 |
270 | 
271 |
272 | 3. 使用脚本自动添加标签。需要在高级设置中选择***自动添加首个标签***并设置为***是***。随后可以随意移除不需要的标签,但之后使用时请记得将此设置重置为***否***来避免增加过多不需要的标签。
273 |
274 | 
275 |
276 | 4. 使用脚本的同义词词典功能。词典中所有的目标标签(用户标签)将会被视为是用户已收藏的标签。关于词典的使用方法请见下文
277 |
278 | ## 开始使用
279 |
280 | - 在管理收藏页面,点击【添加标签】打开脚本页面
281 | - 如果在此前已经设置好用户收藏标签,直接点击开始即可使用
282 | - 否则需要按前文所述选择一种方式来添加一些用户收藏标签
283 | - 假设我们已经添加了【新世紀エヴァンゲリオン】标签
284 |
285 | 
286 |
287 | - 等待运行结束,刷新页面,可以看到所有未分类作品中带有【新世紀エヴァンゲリオン】标签的作品都被自动分类到该标签下
288 |
289 | 
290 |
291 | ## 同义词词典
292 |
293 | - 有些时候作者并没有为作品或人物提供所谓的***官方名称***,这就导致自动识别标签变得困难。如果我们使用一个同义词词典储存一个标签的全部同义词——或者叫别名,那么分类的结果将会更加整洁
294 | - 例如此作品下有【eva】标签,但没有【新世紀エヴァンゲリオン】标签,因此不会被自动分类到【新世紀エヴァンゲリオン】标签下
295 | - 在加载词典区域下,首次使用的用户可以尝试下载样例词典用于导入
296 |
297 | 
298 |
299 | - 在自动标签页面,点击***编辑词典***展开选项
300 | - 目标标签,指的是您希望保存在您收藏夹中的用户标签的名字,例如:新世紀エヴァンゲリオン。同义词则是那些您希望脚本将其识别为目标标签的作品本身提供的标签,例如:EVA
301 | - 所有的同义词之间使用空格或回车分隔
302 | - 点击***更新标签***将输入的内容加载到词典中,然后将会在下方的预览区域展示出来。如果您在同义词一栏空白的情况下更新,将会把目标标签从词典中删除
303 | - 在制作完词典后,可以导出词典到本地进行备份
304 | - 下次使用时,会自动记忆上次使用的词典,也可以从本地导入新的词典
305 | - ***加载标签***按钮用于从词典中载入标签对应的同义词,在***目标标签***一栏中输入标签名,点击***加载标签***即可,直接按Tab键也有同样的效果
306 |
307 | 
308 |
309 | - 再次点击开始。执行完脚本后,含有【EVA】标签的作品已经被分类到了【新世紀エヴァンゲリオン】下
310 |
311 | 
312 |
313 | - 利用此功能可以实现很多事情。例如Pixiv大部分角色都是用片假名作为官方名称,这对非日语母语的人来说识别起来非常痛苦。拿明日香做例子,明日香至少有4种常用称呼:惣流・アスカ・ラングレー/式波・アスカ・ラングレー/そうりゅう・アスカ・ラングレー/しきなみ・アスカ・ラングレー。我们现在就可以使用简单的***asuka***作为目标标签,将上述都做为同义词标签储存。
314 | - 注意自定义的目标标签中不能有空格,因为Pixiv使用空格作为标签间的分隔符
315 |
316 | 
317 |
318 | ## 示例
319 |
320 | - 下图为已经整理好的同义词词典,以及对应的用户收藏标签示例,可以作为参考
321 |
322 | 
323 | 
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 | 
390 |
391 | ## 手动启用部分功能
392 |
393 | - 为了不让UI过于臃肿,部分非核心功能设定为在菜单中手动开启,点击对应按钮即可开启
394 |
395 | 
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 |
1115 |
1116 |
1117 |
R-18
1118 |
1119 |
1120 |
1121 |
1122 |
1123 |
1124 |
${work["pageCount"]}
1125 |
1126 |
1127 |
1128 |
1129 |
1130 | ${tagsString}
1131 |
1132 |
1139 |
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 |
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 |
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 = ` `;
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 |
1561 |
1566 |
1764 |
1775 |
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 |
1810 |
1811 |
1816 |
1817 |
1818 |
1819 |
1820 | 输入要搜索的关键字,使用空格分隔,在关键字前加感叹号 来排除该关键字。将会结合用户设置的同义词词典,
1821 | 在收藏的图片中寻找标签匹配的图片展示在下方。当收藏时间跨度较大时,使用自定义标签缩小范围以加速搜索。
1822 |
1823 | 点击输入框右侧的切换按钮切换至高级搜索模式,此时可以限制该关键词的搜索范围,单个输入框只接受一个关键词。
1824 |
1825 | Enter keywords seperated by spaces to launch a search. Add a Exclamation Mark
1826 | before any keyword to exclude it. The search process will use your synonym dictionary to look up the tags
1827 | of your bookmarked images. Use custom tag to narrow the search if images come from a wide time range.
1828 |
1829 | Clicking the button on the right will toggle to advanced search mode, where you are able to choose
1830 | the search fields of each keyword. Note that in this mode only single keyword is accepted for each input box.
1831 |
1832 |
1833 |
1834 |
1835 |
您是否想要搜索 / Are you looking for:
1836 |
1837 |
1838 |
1839 |
► 高级设置 / Advanced
1842 |
1843 |
1844 | 标签匹配模式 / Match Pattern
1845 |
1846 | 模糊匹配 / Fuzzy Match
1847 | 精确匹配 / Exact Match
1848 |
1849 |
1850 |
1851 | 搜索标签数量匹配 / Search Tags Length Match
1852 |
1853 | 不需匹配 / Not Needed
1854 | 需要匹配 / Must Match
1855 |
1856 |
1857 |
1858 | 自定义标签用于缩小搜索范围 / Custom Tag to Narrow the Search
1859 |
1860 | 所有收藏 / All Works
1861 | 未分类作品 / Uncategorized Works
1862 |
1863 |
1864 |
1865 | 作品公开类型 / Publication Type
1866 |
1867 | 公开收藏 / Public
1868 | 私密收藏 / Private
1869 |
1870 |
1871 |
1872 |
1873 |
1874 |
1875 |
1876 |
1877 |
1878 |
继续搜索 / Search More
1879 |
1880 |
1881 |
1886 |
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 |
1914 |
1915 |
1920 |
1921 |
1922 |
1923 |
1924 |
显示更多 / More
1925 |
1926 |
1927 |
1928 |
1929 |
1930 | 选择已收藏的标签,填写批量大小等参数后,点击生成 按钮生成数组随机图片。
1931 | 点击保存至新标签 可以按批次保存至临时标签,随后可以修改标签名称。
1932 |
1933 | 点击缩略图进入鉴赏模式,可以使用上下左右方向键切换浏览的图片,点击左上角X按钮回到原模式。
1934 |
1935 | 本功能可以用来随机浏览收藏的图片,或是将较大的收藏标签切分为数个较小的收藏标签。本页面仍在开发阶段,欢迎留言。
1936 |
1937 | Select a tag you have bookmarked and set batch size and other configs before generating.
1938 | Click on Generate to get batches of shuffled images. Click on Save to Tag
1939 | will add a temperate tag to each batch that you can edit later.
1940 |
1941 | Click on the thumbnail to enter gallery mode, where you can use four arrow key to switch between pages. Click the X on the top left to exit gallery mode.
1942 |
1943 | This function is used to view random images, or slice a big tag into smaller ones. This page is in development and suggestions are welcomed.
1944 |
1945 |
1946 |
1947 | 未分类作品 / Uncategorized Works
1948 | 所有收藏 / All Works
1949 |
1950 |
1951 |
1952 | 批量大小 / Batch Size
1953 |
1954 |
1955 |
1956 | 作品公开类型 / Publication Type
1957 |
1958 | 公开收藏 / Public
1959 | 私密收藏 / Private
1960 |
1961 |
1962 |
1963 | 作品限制类型 / Restriction Type
1964 |
1965 | 全部作品 / All
1966 | 全年龄 / SFW
1967 | 非全年龄 / NSFW
1968 |
1969 |
1970 |
1971 |
1972 | 清除搜索 / Clear
1973 | 保存至标签 / Save to Tag
1974 | 生成 / Generate
1975 |
1976 |
1977 |
1978 |
1979 |
1980 |
1985 |
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 |
2090 |
2094 |
2095 |
2096 | 本页面用于放置一些较为零散独立的功能。
2097 |
2098 | This page contains some scattered and independent functions.
2099 |
2100 |
2101 |
2102 |
2103 | 作品公开类型 / Publication Type
2104 |
2105 | 公开收藏 / Public
2106 | 私密收藏 / Private
2107 |
2108 |
2109 |
2110 | 作品标签 / Tag
2111 |
2112 | 所有收藏 / All Works
2113 |
2114 |
2115 |
2116 |
2137 |
2138 |
2139 |
2140 |
2141 | 批量标记,或删除不可见作品,如果您有较早的收藏夹备份,可以使用下方的查询失效作品查找失效作品信息
2142 | Batch labeling or removing deleted/private works. If you have a previous backup of your bookmarks, you can use 'Lookup Invalid Works' below to get those invalid work details.
2143 |
2144 |
2145 | 加载失效作品信息 / Load Invalid Works
2146 |
2147 |
2148 |
2149 |
2150 |
2151 |
2152 | 备份收藏夹,可用于查找失效作品信息,或在其他账户上导入收藏
2153 | Backup the bookmarks, in order to look up deleted/privated work details, or import them on another account
2154 |
2155 |
2156 | 备份收藏夹 / Backup Bookmarks
2157 | 查询失效作品信息 / Lookup Invalid Works
2158 |
2159 |
2160 |
导入收藏夹 / Import Bookmarks
2161 |
2162 |
2163 | 选择需要导入的标签进行导入。对于已存在的收藏,可以选择已收藏作品标签的合并模式。 再次点击上方按钮重新选择备份文件
2164 | Select desired tag to import. For existing bookmarks, two options are provided to merge tags. Click upper button to reselect another backup file.
2165 |
2166 |
2167 |
2168 | 公开类型 / Publication
2169 |
2170 | 公开收藏 / Public
2171 | 私密收藏 / Private
2172 |
2173 |
2174 |
2175 | 作品标签 / Tag
2176 |
2177 |
2178 |
2179 | 模式 / Mode
2180 |
2181 | 合并 / Merge
2182 | 覆盖 / Override
2183 | 跳过 / Skip
2184 |
2185 |
2186 |
2187 |
导入 / Import
2188 |
2189 |
2190 |
2191 |
2192 |
2193 |
2194 |
2195 | 替换标签选择对话框,原生对话框使用收藏数进行排序,替换后将依照读音、作品、角色等进行排序
2196 | Replace the native tag-selection dialog, which uses the number of works to sort. New dialog will display the tags in alphabetical order, divided by characters and others.
2197 |
2198 |
${
2199 | tagSelectionDialog ? "禁用 / Disable" : "启用 / Enable"
2200 | }
2201 |
2202 |
2203 | 警告:加速模式下大部分网络请求之间的等待时间被移除,这使得收藏夹的加载更新速度变快,但也会增加您的账号被Pixiv封禁的风险,请谨慎决定是否使用该模式。
2204 | Warning: Most delay time between requests is removed in this mode, in order to speed up the loading and updating process of your bookmarks. But it will also increase the risk your account being banned by Pixiv. Please decide carefully whether to use this function.
2205 |
2206 |
${
2207 | turboMode ? "禁用 / Disable" : "启用 / Enable"
2208 | }
2209 |
2210 |
2211 |
2215 |
2216 |
2221 |
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 | `;
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 |
2906 |
2907 |
2908 |
正在处理 / Working on
2909 |
2910 |
2911 |
2915 |
2916 | 停止 / Stop
2917 |
2918 |
2919 |
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 |
3103 |
3104 |
3105 |
3106 | `;
3107 |
3108 | const clearTagsThemeClass = theme ? "OPGIe" : "bDwYXF";
3109 | const clearTagsText = lang.includes("zh") ? "清除标签" : "Clear Tags";
3110 | const clearTagsButton = document.createElement("div");
3111 | clearTagsButton.id = "clear_tags_button";
3112 | clearTagsButton.className = "sc-15a17794-0 iTJRIU sc-231887f1-7 gzddfG";
3113 | clearTagsButton.setAttribute("aria-disabled", "true");
3114 | clearTagsButton.setAttribute("role", "button");
3115 | clearTagsButton.innerHTML = ``;
3120 | clearTagsButton.addEventListener("click", handleClearBookmarkTags);
3121 |
3122 | const removeTagButton = document.createElement("div");
3123 | removeTagButton.id = "remove_tag_button";
3124 | removeTagButton.style.display = "none";
3125 | removeTagButton.style.marginRight = "16px";
3126 | removeTagButton.style.marginBottom = "12px";
3127 | removeTagButton.style.color = "rgba(0, 0, 0, 0.64)";
3128 | removeTagButton.style.cursor = "pointer";
3129 | removeTagButton.innerHTML = `
3130 |
3131 |
3132 |
3133 |
3134 |
3135 |
3136 |
3137 | `;
3138 | removeTagButton.addEventListener("click", handleDeleteTag);
3139 |
3140 | async function injection(_, injectionObserver) {
3141 | if (_) console.log(_);
3142 | if (pageInfo["userId"] !== uid) {
3143 | if (DEBUG)
3144 | console.log(
3145 | "[Label Bookmarks] Aborted Injection due to mismatch homepage",
3146 | );
3147 | return true;
3148 | }
3149 | if (injectionObserver) injectionObserver.disconnect();
3150 |
3151 | console.log("[Label Bookmarks] Try Injecting");
3152 |
3153 | const workInfo = await updateWorkInfo(true);
3154 | if (!workInfo["works"]) {
3155 | if (injectionObserver)
3156 | injectionObserver.observe(pageBody, { childList: true });
3157 | return console.log(
3158 | "[Label Bookmarks] Abort Injection due to no works detected yet",
3159 | );
3160 | }
3161 | if (DEBUG) {
3162 | console.log("[Label Bookmarks] User Tags", userTags, userTagDict);
3163 | console.log("[Label Bookmarks] Dict:", synonymDict);
3164 | }
3165 |
3166 | root.appendChild(buttonContainer);
3167 | setElementProperties();
3168 | setSynonymEventListener();
3169 | setAdvancedSearch();
3170 |
3171 | // show user-labeled tags
3172 | const ul = await waitForDom(WORK_CONTAINER);
3173 | async function updateAssociatedTagsCallback() {
3174 | const workInfo = await updateWorkInfo(true);
3175 | if (DEBUG) console.log("[Label Bookmarks] Page", workInfo.page, workInfo);
3176 | // TODO
3177 | [...ul.querySelectorAll("li")].forEach((li, i) => {
3178 | const pa = li.firstElementChild.firstElementChild;
3179 | if (showWorkTags) {
3180 | const tagsString = workInfo["works"][i].associatedTags
3181 | .map((i) => "#" + i)
3182 | .join(" ");
3183 | const tagDiv = document.createElement("div");
3184 | tagDiv.className = "my-1";
3185 | tagDiv.style.cssText =
3186 | "font-size: 10px; color: rgb(61, 118, 153); pointer-events: none";
3187 | tagDiv.innerHTML = tagsString;
3188 | pa.insertBefore(tagDiv, pa.children[1]);
3189 | }
3190 | if (workInfo["works"][i]["userName"] === "-----") {
3191 | const pidDiv = document.createElement("div");
3192 | pidDiv.className = "my-1";
3193 | pidDiv.style.cssText =
3194 | "font-size: 10px; color: rgb(61, 118, 153); pointer-events: none";
3195 | pidDiv.innerHTML = `PID: ${workInfo["works"][i].id}`;
3196 | pa.insertBefore(pidDiv, pa.children[1]);
3197 | }
3198 | });
3199 | }
3200 | await updateAssociatedTagsCallback();
3201 | new MutationObserver(updateAssociatedTagsCallback).observe(ul, {
3202 | childList: true,
3203 | });
3204 |
3205 | const editButtonContainer = await waitForDom(EDIT_BUTTON_CONTAINER);
3206 | if (editButtonContainer) {
3207 | editButtonContainer.style.justifyContent = "initial";
3208 | editButtonContainer.firstElementChild.style.marginRight = "auto";
3209 | editButtonContainer.insertBefore(
3210 | removeTagButton,
3211 | editButtonContainer.lastChild,
3212 | );
3213 | let removeBookmarkContainerObserver;
3214 | const editButtonObserver = new MutationObserver(
3215 | async (MutationRecord) => {
3216 | const { tag } = await updateWorkInfo();
3217 | if (!MutationRecord[0].addedNodes.length) {
3218 | // open edit mode
3219 | const removeBookmarkContainer = document.querySelector(
3220 | REMOVE_BOOKMARK_CONTAINER,
3221 | );
3222 | removeBookmarkContainer.appendChild(clearTagsButton);
3223 | removeBookmarkContainerObserver = new MutationObserver(() => {
3224 | const value =
3225 | removeBookmarkContainer.children[2].getAttribute(
3226 | "aria-disabled",
3227 | );
3228 | clearTagsButton.setAttribute("aria-disabled", value);
3229 | clearTagsButton.children[0].setAttribute("aria-disabled", value);
3230 | });
3231 | removeBookmarkContainerObserver.observe(
3232 | removeBookmarkContainer.children[2],
3233 | { attributes: true },
3234 | );
3235 | if (tag && tag !== "未分類") {
3236 | document.querySelector("#remove_tag_prompt").innerText =
3237 | lang.includes("zh") ? "删除标签 " + tag : "Delete Tag " + tag;
3238 | removeTagButton.style.display = "flex";
3239 | }
3240 | } else {
3241 | // exit edit mode
3242 | removeTagButton.style.display = "none";
3243 | if (removeBookmarkContainerObserver)
3244 | removeBookmarkContainerObserver.disconnect();
3245 | clearTagsButton.setAttribute("aria-disabled", "true");
3246 | clearTagsButton.children[0].setAttribute("aria-disabled", "true");
3247 | }
3248 | },
3249 | );
3250 | editButtonObserver.observe(editButtonContainer, {
3251 | childList: true,
3252 | });
3253 | }
3254 |
3255 | let lastTag = workInfo.tag;
3256 | const tagsContainer = await waitForDom(ALL_TAGS_CONTAINER);
3257 | new MutationObserver(async () => {
3258 | const workInfo = await updateWorkInfo();
3259 | if (lastTag !== workInfo.tag) {
3260 | lastTag = workInfo.tag;
3261 | const removeTagButton = document.querySelector("#remove_tag_button");
3262 | if (!workInfo.tag || workInfo.tag === "未分類") {
3263 | if (removeTagButton && removeTagButton.style.display === "flex") {
3264 | removeTagButton.style.display = "none";
3265 | }
3266 | } else {
3267 | if (
3268 | workInfo["editMode"] &&
3269 | removeTagButton &&
3270 | removeTagButton.style.display === "none"
3271 | ) {
3272 | removeTagButton.style.display = "flex";
3273 | }
3274 | const removeTagButtonPrompt =
3275 | document.querySelector("#remove_tag_prompt");
3276 | if (removeTagButtonPrompt)
3277 | removeTagButtonPrompt.innerText = lang.includes("zh")
3278 | ? "删除标签 " + workInfo.tag
3279 | : "Delete Tag " + workInfo.tag;
3280 | }
3281 | }
3282 | if (DEBUG)
3283 | console.log(
3284 | "[Label Bookmarks] Current Tag",
3285 | workInfo.tag || "Uncategorized",
3286 | );
3287 | }).observe(tagsContainer, {
3288 | subtree: true,
3289 | childList: true,
3290 | });
3291 |
3292 | const toUncategorized = document.querySelector(WORK_NUM);
3293 | if (toUncategorized) {
3294 | toUncategorized.style.cursor = "pointer";
3295 | toUncategorized.onclick = () =>
3296 | (window.location.href = `https://www.pixiv.net/users/${uid}/bookmarks/artworks/未分類`);
3297 | }
3298 |
3299 | const tagSelectionDialog =
3300 | getValue("tagSelectionDialog", "false") === "true";
3301 | if (tagSelectionDialog) {
3302 | // sort tags in popup
3303 | new MutationObserver((MutationRecord) => {
3304 | if (MutationRecord[0].addedNodes[0]) {
3305 | const root = MutationRecord[0].addedNodes[0];
3306 | const ul = root.querySelector("ul");
3307 | if (ul) {
3308 | [...ul.children]
3309 | .sort((a, b) => {
3310 | let iA = userTags.indexOf(a.textContent.slice(1));
3311 | let iB = userTags.indexOf(b.textContent.slice(1));
3312 | if (a.querySelector(ADD_TAGS_MODAL_ENTRY)) iA = -1;
3313 | if (b.querySelector(ADD_TAGS_MODAL_ENTRY)) iB = -1;
3314 | return iA - iB;
3315 | })
3316 | .forEach((node) => ul.appendChild(node));
3317 | }
3318 | }
3319 | }).observe(document.body, {
3320 | childList: true,
3321 | subtree: false,
3322 | attributes: false,
3323 | });
3324 |
3325 | // all tags selection control
3326 | const prevAllTagsButton = await waitForDom(ALL_TAGS_BUTTON);
3327 | prevAllTagsButton.style.display = "none";
3328 | addStyle(".ggMyQW { z-index: -1; }");
3329 | const allTagsButton = document.createElement("div");
3330 | allTagsButton.setAttribute("data-bs-toggle", "modal");
3331 | allTagsButton.setAttribute("data-bs-target", "#all_tags_modal");
3332 | allTagsButton.classList.add(ALL_TAGS_BUTTON.slice(1));
3333 | allTagsButton.role = "button";
3334 | allTagsButton.innerHTML = `
3335 |
3336 |
3337 | `;
3338 | const allTagsContainer = await waitForDom(ALL_TAGS_CONTAINER);
3339 | allTagsContainer.appendChild(allTagsButton);
3340 | allTagsButton.addEventListener("click", () => {
3341 | document.querySelector(ALL_TAGS_BUTTON)?.click();
3342 | const modal = document.querySelector("#all_tags_modal");
3343 | modal.addEventListener("shown.bs.modal", () => modal.focus());
3344 | modal.addEventListener("hidden.bs.modal", () => {
3345 | document
3346 | .querySelector(ALL_TAGS_MODAL)
3347 | ?.querySelector("button")
3348 | .click();
3349 | });
3350 | });
3351 | }
3352 |
3353 | console.log("[Label Bookmarks] Injected");
3354 |
3355 | window.addEventListener(
3356 | "popstate",
3357 | () => {
3358 | if (window.location.href.match(/\/users\/\d+\/bookmarks\/artworks/))
3359 | delay(1000)
3360 | .then(() => waitForDom(ALL_TAGS_CONTAINER))
3361 | .then(createModalElements)
3362 | .then(injectElements);
3363 | },
3364 | { once: true },
3365 | );
3366 |
3367 | return true;
3368 | }
3369 |
3370 | if (!(await injection())) {
3371 | console.log("[Label Bookmarks] Injecting Failed on the first try");
3372 | const pageObserver = new MutationObserver(injection);
3373 | pageObserver.observe(pageBody, { childList: true });
3374 | }
3375 | }
3376 |
3377 | let timeout = null,
3378 | prevKeyword = null;
3379 | async function updateSuggestion(
3380 | evt,
3381 | suggestionEl,
3382 | searchDict,
3383 | handleClickCandidateButton,
3384 | ) {
3385 | clearTimeout(timeout);
3386 | const keywordsArray = evt.target.value.split(" ");
3387 | const keyword = keywordsArray[keywordsArray.length - 1]
3388 | .replace(/^!/, "")
3389 | .replace(/^!/, "");
3390 | if (
3391 | window.runFlag ||
3392 | !keyword ||
3393 | !keyword.length ||
3394 | keyword === " " ||
3395 | keyword === prevKeyword
3396 | )
3397 | return;
3398 | timeout = setTimeout(async () => {
3399 | suggestionEl.parentElement.style.display = "none";
3400 | prevKeyword = keyword;
3401 | setTimeout(() => (prevKeyword = null), 3000);
3402 | while (suggestionEl.firstElementChild) {
3403 | suggestionEl.removeChild(suggestionEl.firstElementChild);
3404 | }
3405 | if (keyword.toUpperCase() === "R-18") return;
3406 |
3407 | let candidates = [];
3408 | if (searchDict) {
3409 | let dictKeys = Object.keys(synonymDict).filter((el) =>
3410 | stringIncludes(el, keyword),
3411 | );
3412 | if (dictKeys.length)
3413 | candidates = dictKeys.map((dictKey) => ({
3414 | tag_name: synonymDict[dictKey][0],
3415 | tag_translation: dictKey,
3416 | }));
3417 | if (!candidates.length) {
3418 | dictKeys = Object.keys(synonymDict).filter((key) =>
3419 | arrayIncludes(
3420 | synonymDict[key].map((i) => i.split("(")[0]),
3421 | keyword.split("(")[0],
3422 | ),
3423 | );
3424 | if (dictKeys.length)
3425 | candidates = dictKeys.map((dictKey) => ({
3426 | tag_name: synonymDict[dictKey][0],
3427 | tag_translation: dictKey,
3428 | }));
3429 | }
3430 | }
3431 | if (!candidates.length) {
3432 | const resRaw = await fetch(
3433 | `/rpc/cps.php?keyword=${encodeURI(keyword)}&lang=${lang}`,
3434 | );
3435 | const res = await resRaw.json();
3436 | candidates = res["candidates"].filter((i) => i["tag_name"] !== keyword);
3437 | }
3438 | if (candidates.length) {
3439 | for (let candidate of candidates.filter((_, i) => i < 5)) {
3440 | const candidateButton = document.createElement("button");
3441 | candidateButton.type = "button";
3442 | candidateButton.className = "btn p-0 mb-1 d-block";
3443 | candidateButton.innerHTML = `${
3444 | candidate["tag_translation"] || "🈳 "
3445 | } - ${candidate["tag_name"]}`;
3446 | handleClickCandidateButton(candidate, candidateButton);
3447 | suggestionEl.appendChild(candidateButton);
3448 | }
3449 | } else {
3450 | const noCandidate = document.createElement("div");
3451 | noCandidate.innerText = "无备选 / No Suggestion";
3452 | suggestionEl.appendChild(noCandidate);
3453 | }
3454 | suggestionEl.parentElement.style.display = "block";
3455 | }, 500);
3456 | }
3457 |
3458 | function setElementProperties() {
3459 | // label buttons
3460 | const labelButton = document.querySelector("#label_modal_button");
3461 | const searchButton = document.querySelector("#search_modal_button");
3462 | const generatorButton = document.querySelector("#generator_modal_button");
3463 | const featureButton = document.querySelector("#feature_modal_button");
3464 | if (lang.includes("zh")) {
3465 | labelButton.innerText = "添加标签";
3466 | searchButton.innerText = "搜索图片";
3467 | generatorButton.innerText = "随机图片";
3468 | featureButton.innerText = "其他功能";
3469 | } else {
3470 | labelButton.innerText = "Label";
3471 | searchButton.innerText = "Search";
3472 | generatorButton.innerText = "Shuffle";
3473 | featureButton.innerText = "Function";
3474 | }
3475 | addStyle(
3476 | `.label-button {
3477 | padding: 0 24px;
3478 | background: transparent;
3479 | font-size: 16px;
3480 | font-weight: 700;
3481 | border-top: 4px solid rgba(0, 150, 250, 0);
3482 | border-bottom: none;
3483 | border-left: none;
3484 | border-right: none;
3485 | line-height: 24px;
3486 | background: transparent;
3487 | transition: color 0.4s ease 0s, border 0.4s ease 0s;
3488 | }
3489 | .label-button:hover {
3490 | border-top: 4px solid rgb(0, 150, 250);
3491 | }`,
3492 | );
3493 |
3494 | // append user tags options
3495 | const customSelects = [...document.querySelectorAll(".select-custom-tags")];
3496 | customSelects.forEach((el) => {
3497 | const uncat = el.querySelector("option[value='未分類']");
3498 | if (uncat) {
3499 | const t = "未分類";
3500 | const pb = userTagDict.public.find((e) => e.tag === t)?.["cnt"] || 0;
3501 | const pr = userTagDict["private"].find((e) => e.tag === t)?.["cnt"] || 0;
3502 | uncat.innerText = `未分类作品 / Uncategorized Works (${pb}, ${pr})`;
3503 | }
3504 | userTags.forEach((tag) => {
3505 | const option = document.createElement("option");
3506 | option.value = tag;
3507 | const pb = userTagDict.public.find((e) => e.tag === tag)?.["cnt"] || 0;
3508 | const pr =
3509 | userTagDict["private"].find((e) => e.tag === tag)?.["cnt"] || 0;
3510 | option.innerText = tag + ` (${pb}, ${pr})`;
3511 | el.appendChild(option);
3512 | });
3513 | });
3514 |
3515 | // label bookmark form
3516 | const labelForm = document.querySelector("#label_form");
3517 | labelForm.onsubmit = handleLabel;
3518 | const footerLabel = document.querySelector("#footer_label_button");
3519 | const startLabel = document.querySelector("#start_label_button");
3520 | footerLabel.onclick = () => startLabel.click();
3521 | const stopButton = document.querySelector("#footer_stop_button");
3522 | stopButton.onclick = () => (window.runFlag = false);
3523 |
3524 | // default value
3525 | const addFirst = document.querySelector("#label_add_first");
3526 | addFirst.value = getValue("addFirst", "false");
3527 | addFirst.onchange = () => setValue("addFirst", addFirst.value);
3528 |
3529 | const addAllTags = document.querySelector("#label_add_all_tags");
3530 | addAllTags.value = getValue("addAllTags", "false");
3531 | addAllTags.onchange = () => setValue("addAllTags", addAllTags.value);
3532 |
3533 | const tagToQuery = document.querySelector("#label_tag_query");
3534 | const tag = getValue("tagToQuery", "未分類");
3535 | if (userTags.includes(tag)) tagToQuery.value = tag;
3536 | // in case that tag has been deleted
3537 | else tagToQuery.value = "未分類";
3538 | tagToQuery.onchange = () => setValue("tagToQuery", tagToQuery.value);
3539 |
3540 | const labelR18 = document.querySelector("#label_r18");
3541 | labelR18.value = getValue("labelR18", "true");
3542 | labelR18.onchange = () => setValue("labelR18", labelR18.value);
3543 |
3544 | const labelSafe = document.querySelector("#label_safe");
3545 | labelSafe.value = getValue("labelSafe", "false");
3546 | labelSafe.onchange = () => setValue("labelSafe", labelSafe.value);
3547 |
3548 | const labelAI = document.querySelector("#label_ai");
3549 | labelAI.value = getValue("labelAI", "false");
3550 | labelAI.onchange = () => setValue("labelAI", labelAI.value);
3551 |
3552 | const labelAuthor = document.querySelector("#label_author");
3553 | labelAuthor.value = getValue("labelAuthor", "false");
3554 | labelAuthor.onchange = () => setValue("labelAuthor", labelAuthor.value);
3555 |
3556 | const exclusion = document.querySelector("#label_exclusion");
3557 | exclusion.value = getValue("exclusion", "");
3558 | exclusion.onchange = () => setValue("exclusion", exclusion.value);
3559 |
3560 | const labelStrict = document.querySelector("#label_strict");
3561 | labelStrict.value = getValue("labelStrict", "true");
3562 | labelStrict.onchange = () => setValue("labelStrict", labelStrict.value);
3563 |
3564 | // search bookmark form
3565 | const searchForm = document.querySelector("#search_form");
3566 | searchForm.onsubmit = handleSearch;
3567 | const searchMore = document.querySelector("#search_more");
3568 | const footerSearch = document.querySelector("#footer_search_button");
3569 | footerSearch.onclick = () => searchMore.click();
3570 |
3571 | // generator form
3572 | const generatorForm = document.querySelector("#generator_form");
3573 | generatorForm.onsubmit = handleGenerate;
3574 |
3575 | document
3576 | .querySelector("#stop_remove_tag_button")
3577 | .addEventListener("click", () => (window.runFlag = false));
3578 | if (DEBUG) console.log("[Label Bookmarks] Element Properties Set");
3579 | }
3580 |
3581 | function setSynonymEventListener() {
3582 | const targetTag = document.querySelector("#target_tag");
3583 | const alias = document.querySelector("#tag_alias");
3584 | const preview = document.querySelector("#synonym_preview");
3585 | const buttons = document
3586 | .querySelector("#synonym_buttons")
3587 | .querySelectorAll("button");
3588 | const lineHeight = parseInt(getComputedStyle(preview).lineHeight);
3589 |
3590 | const labelSuggestion = document.querySelector("#label_suggestion");
3591 | targetTag.addEventListener("keyup", (evt) => {
3592 | updateSuggestion(
3593 | evt,
3594 | labelSuggestion,
3595 | false,
3596 | (candidate, candidateButton) =>
3597 | candidateButton.addEventListener("click", () => {
3598 | alias.value = alias.value + " " + candidate["tag_name"];
3599 | }),
3600 | ).catch(console.log);
3601 | });
3602 | targetTag.addEventListener("keyup", (evt) => {
3603 | // scroll to modified entry
3604 | const lines = preview.innerText.split("\n");
3605 | let lineNum = lines.findIndex((line) => line.includes(evt.target.value));
3606 | if (lineNum < 0) return;
3607 | if (lines[lineNum].startsWith("\t")) lineNum--;
3608 | if (lineHeight * lineNum) preview.scrollTop = lineHeight * lineNum;
3609 | });
3610 | targetTag.addEventListener("blur", (evt) => {
3611 | if (Object.keys(synonymDict).includes(evt.target.value)) {
3612 | const value = synonymDict[evt.target.value];
3613 | if (value.length > 4) alias.value = value.join("\n");
3614 | else alias.value = value.join(" ");
3615 | }
3616 | });
3617 |
3618 | // update preview
3619 | function updatePreview(synonymDict) {
3620 | let synonymString = "";
3621 | for (let key of Object.keys(synonymDict)) {
3622 | let value = synonymDict[key];
3623 | if (value.length > 4) value = value.join("\n\t");
3624 | else value = value.join(" ");
3625 | synonymString += key + "\n\t" + value + "\n\n";
3626 | }
3627 | preview.innerText = synonymString
3628 | ? synonymString
3629 | : "加载词典一栏中提供了样例词典,可用于导入\nA synonym dictionary sample is provided in Load Dict section for importing";
3630 | }
3631 | updatePreview(synonymDict);
3632 |
3633 | // on json file load
3634 | document
3635 | .querySelector("#synonym_dict_input")
3636 | .addEventListener("change", (evt) => {
3637 | const reader = new FileReader();
3638 | reader.onload = (evt) => {
3639 | try {
3640 | let json = {};
3641 | eval("json = " + evt.target.result.toString());
3642 | if (Array.isArray(json)) synonymDict = json[0];
3643 | else synonymDict = json;
3644 | setValue("synonymDict", synonymDict);
3645 | updatePreview(synonymDict);
3646 | } catch (err) {
3647 | alert("无法加载词典 / Fail to load dictionary\n" + err);
3648 | }
3649 | };
3650 | reader.readAsText(evt.target.files[0]);
3651 | });
3652 | // export dict
3653 | buttons[0].addEventListener("click", (evt) => {
3654 | evt.preventDefault();
3655 | const a = document.createElement("a");
3656 | a.href = URL.createObjectURL(
3657 | new Blob([JSON.stringify(synonymDict)], {
3658 | type: "application/json",
3659 | }),
3660 | );
3661 | a.setAttribute(
3662 | "download",
3663 | `label_pixiv_bookmarks_synonym_dict_${new Date().toLocaleDateString()}.json`,
3664 | );
3665 | a.click();
3666 | setValue("lastBackupDict", Date.now());
3667 | });
3668 | // load alias
3669 | buttons[1].addEventListener("click", (evt) => {
3670 | evt.preventDefault();
3671 | labelSuggestion.parentElement.style.display = "none";
3672 | const targetValue = targetTag.value;
3673 | for (let key of Object.keys(synonymDict)) {
3674 | if (key === targetValue) {
3675 | alias.value = synonymDict[key].join(" ");
3676 | updatePreview(synonymDict);
3677 | }
3678 | }
3679 | });
3680 | // update the alias array
3681 | buttons[2].addEventListener("click", (evt) => {
3682 | evt.preventDefault();
3683 | labelSuggestion.parentElement.style.display = "none";
3684 | const targetValue = targetTag.value
3685 | .split(" ")[0]
3686 | .replace("(", "(")
3687 | .replace(")", ")");
3688 | // navigator.clipboard.writeText(targetValue).catch(console.log);
3689 | const aliasValue = alias.value;
3690 | if (aliasValue === "") {
3691 | // delete
3692 | if (
3693 | synonymDict[targetValue] &&
3694 | window.confirm(
3695 | `将会删除 ${targetValue},请确认\nWill remove ${targetValue}. Is this okay?`,
3696 | )
3697 | ) {
3698 | delete synonymDict[targetValue];
3699 | }
3700 | } else {
3701 | const value = aliasValue
3702 | .split(/[\s\r\n]/)
3703 | .filter((i) => i)
3704 | .map((i) => i.trim());
3705 | if (synonymDict[targetValue]) {
3706 | synonymDict[targetValue] = value; // update
3707 | } else {
3708 | synonymDict[targetValue] = value; // add and sort
3709 | const newDict = {};
3710 | for (let key of sortByParody(Object.keys(synonymDict))) {
3711 | newDict[key] = synonymDict[key];
3712 | }
3713 | synonymDict = newDict;
3714 | }
3715 | }
3716 | targetTag.value = "";
3717 | alias.value = "";
3718 | setValue("synonymDict", synonymDict);
3719 | updatePreview(synonymDict);
3720 | });
3721 | // filter
3722 | document
3723 | .querySelector("input#synonym_filter")
3724 | .addEventListener("input", (evt) => {
3725 | const filter = evt.target.value;
3726 | if (filter.length) {
3727 | if (filter === " ") return;
3728 | const filteredKeys = Object.keys(synonymDict).filter(
3729 | (key) =>
3730 | stringIncludes(key, filter) ||
3731 | arrayIncludes(synonymDict[key], filter, null, null, true),
3732 | );
3733 | const newDict = {};
3734 | for (let key of filteredKeys) {
3735 | newDict[key] = synonymDict[key];
3736 | }
3737 | updatePreview(newDict);
3738 | } else {
3739 | updatePreview(synonymDict);
3740 | }
3741 | });
3742 | // clear
3743 | document
3744 | .querySelector("button#clear_synonym_filter")
3745 | .addEventListener("click", () => {
3746 | document.querySelector("input#synonym_filter").value = "";
3747 | updatePreview(synonymDict);
3748 | });
3749 | // restore
3750 | document
3751 | .querySelector("button#label_restore_dict")
3752 | .addEventListener("click", restoreSynonymDict);
3753 | // get sample
3754 | document
3755 | .querySelector("button#label_dict_sample")
3756 | .addEventListener("click", () => {
3757 | const s =
3758 | '{"Fate":["FGO","Fate/GrandOrder","Fate/StayNight","Fate/Zero","Fate/Extra","Fate/ExtraCCC","Fate/Apocrypha"],"EVA":["新世紀エヴァンゲリオン","エヴァンゲリオン","evangelion","Evangelion","eva","EVA","新世纪福音战士"]}';
3759 | const a = document.createElement("a");
3760 | a.href = URL.createObjectURL(
3761 | new Blob([s], {
3762 | type: "application/json",
3763 | }),
3764 | );
3765 | a.setAttribute("download", "synonym_dict_sample.json");
3766 | a.click();
3767 | });
3768 | if (DEBUG) console.log("[Label Bookmarks] Synonym Dictionary Ready");
3769 | }
3770 |
3771 | function setAdvancedSearch() {
3772 | function generatePlaceholder() {
3773 | const synonymDictKeys = Object.keys(synonymDict);
3774 | return synonymDictKeys.length
3775 | ? "eg: " +
3776 | synonymDictKeys[
3777 | Math.floor(Math.random() * synonymDictKeys.length)
3778 | ].split("(")[0]
3779 | : "";
3780 | }
3781 | function generateBasicField() {
3782 | const fieldContainer = document.createElement("div");
3783 | fieldContainer.className = "d-flex";
3784 | const searchInput = document.createElement("input");
3785 | searchInput.className = "form-control";
3786 | searchInput.required = true;
3787 | searchInput.id = "search_value";
3788 | searchInput.setAttribute("placeholder", generatePlaceholder());
3789 | // search with suggestion
3790 | const searchSuggestion = document.querySelector("#search_suggestion");
3791 | searchInput.addEventListener("keyup", (evt) => {
3792 | updateSuggestion(
3793 | evt,
3794 | searchSuggestion,
3795 | true,
3796 | (candidate, candidateButton) =>
3797 | candidateButton.addEventListener("click", () => {
3798 | const keywordsArray = searchInput.value.split(" ");
3799 | const keyword = keywordsArray[keywordsArray.length - 1];
3800 | let newKeyword = candidate["tag_name"];
3801 | if (keyword.match(/^!/) || keyword.match(/^!/))
3802 | newKeyword = "!" + newKeyword;
3803 | keywordsArray.splice(keywordsArray.length - 1, 1, newKeyword);
3804 | searchInput.value = keywordsArray.join(" ");
3805 | }),
3806 | ).catch(console.log);
3807 | });
3808 | const toggleBasic = document.createElement("button");
3809 | toggleBasic.style.border = "1px solid #ced4da";
3810 | toggleBasic.className = "btn btn-outline-secondary ms-2";
3811 | toggleBasic.type = "button";
3812 | toggleBasic.addEventListener("click", () => {
3813 | basic.classList.add("d-none");
3814 | basic.removeChild(basic.firstChild);
3815 | advanced.appendChild(generateAdvancedField(0));
3816 | advanced.classList.remove("d-none");
3817 | });
3818 | toggleBasic.innerHTML = `
3819 |
3820 | `;
3821 | fieldContainer.appendChild(searchInput);
3822 | fieldContainer.appendChild(toggleBasic);
3823 | return fieldContainer;
3824 | }
3825 | function generateAdvancedField(index) {
3826 | const fieldContainer = document.createElement("div");
3827 | fieldContainer.className = "mb-3";
3828 | const inputContainer = document.createElement("div");
3829 | inputContainer.className = "d-flex mb-2";
3830 | inputContainer.innerHTML = ` `;
3831 | if (!index) {
3832 | inputContainer.firstElementChild.setAttribute(
3833 | "placeholder",
3834 | generatePlaceholder(),
3835 | );
3836 | const toggleAdvanced = document.createElement("button");
3837 | toggleAdvanced.style.border = "1px solid #ced4da";
3838 | toggleAdvanced.className = "btn btn-outline-secondary ms-2";
3839 | toggleAdvanced.type = "button";
3840 | toggleAdvanced.addEventListener("click", () => {
3841 | const basic = document.querySelector("#basic_search_field");
3842 | const advanced = document.querySelector("#advanced_search_fields");
3843 | basic.appendChild(generateBasicField());
3844 | basic.classList.remove("d-none");
3845 | advanced.classList.add("d-none");
3846 | while (advanced.firstChild) {
3847 | advanced.removeChild(advanced.firstChild);
3848 | }
3849 | });
3850 | toggleAdvanced.innerHTML = `
3851 |
3852 | `;
3853 | const addFieldButton = document.createElement("button");
3854 | addFieldButton.style.border = "1px solid #ced4da";
3855 | addFieldButton.className = "btn btn-outline-secondary ms-2";
3856 | addFieldButton.type = "button";
3857 | addFieldButton.addEventListener("click", () => {
3858 | const advanced = document.querySelector("#advanced_search_fields");
3859 | advanced.appendChild(generateAdvancedField(advanced.childElementCount));
3860 | });
3861 | addFieldButton.innerHTML = `
3862 |
3863 | `;
3864 | inputContainer.appendChild(toggleAdvanced);
3865 | inputContainer.appendChild(addFieldButton);
3866 | } else {
3867 | const removeFieldButton = document.createElement("button");
3868 | removeFieldButton.style.border = "1px solid #ced4da";
3869 | removeFieldButton.className = "btn btn-outline-secondary ms-2";
3870 | removeFieldButton.type = "button";
3871 | removeFieldButton.addEventListener("click", () => {
3872 | const advanced = document.querySelector("#advanced_search_fields");
3873 | advanced.removeChild(fieldContainer);
3874 | });
3875 | removeFieldButton.innerHTML = `
3876 |
3877 | `;
3878 | inputContainer.appendChild(removeFieldButton);
3879 | }
3880 | const configContainer = document.createElement("div");
3881 | configContainer.className = "row";
3882 | [
3883 | "标题/Title",
3884 | "作者/Author",
3885 | "作品标签/Work Tags",
3886 | "用户标签/Bookmark Tags",
3887 | ].forEach((name) => {
3888 | const id = name.split("/")[0] + index;
3889 | const container = document.createElement("div");
3890 | container.className = "col-3";
3891 | container.innerHTML = `
3892 |
3893 | ${name}
3894 |
`;
3895 | configContainer.appendChild(container);
3896 | });
3897 | fieldContainer.appendChild(inputContainer);
3898 | fieldContainer.appendChild(configContainer);
3899 | return fieldContainer;
3900 | }
3901 | const basic = document.querySelector("#basic_search_field");
3902 | const advanced = document.querySelector("#advanced_search_fields");
3903 | basic.appendChild(generateBasicField());
3904 | if (DEBUG) console.log("[Label Bookmarks] Advanced Search Set");
3905 | }
3906 |
3907 | function registerMenu() {
3908 | showWorkTags = getValue("showWorkTags", "false") === "true";
3909 | if (showWorkTags)
3910 | GM_registerMenuCommand_("隐藏用户标签 / Hide User Tags", () => {
3911 | setValue("showWorkTags", "false");
3912 | window.location.reload();
3913 | });
3914 | else
3915 | GM_registerMenuCommand_("显示用户标签 / Show User Tags", () => {
3916 | setValue("showWorkTags", "true");
3917 | window.location.reload();
3918 | });
3919 | generator = getValue("showGenerator", "false") === "true";
3920 | if (generator)
3921 | GM_registerMenuCommand_("关闭随机图片 / Disable Shuffled Images", () => {
3922 | setValue("showGenerator", "false");
3923 | window.location.reload();
3924 | });
3925 | else
3926 | GM_registerMenuCommand_("启用随机图片 / Enable Shuffled Images", () => {
3927 | setValue("showGenerator", "true");
3928 | window.location.reload();
3929 | });
3930 | feature = getValue("showFeature", "false") === "true";
3931 | if (feature)
3932 | GM_registerMenuCommand_(
3933 | "关闭其他功能 / Disable Additional Functions",
3934 | () => {
3935 | setValue("showFeature", "false");
3936 | window.location.reload();
3937 | },
3938 | );
3939 | else
3940 | GM_registerMenuCommand_(
3941 | "显示其他功能 / Enable Additional Functions",
3942 | () => {
3943 | setValue("showFeature", "true");
3944 | window.location.reload();
3945 | },
3946 | );
3947 | DEBUG = getValue("DEBUG", "false") === "true";
3948 | if (DEBUG)
3949 | GM_registerMenuCommand_("关闭详细日志 / Disable Verbose Logging", () => {
3950 | setValue("DEBUG", "false");
3951 | window.location.reload();
3952 | });
3953 | else
3954 | GM_registerMenuCommand_("启用详细日志 / Enable Verbose Logging", () => {
3955 | setValue("DEBUG", "true");
3956 | window.location.reload();
3957 | });
3958 | turboMode = getValue("turboMode", "false") === "true";
3959 | }
3960 |
3961 | (function () {
3962 | "use strict";
3963 | loadResources();
3964 | registerMenu();
3965 | waitForDom("nav")
3966 | .then(initializeVariables)
3967 | .then(createModalElements)
3968 | .then(injectElements);
3969 | })();
3970 |
--------------------------------------------------------------------------------
/stylesheet.css:
--------------------------------------------------------------------------------
1 | .label-button {
2 | padding: 0 24px;
3 | background: transparent;
4 | }
5 |
6 | .label-button {
7 | border: none;
8 | color: #258fb8;
9 | }
10 |
11 | .label-button {
12 | font-size: 16px;
13 | font-weight: 700;
14 | border-top: 4px solid rgba(0, 150, 250, 0);
15 | border-bottom: none;
16 | border-left: none;
17 | border-right: none;
18 | color: rgba(0, 0, 0, 0.32);
19 | line-height: 24px;
20 | background: transparent;
21 | transition: color 0.4s ease 0s, border 0.4s ease 0s;
22 | }
23 |
24 | .label-button:hover {
25 | border-top: 4px solid rgb(0, 150, 250);
26 | color: rgba(0, 0, 0, 0.88);
27 | }
28 |
29 | #advanced_search {
30 | max-height: 0;
31 | transition: max-height 0.4s ease 0s;
32 | }
--------------------------------------------------------------------------------
/synonym_dict_sample.json:
--------------------------------------------------------------------------------
1 | {"Fate":["FGO","Fate/GrandOrder","Fate/StayNight","Fate/Zero","Fate/Extra","Fate/ExtraCCC","Fate/Apocrypha"],"EVA":["新世紀エヴァンゲリオン","エヴァンゲリオン","evangelion","Evangelion","eva","EVA","新世纪福音战士"]}
--------------------------------------------------------------------------------
/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 | Document
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------