├── src_firefox
├── bg.html
├── manifest.json
├── index.html
├── config.js
├── _locales
│ ├── zh
│ │ └── messages.json
│ ├── ja
│ │ └── messages.json
│ └── en
│ │ └── messages.json
├── style.css
├── index.js
├── options.js
├── options.html
└── background.js
├── src
├── manifest.json
├── index.html
├── config.js
├── _locales
│ ├── zh
│ │ └── messages.json
│ ├── ja
│ │ └── messages.json
│ └── en
│ │ └── messages.json
├── style.css
├── index.js
├── options.js
├── options.html
└── background.js
├── readme_cn.md
├── LICENSE
└── readme.md
/src_firefox/bg.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src_firefox/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Newtab Random Pixiv Images",
3 | "version": "1.0.0",
4 | "manifest_version": 2,
5 | "default_locale": "en",
6 | "chrome_url_overrides": {
7 | "newtab": "index.html"
8 | },
9 | "background": {
10 | "page": "bg.html",
11 | "persistent": true
12 | },
13 | "permissions": [
14 | "storage",
15 | "webRequest",
16 | "webRequestBlocking",
17 | "*://*.pixiv.net/*",
18 | "*://*.pximg.net/*"
19 | ],
20 | "options_page": "options.html"
21 | }
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Newtab Random Pixiv Images",
3 | "version": "1.0.0",
4 | "manifest_version": 3,
5 | "default_locale": "en",
6 | "chrome_url_overrides": {
7 | "newtab": "index.html"
8 | },
9 | "background": {
10 | "service_worker": "background.js",
11 | "type": "module"
12 | },
13 | "permissions": [
14 | "storage",
15 | "declarativeNetRequest"
16 | ],
17 | "host_permissions": [
18 | "*://*.pixiv.net/*",
19 | "*://*.pximg.net/*"
20 | ],
21 | "options_page": "options.html"
22 | }
--------------------------------------------------------------------------------
/readme_cn.md:
--------------------------------------------------------------------------------
1 | [English](./readme.md)/中文
2 |
3 | # Newtab Random Pixiv Images
4 |
5 | 本扩展会从 Pixiv 搜索结果中随机选取一张图片作为新标签页背景。默认使用 "10000users入り" 等作为搜索关键词。在右下角您可以刷新图片或查看作品详情。(使用前可能需要先登录 Pixiv 以确保搜索功能正常)
6 |
7 | ## 安装指南
8 | ### chrome/edge:
9 | 1. 克隆或下载本项目到本地
10 | 2. 打开浏览器并访问 chrome://extensions/
11 | 3. 开启右上角的 开发者模式 开关
12 | 4. 点击 加载已解压的扩展程序 按钮,选择项目中的 src 目录
13 | 5. 打开新标签页
14 |
15 | ### firefox:
16 | 1. 克隆或下载本项目到本地
17 | 2. 打开浏览器并访问 about:debugging
18 | 3. 在左侧选择 This Firefox 标签
19 | 4. 点击 Load Temporary Add-on... 按钮,进入 src_firefox 目录选择 manifest.json 文件
20 | 5. 打开新标签页
21 |
22 | ## 许可证
23 |
24 | 本扩展遵循 MIT 开源协议发布
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Permission is hereby granted, free of charge, to any person obtaining a copy
2 | of this software and associated documentation files (the "Software"), to deal
3 | in the Software without restriction, including without limitation the rights
4 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
5 | copies of the Software, and to permit persons to whom the Software is
6 | furnished to do so, subject to the following conditions:
7 |
8 | The above copyright notice and this permission notice shall be included in all
9 | copies or substantial portions of the Software.
10 |
11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
17 | SOFTWARE.
18 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | English/[中文](./readme_cn.md)
2 |
3 | # Newtab Random Pixiv Images
4 |
5 | Randomly picks one image from pixiv search results to replace newtab background. By default search uses "10000users入り" keyword. In bottom right corner, you can load new image or check image details.You may need to login Pixiv first to make search functionality work.
6 |
7 | ## Install
8 | ### chrome/edge:
9 | 1. Clone or download this project to local.
10 | 2. Open chrome and goto "chrome://extensions/" page.
11 | 3. Toggle topright "Developer mode" button to enable chrome developer mode.
12 | 4. Click "Load unpacked" button, then in the open window select the "src" directory from downloaded project.
13 | 5. Open newtab and enjoy a randomly picked pixiv illustration!
14 |
15 | ### firefox:
16 | 1. Clone or download this project to local.
17 | 2. Open firefox and goto "about:debugging" page.
18 | 3. Select "This Firefox" tab on the left side.
19 | 4. Click "Load Temporary Add-on..." button, then in the open window goto the "src_firefox" directory, select "manifest.json" file.
20 | 5. Open newtab and enjoy a randomly picked pixiv illustration!
21 |
22 | ## License
23 |
24 | This extension is distributed under MIT license.
25 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | New Tab
6 |
7 |
8 |
9 |
10 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src_firefox/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | New Tab
6 |
7 |
8 |
9 |
10 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | export const Order = Object.freeze({
2 | date_d: 'date_d',
3 | date: 'date',
4 | popular_d: 'popular_d',
5 | popular_male_d: 'popular_male_d',
6 | popular_female_d: 'popular_female_d'
7 | });
8 |
9 | export const Mode = Object.freeze({
10 | all: 'all',
11 | r18: 'r18',
12 | safe: 'safe'
13 | });
14 |
15 | export const SMode = Object.freeze({
16 | s_tag: 's_tag',
17 | s_tag_full: 's_tag_full',
18 | s_tc: 's_tc'
19 | })
20 |
21 | export const ImageType = Object.freeze({
22 | all: 'all',
23 | illust_and_ugoira: 'illust_and_ugoira',
24 | illust: 'illust',
25 | manga: 'manga',
26 | ugoira: 'ugoira'
27 | });
28 |
29 | export const TimeOption = Object.freeze({
30 | unlimited: 'unlimited',
31 | specific: 'specific'
32 | });
33 |
34 | export const defaultConfig = {
35 | order: Order.date_d, // sort order
36 | mode: Mode.safe, // search mode
37 | timeOption: TimeOption.unlimited,
38 | scd: null, // start date
39 | ecd: null, // end date
40 | blt: null, // minimum likes number
41 | bgt: null, // maximum likes number
42 | s_mode: SMode.s_tag,
43 | type: ImageType.illust,
44 | // sl perhaps means pixiv safe level, 2 is safe, 6 is not safe
45 | min_sl: null,
46 | max_sl: null,
47 | aiType: null, // pixiv ai type, 1 not ai, 2 is ai
48 | orKeywords: "7500users入り 10000users入り 30000users入り 50000users入り",
49 | minusKeywords: "虚偽users入りタグ 描き方 講座 作画資料 創作 素材 漫画",
50 | andKeywords: "",
51 | }
52 |
53 | export function getKeywords(andKeywords, orKeywords, minusKeywords) {
54 | let andKeywordsList = andKeywords.trim().split(/\s+/).filter(Boolean);
55 | let orKeywordsList = orKeywords.trim().split(/\s+/).filter(Boolean);
56 | let minusKeywordsList = minusKeywords.trim().split(/\s+/).filter(Boolean);
57 | let aWord = andKeywordsList.length ? andKeywordsList.join(' ') : '';
58 | let pWord = orKeywordsList.length ? '(' + orKeywordsList.join(" OR ") + ')' : '';
59 | let nWord = minusKeywordsList.length ? "-" + minusKeywordsList.join(" -") : '';
60 | let allWords = []
61 | if (aWord) {
62 | allWords.push(aWord);
63 | }
64 | if (nWord) {
65 | allWords.push(nWord);
66 | }
67 | if (pWord) {
68 | allWords.push(pWord);
69 | }
70 | let word = allWords.join(' ');
71 | return word;
72 | }
73 |
--------------------------------------------------------------------------------
/src_firefox/config.js:
--------------------------------------------------------------------------------
1 | export const Order = Object.freeze({
2 | date_d: 'date_d',
3 | date: 'date',
4 | popular_d: 'popular_d',
5 | popular_male_d: 'popular_male_d',
6 | popular_female_d: 'popular_female_d'
7 | });
8 |
9 | export const Mode = Object.freeze({
10 | all: 'all',
11 | r18: 'r18',
12 | safe: 'safe'
13 | });
14 |
15 | export const SMode = Object.freeze({
16 | s_tag: 's_tag',
17 | s_tag_full: 's_tag_full',
18 | s_tc: 's_tc'
19 | })
20 |
21 | export const ImageType = Object.freeze({
22 | all: 'all',
23 | illust_and_ugoira: 'illust_and_ugoira',
24 | illust: 'illust',
25 | manga: 'manga',
26 | ugoira: 'ugoira'
27 | });
28 |
29 | export const TimeOption = Object.freeze({
30 | unlimited: 'unlimited',
31 | specific: 'specific'
32 | });
33 |
34 | export const defaultConfig = {
35 | order: Order.date_d, // sort order
36 | mode: Mode.safe, // search mode
37 | timeOption: TimeOption.unlimited,
38 | scd: null, // start date
39 | ecd: null, // end date
40 | blt: null, // minimum likes number
41 | bgt: null, // maximum likes number
42 | s_mode: SMode.s_tag,
43 | type: ImageType.illust,
44 | // sl perhaps means pixiv safe level, 2 is safe, 6 is not safe
45 | min_sl: null,
46 | max_sl: null,
47 | aiType: null, // pixiv ai type, 1 not ai, 2 is ai
48 | orKeywords: "7500users入り 10000users入り 30000users入り 50000users入り",
49 | minusKeywords: "虚偽users入りタグ 描き方 講座 作画資料 創作 素材 漫画",
50 | andKeywords: "",
51 | }
52 |
53 | export function getKeywords(andKeywords, orKeywords, minusKeywords) {
54 | let andKeywordsList = andKeywords.trim().split(/\s+/).filter(Boolean);
55 | let orKeywordsList = orKeywords.trim().split(/\s+/).filter(Boolean);
56 | let minusKeywordsList = minusKeywords.trim().split(/\s+/).filter(Boolean);
57 | let aWord = andKeywordsList.length ? andKeywordsList.join(' ') : '';
58 | let pWord = orKeywordsList.length ? '(' + orKeywordsList.join(" OR ") + ')' : '';
59 | let nWord = minusKeywordsList.length ? "-" + minusKeywordsList.join(" -") : '';
60 | let allWords = []
61 | if (aWord) {
62 | allWords.push(aWord);
63 | }
64 | if (nWord) {
65 | allWords.push(nWord);
66 | }
67 | if (pWord) {
68 | allWords.push(pWord);
69 | }
70 | let word = allWords.join(' ');
71 | return word;
72 | }
73 |
--------------------------------------------------------------------------------
/src/_locales/zh/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "keywordsLabel": {
3 | "message": "完整关键词"
4 | },
5 | "andKeywordsLabel": {
6 | "message": "含有全部关键词(AND搜索)"
7 | },
8 | "orKeywordsLabel": {
9 | "message": "含有某个关键词(OR搜索)"
10 | },
11 | "minusKeywordsLabel": {
12 | "message": "不含有关键词(排除搜索)"
13 | },
14 | "keywordsTip": {
15 | "message": "多个关键词用空格隔开"
16 | },
17 | "orderLabel": {
18 | "message": "排序方式"
19 | },
20 | "date_dOrder": {
21 | "message": "按最新排序"
22 | },
23 | "dateOrder": {
24 | "message": "按旧排序"
25 | },
26 | "popular_dOrder": {
27 | "message": "按热门度排序(是pixiv高级会员才有效)"
28 | },
29 | "popular_male_dOrder": {
30 | "message": "受男性欢迎(是pixiv高级会员才有效)"
31 | },
32 | "popular_female_dOrder": {
33 | "message": "受女性欢迎(是pixiv高级会员才有效)"
34 | },
35 | "modeLabel": {
36 | "message": "搜索模式"
37 | },
38 | "allMode": {
39 | "message": "全部"
40 | },
41 | "r18Mode": {
42 | "message": "R18"
43 | },
44 | "safeMode": {
45 | "message": "全年龄"
46 | },
47 | "timeOptionLabel": {
48 | "message": "时间"
49 | },
50 | "unlimitedTimeOption": {
51 | "message": "不限时间"
52 | },
53 | "specificTimeOption": {
54 | "message": "指定时间"
55 | },
56 | "scdLabel": {
57 | "message": "开始时间"
58 | },
59 | "ecdLabel": {
60 | "message": "结束时间"
61 | },
62 | "bookmarksLabel": {
63 | "message": "收藏数(是pixiv高级会员才有效)"
64 | },
65 | "bltLabel": {
66 | "message": "最低收藏数(是pixiv高级会员才有效)"
67 | },
68 | "bgtLabel": {
69 | "message": "最高收藏数(是pixiv高级会员才有效)"
70 | },
71 | "s_modeLabel": {
72 | "message": "标签搜索模式"
73 | },
74 | "s_tagOption": {
75 | "message": "标签(部分一致)"
76 | },
77 | "s_tag_fullOption": {
78 | "message": "标签(完全一致)"
79 | },
80 | "s_tcOption": {
81 | "message": "标题、说明文字"
82 | },
83 | "typeLabel": {
84 | "message": "图片类型搜索模式"
85 | },
86 | "allTypeLabel": {
87 | "message": "插画、漫画、动图"
88 | },
89 | "illust_and_ugoiraTypeLabel": {
90 | "message": "插画、动图"
91 | },
92 | "illustTypeLabel": {
93 | "message": "插画"
94 | },
95 | "mangaTypeLabel": {
96 | "message": "漫画"
97 | },
98 | "ugoiraTypeLabel": {
99 | "message": "动图"
100 | },
101 | "leave_blank_for_unlimited": {
102 | "message": "留空表示不限"
103 | },
104 | "slLabel": {
105 | "message": "sl filter(perhaps pixiv safe level, 2 is safe, 6 is not safe)"
106 | },
107 | "min_sl": {
108 | "message": "min sl"
109 | },
110 | "max_sl": {
111 | "message": "max sl"
112 | },
113 | "aiTypeLabel": {
114 | "message": "aiType (pixiv ai type, 1 not ai, 2 is ai, empty is unset)"
115 | },
116 | "save": {
117 | "message": "Save"
118 | },
119 | "reset": {
120 | "message": "Reset"
121 | }
122 | }
--------------------------------------------------------------------------------
/src_firefox/_locales/zh/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "keywordsLabel": {
3 | "message": "完整关键词"
4 | },
5 | "andKeywordsLabel": {
6 | "message": "含有全部关键词(AND搜索)"
7 | },
8 | "orKeywordsLabel": {
9 | "message": "含有某个关键词(OR搜索)"
10 | },
11 | "minusKeywordsLabel": {
12 | "message": "不含有关键词(排除搜索)"
13 | },
14 | "keywordsTip": {
15 | "message": "多个关键词用空格隔开"
16 | },
17 | "orderLabel": {
18 | "message": "排序方式"
19 | },
20 | "date_dOrder": {
21 | "message": "按最新排序"
22 | },
23 | "dateOrder": {
24 | "message": "按旧排序"
25 | },
26 | "popular_dOrder": {
27 | "message": "按热门度排序(是pixiv高级会员才有效)"
28 | },
29 | "popular_male_dOrder": {
30 | "message": "受男性欢迎(是pixiv高级会员才有效)"
31 | },
32 | "popular_female_dOrder": {
33 | "message": "受女性欢迎(是pixiv高级会员才有效)"
34 | },
35 | "modeLabel": {
36 | "message": "搜索模式"
37 | },
38 | "allMode": {
39 | "message": "全部"
40 | },
41 | "r18Mode": {
42 | "message": "R18"
43 | },
44 | "safeMode": {
45 | "message": "全年龄"
46 | },
47 | "timeOptionLabel": {
48 | "message": "时间"
49 | },
50 | "unlimitedTimeOption": {
51 | "message": "不限时间"
52 | },
53 | "specificTimeOption": {
54 | "message": "指定时间"
55 | },
56 | "scdLabel": {
57 | "message": "开始时间"
58 | },
59 | "ecdLabel": {
60 | "message": "结束时间"
61 | },
62 | "bookmarksLabel": {
63 | "message": "收藏数(是pixiv高级会员才有效)"
64 | },
65 | "bltLabel": {
66 | "message": "最低收藏数(是pixiv高级会员才有效)"
67 | },
68 | "bgtLabel": {
69 | "message": "最高收藏数(是pixiv高级会员才有效)"
70 | },
71 | "s_modeLabel": {
72 | "message": "标签搜索模式"
73 | },
74 | "s_tagOption": {
75 | "message": "标签(部分一致)"
76 | },
77 | "s_tag_fullOption": {
78 | "message": "标签(完全一致)"
79 | },
80 | "s_tcOption": {
81 | "message": "标题、说明文字"
82 | },
83 | "typeLabel": {
84 | "message": "图片类型搜索模式"
85 | },
86 | "allTypeLabel": {
87 | "message": "插画、漫画、动图"
88 | },
89 | "illust_and_ugoiraTypeLabel": {
90 | "message": "插画、动图"
91 | },
92 | "illustTypeLabel": {
93 | "message": "插画"
94 | },
95 | "mangaTypeLabel": {
96 | "message": "漫画"
97 | },
98 | "ugoiraTypeLabel": {
99 | "message": "动图"
100 | },
101 | "leave_blank_for_unlimited": {
102 | "message": "留空表示不限"
103 | },
104 | "slLabel": {
105 | "message": "sl filter(perhaps pixiv safe level, 2 is safe, 6 is not safe)"
106 | },
107 | "min_sl": {
108 | "message": "min sl"
109 | },
110 | "max_sl": {
111 | "message": "max sl"
112 | },
113 | "aiTypeLabel": {
114 | "message": "aiType (pixiv ai type, 1 not ai, 2 is ai, empty is unset)"
115 | },
116 | "save": {
117 | "message": "Save"
118 | },
119 | "reset": {
120 | "message": "Reset"
121 | }
122 | }
--------------------------------------------------------------------------------
/src/_locales/ja/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "keywordsLabel": {
3 | "message": "キーワード"
4 | },
5 | "andKeywordsLabel": {
6 | "message": "キーワードをすべて含む"
7 | },
8 | "orKeywordsLabel": {
9 | "message": "キーワードのいずれかを含む"
10 | },
11 | "minusKeywordsLabel": {
12 | "message": "キーワードを含まない"
13 | },
14 | "keywordsTip": {
15 | "message": "スペース区切りで複数のキーワードを指定できます"
16 | },
17 | "orderLabel": {
18 | "message": "並べ替え"
19 | },
20 | "date_dOrder": {
21 | "message": "新しい順"
22 | },
23 | "dateOrder": {
24 | "message": "古い順"
25 | },
26 | "popular_dOrder": {
27 | "message": "全体の人気順 (Pixivプレミアム会員限定)"
28 | },
29 | "popular_male_dOrder": {
30 | "message": "男性の人気順 (Pixivプレミアム会員限定)"
31 | },
32 | "popular_female_dOrder": {
33 | "message": "女性の人気順 (Pixivプレミアム会員限定)"
34 | },
35 | "modeLabel": {
36 | "message": "検索モード"
37 | },
38 | "allMode": {
39 | "message": "すべて"
40 | },
41 | "r18Mode": {
42 | "message": "R-18"
43 | },
44 | "safeMode": {
45 | "message": "全年齢"
46 | },
47 | "timeOptionLabel": {
48 | "message": "期間"
49 | },
50 | "unlimitedTimeOption": {
51 | "message": "すべての期間"
52 | },
53 | "specificTimeOption": {
54 | "message": "日付を指定"
55 | },
56 | "scdLabel": {
57 | "message": "開始日"
58 | },
59 | "ecdLabel": {
60 | "message": "終了日"
61 | },
62 | "bookmarksLabel": {
63 | "message": "ブックマーク数 (Pixivプレミアム会員限定)"
64 | },
65 | "bltLabel": {
66 | "message": "最小ブックマーク数 (Pixivプレミアム会員限定)"
67 | },
68 | "bgtLabel": {
69 | "message": "最大ブックマーク数 (Pixivプレミアム会員限定)"
70 | },
71 | "s_modeLabel": {
72 | "message": "タグ検索モード"
73 | },
74 | "s_tagOption": {
75 | "message": "タグ(部分一致)"
76 | },
77 | "s_tag_fullOption": {
78 | "message": "タグ(完全一致)"
79 | },
80 | "s_tcOption": {
81 | "message": "タイトル・キャプション"
82 | },
83 | "typeLabel": {
84 | "message": "画像タイプ検索モード"
85 | },
86 | "allTypeLabel": {
87 | "message": "イラスト・マンガ・うごくイラスト"
88 | },
89 | "illust_and_ugoiraTypeLabel": {
90 | "message": "イラスト・うごくイラスト"
91 | },
92 | "illustTypeLabel": {
93 | "message": "イラスト"
94 | },
95 | "mangaTypeLabel": {
96 | "message": "マンガ"
97 | },
98 | "ugoiraTypeLabel": {
99 | "message": "うごくイラスト"
100 | },
101 | "leave_blank_for_unlimited": {
102 | "message": "Leave blank for unlimited"
103 | },
104 | "slLabel": {
105 | "message": "sl filter(perhaps pixiv safe level, 2 is safe, 6 is not safe)"
106 | },
107 | "min_sl": {
108 | "message": "min sl"
109 | },
110 | "max_sl": {
111 | "message": "max sl"
112 | },
113 | "aiTypeLabel": {
114 | "message": "aiType (pixiv ai type, 1 not ai, 2 is ai, empty is unset)"
115 | },
116 | "save": {
117 | "message": "Save"
118 | },
119 | "reset": {
120 | "message": "Reset"
121 | }
122 | }
--------------------------------------------------------------------------------
/src_firefox/_locales/ja/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "keywordsLabel": {
3 | "message": "キーワード"
4 | },
5 | "andKeywordsLabel": {
6 | "message": "キーワードをすべて含む"
7 | },
8 | "orKeywordsLabel": {
9 | "message": "キーワードのいずれかを含む"
10 | },
11 | "minusKeywordsLabel": {
12 | "message": "キーワードを含まない"
13 | },
14 | "keywordsTip": {
15 | "message": "スペース区切りで複数のキーワードを指定できます"
16 | },
17 | "orderLabel": {
18 | "message": "並べ替え"
19 | },
20 | "date_dOrder": {
21 | "message": "新しい順"
22 | },
23 | "dateOrder": {
24 | "message": "古い順"
25 | },
26 | "popular_dOrder": {
27 | "message": "全体の人気順 (Pixivプレミアム会員限定)"
28 | },
29 | "popular_male_dOrder": {
30 | "message": "男性の人気順 (Pixivプレミアム会員限定)"
31 | },
32 | "popular_female_dOrder": {
33 | "message": "女性の人気順 (Pixivプレミアム会員限定)"
34 | },
35 | "modeLabel": {
36 | "message": "検索モード"
37 | },
38 | "allMode": {
39 | "message": "すべて"
40 | },
41 | "r18Mode": {
42 | "message": "R-18"
43 | },
44 | "safeMode": {
45 | "message": "全年齢"
46 | },
47 | "timeOptionLabel": {
48 | "message": "期間"
49 | },
50 | "unlimitedTimeOption": {
51 | "message": "すべての期間"
52 | },
53 | "specificTimeOption": {
54 | "message": "日付を指定"
55 | },
56 | "scdLabel": {
57 | "message": "開始日"
58 | },
59 | "ecdLabel": {
60 | "message": "終了日"
61 | },
62 | "bookmarksLabel": {
63 | "message": "ブックマーク数 (Pixivプレミアム会員限定)"
64 | },
65 | "bltLabel": {
66 | "message": "最小ブックマーク数 (Pixivプレミアム会員限定)"
67 | },
68 | "bgtLabel": {
69 | "message": "最大ブックマーク数 (Pixivプレミアム会員限定)"
70 | },
71 | "s_modeLabel": {
72 | "message": "タグ検索モード"
73 | },
74 | "s_tagOption": {
75 | "message": "タグ(部分一致)"
76 | },
77 | "s_tag_fullOption": {
78 | "message": "タグ(完全一致)"
79 | },
80 | "s_tcOption": {
81 | "message": "タイトル・キャプション"
82 | },
83 | "typeLabel": {
84 | "message": "画像タイプ検索モード"
85 | },
86 | "allTypeLabel": {
87 | "message": "イラスト・マンガ・うごくイラスト"
88 | },
89 | "illust_and_ugoiraTypeLabel": {
90 | "message": "イラスト・うごくイラスト"
91 | },
92 | "illustTypeLabel": {
93 | "message": "イラスト"
94 | },
95 | "mangaTypeLabel": {
96 | "message": "マンガ"
97 | },
98 | "ugoiraTypeLabel": {
99 | "message": "うごくイラスト"
100 | },
101 | "leave_blank_for_unlimited": {
102 | "message": "Leave blank for unlimited"
103 | },
104 | "slLabel": {
105 | "message": "sl filter(perhaps pixiv safe level, 2 is safe, 6 is not safe)"
106 | },
107 | "min_sl": {
108 | "message": "min sl"
109 | },
110 | "max_sl": {
111 | "message": "max sl"
112 | },
113 | "aiTypeLabel": {
114 | "message": "aiType (pixiv ai type, 1 not ai, 2 is ai, empty is unset)"
115 | },
116 | "save": {
117 | "message": "Save"
118 | },
119 | "reset": {
120 | "message": "Reset"
121 | }
122 | }
--------------------------------------------------------------------------------
/src/_locales/en/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "keywordsLabel": {
3 | "message": "Full Keywords"
4 | },
5 | "andKeywordsLabel": {
6 | "message": "Contains All Keywords (AND Search)"
7 | },
8 | "orKeywordsLabel": {
9 | "message": "Contains Any Keyword (OR Search)"
10 | },
11 | "minusKeywordsLabel": {
12 | "message": "Does Not Contain Keywords (Exclude Search)"
13 | },
14 | "keywordsTip": {
15 | "message": "You can specify multiple keywords by separating them with a space"
16 | },
17 | "orderLabel": {
18 | "message": "Sort Order"
19 | },
20 | "date_dOrder": {
21 | "message": "Sort by Newest"
22 | },
23 | "dateOrder": {
24 | "message": "Sort by Oldest"
25 | },
26 | "popular_dOrder": {
27 | "message": "Sort by Popularity (Only for Pixiv Premium Members)"
28 | },
29 | "popular_male_dOrder": {
30 | "message": "Popular Among Males (Only for Pixiv Premium Members)"
31 | },
32 | "popular_female_dOrder": {
33 | "message": "Popular Among Females (Only for Pixiv Premium Members)"
34 | },
35 | "modeLabel": {
36 | "message": "Search Mode"
37 | },
38 | "allMode": {
39 | "message": "All"
40 | },
41 | "r18Mode": {
42 | "message": "R18"
43 | },
44 | "safeMode": {
45 | "message": "All ages"
46 | },
47 | "timeOptionLabel": {
48 | "message": "Time"
49 | },
50 | "unlimitedTimeOption": {
51 | "message": "All Time"
52 | },
53 | "specificTimeOption": {
54 | "message": "Specific Time"
55 | },
56 | "scdLabel": {
57 | "message": "Start Date"
58 | },
59 | "ecdLabel": {
60 | "message": "End Date"
61 | },
62 | "bookmarksLabel": {
63 | "message": "Bookmarks Count (Only for Pixiv Premium Members)"
64 | },
65 | "bltLabel": {
66 | "message": "Minimum Bookmarks Count (Only for Pixiv Premium Members)"
67 | },
68 | "bgtLabel": {
69 | "message": "Maximum Bookmarks Count (Only for Pixiv Premium Members)"
70 | },
71 | "s_modeLabel": {
72 | "message": "Tag Search Mode"
73 | },
74 | "s_tagOption": {
75 | "message": "Tag (Partial Match)"
76 | },
77 | "s_tag_fullOption": {
78 | "message": "Tag (Exact Match)"
79 | },
80 | "s_tcOption": {
81 | "message": "Title, Caption"
82 | },
83 | "typeLabel": {
84 | "message": "Image Type Search Mode"
85 | },
86 | "allTypeLabel": {
87 | "message": "Illustrations, Manga, Ugoira"
88 | },
89 | "illust_and_ugoiraTypeLabel": {
90 | "message": "Illustrations, Ugoira"
91 | },
92 | "illustTypeLabel": {
93 | "message": "Illustrations"
94 | },
95 | "mangaTypeLabel": {
96 | "message": "Manga"
97 | },
98 | "ugoiraTypeLabel": {
99 | "message": "Ugoira"
100 | },
101 | "leave_blank_for_unlimited": {
102 | "message": "Leave blank for unlimited"
103 | },
104 | "slLabel": {
105 | "message": "sl filter(perhaps pixiv safe level, 2 is safe, 6 is not safe)"
106 | },
107 | "min_sl": {
108 | "message": "min sl"
109 | },
110 | "max_sl": {
111 | "message": "max sl"
112 | },
113 | "aiTypeLabel": {
114 | "message": "aiType (pixiv ai type, 1 not ai, 2 is ai, empty is unset)"
115 | },
116 | "save": {
117 | "message": "Save"
118 | },
119 | "reset": {
120 | "message": "Reset"
121 | }
122 | }
--------------------------------------------------------------------------------
/src_firefox/_locales/en/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "keywordsLabel": {
3 | "message": "Full Keywords"
4 | },
5 | "andKeywordsLabel": {
6 | "message": "Contains All Keywords (AND Search)"
7 | },
8 | "orKeywordsLabel": {
9 | "message": "Contains Any Keyword (OR Search)"
10 | },
11 | "minusKeywordsLabel": {
12 | "message": "Does Not Contain Keywords (Exclude Search)"
13 | },
14 | "keywordsTip": {
15 | "message": "You can specify multiple keywords by separating them with a space"
16 | },
17 | "orderLabel": {
18 | "message": "Sort Order"
19 | },
20 | "date_dOrder": {
21 | "message": "Sort by Newest"
22 | },
23 | "dateOrder": {
24 | "message": "Sort by Oldest"
25 | },
26 | "popular_dOrder": {
27 | "message": "Sort by Popularity (Only for Pixiv Premium Members)"
28 | },
29 | "popular_male_dOrder": {
30 | "message": "Popular Among Males (Only for Pixiv Premium Members)"
31 | },
32 | "popular_female_dOrder": {
33 | "message": "Popular Among Females (Only for Pixiv Premium Members)"
34 | },
35 | "modeLabel": {
36 | "message": "Search Mode"
37 | },
38 | "allMode": {
39 | "message": "All"
40 | },
41 | "r18Mode": {
42 | "message": "R18"
43 | },
44 | "safeMode": {
45 | "message": "All ages"
46 | },
47 | "timeOptionLabel": {
48 | "message": "Time"
49 | },
50 | "unlimitedTimeOption": {
51 | "message": "All Time"
52 | },
53 | "specificTimeOption": {
54 | "message": "Specific Time"
55 | },
56 | "scdLabel": {
57 | "message": "Start Date"
58 | },
59 | "ecdLabel": {
60 | "message": "End Date"
61 | },
62 | "bookmarksLabel": {
63 | "message": "Bookmarks Count (Only for Pixiv Premium Members)"
64 | },
65 | "bltLabel": {
66 | "message": "Minimum Bookmarks Count (Only for Pixiv Premium Members)"
67 | },
68 | "bgtLabel": {
69 | "message": "Maximum Bookmarks Count (Only for Pixiv Premium Members)"
70 | },
71 | "s_modeLabel": {
72 | "message": "Tag Search Mode"
73 | },
74 | "s_tagOption": {
75 | "message": "Tag (Partial Match)"
76 | },
77 | "s_tag_fullOption": {
78 | "message": "Tag (Exact Match)"
79 | },
80 | "s_tcOption": {
81 | "message": "Title, Caption"
82 | },
83 | "typeLabel": {
84 | "message": "Image Type Search Mode"
85 | },
86 | "allTypeLabel": {
87 | "message": "Illustrations, Manga, Ugoira"
88 | },
89 | "illust_and_ugoiraTypeLabel": {
90 | "message": "Illustrations, Ugoira"
91 | },
92 | "illustTypeLabel": {
93 | "message": "Illustrations"
94 | },
95 | "mangaTypeLabel": {
96 | "message": "Manga"
97 | },
98 | "ugoiraTypeLabel": {
99 | "message": "Ugoira"
100 | },
101 | "leave_blank_for_unlimited": {
102 | "message": "Leave blank for unlimited"
103 | },
104 | "slLabel": {
105 | "message": "sl filter(perhaps pixiv safe level, 2 is safe, 6 is not safe)"
106 | },
107 | "min_sl": {
108 | "message": "min sl"
109 | },
110 | "max_sl": {
111 | "message": "max sl"
112 | },
113 | "aiTypeLabel": {
114 | "message": "aiType (pixiv ai type, 1 not ai, 2 is ai, empty is unset)"
115 | },
116 | "save": {
117 | "message": "Save"
118 | },
119 | "reset": {
120 | "message": "Reset"
121 | }
122 | }
--------------------------------------------------------------------------------
/src/style.css:
--------------------------------------------------------------------------------
1 | #illustInfo a:active,
2 | #illustInfo a:hover,
3 | #illustInfo a:link,
4 | #illustInfo a:visited {
5 | text-decoration: none;
6 | color: #fff;
7 | }
8 |
9 | body {
10 | margin: 0;
11 | padding: 0;
12 | border: 0;
13 | }
14 |
15 | .notReady {
16 | visibility: hidden;
17 | }
18 |
19 | .maskLayer {
20 | position: fixed;
21 | width: 100%;
22 | height: 100%;
23 | }
24 |
25 | #container {
26 | left: 0;
27 | right: 0;
28 | top: 0;
29 | bottom: 0;
30 | }
31 |
32 | #wallpaper {
33 | left: 0;
34 | right: 0;
35 | top: 0;
36 | bottom: 0;
37 | z-index: -1;
38 | }
39 |
40 | #backgroundImage {
41 | position: fixed;
42 | width: 100%;
43 | height: 100%;
44 | background-size: cover;
45 | background-repeat: no-repeat;
46 | background-position: center center;
47 | z-index: inherit;
48 | transition: background 0.3s;
49 | }
50 |
51 | #foregroundImage {
52 | position: fixed;
53 | width: 100%;
54 | height: 100%;
55 | background-size: contain;
56 | background-repeat: no-repeat;
57 | background-position: center center;
58 | backdrop-filter: blur(5px);
59 | z-index: inherit;
60 | transition: background 0.3s;
61 | }
62 |
63 | #refreshButton {
64 | display: inline-block;
65 | border-radius: 100%;
66 | width: 18px;
67 | height: 18px;
68 | margin-top: 16px;
69 | margin-left: 5px;
70 | vertical-align: top;
71 | cursor: pointer;
72 | transition: background-color 0.5s;
73 | }
74 |
75 | #refreshButton:hover {
76 | background-color: rgba(23, 24, 25, 1);
77 | }
78 |
79 | #refreshButton.pressed {
80 | animation-name: beginRotation;
81 | animation-fill-mode: forwards;
82 | animation-duration: 0.2s;
83 | }
84 |
85 | #refreshButton.unpressed {
86 | animation-name: endRotation;
87 | animation-fill-mode: forwards;
88 | animation-duration: 0.3s;
89 | }
90 |
91 | @keyframes beginRotation {
92 | from {
93 | transform: rotate(0deg);
94 | }
95 |
96 | to {
97 | transform: rotate(45deg);
98 | }
99 | }
100 |
101 | @keyframes endRotation {
102 | from {
103 | transform: rotate(45deg);
104 | }
105 |
106 | to {
107 | transform: rotate(360deg);
108 | }
109 | }
110 |
111 | #illustInfo {
112 | display: block;
113 | position: fixed;
114 | width: 220px;
115 | height: 50px;
116 | bottom: 38px;
117 | right: 8px;
118 | font-family: HiraginoSans-W3, Hiragino Kaku Gothic Pro, Meiryo, Verdana,
119 | sans-serif;
120 | border-radius: 4px;
121 | background-color: rgba(23, 24, 25, 0.3);
122 | color: #fff;
123 | transition: opacity 1s;
124 | }
125 |
126 | #illustInfo.unfocused {
127 | opacity: 0;
128 | }
129 |
130 | #inllustInfo.focused {
131 | opacity: 1;
132 | }
133 |
134 | #avatarImage {
135 | display: inline-block;
136 | width: 40px;
137 | height: 40px;
138 | border-radius: 50%;
139 | margin-top: 5px;
140 | margin-left: 5px;
141 | }
142 |
143 | #description {
144 | display: inline-block;
145 | margin-top: 5px;
146 | margin-left: 5px;
147 | vertical-align: top;
148 | width: 135px;
149 | }
150 |
151 | #illustTitle {
152 | display: block;
153 | font-size: 14px;
154 | overflow: hidden;
155 | text-overflow: ellipsis;
156 | white-space: nowrap;
157 | }
158 |
159 | #illustName {
160 | display: block;
161 | font-size: 12px;
162 | overflow: hidden;
163 | text-overflow: ellipsis;
164 | white-space: nowrap;
165 | }
--------------------------------------------------------------------------------
/src_firefox/style.css:
--------------------------------------------------------------------------------
1 | #illustInfo a:active,
2 | #illustInfo a:hover,
3 | #illustInfo a:link,
4 | #illustInfo a:visited {
5 | text-decoration: none;
6 | color: #fff;
7 | }
8 |
9 | body {
10 | margin: 0;
11 | padding: 0;
12 | border: 0;
13 | }
14 |
15 | .notReady {
16 | visibility: hidden;
17 | }
18 |
19 | .maskLayer {
20 | position: fixed;
21 | width: 100%;
22 | height: 100%;
23 | }
24 |
25 | #container {
26 | left: 0;
27 | right: 0;
28 | top: 0;
29 | bottom: 0;
30 | }
31 |
32 | #wallpaper {
33 | left: 0;
34 | right: 0;
35 | top: 0;
36 | bottom: 0;
37 | z-index: -1;
38 | }
39 |
40 | #backgroundImage {
41 | position: fixed;
42 | width: 100%;
43 | height: 100%;
44 | background-size: cover;
45 | background-repeat: no-repeat;
46 | background-position: center center;
47 | z-index: inherit;
48 | transition: background 0.3s;
49 | }
50 |
51 | #foregroundImage {
52 | position: fixed;
53 | width: 100%;
54 | height: 100%;
55 | background-size: contain;
56 | background-repeat: no-repeat;
57 | background-position: center center;
58 | backdrop-filter: blur(5px);
59 | z-index: inherit;
60 | transition: background 0.3s;
61 | }
62 |
63 | #refreshButton {
64 | display: inline-block;
65 | border-radius: 100%;
66 | width: 18px;
67 | height: 18px;
68 | margin-top: 16px;
69 | margin-left: 5px;
70 | vertical-align: top;
71 | cursor: pointer;
72 | transition: background-color 0.5s;
73 | }
74 |
75 | #refreshButton:hover {
76 | background-color: rgba(23, 24, 25, 1);
77 | }
78 |
79 | #refreshButton.pressed {
80 | animation-name: beginRotation;
81 | animation-fill-mode: forwards;
82 | animation-duration: 0.2s;
83 | }
84 |
85 | #refreshButton.unpressed {
86 | animation-name: endRotation;
87 | animation-fill-mode: forwards;
88 | animation-duration: 0.3s;
89 | }
90 |
91 | @keyframes beginRotation {
92 | from {
93 | transform: rotate(0deg);
94 | }
95 |
96 | to {
97 | transform: rotate(45deg);
98 | }
99 | }
100 |
101 | @keyframes endRotation {
102 | from {
103 | transform: rotate(45deg);
104 | }
105 |
106 | to {
107 | transform: rotate(360deg);
108 | }
109 | }
110 |
111 | #illustInfo {
112 | display: block;
113 | position: fixed;
114 | width: 220px;
115 | height: 50px;
116 | bottom: 38px;
117 | right: 8px;
118 | font-family: HiraginoSans-W3, Hiragino Kaku Gothic Pro, Meiryo, Verdana,
119 | sans-serif;
120 | border-radius: 4px;
121 | background-color: rgba(23, 24, 25, 0.3);
122 | color: #fff;
123 | transition: opacity 1s;
124 | }
125 |
126 | #illustInfo.unfocused {
127 | opacity: 0;
128 | }
129 |
130 | #inllustInfo.focused {
131 | opacity: 1;
132 | }
133 |
134 | #avatarImage {
135 | display: inline-block;
136 | width: 40px;
137 | height: 40px;
138 | border-radius: 50%;
139 | margin-top: 5px;
140 | margin-left: 5px;
141 | }
142 |
143 | #description {
144 | display: inline-block;
145 | margin-top: 5px;
146 | margin-left: 5px;
147 | vertical-align: top;
148 | width: 135px;
149 | }
150 |
151 | #illustTitle {
152 | display: block;
153 | font-size: 14px;
154 | overflow: hidden;
155 | text-overflow: ellipsis;
156 | white-space: nowrap;
157 | }
158 |
159 | #illustName {
160 | display: block;
161 | font-size: 12px;
162 | overflow: hidden;
163 | text-overflow: ellipsis;
164 | white-space: nowrap;
165 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | class Binding {
3 | constructor() {
4 | const bgElement = document.body.querySelector("#backgroundImage");
5 | const fgImageElement = document.body.querySelector("#foregroundImage");
6 | const avatarElement = document.body.querySelector("#avatar");
7 | const avatarImageElement = document.body.querySelector("#avatarImage");
8 | const illustTitleElement = document.body.querySelector("#illustTitle");
9 | const illustNameElement = document.body.querySelector("#illustName");
10 | const refreshElement = document.body.querySelector("#refreshButton");
11 | const containerElement = document.body.querySelector("#container");
12 | const wallpaperElement = document.body.querySelector("#wallpaper");
13 | const illustInfoElement = document.body.querySelector("#illustInfo");
14 | this.containerElement = containerElement;
15 | this.illustInfoElement = illustInfoElement;
16 |
17 | const userNameBinding = (v) => {
18 | avatarImageElement.title = v;
19 | let e = illustNameElement.querySelector("a");
20 | e.text = v;
21 | let sw = e.scrollWidth;
22 | if (sw > 133) {
23 | let cutIndex = Math.floor((e.text.length * 123) / sw);
24 | e.text = v.slice(0, cutIndex) + "...";
25 | }
26 | };
27 | const userIdUrlBinding = (v) => {
28 | illustNameElement.querySelector("a").href = v;
29 | };
30 | const titleBinding = (v) => {
31 | illustTitleElement.title = v;
32 | let e = illustTitleElement.querySelector("a");
33 | e.text = v;
34 | let sw = e.scrollWidth;
35 | if (sw > 133) {
36 | let cutIndex = Math.floor((e.text.length * 123) / sw);
37 | e.text = v.slice(0, cutIndex) + "...";
38 | }
39 | };
40 | const illustIdUrlBinding = (v) => {
41 | illustTitleElement.querySelector("a").href = v;
42 | };
43 | const avatarBinding = (v) => {
44 | avatarElement.href = v;
45 | };
46 | const avatarImageBinding = (v) => {
47 | avatarImageElement.style["background-image"] = `url(${v})`;
48 | };
49 | const bgImageBinding = (v) => {
50 | bgElement.style["background-image"] = `url(${v})`;
51 | };
52 | const fgElementBinding = (v) => {
53 | fgImageElement.style["background-image"] = `url(${v})`;
54 | };
55 | this.ref = {
56 | userName: [userNameBinding],
57 | userIdUrl: [userIdUrlBinding, avatarBinding],
58 | illustIdUrl: [illustIdUrlBinding],
59 | title: [titleBinding],
60 | profileImageUrl: [avatarImageBinding],
61 | imageObjectUrl: [bgImageBinding, fgElementBinding],
62 | };
63 |
64 | refreshElement.addEventListener("mousedown", () => {
65 | refreshElement.className = "pressed";
66 | });
67 | refreshElement.addEventListener("mouseup", () => {
68 | refreshElement.className = "unpressed";
69 | });
70 | refreshElement.addEventListener("click", sendRefreshMessage);
71 | this.illustInfoFadeOutTimeoutId = null;
72 | illustInfoElement.addEventListener("mouseleave", () => {
73 | this.illustInfoFadeOutTimeoutId = setTimeout(() => {
74 | this.illustInfoElement.className = "unfocused";
75 | }, 10000);
76 | });
77 | illustInfoElement.addEventListener("mouseenter", () => {
78 | illustInfoElement.className = "focused";
79 | clearTimeout(this.illustInfoFadeOutTimeoutId);
80 | });
81 | illustInfoElement.addEventListener("mouseover", () => {
82 | illustInfoElement.className = "focused";
83 | clearTimeout(this.illustInfoFadeOutTimeoutId);
84 | });
85 | }
86 | }
87 | var binding = null;
88 | function initApplication() {
89 | binding = new Binding();
90 | }
91 |
92 | async function changeElement(illustObject) {
93 | if (!illustObject) { return; }
94 | for (let k in binding.ref) {
95 | if (illustObject.hasOwnProperty(k)) {
96 | let value = illustObject[k];
97 | if (value === null || value === undefined) {
98 | if (k === 'userName' || k === 'title') value = '';
99 | }
100 | for (let o of binding.ref[k]) {
101 | o(value);
102 | }
103 | }
104 | }
105 | binding.containerElement.classList.toggle("notReady", false);
106 | clearTimeout(binding.illustInfoFadeOutTimeoutId);
107 | binding.illustInfoFadeOutTimeoutId = setTimeout(() => {
108 | binding.illustInfoElement.className = "unfocused";
109 | }, 10000);
110 | }
111 |
112 | const sendRefreshMessage = (() => {
113 | let isRequestInProgress = false;
114 | return () => {
115 | if (isRequestInProgress) {
116 | return;
117 | }
118 | isRequestInProgress = true;
119 | chrome.runtime.sendMessage({ action: "fetchImage" }, (res) => {
120 | if (chrome.runtime.lastError) {
121 | console.warn("Context invalidated, message could not be processed:", chrome.runtime.lastError.message);
122 | isRequestInProgress = false;
123 | return;
124 | }
125 | changeElement(res).finally(() => {
126 | isRequestInProgress = false;
127 | });
128 | });
129 | };
130 | })();
131 |
132 | initApplication();
133 | sendRefreshMessage();
134 | console.log("content script loaded");
135 | })();
136 |
--------------------------------------------------------------------------------
/src_firefox/index.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | class Binding {
3 | constructor() {
4 | const bgElement = document.body.querySelector("#backgroundImage");
5 | const fgImageElement = document.body.querySelector("#foregroundImage");
6 | const avatarElement = document.body.querySelector("#avatar");
7 | const avatarImageElement = document.body.querySelector("#avatarImage");
8 | const illustTitleElement = document.body.querySelector("#illustTitle");
9 | const illustNameElement = document.body.querySelector("#illustName");
10 | const refreshElement = document.body.querySelector("#refreshButton");
11 | const containerElement = document.body.querySelector("#container");
12 | const wallpaperElement = document.body.querySelector("#wallpaper");
13 | const illustInfoElement = document.body.querySelector("#illustInfo");
14 | this.containerElement = containerElement;
15 | this.illustInfoElement = illustInfoElement;
16 |
17 | const userNameBinding = (v) => {
18 | avatarImageElement.title = v;
19 | let e = illustNameElement.querySelector("a");
20 | e.text = v;
21 | let sw = e.scrollWidth;
22 | if (sw > 133) {
23 | let cutIndex = Math.floor((e.text.length * 123) / sw);
24 | e.text = v.slice(0, cutIndex) + "...";
25 | }
26 | };
27 | const userIdUrlBinding = (v) => {
28 | illustNameElement.querySelector("a").href = v;
29 | };
30 | const titleBinding = (v) => {
31 | illustTitleElement.title = v;
32 | let e = illustTitleElement.querySelector("a");
33 | e.text = v;
34 | let sw = e.scrollWidth;
35 | if (sw > 133) {
36 | let cutIndex = Math.floor((e.text.length * 123) / sw);
37 | e.text = v.slice(0, cutIndex) + "...";
38 | }
39 | };
40 | const illustIdUrlBinding = (v) => {
41 | illustTitleElement.querySelector("a").href = v;
42 | };
43 | const avatarBinding = (v) => {
44 | avatarElement.href = v;
45 | };
46 | const avatarImageBinding = (v) => {
47 | avatarImageElement.style["background-image"] = `url(${v})`;
48 | };
49 | const bgImageBinding = (v) => {
50 | bgElement.style["background-image"] = `url(${v})`;
51 | };
52 | const fgElementBinding = (v) => {
53 | fgImageElement.style["background-image"] = `url(${v})`;
54 | };
55 | this.ref = {
56 | userName: [userNameBinding],
57 | userIdUrl: [userIdUrlBinding, avatarBinding],
58 | illustIdUrl: [illustIdUrlBinding],
59 | title: [titleBinding],
60 | profileImageUrl: [avatarImageBinding],
61 | imageObjectUrl: [bgImageBinding, fgElementBinding],
62 | };
63 |
64 | refreshElement.addEventListener("mousedown", () => {
65 | refreshElement.className = "pressed";
66 | });
67 | refreshElement.addEventListener("mouseup", () => {
68 | refreshElement.className = "unpressed";
69 | });
70 | refreshElement.addEventListener("click", sendRefreshMessage);
71 | this.illustInfoFadeOutTimeoutId = null;
72 | illustInfoElement.addEventListener("mouseleave", () => {
73 | this.illustInfoFadeOutTimeoutId = setTimeout(() => {
74 | this.illustInfoElement.className = "unfocused";
75 | }, 10000);
76 | });
77 | illustInfoElement.addEventListener("mouseenter", () => {
78 | illustInfoElement.className = "focused";
79 | clearTimeout(this.illustInfoFadeOutTimeoutId);
80 | });
81 | illustInfoElement.addEventListener("mouseover", () => {
82 | illustInfoElement.className = "focused";
83 | clearTimeout(this.illustInfoFadeOutTimeoutId);
84 | });
85 | }
86 | }
87 | var binding = null;
88 | function initApplication() {
89 | binding = new Binding();
90 | }
91 |
92 | async function changeElement(illustObject) {
93 | if (!illustObject) { return; }
94 | for (let k in binding.ref) {
95 | if (illustObject.hasOwnProperty(k)) {
96 | let value = illustObject[k];
97 | if (value === null || value === undefined) {
98 | if (k === 'userName' || k === 'title') value = '';
99 | }
100 | for (let o of binding.ref[k]) {
101 | o(value);
102 | }
103 | }
104 | }
105 | binding.containerElement.classList.toggle("notReady", false);
106 | clearTimeout(binding.illustInfoFadeOutTimeoutId);
107 | binding.illustInfoFadeOutTimeoutId = setTimeout(() => {
108 | binding.illustInfoElement.className = "unfocused";
109 | }, 10000);
110 | }
111 |
112 | const sendRefreshMessage = (() => {
113 | let isRequestInProgress = false;
114 | return () => {
115 | if (isRequestInProgress) {
116 | return;
117 | }
118 | isRequestInProgress = true;
119 | chrome.runtime.sendMessage({ action: "fetchImage" }, (res) => {
120 | if (chrome.runtime.lastError) {
121 | console.warn("Context invalidated, message could not be processed:", chrome.runtime.lastError.message);
122 | isRequestInProgress = false;
123 | return;
124 | }
125 | changeElement(res).finally(() => {
126 | isRequestInProgress = false;
127 | });
128 | });
129 | };
130 | })();
131 |
132 | initApplication();
133 | sendRefreshMessage();
134 | console.log("content script loaded");
135 | })();
136 |
--------------------------------------------------------------------------------
/src/options.js:
--------------------------------------------------------------------------------
1 | import { defaultConfig, getKeywords } from "./config.js";
2 |
3 | const saveOptions = () => {
4 | updateKeywords();
5 | const newConfig = {
6 | order: document.getElementById('order').value,
7 | mode: document.getElementById('mode').value,
8 | timeOption: document.getElementById('timeOption').value,
9 | scd: document.getElementById('scd').value || null,
10 | ecd: document.getElementById('ecd').value || null,
11 | blt: document.getElementById('blt').value ? Number(document.getElementById('blt').value) : null,
12 | bgt: document.getElementById('bgt').value ? Number(document.getElementById('bgt').value) : null,
13 | s_mode: document.getElementById('s_mode').value,
14 | type: document.getElementById('type').value,
15 | min_sl: document.getElementById('min_sl').value ? Number(document.getElementById('min_sl').value) : null,
16 | max_sl: document.getElementById('max_sl').value ? Number(document.getElementById('max_sl').value) : null,
17 | aiType: document.getElementById('aiType').value ? Number(document.getElementById('aiType').value) : null,
18 | orKeywords: document.getElementById('orKeywords').value.trim(),
19 | minusKeywords: document.getElementById('minusKeywords').value.trim(),
20 | andKeywords: document.getElementById('andKeywords').value.trim(),
21 | keywords: document.getElementById('keywords').value.trim()
22 | };
23 |
24 | chrome.storage.local.set(
25 | newConfig,
26 | () => {
27 | const status = document.getElementById('status');
28 | status.textContent = 'Options saved.';
29 | setTimeout(() => {
30 | status.textContent = '';
31 | }, 1000);
32 | console.log("Save config");
33 | console.log(newConfig);
34 | }
35 | );
36 |
37 | chrome.runtime.sendMessage({ action: "updateConfig" }, (response) => {
38 | let lastError = chrome.runtime.lastError;
39 | if (lastError) {
40 | console.log(lastError.message);
41 | return;
42 | }
43 | });
44 | };
45 |
46 | const resetOptions = () => {
47 | chrome.storage.local.set(
48 | defaultConfig,
49 | () => {
50 | console.log("Reset config");
51 | console.log(defaultConfig);
52 | let items = defaultConfig;
53 | document.getElementById('order').value = items.order;
54 | document.getElementById('mode').value = items.mode;
55 | document.getElementById('timeOption').value = items.timeOption;
56 | toggleDateInputs(items.timeOption);
57 | document.getElementById('scd').value = items.scd;
58 | document.getElementById('ecd').value = items.ecd;
59 | document.getElementById('blt').value = items.blt;
60 | document.getElementById('bgt').value = items.bgt;
61 | document.getElementById('s_mode').value = items.s_mode;
62 | document.getElementById('type').value = items.type;
63 | document.getElementById('min_sl').value = items.min_sl;
64 | document.getElementById('max_sl').value = items.max_sl;
65 | document.getElementById('aiType').value = items.aiType;
66 | document.getElementById('andKeywords').value = items.andKeywords;
67 | document.getElementById('orKeywords').value = items.orKeywords;
68 | document.getElementById('minusKeywords').value = items.minusKeywords;
69 | updateKeywords();
70 | const status = document.getElementById('status');
71 | status.textContent = 'Options reset.';
72 | setTimeout(() => {
73 | status.textContent = '';
74 | }, 1000);
75 | console.log("Reset config");
76 | console.log(items);
77 | }
78 | );
79 | };
80 |
81 |
82 | const restoreOptions = () => {
83 | chrome.storage.local.get(defaultConfig, (items) => {
84 | console.log("Load config");
85 | console.log(items);
86 | document.getElementById('order').value = items.order;
87 | document.getElementById('mode').value = items.mode;
88 | document.getElementById('timeOption').value = items.timeOption;
89 | toggleDateInputs(items.timeOption);
90 | document.getElementById('scd').value = items.scd;
91 | document.getElementById('ecd').value = items.ecd;
92 | document.getElementById('blt').value = items.blt;
93 | document.getElementById('bgt').value = items.bgt;
94 | document.getElementById('s_mode').value = items.s_mode;
95 | document.getElementById('type').value = items.type;
96 | document.getElementById('min_sl').value = items.min_sl;
97 | document.getElementById('max_sl').value = items.max_sl;
98 | document.getElementById('aiType').value = items.aiType === null ? '' : items.aiType;
99 | document.getElementById('andKeywords').value = items.andKeywords;
100 | document.getElementById('orKeywords').value = items.orKeywords;
101 | document.getElementById('minusKeywords').value = items.minusKeywords;
102 | updateKeywords();
103 | });
104 | };
105 |
106 | function toggleDateInputs(option) {
107 | const dateInputs = document.getElementById('dateInputs');
108 | if (option === "specific") {
109 | dateInputs.style.display = "block";
110 | } else {
111 | dateInputs.style.display = "none";
112 | document.getElementById('scd').value = "";
113 | document.getElementById('ecd').value = "";
114 | }
115 | }
116 |
117 | function updateKeywords() {
118 | let andKeywords = document.getElementById('andKeywords').value;
119 | let orKeywords = document.getElementById('orKeywords').value;
120 | let minusKeywords = document.getElementById('minusKeywords').value;
121 | let word = getKeywords(andKeywords, orKeywords, minusKeywords);
122 | document.getElementById('keywords').value = word;
123 | }
124 |
125 | document.getElementById('timeOption').addEventListener('change', function () { toggleDateInputs(this.value); });
126 | document.getElementById('orKeywords').addEventListener('input', updateKeywords);
127 | document.getElementById('minusKeywords').addEventListener('input', updateKeywords);
128 | document.getElementById('andKeywords').addEventListener('input', updateKeywords);
129 | document.addEventListener('DOMContentLoaded', restoreOptions);
130 | document.getElementById('save').addEventListener('click', saveOptions);
131 | document.getElementById('reset').addEventListener('click', resetOptions);
132 |
133 | document.addEventListener("DOMContentLoaded", function () {
134 | const langSelect = document.getElementById("languageSelect");
135 | const defaultLang = localStorage.getItem("language") || "en";
136 | langSelect.value = defaultLang;
137 |
138 | function loadTranslations(lang) {
139 | fetch(`_locales/${lang}/messages.json`)
140 | .then((response) => response.json())
141 | .then((data) => {
142 | document.querySelectorAll("[id]").forEach((el) => {
143 | if (data[el.id]) {
144 | el.textContent = data[el.id].message;
145 | }
146 | });
147 | document.querySelectorAll("[data-i18n-placeholder]").forEach((el) => {
148 | const key = el.getAttribute("data-i18n-placeholder");
149 | if (data[key]) {
150 | el.placeholder = data[key].message;
151 | }
152 | });
153 | })
154 | .catch((error) => console.error("Error loading translations:", error));
155 | }
156 |
157 | loadTranslations(defaultLang);
158 |
159 | langSelect.addEventListener("change", (event) => {
160 | const selectedLang = event.target.value;
161 | localStorage.setItem("language", selectedLang);
162 | loadTranslations(selectedLang);
163 | });
164 | });
165 |
--------------------------------------------------------------------------------
/src_firefox/options.js:
--------------------------------------------------------------------------------
1 | import { defaultConfig, getKeywords } from "./config.js";
2 |
3 | const saveOptions = () => {
4 | updateKeywords();
5 | const newConfig = {
6 | order: document.getElementById('order').value,
7 | mode: document.getElementById('mode').value,
8 | timeOption: document.getElementById('timeOption').value,
9 | scd: document.getElementById('scd').value || null,
10 | ecd: document.getElementById('ecd').value || null,
11 | blt: document.getElementById('blt').value ? Number(document.getElementById('blt').value) : null,
12 | bgt: document.getElementById('bgt').value ? Number(document.getElementById('bgt').value) : null,
13 | s_mode: document.getElementById('s_mode').value,
14 | type: document.getElementById('type').value,
15 | min_sl: document.getElementById('min_sl').value ? Number(document.getElementById('min_sl').value) : null,
16 | max_sl: document.getElementById('max_sl').value ? Number(document.getElementById('max_sl').value) : null,
17 | aiType: document.getElementById('aiType').value ? Number(document.getElementById('aiType').value) : null,
18 | orKeywords: document.getElementById('orKeywords').value.trim(),
19 | minusKeywords: document.getElementById('minusKeywords').value.trim(),
20 | andKeywords: document.getElementById('andKeywords').value.trim(),
21 | keywords: document.getElementById('keywords').value.trim()
22 | };
23 |
24 | chrome.storage.local.set(
25 | newConfig,
26 | () => {
27 | const status = document.getElementById('status');
28 | status.textContent = 'Options saved.';
29 | setTimeout(() => {
30 | status.textContent = '';
31 | }, 1000);
32 | console.log("Save config");
33 | console.log(newConfig);
34 | }
35 | );
36 |
37 | chrome.runtime.sendMessage({ action: "updateConfig" }, (response) => {
38 | let lastError = chrome.runtime.lastError;
39 | if (lastError) {
40 | console.log(lastError.message);
41 | return;
42 | }
43 | });
44 | };
45 |
46 | const resetOptions = () => {
47 | chrome.storage.local.set(
48 | defaultConfig,
49 | () => {
50 | console.log("Reset config");
51 | console.log(defaultConfig);
52 | let items = defaultConfig;
53 | document.getElementById('order').value = items.order;
54 | document.getElementById('mode').value = items.mode;
55 | document.getElementById('timeOption').value = items.timeOption;
56 | toggleDateInputs(items.timeOption);
57 | document.getElementById('scd').value = items.scd;
58 | document.getElementById('ecd').value = items.ecd;
59 | document.getElementById('blt').value = items.blt;
60 | document.getElementById('bgt').value = items.bgt;
61 | document.getElementById('s_mode').value = items.s_mode;
62 | document.getElementById('type').value = items.type;
63 | document.getElementById('min_sl').value = items.min_sl;
64 | document.getElementById('max_sl').value = items.max_sl;
65 | document.getElementById('aiType').value = items.aiType;
66 | document.getElementById('andKeywords').value = items.andKeywords;
67 | document.getElementById('orKeywords').value = items.orKeywords;
68 | document.getElementById('minusKeywords').value = items.minusKeywords;
69 | updateKeywords();
70 | const status = document.getElementById('status');
71 | status.textContent = 'Options reset.';
72 | setTimeout(() => {
73 | status.textContent = '';
74 | }, 1000);
75 | console.log("Reset config");
76 | console.log(items);
77 | }
78 | );
79 | };
80 |
81 |
82 | const restoreOptions = () => {
83 | chrome.storage.local.get(defaultConfig, (items) => {
84 | console.log("Load config");
85 | console.log(items);
86 | document.getElementById('order').value = items.order;
87 | document.getElementById('mode').value = items.mode;
88 | document.getElementById('timeOption').value = items.timeOption;
89 | toggleDateInputs(items.timeOption);
90 | document.getElementById('scd').value = items.scd;
91 | document.getElementById('ecd').value = items.ecd;
92 | document.getElementById('blt').value = items.blt;
93 | document.getElementById('bgt').value = items.bgt;
94 | document.getElementById('s_mode').value = items.s_mode;
95 | document.getElementById('type').value = items.type;
96 | document.getElementById('min_sl').value = items.min_sl;
97 | document.getElementById('max_sl').value = items.max_sl;
98 | document.getElementById('aiType').value = items.aiType === null ? '' : items.aiType;
99 | document.getElementById('andKeywords').value = items.andKeywords;
100 | document.getElementById('orKeywords').value = items.orKeywords;
101 | document.getElementById('minusKeywords').value = items.minusKeywords;
102 | updateKeywords();
103 | });
104 | };
105 |
106 | function toggleDateInputs(option) {
107 | const dateInputs = document.getElementById('dateInputs');
108 | if (option === "specific") {
109 | dateInputs.style.display = "block";
110 | } else {
111 | dateInputs.style.display = "none";
112 | document.getElementById('scd').value = "";
113 | document.getElementById('ecd').value = "";
114 | }
115 | }
116 |
117 | function updateKeywords() {
118 | let andKeywords = document.getElementById('andKeywords').value;
119 | let orKeywords = document.getElementById('orKeywords').value;
120 | let minusKeywords = document.getElementById('minusKeywords').value;
121 | let word = getKeywords(andKeywords, orKeywords, minusKeywords);
122 | document.getElementById('keywords').value = word;
123 | }
124 |
125 | document.getElementById('timeOption').addEventListener('change', function () { toggleDateInputs(this.value); });
126 | document.getElementById('orKeywords').addEventListener('input', updateKeywords);
127 | document.getElementById('minusKeywords').addEventListener('input', updateKeywords);
128 | document.getElementById('andKeywords').addEventListener('input', updateKeywords);
129 | document.addEventListener('DOMContentLoaded', restoreOptions);
130 | document.getElementById('save').addEventListener('click', saveOptions);
131 | document.getElementById('reset').addEventListener('click', resetOptions);
132 |
133 | document.addEventListener("DOMContentLoaded", function () {
134 | const langSelect = document.getElementById("languageSelect");
135 | const defaultLang = localStorage.getItem("language") || "en";
136 | langSelect.value = defaultLang;
137 |
138 | function loadTranslations(lang) {
139 | fetch(`_locales/${lang}/messages.json`)
140 | .then((response) => response.json())
141 | .then((data) => {
142 | document.querySelectorAll("[id]").forEach((el) => {
143 | if (data[el.id]) {
144 | el.textContent = data[el.id].message;
145 | }
146 | });
147 | document.querySelectorAll("[data-i18n-placeholder]").forEach((el) => {
148 | const key = el.getAttribute("data-i18n-placeholder");
149 | if (data[key]) {
150 | el.placeholder = data[key].message;
151 | }
152 | });
153 | })
154 | .catch((error) => console.error("Error loading translations:", error));
155 | }
156 |
157 | loadTranslations(defaultLang);
158 |
159 | langSelect.addEventListener("change", (event) => {
160 | const selectedLang = event.target.value;
161 | localStorage.setItem("language", selectedLang);
162 | loadTranslations(selectedLang);
163 | });
164 | });
165 |
--------------------------------------------------------------------------------
/src/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Newtab Random Pixiv Images Options
7 |
116 |
117 |
118 |
119 |
120 |
121 | 🌐
122 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
162 |
163 |
164 |
165 |
166 |
171 |
172 |
173 |
174 |
175 |
179 |
180 |
181 |
191 |
192 |
201 |
202 |
203 |
204 |
209 |
210 |
211 |
212 |
213 |
220 |
221 |
222 |
233 |
234 |
235 |
236 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
--------------------------------------------------------------------------------
/src_firefox/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Newtab Random Pixiv Images Options
7 |
116 |
117 |
118 |
119 |
120 |
121 | 🌐
122 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
162 |
163 |
164 |
165 |
166 |
171 |
172 |
173 |
174 |
175 |
179 |
180 |
181 |
191 |
192 |
201 |
202 |
203 |
204 |
209 |
210 |
211 |
212 |
213 |
220 |
221 |
222 |
233 |
234 |
235 |
236 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
--------------------------------------------------------------------------------
/src_firefox/background.js:
--------------------------------------------------------------------------------
1 | import { defaultConfig, getKeywords } from "./config.js";
2 |
3 | browser.webRequest.onBeforeSendHeaders.addListener(
4 | function (details) {
5 | let existed = false;
6 | let refName = "Referer";
7 | let refValue = "https://www.pixiv.net/";
8 | for (var i = 0; i < details.requestHeaders.length; ++i) {
9 | if (
10 | details.requestHeaders[i].name.toLowerCase() === refName.toLowerCase()
11 | ) {
12 | details.requestHeaders[i].value = refValue;
13 | existed = true;
14 | break;
15 | }
16 | }
17 | if (!existed) {
18 | details.requestHeaders.push({
19 | name: refName,
20 | value: refValue,
21 | });
22 | }
23 | return { requestHeaders: details.requestHeaders };
24 | },
25 | { urls: ["*://*.pixiv.net/*", "*://*.pximg.net/*"] },
26 | ["blocking", "requestHeaders"]
27 | );
28 |
29 | function getRandomInt(min, max) {
30 | min = Math.ceil(min);
31 | max = Math.floor(max);
32 | return Math.floor(Math.random() * (max - min) + min);
33 | }
34 | class Queue {
35 | constructor(maxsize) {
36 | this.maxsize = maxsize;
37 | this.array = [];
38 | }
39 | empty() {
40 | return this.array.length === 0;
41 | }
42 | full() {
43 | return this.array.length === this.maxsize;
44 | }
45 | size() {
46 | return this.array.length;
47 | }
48 | capacity() {
49 | return this.maxsize;
50 | }
51 | pop() {
52 | if (!this.empty()) {
53 | return this.array.shift();
54 | }
55 | }
56 | push(item) {
57 | if (!this.full()) {
58 | this.array.push(item);
59 | return true;
60 | }
61 | return false;
62 | }
63 | }
64 |
65 | async function fetchPixivJson(url) {
66 | try {
67 | let res = await fetch(url);
68 | if (!res.ok) {
69 | console.error(`Fetch pixiv json failed: ${res.status} ${res.statusText}`);
70 | return null;
71 | }
72 | let res_json = await res.json();
73 | if (res_json.error) {
74 | console.error(`Pixiv API error: ${res_json.message}`);
75 | return null;
76 | }
77 | return res_json;
78 | } catch (e) {
79 | console.error(`Fetch pixiv json error:`, e);
80 | return null;
81 | }
82 | }
83 |
84 | async function fetchImage(url) {
85 | try {
86 | let res = await fetch(url);
87 | if (!res.ok) return null;
88 | return await res.blob();
89 | } catch (e) {
90 | console.error(`Fetch image error:`, e);
91 | return null;
92 | }
93 | }
94 |
95 | let baseUrl = "https://www.pixiv.net";
96 | let illustInfoUrl = "/ajax/illust/";
97 | let searchUrl = "/ajax/search/illustrations/";
98 |
99 | class SearchSource {
100 | constructor(config) {
101 | this.searchParam = config;
102 | this.params = ["order", "mode", "p", "s_mode", "type", "scd", "ecd", "blt", "bgt"];
103 | this.totalPage = 0;
104 | this.itemsPerPage = 60;
105 | this.illustInfoPages = {};
106 | }
107 |
108 | updateConfig(config) {
109 | this.searchParam = config;
110 | this.totalPage = 0;
111 | this.illustInfoPages = {};
112 | }
113 |
114 | replaceSpecialCharacter = (function () {
115 | var reg = /[!'()~]/g;
116 | var mapping = {
117 | "!": "%21",
118 | "'": "%27",
119 | "(": "%28",
120 | ")": "%29",
121 | "~": "%7E",
122 | };
123 | var map = function (e) {
124 | return mapping[e];
125 | };
126 | var fn = function (e) {
127 | return encodeURIComponent(e).replace(reg, map);
128 | };
129 | return fn;
130 | })();
131 |
132 | generateSearchUrl(p = 1) {
133 | let sp = this.searchParam;
134 | sp.p = p;
135 | let word = getKeywords(sp.andKeywords, sp.orKeywords, sp.minusKeywords);
136 | let firstPart = encodeURIComponent(word);
137 | let secondPartArray = [];
138 | secondPartArray.push("?word=" + this.replaceSpecialCharacter(word));
139 | for (let o of this.params) {
140 | if (sp.hasOwnProperty(o) && sp[o]) {
141 | secondPartArray.push(`${o}=${sp[o]}`);
142 | }
143 | }
144 | let secondPart = secondPartArray.join("&");
145 | return firstPart + secondPart;
146 | }
147 |
148 | async searchIllustPage(p) {
149 | let paramUrl = this.generateSearchUrl(p);
150 | let jsonResult = await fetchPixivJson(baseUrl + searchUrl + paramUrl);
151 | return jsonResult;
152 | }
153 |
154 | async getRandomIllust() {
155 | const MAX_RETRIES = 5;
156 | for (let i = 0; i < MAX_RETRIES; i++) {
157 | try {
158 | if (this.totalPage === 0) {
159 | let firstPage = await this.searchIllustPage(1);
160 | if (!firstPage || !firstPage.body) continue;
161 | let total = firstPage.body.illust.total;
162 | this.totalPage = Math.ceil(total / this.itemsPerPage);
163 | if (this.totalPage === 0) return null;
164 | }
165 |
166 | let randomPage = getRandomInt(0, this.totalPage) + 1;
167 | if (!this.illustInfoPages[randomPage]) {
168 | let pageObj = await this.searchIllustPage(randomPage);
169 | if (!pageObj || !pageObj.body) continue;
170 |
171 | let total = pageObj.body.illust.total;
172 | let tp = Math.ceil(total / this.itemsPerPage);
173 | if (tp > this.totalPage) {
174 | this.totalPage = tp;
175 | }
176 |
177 | // filter images
178 | pageObj.body.illust.data = pageObj.body.illust.data.filter(
179 | (el) => {
180 | let condition1 = !this.searchParam.min_sl || el.sl >= this.searchParam.min_sl;
181 | let condition2 = !this.searchParam.max_sl || el.sl <= this.searchParam.max_sl;
182 | let condition3 = !this.searchParam.aiType || el.aiType == this.searchParam.aiType;
183 | return condition1 && condition2 && condition3;
184 | }
185 | );
186 | this.illustInfoPages[randomPage] = pageObj.body.illust.data;
187 | }
188 |
189 | let illustArray = this.illustInfoPages[randomPage];
190 | if (!illustArray || illustArray.length === 0) continue;
191 |
192 | let randomIndex = getRandomInt(0, illustArray.length);
193 | let res = {};
194 | res.illustId = illustArray[randomIndex].id;
195 | res.profileImageUrl = illustArray[randomIndex].profileImageUrl;
196 |
197 | let illustInfo = await fetchPixivJson(baseUrl + illustInfoUrl + res.illustId);
198 | if (!illustInfo || !illustInfo.body) continue;
199 |
200 | res.userName = illustInfo.body.userName;
201 | res.userId = illustInfo.body.userId;
202 | res.illustId = illustInfo.body.illustId;
203 | res.userIdUrl = baseUrl + "/users/" + illustInfo.body.userId;
204 | res.illustIdUrl = baseUrl + "/artworks/" + illustInfo.body.illustId;
205 | res.title = illustInfo.body.title;
206 | res.imageObjectUrl = illustInfo.body.urls.regular;
207 |
208 | let [imgBlob, profileBlob] = await Promise.all([
209 | fetchImage(res.imageObjectUrl),
210 | fetchImage(res.profileImageUrl)
211 | ]);
212 |
213 | if (!imgBlob) continue;
214 | res.imageObjectUrl = await blobToDataUrl(imgBlob);
215 |
216 | if (profileBlob) {
217 | try {
218 | res.profileImageUrl = await blobToDataUrl(profileBlob);
219 | } catch (e) {
220 | // ignore profile image error
221 | }
222 | }
223 | return res;
224 | } catch (e) {
225 | console.error("Error in getRandomIllust loop:", e);
226 | continue;
227 | }
228 | }
229 | return null;
230 | }
231 | }
232 |
233 | function blobToDataUrl(blob) {
234 | return new Promise((resolve, reject) => {
235 | let reader = new FileReader();
236 | reader.onload = () => resolve(reader.result);
237 | reader.onerror = reject;
238 | reader.readAsDataURL(blob);
239 | });
240 | }
241 |
242 | let searchSource;
243 | let illust_queue;
244 | let running = 0;
245 |
246 | function fillQueue() {
247 | while (running < illust_queue.capacity() - illust_queue.size()) {
248 | ++running;
249 | setTimeout(async () => {
250 | if (illust_queue.full()) { return; }
251 | let res = await searchSource.getRandomIllust();
252 | if (res) {
253 | illust_queue.push(res);
254 | browser.storage.session.set({ illustQueue: illust_queue });
255 | }
256 | --running;
257 | }, 0);
258 | }
259 | }
260 |
261 | async function start() {
262 | let config = await browser.storage.local.get(defaultConfig);
263 | searchSource = new SearchSource(config);
264 | let queue_cache = await browser.storage.session.get("illustQueue");
265 |
266 | if (Object.keys(queue_cache).length === 0) {
267 | illust_queue = new Queue(2);
268 | } else {
269 | illust_queue = Object.setPrototypeOf(queue_cache.illustQueue, Queue.prototype)
270 | }
271 |
272 | fillQueue();
273 | console.log("background script loaded");
274 | }
275 |
276 | let initPromise = start();
277 |
278 | browser.runtime.onMessage.addListener(function (
279 | message,
280 | sender,
281 | sendResponse
282 | ) {
283 | (
284 | async () => {
285 | await initPromise;
286 | if (message.action === "fetchImage") {
287 | let res = illust_queue.pop();
288 | if (!res) {
289 | res = await searchSource.getRandomIllust();
290 | }
291 | if (res) {
292 | sendResponse(res);
293 | let { profileImageUrl, imageObjectUrl, ...filteredRes } = res;
294 | console.log(filteredRes);
295 | } else {
296 | sendResponse(null);
297 | }
298 | fillQueue();
299 | } else if (message.action === "updateConfig") {
300 | let config = await browser.storage.local.get(defaultConfig);
301 | searchSource.updateConfig(config);
302 | illust_queue = new Queue(2);
303 | browser.storage.session.set({ illustQueue: illust_queue });
304 | fillQueue();
305 | }
306 | }
307 | )();
308 | return true;
309 | });
310 |
--------------------------------------------------------------------------------
/src/background.js:
--------------------------------------------------------------------------------
1 | import { defaultConfig, getKeywords } from "./config.js";
2 |
3 | chrome.runtime.onInstalled.addListener((details) => {
4 | const RULE = [
5 | {
6 | "id": 1,
7 | "priority": 1,
8 | "action": {
9 | "type": "modifyHeaders",
10 | "requestHeaders": [
11 | {
12 | "header": "referer",
13 | "operation": "set",
14 | "value": "https://www.pixiv.net/"
15 | }
16 | ]
17 | },
18 | "condition": {
19 | initiatorDomains: [chrome.runtime.id],
20 | "urlFilter": "pixiv.net",
21 | "resourceTypes": [
22 | "xmlhttprequest",
23 | ]
24 | }
25 | },
26 | {
27 | "id": 2,
28 | "priority": 1,
29 | "action": {
30 | "type": "modifyHeaders",
31 | "requestHeaders": [
32 | {
33 | "header": "referer",
34 | "operation": "set",
35 | "value": "https://www.pixiv.net/"
36 | }
37 | ]
38 | },
39 | "condition": {
40 | initiatorDomains: [chrome.runtime.id],
41 | "urlFilter": "pximg.net",
42 | "resourceTypes": [
43 | "xmlhttprequest",
44 | ]
45 | }
46 | }
47 | ];
48 | chrome.declarativeNetRequest.updateDynamicRules({
49 | removeRuleIds: RULE.map(o => o.id),
50 | addRules: RULE,
51 | });
52 | });
53 |
54 | function getRandomInt(min, max) {
55 | min = Math.ceil(min);
56 | max = Math.floor(max);
57 | return Math.floor(Math.random() * (max - min) + min);
58 | }
59 | class Queue {
60 | constructor(maxsize) {
61 | this.maxsize = maxsize;
62 | this.array = [];
63 | }
64 | empty() {
65 | return this.array.length === 0;
66 | }
67 | full() {
68 | return this.array.length === this.maxsize;
69 | }
70 | size() {
71 | return this.array.length;
72 | }
73 | capacity() {
74 | return this.maxsize;
75 | }
76 | pop() {
77 | if (!this.empty()) {
78 | return this.array.shift();
79 | }
80 | }
81 | push(item) {
82 | if (!this.full()) {
83 | this.array.push(item);
84 | return true;
85 | }
86 | return false;
87 | }
88 | }
89 |
90 | async function fetchPixivJson(url) {
91 | try {
92 | let res = await fetch(url);
93 | if (!res.ok) {
94 | console.error(`Fetch pixiv json failed: ${res.status} ${res.statusText}`);
95 | return null;
96 | }
97 | let res_json = await res.json();
98 | if (res_json.error) {
99 | console.error(`Pixiv API error: ${res_json.message}`);
100 | return null;
101 | }
102 | return res_json;
103 | } catch (e) {
104 | console.error(`Fetch pixiv json error:`, e);
105 | return null;
106 | }
107 | }
108 |
109 | async function fetchImage(url) {
110 | try {
111 | let res = await fetch(url);
112 | if (!res.ok) return null;
113 | return await res.blob();
114 | } catch (e) {
115 | console.error(`Fetch image error:`, e);
116 | return null;
117 | }
118 | }
119 |
120 | let baseUrl = "https://www.pixiv.net";
121 | let illustInfoUrl = "/ajax/illust/";
122 | let searchUrl = "/ajax/search/illustrations/";
123 |
124 | class SearchSource {
125 | constructor(config) {
126 | this.searchParam = config;
127 | this.params = ["order", "mode", "p", "s_mode", "type", "scd", "ecd", "blt", "bgt"];
128 | this.totalPage = 0;
129 | this.itemsPerPage = 60;
130 | this.illustInfoPages = {};
131 | }
132 |
133 | updateConfig(config) {
134 | this.searchParam = config;
135 | this.totalPage = 0;
136 | this.illustInfoPages = {};
137 | }
138 |
139 | replaceSpecialCharacter = (function () {
140 | var reg = /[!'()~]/g;
141 | var mapping = {
142 | "!": "%21",
143 | "'": "%27",
144 | "(": "%28",
145 | ")": "%29",
146 | "~": "%7E",
147 | };
148 | var map = function (e) {
149 | return mapping[e];
150 | };
151 | var fn = function (e) {
152 | return encodeURIComponent(e).replace(reg, map);
153 | };
154 | return fn;
155 | })();
156 |
157 | generateSearchUrl(p = 1) {
158 | let sp = this.searchParam;
159 | sp.p = p;
160 | let word = getKeywords(sp.andKeywords, sp.orKeywords, sp.minusKeywords);
161 | let firstPart = encodeURIComponent(word);
162 | let secondPartArray = [];
163 | secondPartArray.push("?word=" + this.replaceSpecialCharacter(word));
164 | for (let o of this.params) {
165 | if (sp.hasOwnProperty(o) && sp[o]) {
166 | secondPartArray.push(`${o}=${sp[o]}`);
167 | }
168 | }
169 | let secondPart = secondPartArray.join("&");
170 | return firstPart + secondPart;
171 | }
172 |
173 | async searchIllustPage(p) {
174 | let paramUrl = this.generateSearchUrl(p);
175 | let jsonResult = await fetchPixivJson(baseUrl + searchUrl + paramUrl);
176 | return jsonResult;
177 | }
178 |
179 | async getRandomIllust() {
180 | const MAX_RETRIES = 5;
181 | for (let i = 0; i < MAX_RETRIES; i++) {
182 | try {
183 | if (this.totalPage === 0) {
184 | let firstPage = await this.searchIllustPage(1);
185 | if (!firstPage || !firstPage.body) continue;
186 | let total = firstPage.body.illust.total;
187 | this.totalPage = Math.ceil(total / this.itemsPerPage);
188 | if (this.totalPage === 0) return null;
189 | }
190 |
191 | let randomPage = getRandomInt(0, this.totalPage) + 1;
192 | if (!this.illustInfoPages[randomPage]) {
193 | let pageObj = await this.searchIllustPage(randomPage);
194 | if (!pageObj || !pageObj.body) continue;
195 |
196 | let total = pageObj.body.illust.total;
197 | let tp = Math.ceil(total / this.itemsPerPage);
198 | if (tp > this.totalPage) {
199 | this.totalPage = tp;
200 | }
201 |
202 | // filter images
203 | pageObj.body.illust.data = pageObj.body.illust.data.filter(
204 | (el) => {
205 | let condition1 = !this.searchParam.min_sl || el.sl >= this.searchParam.min_sl;
206 | let condition2 = !this.searchParam.max_sl || el.sl <= this.searchParam.max_sl;
207 | let condition3 = !this.searchParam.aiType || el.aiType == this.searchParam.aiType;
208 | return condition1 && condition2 && condition3;
209 | }
210 | );
211 | this.illustInfoPages[randomPage] = pageObj.body.illust.data;
212 | }
213 |
214 | let illustArray = this.illustInfoPages[randomPage];
215 | if (!illustArray || illustArray.length === 0) continue;
216 |
217 | let randomIndex = getRandomInt(0, illustArray.length);
218 | let res = {};
219 | res.illustId = illustArray[randomIndex].id;
220 | res.profileImageUrl = illustArray[randomIndex].profileImageUrl;
221 |
222 | let illustInfo = await fetchPixivJson(baseUrl + illustInfoUrl + res.illustId);
223 | if (!illustInfo || !illustInfo.body) continue;
224 |
225 | res.userName = illustInfo.body.userName;
226 | res.userId = illustInfo.body.userId;
227 | res.illustId = illustInfo.body.illustId;
228 | res.userIdUrl = baseUrl + "/users/" + illustInfo.body.userId;
229 | res.illustIdUrl = baseUrl + "/artworks/" + illustInfo.body.illustId;
230 | res.title = illustInfo.body.title;
231 | res.imageObjectUrl = illustInfo.body.urls.regular;
232 |
233 | let [imgBlob, profileBlob] = await Promise.all([
234 | fetchImage(res.imageObjectUrl),
235 | fetchImage(res.profileImageUrl)
236 | ]);
237 |
238 | if (!imgBlob) continue;
239 | res.imageObjectUrl = await blobToDataUrl(imgBlob);
240 |
241 | if (profileBlob) {
242 | try {
243 | res.profileImageUrl = await blobToDataUrl(profileBlob);
244 | } catch (e) {
245 | // ignore profile image error
246 | }
247 | }
248 | return res;
249 | } catch (e) {
250 | console.error("Error in getRandomIllust loop:", e);
251 | continue;
252 | }
253 | }
254 | return null;
255 | }
256 | }
257 |
258 | function blobToDataUrl(blob) {
259 | return new Promise((resolve, reject) => {
260 | let reader = new FileReader();
261 | reader.onload = () => resolve(reader.result);
262 | reader.onerror = reject;
263 | reader.readAsDataURL(blob);
264 | });
265 | }
266 |
267 | let searchSource;
268 | let illust_queue;
269 | let running = 0;
270 |
271 | function fillQueue() {
272 | while (running < illust_queue.capacity() - illust_queue.size()) {
273 | ++running;
274 | setTimeout(async () => {
275 | if (illust_queue.full()) { return; }
276 | let res = await searchSource.getRandomIllust();
277 | if (res) {
278 | illust_queue.push(res);
279 | chrome.storage.session.set({ illustQueue: illust_queue });
280 | }
281 | --running;
282 | }, 0);
283 | }
284 | }
285 |
286 | async function start() {
287 | let config = await chrome.storage.local.get(defaultConfig);
288 | searchSource = new SearchSource(config);
289 | let queue_cache = await chrome.storage.session.get("illustQueue");
290 |
291 | if (Object.keys(queue_cache).length === 0) {
292 | illust_queue = new Queue(2);
293 | } else {
294 | illust_queue = Object.setPrototypeOf(queue_cache.illustQueue, Queue.prototype)
295 | }
296 |
297 | fillQueue();
298 | console.log("background script loaded");
299 | }
300 |
301 | let initPromise = start();
302 |
303 | chrome.runtime.onMessage.addListener(function (
304 | message,
305 | sender,
306 | sendResponse
307 | ) {
308 | (
309 | async () => {
310 | await initPromise;
311 | if (message.action === "fetchImage") {
312 | let res = illust_queue.pop();
313 | if (!res) {
314 | res = await searchSource.getRandomIllust();
315 | }
316 | if (res) {
317 | sendResponse(res);
318 | let { profileImageUrl, imageObjectUrl, ...filteredRes } = res;
319 | console.log(filteredRes);
320 | } else {
321 | sendResponse(null);
322 | }
323 | fillQueue();
324 | } else if (message.action === "updateConfig") {
325 | let config = await chrome.storage.local.get(defaultConfig);
326 | searchSource.updateConfig(config);
327 | illust_queue = new Queue(2);
328 | chrome.storage.session.set({ illustQueue: illust_queue });
329 | fillQueue();
330 | }
331 | }
332 | )();
333 | return true;
334 | });
335 |
--------------------------------------------------------------------------------