├── .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 |
225 | 226 |
227 | 228 | 229 |
230 |
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 |
324 | 325 |
326 | 327 | 328 |
329 |
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 | } --------------------------------------------------------------------------------