diff --git a/extensions/gif-selector/README.md b/extensions/gif-selector/README.md new file mode 100644 index 00000000..b53fac4a --- /dev/null +++ b/extensions/gif-selector/README.md @@ -0,0 +1,33 @@ +# GIF Selector - Vicinae Extension + +An extension for Vicinae to search and copy GIFs directly to your clipboard. Supports both **Giphy** and **Klipy** API providers and offers network optimizations for previewing GIFs and adds fallback. + +--- + +## Installation + +1. Install dependencies: + ```bash + npm install + ``` + +2. Run in development/watch mode: + ```bash + npm run dev + ``` + +3. Build for production: + ```bash + npm run build + ``` + +--- + +## Getting Started + +1. Get a developer API key: + - **Klipy**: [klipy.com/developers](https://klipy.com/developers) + - **Giphy**: [developers.giphy.com](https://developers.giphy.com) +2. Open the extension in Vicinae. +3. Fill out the inline onboarding form with your keys and select your default provider. +4. Click **Save Configuration** to start searching! \ No newline at end of file diff --git a/extensions/gif-selector/assets/extension_icon.png b/extensions/gif-selector/assets/extension_icon.png new file mode 100644 index 00000000..1c3457fc Binary files /dev/null and b/extensions/gif-selector/assets/extension_icon.png differ diff --git a/extensions/gif-selector/package-lock.json b/extensions/gif-selector/package-lock.json new file mode 100644 index 00000000..a84a255b --- /dev/null +++ b/extensions/gif-selector/package-lock.json @@ -0,0 +1,763 @@ +{ + "name": "gif-selector", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gif-selector", + "license": "MIT", + "dependencies": { + "@vicinae/api": "^0.21.0" + }, + "devDependencies": { + "@biomejs/biome": "2.3.2", + "typescript": "^5.9.2" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.2.tgz", + "integrity": "sha512-8e9tzamuDycx7fdrcJ/F/GDZ8SYukc5ud6tDicjjFqURKYFSWMl0H0iXNXZEGmcmNUmABgGuHThPykcM41INgg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.3.2", + "@biomejs/cli-darwin-x64": "2.3.2", + "@biomejs/cli-linux-arm64": "2.3.2", + "@biomejs/cli-linux-arm64-musl": "2.3.2", + "@biomejs/cli-linux-x64": "2.3.2", + "@biomejs/cli-linux-x64-musl": "2.3.2", + "@biomejs/cli-win32-arm64": "2.3.2", + "@biomejs/cli-win32-x64": "2.3.2" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.2.tgz", + "integrity": "sha512-4LECm4kc3If0JISai4c3KWQzukoUdpxy4fRzlrPcrdMSRFksR9ZoXK7JBcPuLBmd2SoT4/d7CQS33VnZpgBjew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.2.tgz", + "integrity": "sha512-jNMnfwHT4N3wi+ypRfMTjLGnDmKYGzxVr1EYAPBcauRcDnICFXN81wD6wxJcSUrLynoyyYCdfW6vJHS/IAoTDA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.2.tgz", + "integrity": "sha512-amnqvk+gWybbQleRRq8TMe0rIv7GHss8mFJEaGuEZYWg1Tw14YKOkeo8h6pf1c+d3qR+JU4iT9KXnBKGON4klw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.2.tgz", + "integrity": "sha512-2Zz4usDG1GTTPQnliIeNx6eVGGP2ry5vE/v39nT73a3cKN6t5H5XxjcEoZZh62uVZvED7hXXikclvI64vZkYqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.2.tgz", + "integrity": "sha512-8BG/vRAhFz1pmuyd24FQPhNeueLqPtwvZk6yblABY2gzL2H8fLQAF/Z2OPIc+BPIVPld+8cSiKY/KFh6k81xfA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.2.tgz", + "integrity": "sha512-gzB19MpRdTuOuLtPpFBGrV3Lq424gHyq2lFj8wfX9tvLMLdmA/R9C7k/mqBp/spcbWuHeIEKgEs3RviOPcWGBA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.2.tgz", + "integrity": "sha512-lCruqQlfWjhMlOdyf5pDHOxoNm4WoyY2vZ4YN33/nuZBRstVDuqPPjS0yBkbUlLEte11FbpW+wWSlfnZfSIZvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.2.tgz", + "integrity": "sha512-6Ee9P26DTb4D8sN9nXxgbi9Dw5vSOfH98M7UlmkjKB2vtUbrRqCbZiNfryGiwnPIpd6YUoTl7rLVD2/x1CyEHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/react": { + "version": "19.0.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz", + "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@vicinae/api": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@vicinae/api/-/api-0.21.1.tgz", + "integrity": "sha512-np/+OicGeYK28OsvdKeoiCUt39Y0ZCP5CVOY5HHWS9oBEH78oVva+Diw47fZviR+Z+AoApSQJq5mshp38vwD0Q==", + "license": "ISC", + "dependencies": { + "chokidar": "^4.0.3", + "esbuild": "^0.25.2", + "react": "^19.0.0", + "zod": "^4.0.17" + }, + "bin": { + "vici": "dist/bin.js" + }, + "peerDependencies": { + "@types/node": ">=18", + "@types/react": "19.0.10" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT", + "peer": true + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT", + "peer": true + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/extensions/gif-selector/package.json b/extensions/gif-selector/package.json new file mode 100644 index 00000000..fe3b0e46 --- /dev/null +++ b/extensions/gif-selector/package.json @@ -0,0 +1,93 @@ +{ + "$schema": "https://raw.githubusercontent.com/vicinaehq/vicinae/refs/heads/main/extra/schemas/extension.json", + "name": "gif-selector", + "title": "GIF Selector", + "description": "Search Klipy and/or Giphy for GIFs and copy them directly to your clipboard", + "categories": [ + "Media", + "Fun" + ], + "license": "MIT", + "author": "RealThanosP", + "contributors": [], + "pastContributors": [], + "icon": "extension_icon.png", + "commands": [ + { + "name": "gif-selector", + "title": "Search GIFs", + "subtitle": "Giphy & Klipy", + "description": "Search for GIFs and copy them to your clipboard", + "mode": "view" + } + ], + "preferences": [ + { + "name": "provider", + "title": "GIF Provider", + "description": "Select whether to use Giphy or Klipy by default.", + "type": "dropdown", + "required": false, + "default": "klipy", + "data": [ + { + "title": "Klipy", + "value": "klipy" + }, + { + "title": "Giphy", + "value": "giphy" + } + ] + }, + { + "name": "klipyApiKey", + "title": "Klipy API Key", + "description": "Your Klipy API key. Get one for free at https://klipy.com/developers", + "type": "password", + "required": false + }, + { + "name": "giphyApiKey", + "title": "Giphy API Key", + "description": "Your Giphy API key. Get one at https://developers.giphy.com", + "type": "password", + "required": false + }, + { + "name": "previewQuality", + "title": "Preview Quality", + "description": "Choose the GIF preview quality in the details panel. Lower quality loads faster.", + "type": "dropdown", + "required": false, + "default": "medium", + "data": [ + { + "title": "Low (Fastest)", + "value": "low" + }, + { + "title": "Medium (Balanced)", + "value": "medium" + }, + { + "title": "High (Best Quality)", + "value": "high" + } + ] + } + ], + "scripts": { + "build": "vici build", + "dev": "vici develop", + "format": "biome format --write src", + "lint": "vici lint" + }, + "dependencies": { + "@vicinae/api": "^0.21.0" + }, + "devDependencies": { + "typescript": "^5.9.2", + "@biomejs/biome": "2.3.2" + } +} \ No newline at end of file diff --git a/extensions/gif-selector/src/gif-selector.tsx b/extensions/gif-selector/src/gif-selector.tsx new file mode 100644 index 00000000..32707939 --- /dev/null +++ b/extensions/gif-selector/src/gif-selector.tsx @@ -0,0 +1,539 @@ +import { + Action, + ActionPanel, + Clipboard, + Icon, + List, + Toast, + environment, + getPreferenceValues, + openExtensionPreferences, + showToast, + Form, + LocalStorage, +} from "@vicinae/api"; +import { useCallback, useEffect, useRef, useState } from "react"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import * as https from "node:https"; +import * as http from "node:http"; +import * as url from "node:url"; + +// ─── Types ──────────────────────────────────────────────────────────────────── + +interface Preferences { + provider?: "klipy" | "giphy"; + klipyApiKey?: string; + giphyApiKey?: string; + previewQuality?: "low" | "medium" | "high"; +} + +interface UnifiedGif { + id: string; // e.g. "klipy_123" or "giphy_abc" + title: string; + previewStillUrl?: string; + previewAnimatedUrl?: string; + copyUrl?: string; +} + +// ─── Klipy Specific Types ───────────────────────────────────────────────────── + +interface KlipyMediaFormat { + url: string; + width: number; + height: number; + size?: number; +} + +interface KlipyMediaTier { + gif?: KlipyMediaFormat; + webp?: KlipyMediaFormat; + jpg?: KlipyMediaFormat; + mp4?: KlipyMediaFormat; + webm?: KlipyMediaFormat; +} + +interface KlipyGif { + id: number | string; + title: string; + slug?: string; + type?: string; + blur_preview?: string; + file: { + hd?: KlipyMediaTier; + md?: KlipyMediaTier; + sm?: KlipyMediaTier; + xs?: KlipyMediaTier; + }; +} + +// ─── Giphy Specific Types ───────────────────────────────────────────────────── + +interface GiphyMediaFormat { + url?: string; + width?: string; + height?: string; + size?: string; +} + +interface GiphyGif { + id: string; + title: string; + slug?: string; + type?: string; + images: { + original?: GiphyMediaFormat; + downsized?: GiphyMediaFormat; + fixed_width?: GiphyMediaFormat; + fixed_width_still?: GiphyMediaFormat; + preview_gif?: GiphyMediaFormat; + }; +} + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +function downloadFile(fileUrl: string, dest: string, redirects = 0): Promise { + return new Promise((resolve, reject) => { + if (redirects > 5) { + reject(new Error("Too many redirects")); + return; + } + + const parsedUrl = url.parse(fileUrl); + const protocol = parsedUrl.protocol === "https:" ? https : http; + const file = fs.createWriteStream(dest); + + const request = protocol.get(fileUrl, (response) => { + if ( + response.statusCode && + response.statusCode >= 300 && + response.statusCode < 400 && + response.headers.location + ) { + file.close(); + fs.unlink(dest, () => {}); + downloadFile(response.headers.location, dest, redirects + 1) + .then(resolve) + .catch(reject); + return; + } + + if (response.statusCode !== 200) { + file.close(); + fs.unlink(dest, () => {}); + reject(new Error(`HTTP ${response.statusCode}`)); + return; + } + + response.pipe(file); + file.on("finish", () => { file.close(); resolve(); }); + }); + + request.on("error", (err) => { file.close(); fs.unlink(dest, () => {}); reject(err); }); + file.on("error", (err) => { file.close(); fs.unlink(dest, () => {}); reject(err); }); + }); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function klipyFetch(apiKey: string, endpoint: string, params: Record = {}): Promise { + const qs = new URLSearchParams({ limit: "24", ...params }).toString(); + const fullUrl = `https://api.klipy.com/api/v1/${encodeURIComponent(apiKey)}/${endpoint}?${qs}`; + + return new Promise((resolve, reject) => { + https.get(fullUrl, (res) => { + let raw = ""; + res.on("data", (chunk: string) => (raw += chunk)); + res.on("end", () => { + try { + const parsed = JSON.parse(raw); + resolve(parsed); + } catch (e) { + reject(new Error(`Failed to parse Klipy response (HTTP ${res.statusCode}): ${raw.slice(0, 300)}`)); + } + }); + }).on("error", reject); + }); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function extractGifArray(response: any): KlipyGif[] { + // Try every plausible shape the Klipy API might return + const candidates = [ + response?.data, // { data: [...] } + response?.data?.data, // { data: { data: [...] } } + response?.data?.list, // { data: { list: [...] } } + response?.data?.gifs, // { data: { gifs: [...] } } + response?.data?.items, // { data: { items: [...] } } + response?.results, // { results: [...] } + response?.gifs, // { gifs: [...] } + response?.list, // { list: [...] } + response?.items, // { items: [...] } + ]; + for (const candidate of candidates) { + if (Array.isArray(candidate) && candidate.length >= 0) { + return candidate as KlipyGif[]; + } + } + return []; +} + +async function fetchKlipy(query: string, apiKey: string): Promise { + const endpoint = query.trim() ? "gifs/search" : "gifs/trending"; + const params: Record = query.trim() ? { q: query } : {}; + const response = await klipyFetch(apiKey, endpoint, params); + return extractGifArray(response); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function giphyFetch(apiKey: string, endpoint: string, params: Record = {}): Promise { + const qs = new URLSearchParams({ limit: "24", ...params }).toString(); + const fullUrl = `https://api.giphy.com/v1/gifs/${endpoint}?api_key=${encodeURIComponent(apiKey)}&${qs}`; + + return new Promise((resolve, reject) => { + https.get(fullUrl, (res) => { + let raw = ""; + res.on("data", (chunk: string) => (raw += chunk)); + res.on("end", () => { + try { + const parsed = JSON.parse(raw); + resolve(parsed); + } catch (e) { + reject(new Error(`Failed to parse Giphy response (HTTP ${res.statusCode}): ${raw.slice(0, 300)}`)); + } + }); + }).on("error", reject); + }); +} + +async function fetchGiphy(query: string, apiKey: string): Promise { + const endpoint = query.trim() ? "search" : "trending"; + const params: Record = query.trim() ? { q: query } : {}; + const response = await giphyFetch(apiKey, endpoint, params); + return Array.isArray(response?.data) ? response.data : []; +} + +async function fetchGifs( + query: string, + provider: "klipy" | "giphy", + apiKey: string, + quality: "low" | "medium" | "high" +): Promise { + if (provider === "giphy") { + const rawGifs = await fetchGiphy(query, apiKey); + return rawGifs.map((gif) => { + const previewStillUrl = gif.images.fixed_width_still?.url ?? gif.images.preview_gif?.url; + + let previewAnimatedUrl = gif.images.fixed_width?.url; + if (quality === "low") { + previewAnimatedUrl = gif.images.preview_gif?.url ?? gif.images.fixed_width?.url; + } else if (quality === "high") { + previewAnimatedUrl = gif.images.original?.url ?? gif.images.downsized?.url; + } + previewAnimatedUrl = previewAnimatedUrl ?? gif.images.downsized?.url ?? gif.images.original?.url; + + const copyUrl = gif.images.downsized?.url ?? gif.images.fixed_width?.url ?? gif.images.original?.url; + + return { + id: `giphy_${gif.id}`, + title: gif.title || "(untitled)", + previewStillUrl, + previewAnimatedUrl, + copyUrl, + }; + }); + } else { + const rawGifs = await fetchKlipy(query, apiKey); + return rawGifs.map((gif) => { + const previewStillUrl = quality === "low" + ? (gif.blur_preview ?? gif.file.xs?.jpg?.url) + : (gif.file.xs?.jpg?.url ?? gif.file.sm?.jpg?.url ?? gif.blur_preview); + + let previewAnimatedUrl = gif.file.md?.gif?.url; + if (quality === "low") { + previewAnimatedUrl = gif.file.sm?.gif?.url ?? gif.file.md?.gif?.url; + } else if (quality === "high") { + previewAnimatedUrl = gif.file.hd?.gif?.url ?? gif.file.md?.gif?.url; + } + previewAnimatedUrl = previewAnimatedUrl ?? gif.file.hd?.gif?.url ?? gif.file.md?.gif?.url ?? gif.file.sm?.gif?.url; + + const copyUrl = gif.file.md?.gif?.url ?? gif.file.hd?.gif?.url ?? gif.file.sm?.gif?.url; + + return { + id: `klipy_${gif.id}`, + title: gif.title || "(untitled)", + previewStillUrl, + previewAnimatedUrl, + copyUrl, + }; + }); + } +} + +// ─── Copy GIF to clipboard ──────────────────────────────────────────────────── + +async function copyGifToClipboard(gif: UnifiedGif): Promise { + if (!gif.copyUrl) throw new Error("No GIF URL available"); + + const supportDir = environment.supportPath; + if (!fs.existsSync(supportDir)) { + fs.mkdirSync(supportDir, { recursive: true }); + } + + const destPath = path.join(supportDir, `${gif.id}.gif`); + if (!fs.existsSync(destPath)) { + await downloadFile(gif.copyUrl, destPath); + } + + await Clipboard.copy({ file: destPath }); +} + +// ─── Main Command ───────────────────────────────────────────────────────────── + +export default function GifSelector() { + const preferences = getPreferenceValues(); + + const [storedKlipyKey, setStoredKlipyKey] = useState(null); + const [storedGiphyKey, setStoredGiphyKey] = useState(null); + const [storedProvider, setStoredProvider] = useState<"klipy" | "giphy" | null>(null); + const [isStorageLoaded, setIsStorageLoaded] = useState(false); + + useEffect(() => { + async function loadStorage() { + try { + const kKey = await LocalStorage.getItem("klipyApiKey"); + const gKey = await LocalStorage.getItem("giphyApiKey"); + const prov = await LocalStorage.getItem("provider"); + if (kKey) setStoredKlipyKey(kKey); + if (gKey) setStoredGiphyKey(gKey); + if (prov === "klipy" || prov === "giphy") setStoredProvider(prov); + } catch (err) { + console.error("Failed to load local storage", err); + } finally { + setIsStorageLoaded(true); + } + } + loadStorage(); + }, []); + + // Determine provider dynamically based on settings, falling back to storage + let provider = preferences.provider || storedProvider || "klipy"; + + // Resolve active API key + let apiKey = (provider === "giphy" + ? (preferences.giphyApiKey || storedGiphyKey) + : (preferences.klipyApiKey || storedKlipyKey))?.trim(); + + // Auto fallback if key is missing for selected but present for the other (checking both prefs and storage) + const effectiveGiphyKey = (preferences.giphyApiKey || storedGiphyKey)?.trim(); + const effectiveKlipyKey = (preferences.klipyApiKey || storedKlipyKey)?.trim(); + + if (!apiKey) { + if (provider === "klipy" && effectiveGiphyKey) { + provider = "giphy"; + apiKey = effectiveGiphyKey; + } else if (provider === "giphy" && effectiveKlipyKey) { + provider = "klipy"; + apiKey = effectiveKlipyKey; + } + } + + const quality = preferences.previewQuality || "medium"; + + const [searchText, setSearchText] = useState(""); + const [gifs, setGifs] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [copyingId, setCopyingId] = useState(null); + + const searchRef = useRef(searchText); + searchRef.current = searchText; + + const load = useCallback(async (query: string) => { + if (!apiKey) { + setIsLoading(false); + return; + } + setIsLoading(true); + try { + const results = await fetchGifs(query, provider, apiKey, quality); + if (searchRef.current === query) setGifs(results); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + await showToast({ style: Toast.Style.Failure, title: "Failed to fetch GIFs", message: msg }); + } finally { + if (searchRef.current === query) setIsLoading(false); + } + }, [apiKey, provider, quality]); + + useEffect(() => { + if (isStorageLoaded) { + load(""); + } + }, [load, isStorageLoaded]); + + const handleSearchChange = useCallback((text: string) => { + setSearchText(text); + load(text); + }, [load]); + + const handleCopy = useCallback(async (gif: UnifiedGif) => { + setCopyingId(gif.id); + const toast = await showToast({ + style: Toast.Style.Animated, + title: "Copying GIF…", + message: gif.title || gif.id, + }); + try { + await copyGifToClipboard(gif); + toast.style = Toast.Style.Success; + toast.title = "GIF Copied!"; + toast.message = gif.title || gif.id; + setTimeout(() => toast.hide(), 1500); + } catch (err) { + toast.style = Toast.Style.Failure; + toast.title = "Copy Failed"; + toast.message = err instanceof Error ? err.message : String(err); + } finally { + setCopyingId(null); + } + }, []); + + // ── Show loading list if storage is still reading ────────────────────────── + if (!isStorageLoaded) { + return ; + } + + // ── Setup View (if no API keys are configured anywhere) ──────────────────── + if (!apiKey) { + return ( +
+ { + const kKey = (values.klipyApiKey as string || "").trim(); + const gKey = (values.giphyApiKey as string || "").trim(); + const prov = values.provider as "klipy" | "giphy"; + + if (!kKey && !gKey) { + await showToast({ + style: Toast.Style.Failure, + title: "API Key Required", + message: "Please enter at least one API key.", + }); + return false; + } + + try { + if (kKey) await LocalStorage.setItem("klipyApiKey", kKey); + if (gKey) await LocalStorage.setItem("giphyApiKey", gKey); + await LocalStorage.setItem("provider", prov); + + setStoredKlipyKey(kKey || null); + setStoredGiphyKey(gKey || null); + setStoredProvider(prov); + + await showToast({ + style: Toast.Style.Success, + title: "Configuration Saved", + message: "GIF Selector is ready to use!", + }); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + await showToast({ + style: Toast.Style.Failure, + title: "Save Failed", + message: msg, + }); + } + }} + /> + + + } + > + + + + + + + + + ); + } + + // ── Main list view ───────────────────────────────────────────────────────── + const providerLabel = provider === "giphy" ? "Giphy" : "Klipy"; + return ( + + {!isLoading && gifs.length === 0 ? ( + + ) : ( + + {gifs.map((gif) => { + const isCopying = copyingId === gif.id; + const animatedUrl = gif.previewAnimatedUrl; + const previewUrl = gif.previewStillUrl; + + return ( + + } + actions={ + + + handleCopy(gif)} + /> + + + } + /> + ); + })} + + )} + + ); +} diff --git a/extensions/gif-selector/tsconfig.json b/extensions/gif-selector/tsconfig.json new file mode 100644 index 00000000..8d9b6bbc --- /dev/null +++ b/extensions/gif-selector/tsconfig.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Node 16", + "include": ["src/**/*"], + "compilerOptions": { + //"lib": ["es2020"], + "module": "commonjs", + "target": "es2020", + "strict": true, + "isolatedModules": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "jsx": "react-jsx", + "types": ["node"] + } +}