diff --git a/CHANGELOG.md b/CHANGELOG.md index ec2c71d..f2ee6f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,9 @@ -# Change Log +# Changelog All notable changes to the "image-search" extension will be documented in this file. -Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. + -## [Unreleased] +## [1.0.0] -- Initial release \ No newline at end of file +Released diff --git a/README.md b/README.md index 842533e..da67529 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,53 @@ # ![Banner](images/banner.png) -This extension allows you to search for images on Pixabay directly from within VS Code and download them to your workspace. +Search and download images from Pixabay directly in VS Code -While the preview images are low resolution, the downloaded images are medium sized. +![Feature Screenshot](images/feature.gif) Using the [Pixabay API](https://pixabay.com/api/) Check their [Content license](https://pixabay.com/service/terms/) -## Features +While the preview images are low resolution, the downloaded images are medium-sized, ready for web development. -- **Search Images**: Enter a description to search for images on Pixabay. -- **View Results**: Display search results with thumbnails in a webview. -- **Download Images**: Click on an image to download it directly to your workspace. +## Requirements -![Feature Screenshot](images/feature.gif) +- **API Key Required**: The extension requires a valid Pixabay API key. -## Requirements +## Commands -- **VS Code**: Ensure you are using the latest version of Visual Studio Code. -- **Internet Connection**: Required to fetch images from the Pixabay API. +- **Image-Search: Set Pixabay API key**: Set your Pixabay API key for image searches. +- **Image-Search: Set resolution of downloaded images**: Choose the resolution for the images you download. +- **Image-Search: Search Image**: Search for images on Pixabay. ## Extension Settings -This extension uses the following settings: +- **`image-search.pixabayAPIKey`**: -- `image-search.pixabayAPIKey`: Your Pixabay API key for accessing image search functionality. You can set this using the command `Image Search: Set Pixabay API Key`. + - **Type**: `string` + - **Default**: `""` + - **Description**: Your Pixabay API key for searching images. + +- **`image-search.downloadedImageResolution`**: + - **Type**: `string` + - **Enum**: + - `"webformat"`: Medium sized image with a maximum width or height of 640 px (webformatWidth x webformatHeight). + - `"largeImage"`: Scaled image with a maximum width/height of 1280px. + - `"fullHD"`: Full HD scaled image with a maximum width/height of 1920px. + - `"image"`: URL to the original image (imageWidth x imageHeight). + - **Default**: `"webformat"` + - **Description**: You can choose between 4 predefined resolutions. FullHD and Image only work if your account has been approved for full access. Check their website for more information. ## Known Issues - **Needs caching** - **Needs Testing** - **No Workspace Folder Open**: If no workspace folder is open, the extension will not be able to save downloaded images. -- **API Key Required**: The extension requires a valid Pixabay API key. Ensure you set it before attempting to search for images. - -## Release Notes -### 0.0.1 +## License -Still working on it +This extension is licensed under the [MIT License](LICENSE). --- -### Icon - Icon generated with Microsoft Bing ∙ 17 September 2024 at 8:55 pm diff --git a/eslint.config.mjs b/eslint.config.mjs index 93feb28..4b885ef 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -17,7 +17,7 @@ export default [ }, rules: { - "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/naming-convention": [ "warn", { diff --git a/images/feature.gif b/images/feature.gif index b590cbf..00172e9 100644 Binary files a/images/feature.gif and b/images/feature.gif differ diff --git a/package-lock.json b/package-lock.json index 22c867d..6e5b1ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,14 @@ { "name": "image-search", - "version": "0.0.1", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "image-search", - "version": "0.0.1", + "version": "1.0.0", + "license": "MIT", "devDependencies": { - "@types/mocha": "^10.0.7", "@types/node": "20.x", "@types/vscode": "^1.93.0", "@typescript-eslint/eslint-plugin": "^8.3.0", @@ -357,6 +357,56 @@ "node": ">=14" } }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.2.tgz", + "integrity": "sha512-4Bb+oqXZTSTZ1q27Izly9lv8B9dlV61CROxPiVtywwzv5SnytJqhvYe6FclHYuXml4cd1VHPo1zd5PmTeJozvA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", + "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "lodash.get": "^4.4.2", + "type-detect": "^4.1.0" + } + }, + "node_modules/@sinonjs/samsam/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "dev": true + }, + "node_modules/@types/chai": { + "version": "4.3.19", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.19.tgz", + "integrity": "sha512-2hHHvQBVE2FiSK4eN0Br6snX9MtolHaTo/batnLjlGRhoQzlCL61iVpxoqO7SfFyOw+P/pwv+0zNHzKoGWz9Cw==", + "dev": true + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -390,6 +440,21 @@ "undici-types": "~6.19.2" } }, + "node_modules/@types/sinon": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", + "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", + "dev": true, + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true + }, "node_modules/@types/vscode": { "version": "1.93.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.93.0.tgz", @@ -938,6 +1003,15 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1156,6 +1230,22 @@ } ] }, + "node_modules/chai": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", + "dev": true, + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1184,6 +1274,15 @@ "node": ">=8" } }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1400,6 +1499,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1872,6 +1980,15 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -2382,6 +2499,12 @@ "setimmediate": "^1.0.5" } }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2446,6 +2569,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2468,6 +2597,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/loupe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -2746,6 +2884,19 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/nise": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", + "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.1", + "@sinonjs/text-encoding": "^0.7.3", + "just-extend": "^6.2.0", + "path-to-regexp": "^8.1.0" + } + }, "node_modules/node-releases": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", @@ -3027,6 +3178,24 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/path-to-regexp": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.1.0.tgz", + "integrity": "sha512-Bqn3vc8CMHty6zuD+tG23s6v2kwxslHEhTj4eYaVKGIEB+YX/2wd0/rgXLFD9G9id9KCtbVy/3ZgmvZjpa0UdQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", @@ -3408,6 +3577,55 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sinon": { + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.2.tgz", + "integrity": "sha512-euuToqM+PjO4UgXeLETsfQiuoyPXlqFezr6YZDFwHR3t4qaX0fZUe1MfPMznTL5f8BWrVS89KduLdMUsxFCO6g==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.2", + "@sinonjs/samsam": "^8.0.1", + "diff": "^7.0.0", + "nise": "^6.1.1", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon-chai": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-4.0.0.tgz", + "integrity": "sha512-cWqO7O2I4XfJDWyWElAQ9D/dtdh5Mo0RHndsfiiYyjWnlPzBJdIvjCVURO4EjyYaC3BjV+ISNXCfTXPXTEIEWA==", + "dev": true, + "peerDependencies": { + "chai": "^5.0.0", + "sinon": ">=4.0.0" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -3766,6 +3984,15 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/typescript": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", diff --git a/package.json b/package.json index cc17dbc..db474fc 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,8 @@ "url": "git+https://github.com/maxzirps/image-search-vscode.git" }, "displayName": "Image Search", - "description": "Search the web for stock images", - "version": "0.0.1", - "preview": true, + "description": "Search and download stock images from pixabay.com", + "version": "1.0.0", "galleryBanner": { "theme": "light" }, @@ -41,6 +40,10 @@ "command": "image-search.setPixabayAPIKey", "title": "Image-Search: Set Pixabay API key" }, + { + "command": "image-search.setImageResolution", + "title": "Image-Search: Set resolution of downloaded images" + }, { "command": "image-search.searchImage", "title": "Image-Search: Search Image" @@ -53,6 +56,23 @@ "type": "string", "default": "", "description": "Your Pixabay API key for searching images." + }, + "image-search.downloadedImageResolution": { + "type": "string", + "enum": [ + "webformat", + "largeImage", + "fullHD", + "image" + ], + "default": "webformat", + "description": "You can choose between 4 pre-redfined resolutions. FullHD and Image only work if your account has been approved for full access. Check their website for more information. ", + "enumDescriptions": [ + "Medium sized image with a maximum width or height of 640 px (webformatWidth x webformatHeight).", + "Scaled image with a maximum width/height of 1280px.", + "Full HD scaled image with a maximum width/height of 1920px.", + "URL to the original image (imageWidth x imageHeight)." + ] } } } @@ -69,17 +89,16 @@ "test": "vscode-test" }, "devDependencies": { - "@types/vscode": "^1.93.0", - "@types/mocha": "^10.0.7", "@types/node": "20.x", + "@types/vscode": "^1.93.0", "@typescript-eslint/eslint-plugin": "^8.3.0", "@typescript-eslint/parser": "^8.3.0", + "@vscode/test-cli": "^0.0.10", + "@vscode/test-electron": "^2.4.1", "eslint": "^9.9.1", - "typescript": "^5.5.4", "ts-loader": "^9.5.1", + "typescript": "^5.5.4", "webpack": "^5.94.0", - "webpack-cli": "^5.1.4", - "@vscode/test-cli": "^0.0.10", - "@vscode/test-electron": "^2.4.1" + "webpack-cli": "^5.1.4" } } diff --git a/src/commands/search-image.ts b/src/commands/search-image.ts new file mode 100644 index 0000000..ecaf162 --- /dev/null +++ b/src/commands/search-image.ts @@ -0,0 +1,133 @@ +import path from "path"; +import * as vscodeModule from "vscode"; +import { downloadFile } from "../utils/download-file"; +import { getWebviewContent } from "../view/get-webview-content"; + +export const searchImageCommand = ( + vscode: typeof vscodeModule, + outputChannel: vscodeModule.OutputChannel, + context: vscodeModule.ExtensionContext +) => + vscode.commands.registerCommand("image-search.searchImage", async () => { + const apiKey = vscode.workspace + .getConfiguration("image-search") + .get("pixabayAPIKey"); + if (!apiKey) { + vscode.window + .showErrorMessage("Please set an API-Key first.", "Set API Key") + .then((selection) => { + if (selection === "Set API Key") { + vscode.commands.executeCommand("image-search.setPixabayAPIKey"); + } + }); + return; + } + + let imageResolution: string | undefined = vscode.workspace + .getConfiguration("image-search") + .get("downloadedImageResolution"); + + if (!imageResolution) { + vscode.window.showWarningMessage( + "No downloadedImageResolution setting found. Defaulting to webformat." + ); + imageResolution = "webformat"; + } + + const searchString = await vscode.window.showInputBox({ + placeHolder: "What image are you looking for?", + prompt: "Image description", + }); + + if (searchString) { + try { + const res = await fetch( + `https://pixabay.com/api/?key=${apiKey}&q=${encodeURIComponent( + searchString + )}&image_type=photo&per_page=50&safesearch=true` + ); + if (res.headers.get("content-type")?.includes("application/json")) { + const data: any = await res.json(); + if ( + ["fullHD", "image"].includes(imageResolution) && + !Object.keys(data.hits[0]).includes("fullHDURL") + ) { + vscode.window.showWarningMessage( + `You set 'downloadedImageResolution' to ${imageResolution} but don't have full access to the API. Changing settings to 'largeImage'.` + ); + vscode.workspace + .getConfiguration() + .update( + "image-search.downloadedImageResolution", + "largeImage", + vscode.ConfigurationTarget.Global + ); + imageResolution = "largeImage"; + } + const imgEntries: { + previewURL: string; + imageURL: string; + }[] = data.hits.map((entry: any) => ({ + previewURL: entry.previewURL, + imageURL: entry[imageResolution + "URL"], + })); + + if (imgEntries.length === 0) { + vscode.window.showWarningMessage( + `No results found for ${searchString}` + ); + return; + } + const panel = vscode.window.createWebviewPanel( + "imageViewer", + "Image Search: Results", + vscode.ViewColumn.One, + { enableScripts: true } + ); + + panel.webview.html = getWebviewContent(imgEntries); + + panel.webview.onDidReceiveMessage( + async (message) => { + if (message.command === "imageClicked") { + panel.dispose(); + const fileUrl = message.imageUrl; + const fileType = message.imageUrl.split(".").pop(); + const fileName = + searchString.replaceAll(" ", "-") + "." + fileType; + const workspaceFolder = + vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + + if (workspaceFolder) { + const filePath = path.join(workspaceFolder, fileName); + try { + await downloadFile(fileUrl, filePath); + outputChannel.append( + `File downloaded and saved to ${filePath}` + ); + } catch (error: any) { + vscode.window.showErrorMessage( + `Failed to download file: ${error.message}` + ); + } + } else { + vscode.window.showErrorMessage( + "No workspace folder is open." + ); + } + } + }, + undefined, + context.subscriptions + ); + } else { + throw new Error(await res.text()); + } + } catch (err: any) { + vscode.window.showErrorMessage( + "Something went wrong fetching the image. " + err + ); + outputChannel.append(err); + } + } + }); diff --git a/src/commands/set-image-resolution.ts b/src/commands/set-image-resolution.ts new file mode 100644 index 0000000..76dfab6 --- /dev/null +++ b/src/commands/set-image-resolution.ts @@ -0,0 +1,54 @@ +import * as vscodeModule from "vscode"; + +export const setImageResolutionCommand = ( + vscode: typeof vscodeModule, + outputChannel: vscodeModule.OutputChannel +) => + vscode.commands.registerCommand( + "image-search.setImageResolution", + async () => { + const options = [ + { + label: "Web Format", + description: + "Medium sized image with a maximum width or height of 640 px", + value: "webformat", + }, + { + label: "Large Image", + description: "Scaled image with a maximum width/height of 1280px", + value: "largeImage", + }, + { + label: "Full HD", + description: + "Full HD scaled image with a maximum width/height of 1920px [Needs full API access]", + value: "fullHD", + }, + { + label: "Original Image", + description: "URL to the original image [Needs full API access]", + value: "image", + }, + ]; + + const selectedOption = await vscode.window.showQuickPick(options, { + placeHolder: "Select image resolution", + }); + + if (selectedOption) { + const config = vscode.workspace.getConfiguration(); + config + .update( + "image-search.downloadedImageResolution", + selectedOption.value, + vscode.ConfigurationTarget.Global + ) + .then(() => + outputChannel.appendLine( + `Resolution set to: ${selectedOption.label}` + ) + ); + } + } + ); diff --git a/src/commands/set-pixabay-api-key.ts b/src/commands/set-pixabay-api-key.ts new file mode 100644 index 0000000..9923ff1 --- /dev/null +++ b/src/commands/set-pixabay-api-key.ts @@ -0,0 +1,27 @@ +import * as vscodeModule from "vscode"; + +export const setPixabayApiKeyCommand = ( + vscode: typeof vscodeModule, + outputChannel: vscodeModule.OutputChannel +) => + vscode.commands.registerCommand("image-search.setPixabayAPIKey", async () => { + const apiKey = await vscode.window.showInputBox({ + prompt: "Enter your Pixabay API Key", + }); + const config = vscode.workspace.getConfiguration(); + if (apiKey) { + config + .update( + "image-search.pixabayAPIKey", + apiKey, + vscode.ConfigurationTarget.Global + ) + .then(() => outputChannel.appendLine(`Setting updated successfully!`)); + } else { + config.update( + "image-search.pixabayAPIKey", + undefined, + vscode.ConfigurationTarget.Global + ); + } + }); diff --git a/src/extension.ts b/src/extension.ts index 6708996..2f041a5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,196 +1,16 @@ import * as vscode from "vscode"; -import * as https from "https"; -import * as fs from "fs"; -import * as path from "path"; +import { setPixabayApiKeyCommand } from "./commands/set-pixabay-api-key"; +import { searchImageCommand } from "./commands/search-image"; +import { setImageResolutionCommand } from "./commands/set-image-resolution"; export function activate(context: vscode.ExtensionContext) { const outputChannel = vscode.window.createOutputChannel("Image Search"); - const setPixabayApiKeyCommand = vscode.commands.registerCommand( - "image-search.setPixabayAPIKey", - async () => { - const apiKey = await vscode.window.showInputBox({ - prompt: "Enter your Pixabay API Key", - }); - const config = vscode.workspace.getConfiguration(); - if (apiKey) { - config - .update( - "image-search.pixabayAPIKey", - apiKey, - vscode.ConfigurationTarget.Global - ) - .then(() => - vscode.window.showInformationMessage( - `Setting updated successfully!` - ) - ); - } else { - config.update( - "image-search.pixabayAPIKey", - undefined, - vscode.ConfigurationTarget.Global - ); - } - } + context.subscriptions.push(setPixabayApiKeyCommand(vscode, outputChannel)); + context.subscriptions.push(setImageResolutionCommand(vscode, outputChannel)); + context.subscriptions.push( + searchImageCommand(vscode, outputChannel, context) ); - - const searchImageCommand = vscode.commands.registerCommand( - "image-search.searchImage", - async () => { - const apiKey = vscode.workspace - .getConfiguration("image-search") - .get("pixabayAPIKey"); - if (!apiKey) { - vscode.window.showErrorMessage( - "Please set an API-Key first. Use 'Image-Search: Set Pixabay API key' command." - ); - return; - } - - const searchString = await vscode.window.showInputBox({ - placeHolder: "What image are you looking for?", - prompt: "Image description", - }); - - if (searchString) { - try { - const res = await fetch( - `https://pixabay.com/api/?key=${apiKey}&q=${encodeURIComponent( - searchString - )}&image_type=photo` - ); - if (res.headers.get("content-type")?.includes("application/json")) { - const data: any = await res.json(); - const imgEntries = data.hits.map((entry: any) => ({ - previewURL: entry.previewURL, - webformatURL: entry.webformatURL, - })); - - const panel = vscode.window.createWebviewPanel( - "imageViewer", - "Image Search: Results", - vscode.ViewColumn.One, - { enableScripts: true } - ); - - panel.webview.html = getWebviewContent(imgEntries); - - panel.webview.onDidReceiveMessage( - async (message) => { - if (message.command === "imageClicked") { - const fileUrl = message.imageUrl; - const fileName = message.imageUrl.split("/").pop(); - const workspaceFolder = - vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; - - if (workspaceFolder) { - const filePath = path.join(workspaceFolder, fileName); - try { - await downloadFile(fileUrl, filePath); - vscode.window.showInformationMessage( - `File downloaded and saved to ${filePath}` - ); - } catch (error: any) { - vscode.window.showErrorMessage( - `Failed to download file: ${error.message}` - ); - } - } else { - vscode.window.showErrorMessage( - "No workspace folder is open." - ); - } - panel.dispose(); - } - }, - undefined, - context.subscriptions - ); - } else { - throw new Error(await res.text()); - } - } catch (err: any) { - vscode.window.showErrorMessage( - "Something went wrong fetching the image. " + err - ); - outputChannel.append(err); - } - } - } - ); - - context.subscriptions.push(setPixabayApiKeyCommand); - context.subscriptions.push(searchImageCommand); -} - -function downloadFile(url: string, filePath: string): Promise { - return new Promise((resolve: any, reject) => { - const file = fs.createWriteStream(filePath); - https - .get(url, (response) => { - if (response.statusCode !== 200) { - reject(new Error(`Failed to get file: ${response.statusCode}`)); - return; - } - response.pipe(file); - file.on("finish", () => file.close(resolve)); - file.on("error", (err) => { - fs.unlink(filePath, () => reject(err)); - }); - }) - .on("error", (err) => { - fs.unlink(filePath, () => reject(err)); - }); - }); -} - -function getWebviewContent( - imageEntries: { previewURL: string; webformatURL: string }[] -): string { - const imageElements = imageEntries - .map( - (entry) => - `` - ) - .join(""); - - return ` - - - - - - - - -
- ${imageElements} -
- - - - `; } export function deactivate() {} diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts deleted file mode 100644 index 4ca0ab4..0000000 --- a/src/test/extension.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as assert from 'assert'; - -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it -import * as vscode from 'vscode'; -// import * as myExtension from '../../extension'; - -suite('Extension Test Suite', () => { - vscode.window.showInformationMessage('Start all tests.'); - - test('Sample test', () => { - assert.strictEqual(-1, [1, 2, 3].indexOf(5)); - assert.strictEqual(-1, [1, 2, 3].indexOf(0)); - }); -}); diff --git a/src/utils/download-file.ts b/src/utils/download-file.ts new file mode 100644 index 0000000..27dcbd4 --- /dev/null +++ b/src/utils/download-file.ts @@ -0,0 +1,43 @@ +import * as https from "https"; +import * as fs from "fs"; +import * as path from "path"; + +export const downloadFile = (url: string, filePath: string): Promise => { + return new Promise((resolve, reject) => { + const getAvailableFilePath = (filePath: string): string => { + let counter = 1; + let ext = path.extname(filePath); + let baseName = path.basename(filePath, ext); + let dir = path.dirname(filePath); + let newFilePath = filePath; + + while (fs.existsSync(newFilePath)) { + newFilePath = path.join(dir, `${baseName}-${counter}${ext}`); + counter++; + } + return newFilePath; + }; + + const finalFilePath = getAvailableFilePath(filePath); + const file = fs.createWriteStream(finalFilePath); + + https + .get(url, (response) => { + if (response.statusCode !== 200) { + reject(new Error(`Failed to get file: ${response.statusCode}`)); + return; + } + response.pipe(file); + file.on("finish", () => { + file.close(); + resolve(); + }); + file.on("error", (err) => { + fs.unlink(finalFilePath, () => reject(err)); + }); + }) + .on("error", (err) => { + fs.unlink(finalFilePath, () => reject(err)); + }); + }); +}; diff --git a/src/view/get-webview-content.ts b/src/view/get-webview-content.ts new file mode 100644 index 0000000..1453df9 --- /dev/null +++ b/src/view/get-webview-content.ts @@ -0,0 +1,134 @@ +export const getWebviewContent = ( + imageEntries: { previewURL: string; imageURL: string }[] +): string => { + const imagesPerPage = 20; + const totalImages = imageEntries.length; + const totalPages = Math.ceil(totalImages / imagesPerPage); + + return ` + + + + + + + + +
+ +
+ + + + + + + + `; +}; diff --git a/tsconfig.json b/tsconfig.json index 8a79f20..4e88130 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,13 @@ { - "compilerOptions": { - "module": "Node16", - "target": "ES2022", - "lib": [ - "ES2022" - ], - "sourceMap": true, - "rootDir": "src", - "strict": true /* enable all strict type-checking options */ - /* Additional Checks */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - } + "compilerOptions": { + "module": "Node16", + "target": "ES2022", + "lib": ["ES2022"], + "sourceMap": true, + "rootDir": "src", + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUnusedParameters": true + } }