├── .gitignore
├── README.md
├── package.json
└── src
├── icon128.png
├── manifest.json
├── popup.html
└── popup.js
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | .DS_Store
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Advanced Image Search
2 | [Chrome extension](https://chrome.google.com/webstore/detail/advanced-image-search/cahpmepdjiejandeladmhfpapeagobnp) to help perform an advanced Google Image search.
3 |
4 |
5 | ## Warning: Exact size feature may no longer work.
6 |
7 | ## Build
8 | 1. `npm run build` creates a build/ folder with two build versions. An unpacked/ folder and packed.zip.
9 | 2. Open chrome extensions window and load unpacked/ folder.
10 |
11 |
12 | ## Check out my other extension.
13 |
14 | # [Global Speed](https://github.com/polywock/globalSpeed)
15 | Web extension that sets a default speed for HTML media elements (video and audio).
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "advanced_image_search",
3 | "version": "1.0.0",
4 | "description": "",
5 | "scripts": {
6 | "build": "rm -rf build; mkdir -p build/unpacked && cp -a src/ build/unpacked && find build -name '.DS_Store' -type f -delete && zip -r build/packed.zip build/unpacked"
7 | },
8 | "author": "",
9 | "license": "ISC",
10 | "dependencies": {
11 | "@types/chrome": "0.0.82"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/icon128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/polywock/advanced-image-search/2486b3b2f2f69025086d62d3a5d1cc50fa785bae/src/icon128.png
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Advanced Image Search",
3 | "short_name": "Advanced Image Search",
4 | "version": "1.8",
5 | "description": "Extension to help perform an advanced Google Images search.",
6 | "manifest_version": 3,
7 | "permissions": ["storage"],
8 | "action": {
9 | "default_popup": "popup.html"
10 | },
11 | "icons": {
12 | "128": "icon128.png"
13 | }
14 | }
--------------------------------------------------------------------------------
/src/popup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
222 |
223 |
224 |
231 |
232 |
233 |
234 |
241 |
242 |
243 |
244 |
245 |
246 |
264 |
265 |
266 |
267 |
268 |
269 |
277 |
278 |
279 |
280 |
281 |
282 |
293 |
294 |
295 |
296 |
297 |
298 |
307 |
308 |
309 |
310 |
311 |
312 |
321 |
322 |
323 |
330 |
331 |
332 |
333 |
573 |
574 |
575 |
576 |
583 |
584 |
585 |
--------------------------------------------------------------------------------
/src/popup.js:
--------------------------------------------------------------------------------
1 |
2 | const m = {
3 | profiles: document.querySelector("#profiles"),
4 | addProfile: document.querySelector("#add-profile"),
5 | query: document.querySelector("#query"),
6 | size: document.querySelector("#size"),
7 | exactWidth: document.querySelector("#exact-width"),
8 | exactHeight: document.querySelector("#exact-height"),
9 | aspectRatio: document.querySelector("#aspect-ratio"),
10 | color: document.querySelector("#color"),
11 | type: document.querySelector("#type"),
12 | format: document.querySelector("#format"),
13 | usageRights: document.querySelector("#usage-rights"),
14 | region: document.querySelector("#region"),
15 | date: document.querySelector("#date"),
16 | minDate: document.querySelector("#min-date"),
17 | maxDate: document.querySelector("#max-date"),
18 | search: document.querySelector("#find"),
19 | clear: document.querySelector("#clear"),
20 | darkTheme: document.querySelector("#dark-theme"),
21 | github: document.querySelector("#github"),
22 | rate: document.querySelector("#rate"),
23 | cSize: document.querySelector("#cSize"),
24 | cDate: document.querySelector("#cDate")
25 | }
26 |
27 | let state = {
28 | forms: [getDefaultForm()],
29 | index: 0,
30 | darkTheme: !!prefersDarkTheme()
31 | }
32 |
33 |
34 | m.query.addEventListener("keydown", e => {
35 | if (e.key === "Enter") {
36 | handleSubmit()
37 | }
38 | })
39 |
40 | m.search.addEventListener("click", handleSubmit)
41 | m.clear.addEventListener("click", handleClear)
42 | m.addProfile.addEventListener("click", () => {
43 | syncStateFromDOM()
44 | state.forms.push(getDefaultForm())
45 | state.index = state.forms.length - 1
46 | writeDOM()
47 | })
48 |
49 |
50 | // dark theme button
51 | const handleDarkTheme = () => {
52 | state.darkTheme = !state.darkTheme
53 | syncDarkTheme()
54 | persistState()
55 | }
56 |
57 | m.darkTheme.addEventListener("click", handleDarkTheme)
58 | m.darkTheme.addEventListener("keydown", e => {
59 | if (e.key === "Enter") {
60 | handleDarkTheme()
61 | }
62 | })
63 |
64 | // github button
65 | const handleGithub = () => {
66 | window.open(`https://github.com/polywock/advanced-image-search`, '_blank');
67 | }
68 |
69 | m.github.addEventListener("click", handleGithub)
70 | m.github.addEventListener("keydown", e => {
71 | if (e.key === "Enter") {
72 | handleGithub()
73 | }
74 | })
75 |
76 | m.rate.addEventListener("click", () => {
77 | window.open(`https://chrome.google.com/webstore/detail/advanced-image-search/cahpmepdjiejandeladmhfpapeagobnp`, '_blank');
78 | })
79 |
80 | m.size.addEventListener("change", handleConditionalField)
81 | m.date.addEventListener("change", handleConditionalField)
82 |
83 | function handleConditionalField(e) {
84 | syncStateFromDOM()
85 | syncConditionalFields()
86 | }
87 |
88 |
89 | function getDefaultForm() {
90 | return {
91 | query: "",
92 | size: "",
93 | aspectRatio: "",
94 | color: "",
95 | type: "",
96 | format: "",
97 | usageRights: "",
98 | exactWidth: "",
99 | exactHeight: "",
100 | date: "",
101 | minDate: "",
102 | maxDate: "",
103 | region: ""
104 | }
105 | }
106 |
107 |
108 | function handleClear() {
109 | writeDOMForm(getDefaultForm())
110 | persistState()
111 | }
112 |
113 | function handleSubmit() {
114 | persistState()
115 |
116 | let form = readDOMForm()
117 | if (!form.query) return
118 | let params = new URLSearchParams();
119 | let tbs = [];
120 | params.append("tbm", "isch") // search images
121 | params.append("q", form.query) // search query
122 | form.region && params.append("cr", `country${form.region}`)
123 |
124 | if (form.size === "qq") {
125 | if (form.exactWidth || form.exactHeight) {
126 | tbs.push(`isz:ex,iszw:${form.exactWidth || form.exactHeight},iszh:${form.exactHeight || form.exactWidth}`)
127 | }
128 | } else if (form.size) {
129 |
130 | if (form.size.length === 1) {
131 | tbs.push(`isz:${form.size}`)
132 | } else {
133 | tbs.push(`isz:lt,islt:${form.size}`)
134 | }
135 |
136 | }
137 |
138 | if (form.date === "qq") {
139 | let minDate = reformatDate(form.minDate)
140 | let maxDate = reformatDate(form.maxDate)
141 |
142 | if (minDate || maxDate) {
143 | tbs.push(`cdr:1,cd_min:${minDate},cd_max:${maxDate}`)
144 | }
145 | alert(`cdr:1,cd_min:${minDate},cd_max:${maxDate}`)
146 | } else if (form.date) {
147 | tbs.push(`qdr:${form.date}`)
148 | }
149 |
150 |
151 |
152 | form.aspectRatio && tbs.push(`iar:${form.aspectRatio}`)
153 |
154 | form.color && tbs.push(`ic:${form.color}`)
155 | form.type && tbs.push(`itp:${form.type}`)
156 | form.format && tbs.push(`ift:${form.format}`)
157 | form.usageRights && tbs.push(form.usageRights)
158 |
159 |
160 | if (tbs.length > 0) {
161 | params.append("tbs", tbs.join(","))
162 | }
163 |
164 | window.open(`https://google.com/search?${params.toString()}`,
165 | '_blank'
166 | );
167 | }
168 |
169 | function reformatDate(date) {
170 | if (!date) return ""
171 | const [y, m, d] = date.split("-")
172 | return [m, d, y].join("/")
173 | }
174 |
175 | function syncStateFromDOM() {
176 | state.forms[state.index] = readDOMForm()
177 | }
178 |
179 | function persistState() {
180 | syncStateFromDOM()
181 | chrome.storage.local.set(state)
182 | }
183 |
184 | function readDOMForm() {
185 | return {
186 | query: m.query.value,
187 | size: m.size.value,
188 | aspectRatio: m.aspectRatio.value,
189 | color: m.color.value,
190 | type: m.type.value,
191 | format: m.format.value,
192 | usageRights: m.usageRights.value,
193 | exactWidth: m.exactWidth.value,
194 | exactHeight: m.exactHeight.value,
195 | date: m.date.value,
196 | minDate: m.minDate.value,
197 | maxDate: m.maxDate.value,
198 | region: m.region.value
199 | }
200 | }
201 |
202 | function syncDarkTheme() {
203 | if (state.darkTheme) {
204 | document.documentElement.classList.add("darkTheme")
205 | } else {
206 | document.documentElement.classList.remove("darkTheme")
207 | }
208 | }
209 |
210 | function handleProfileClick(e) {
211 | if (e.target.parentElement.id !== "profiles") return
212 | syncStateFromDOM()
213 | state.index = clampIndex([...e.target.parentElement.children].filter(v => v.tagName === "BUTTON").findIndex(v => v === e.target))
214 | writeDOM()
215 | }
216 |
217 | function handleProfileDelete(e) {
218 | if (state.forms.length <= 1) {
219 | return
220 | }
221 |
222 | syncStateFromDOM()
223 |
224 | const index = [...e.target.parentElement.parentElement.children].filter(v => v.tagName === "BUTTON").findIndex(v => v === e.target.parentElement)
225 | state.forms.splice(index, 1)
226 | if (state.index >= index) {
227 | state.index = clampIndex(state.index - 1)
228 | }
229 | writeDOM()
230 | }
231 |
232 |
233 | function syncProfiles() {
234 | m.profiles.innerHTML = ""
235 | Array(state.forms.length).fill(0).forEach((v, i) => {
236 | const b = document.createElement("button")
237 | b.style.zIndex = 1000 - i
238 | b.innerText = i + 1
239 | if (i === state.index) {
240 | b.classList.add("selected")
241 | }
242 |
243 | b.addEventListener("click", handleProfileClick)
244 |
245 | if (state.forms.length > 1) {
246 |
247 |
248 | const x = document.createElement("div")
249 | x.innerText = "X"
250 | x.classList.add("delete")
251 | x.addEventListener("click", handleProfileDelete)
252 |
253 | b.appendChild(x)
254 | }
255 |
256 | m.profiles.appendChild(b)
257 | })
258 | }
259 |
260 | function syncConditionalFields() {
261 | // exact size
262 | if (state.forms[state.index].size === "qq") {
263 | m.cSize.style.display = "grid"
264 | } else {
265 | m.cSize.style.display = "none"
266 | }
267 |
268 | if (state.forms[state.index].date === "qq") {
269 | m.cDate.style.display = "grid"
270 | } else {
271 | m.cDate.style.display = "none"
272 | }
273 | }
274 |
275 | function writeDOM() {
276 | writeDOMForm(state.forms[state.index])
277 | syncDarkTheme()
278 | syncProfiles()
279 | syncConditionalFields()
280 | }
281 |
282 | function writeDOMForm(form) {
283 | document.querySelector("#query").value = form.query
284 | document.querySelector("#size").value = form.size
285 | document.querySelector("#aspect-ratio").value = form.aspectRatio
286 | m.color.value = form.color
287 | m.type.value = form.type
288 | m.format.value = form.format
289 | m.usageRights.value = form.usageRights
290 | m.exactWidth.value = form.exactWidth
291 | m.exactHeight.value = form.exactHeight
292 | m.date.value = form.date ?? ""
293 | m.minDate.value = form.minDate ?? ""
294 | m.maxDate.value = form.maxDate ?? ""
295 | m.region.value = form.region ?? ""
296 | }
297 |
298 |
299 |
300 | function getStorage() {
301 | return new Promise((res, rej) => {
302 | chrome.storage.local.get(items => {
303 | if (chrome.runtime.lastError) {
304 | rej(chrome.runtime.lastError)
305 | } else {
306 | res(items)
307 | }
308 | return
309 | })
310 | })
311 | }
312 |
313 | window.onload = async () => {
314 | let storage = await getStorage()
315 |
316 | if (storage.forms) {
317 | state.forms = storage.forms
318 | state.index = storage.index ?? 0
319 | state.darkTheme = storage.darkTheme ?? false
320 | } else if (storage.form) {
321 |
322 | // migrate old version
323 | state.forms = [storage.form]
324 | state.forms[0].usageRights = ""
325 | state.darkTheme = storage.form.darkTheme ?? false
326 | delete storage.form.darkTheme
327 | chrome.storage.local.remove("form")
328 | }
329 |
330 | writeDOM()
331 | }
332 |
333 | setInterval(() => {
334 | persistState()
335 | }, 100)
336 |
337 |
338 | function clampIndex(v) {
339 | return Math.max(0, Math.min(v, state.forms.length - 1))
340 | }
341 |
342 | function prefersDarkTheme() {
343 | return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
344 | }
--------------------------------------------------------------------------------