├── .npmrc ├── ezup.ai ├── src-tauri ├── build.rs ├── icons │ ├── 32x32.png │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── StoreLogo.png │ ├── Square30x30Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ └── Square310x310Logo.png ├── .gitignore ├── Cargo.toml ├── src │ ├── putobj.rs │ └── main.rs ├── tauri.conf.json └── s3 │ └── main.rs ├── static ├── favicon.png ├── aws-logo.jpg ├── imgur-logo.webp ├── vite.svg ├── svelte.svg └── tauri.svg ├── src ├── routes │ ├── +layout.ts │ ├── preference │ │ ├── +layout.svelte │ │ └── +page.svelte │ ├── service │ │ ├── +layout.svelte │ │ └── +page.svelte │ ├── display │ │ ├── +layout.svelte │ │ └── +page.svelte │ ├── debug │ │ └── +page.svelte │ ├── about │ │ └── +page.svelte │ ├── +page.svelte │ └── +layout.svelte ├── app.d.ts ├── app.css ├── lib │ ├── constants.ts │ ├── components │ │ ├── ServiceAvatar.svelte │ │ ├── ImageDisplayListItem.svelte │ │ ├── ServiceSettings │ │ │ ├── ImgurSetting.svelte │ │ │ └── S3Setting.svelte │ │ ├── TitleBar.svelte │ │ ├── UploadURL.svelte │ │ ├── Navbar.svelte │ │ ├── ServicesDropdown.svelte │ │ ├── HotkeySelection.svelte │ │ ├── ServiceListItem.svelte │ │ ├── DropUpload.svelte │ │ └── ServiceSetting.svelte │ ├── shortcut.ts │ ├── images │ │ └── ezup.svg │ ├── notify.ts │ ├── types.ts │ ├── store.ts │ ├── util.ts │ └── uploader.ts └── app.html ├── .prettierrc ├── postcss.config.cjs ├── .vscode └── extensions.json ├── index.html ├── svelte.config.js ├── scripts └── prepare_build.sh ├── tailwind.config.cjs ├── .gitignore ├── tsconfig.json ├── vite.config.js ├── LICENSE ├── package.json ├── .github └── workflows │ ├── publish-macos.yml │ └── publish.yml └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /ezup.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuakunShen/EzUp/HEAD/ezup.ai -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuakunShen/EzUp/HEAD/static/favicon.png -------------------------------------------------------------------------------- /static/aws-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuakunShen/EzUp/HEAD/static/aws-logo.jpg -------------------------------------------------------------------------------- /static/imgur-logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuakunShen/EzUp/HEAD/static/imgur-logo.webp -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuakunShen/EzUp/HEAD/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuakunShen/EzUp/HEAD/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuakunShen/EzUp/HEAD/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuakunShen/EzUp/HEAD/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuakunShen/EzUp/HEAD/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuakunShen/EzUp/HEAD/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuakunShen/EzUp/HEAD/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | export const ssr = false; 2 | export const prerender = true; 3 | export const csr = true; 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuakunShen/EzUp/HEAD/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuakunShen/EzUp/HEAD/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuakunShen/EzUp/HEAD/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuakunShen/EzUp/HEAD/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuakunShen/EzUp/HEAD/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuakunShen/EzUp/HEAD/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuakunShen/EzUp/HEAD/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuakunShen/EzUp/HEAD/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuakunShen/EzUp/HEAD/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "svelte.svelte-vscode", 4 | "tauri-apps.tauri-vscode", 5 | "rust-lang.rust-analyzer" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | // and what to do when importing types 4 | declare namespace App {} 5 | -------------------------------------------------------------------------------- /src/routes/preference/+layout.svelte: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | -------------------------------------------------------------------------------- /src/routes/service/+layout.svelte: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | -------------------------------------------------------------------------------- /src/routes/display/+layout.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | /* background-color: #131826; */ 7 | background-color: rgb(19, 24, 38); 8 | color: white; 9 | border-radius: 40px; 10 | } 11 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export enum ServiceTypes { 2 | S3 = 's3', 3 | Imgur = 'imgur', 4 | } 5 | 6 | export const KeyServices = 'services'; 7 | export const KeyCurSerivce = 'curService'; 8 | export const KeySelectedServiceId = 'curServiceId'; 9 | export const KeyShortcuts = 'shortcuts'; 10 | export const KeyFormatter = 'formatter'; 11 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import staticAdapter from "@sveltejs/adapter-static"; 2 | import { vitePreprocess } from "@sveltejs/kit/vite"; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | preprocess: [vitePreprocess()], 7 | kit: { 8 | adapter: staticAdapter(), 9 | }, 10 | }; 11 | 12 | export default config; 13 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/components/ServiceAvatar.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | {#if service === "s3"} 7 | 8 | {:else if service === "imgur"} 9 | 10 | {/if} 11 | -------------------------------------------------------------------------------- /scripts/prepare_build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | PRIVATE_KEY_PATH="$HOME/.tauri/ezup.key"; 3 | if test -f "$PRIVATE_KEY_PATH"; then 4 | export TAURI_PRIVATE_KEY=$(cat ~/.tauri/ezup.key); # if the private key is stored on disk 5 | export TAURI_KEY_PASSWORD=""; 6 | echo "private key loaded" 7 | else 8 | echo "Warning: Private Key File Not Found"; 9 | fi 10 | -------------------------------------------------------------------------------- /src/lib/shortcut.ts: -------------------------------------------------------------------------------- 1 | import { appWindow } from '@tauri-apps/api/window'; 2 | 3 | export function toggleWindow() { 4 | return appWindow.isVisible().then((visible) => { 5 | if (visible) { 6 | return appWindow.hide(); 7 | } else { 8 | return appWindow.show().then(() => { 9 | return appWindow.setFocus(); 10 | }); 11 | } 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./src/**/*.{html,js,svelte,ts}", 5 | "./src/**/*.{html,js,svelte,ts}", 6 | "./node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}", 7 | ], 8 | theme: { 9 | extend: {}, 10 | }, 11 | plugins: [require("flowbite/plugin")], 12 | darkMode: "class", 13 | }; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | /build 27 | /.svelte-kit 28 | /package 29 | .env 30 | .env.* 31 | !.env.example 32 | 33 | rust-imgurs 34 | clipboard-image -------------------------------------------------------------------------------- /src/routes/display/+page.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 |
8 |

Upload Images Will Be Displayed Here

9 |

Data Will Be Cleared After Quiting This App

10 |
11 |
12 | {#each $logImagesUrls as imageUrl, i} 13 | 14 | {/each} 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true 12 | } 13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 14 | // 15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 16 | // from the referenced tsconfig.json - TypeScript does not merge them in 17 | } 18 | -------------------------------------------------------------------------------- /src/routes/debug/+page.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { sveltekit } from "@sveltejs/kit/vite"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [sveltekit()], 7 | 8 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` 9 | // prevent vite from obscuring rust errors 10 | clearScreen: false, 11 | // tauri expects a fixed port, fail if that port is not available 12 | server: { 13 | port: 1420, 14 | strictPort: true, 15 | }, 16 | // to make use of `TAURI_DEBUG` and other env variables 17 | // https://tauri.studio/v1/api/config#buildconfig.beforedevcommand 18 | envPrefix: ["VITE_", "TAURI_"], 19 | build: { 20 | // Tauri supports es2021 21 | target: process.env.TAURI_PLATFORM == "windows" ? "chrome105" : "safari13", 22 | // don't minify for debug builds 23 | minify: !process.env.TAURI_DEBUG ? "esbuild" : false, 24 | // produce sourcemaps for debug builds 25 | sourcemap: !!process.env.TAURI_DEBUG, 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /src/lib/components/ImageDisplayListItem.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 |
20 |
21 |
22 | 23 |
24 |
25 | 26 | 27 |
28 |
29 |
30 | 31 | 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Huakun Shen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/lib/images/ezup.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/lib/components/ServiceSettings/ImgurSetting.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 |
23 |
24 | ImgurSetting 25 | 26 | 33 |
34 |
35 | -------------------------------------------------------------------------------- /src/lib/notify.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Huakun Shen 3 | * @email huakun.shen@huakunshen.com 4 | * @create date 2023-01-16 16:46:03 5 | * @modify date 2023-01-16 16:46:03 6 | * @desc this file contains notification-related helpers 7 | */ 8 | import { 9 | isPermissionGranted, 10 | requestPermission, 11 | sendNotification, 12 | } from '@tauri-apps/api/notification'; 13 | 14 | /** 15 | * @returns Whether Notification is granted 16 | */ 17 | export async function getNotificationPermision(): Promise { 18 | let permissionGranted = await isPermissionGranted(); 19 | if (!permissionGranted) { 20 | const permission = await requestPermission(); 21 | permissionGranted = permission === 'granted'; 22 | } 23 | return permissionGranted; 24 | } 25 | 26 | /** 27 | * Send notification 28 | * @param title Title of Notification 29 | * @param body Message of Notification 30 | */ 31 | export async function notify(title: string, body?: string): Promise { 32 | const permissionGranted = await getNotificationPermision(); 33 | if (permissionGranted) { 34 | if (!body) sendNotification(title); 35 | else sendNotification({ title, body }); 36 | return Promise.resolve(); 37 | } else { 38 | return Promise.reject(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/lib/components/TitleBar.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | 14 | 21 | 28 |
29 | 30 | 50 | -------------------------------------------------------------------------------- /src/lib/components/UploadURL.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |
18 | 24 | 39 | 46 | 47 |
48 | -------------------------------------------------------------------------------- /src/lib/components/Navbar.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | Flowbite Logo 20 | 23 | EzUp 24 | 25 | 26 |
27 | 28 | 29 |
30 | 31 | Home 32 | Preference 35 | Service 37 | Display 39 | About 40 | {#if isDev()} 41 | Debug 42 | {:else}{/if} 43 | 44 |
45 | -------------------------------------------------------------------------------- /static/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lib/components/ServicesDropdown.svelte: -------------------------------------------------------------------------------- 1 | 32 | 33 | 34 | 35 | addService(ServiceTypes.S3)}> 36 | 37 | S3 38 | 39 | addService(ServiceTypes.Imgur)}> 40 | 41 | Imgur 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { ServiceTypes } from './constants'; 3 | 4 | export const ShortcutsSchema = z.object({ 5 | toggleWindow: z.string(), 6 | upload: z.string(), 7 | }); 8 | export type ShortcutsMap = ReturnType; 9 | 10 | export const ImgurSettingSchema = z.object({ 11 | clientId: z.string(), 12 | }); 13 | export type ImgurSetting = ReturnType; 14 | export const S3SettingSchema = z.object({ 15 | region: z.string(), 16 | bucket: z.string(), 17 | accessKey: z.string(), 18 | secretKey: z.string(), 19 | prefix: z.string(), 20 | }); 21 | export type S3Setting = ReturnType; 22 | export type ServiceSetting = S3Setting | ImgurSetting; 23 | export const serviceTypes = [ServiceTypes.S3, ServiceTypes.Imgur] as const; 24 | export const ServiceTypesEnum = z.enum(serviceTypes); 25 | export type ServiceType = ReturnType; 26 | export const NonNullServiceSchema = z.object({ 27 | id: z.string(), 28 | type: ServiceTypesEnum, 29 | name: z.string(), 30 | setting: ImgurSettingSchema.or(S3SettingSchema), 31 | }); 32 | export type Service = ReturnType; 33 | export const ServiceSchema = z.nullable(NonNullServiceSchema); 34 | export const ServicesSchema = z.array(NonNullServiceSchema); 35 | 36 | export enum ToastType { 37 | Success = 'success', 38 | Warning = 'warning', 39 | Error = 'error', 40 | } 41 | export type Toast = { 42 | id: string; 43 | type: ToastType; 44 | msg: string; 45 | }; 46 | -------------------------------------------------------------------------------- /src/lib/components/HotkeySelection.svelte: -------------------------------------------------------------------------------- 1 | 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ezup", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite dev", 8 | "build": "vite build", 9 | "tauri:build": "npm run prepare:build:env && tauri build", 10 | "preview": "vite preview", 11 | "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json", 12 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch", 13 | "tauri": "tauri", 14 | "prepare:build:env": "./scripts/prepare_build.sh", 15 | "build:mac:universal": "tauri build --target universal-apple-darwin" 16 | }, 17 | "dependencies": { 18 | "@tauri-apps/api": "^1.5.1", 19 | "@types/path-browserify": "^1.0.0", 20 | "lodash": "^4.17.21", 21 | "path-browserify": "^1.0.1", 22 | "svelte-tauri-filedrop": "^1.0.0", 23 | "tauri-plugin-clipboard-api": "^0.2.4", 24 | "tauri-plugin-store-api": "github:tauri-apps/tauri-plugin-store", 25 | "uuid": "^9.0.0", 26 | "zod": "^3.21.4" 27 | }, 28 | "devDependencies": { 29 | "@iconify/svelte": "^3.1.4", 30 | "@popperjs/core": "^2.11.6", 31 | "@sveltejs/adapter-static": "next", 32 | "@sveltejs/kit": "^1.15.2", 33 | "@tauri-apps/cli": "^1.5.6", 34 | "@types/lodash": "^4.14.191", 35 | "@types/uuid": "^9.0.0", 36 | "autoprefixer": "^10.4.13", 37 | "classnames": "^2.3.2", 38 | "flowbite": "^1.6.0", 39 | "flowbite-svelte": "^0.29.9", 40 | "postcss": "^8.4.21", 41 | "svelte": "^4.0.5", 42 | "svelte-check": "^3.4.6", 43 | "tailwindcss": "^3.2.4", 44 | "tslib": "^2.6.0", 45 | "typescript": "^5.0.2", 46 | "vite": "^4.4.4" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ezup" 3 | version = "0.0.0" 4 | description = "A Tauri App" 5 | authors = ["you"] 6 | license = "" 7 | repository = "" 8 | edition = "2021" 9 | rust-version = "1.57" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [build-dependencies] 14 | tauri-build = { version = "1.5", features = [] } 15 | 16 | [dependencies] 17 | serde_json = "1.0" 18 | serde = { version = "1.0", features = ["derive"] } 19 | tauri = { version = "1.5", features = ["app-hide", "app-show", "clipboard-all", "dialog-open", "dialog-save", "fs-create-dir", "fs-remove-file", "fs-write-file", "global-shortcut-all", "notification-all", "os-all", "path-all", "shell-open", "system-tray", "updater", "window-all"] } 20 | aws-config = "0.52.0" 21 | 22 | tauri-plugin-stronghold = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" } 23 | tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" } 24 | # tauri-plugin-clipboard = { git = "https://github.com/CrossCopy/tauri-plugin-clipboard-api", branch = "main" } 25 | tauri-plugin-clipboard="0.2.4" 26 | imgurs = "0.11.1" 27 | aws-sdk-s3 = "0.22.0" 28 | aws-types = { version = "0.52.0", features = ["hardcoded-credentials"] } 29 | arboard = "3.2.0" 30 | image = "0.24.5" 31 | uuid = "1.2.2" 32 | tempfile = "3.3.0" 33 | error-chain = "0.12.4" 34 | reqwest = {version="0.11.13", features= ["json"] } 35 | window-vibrancy = "0.4.3" 36 | 37 | [features] 38 | # by default Tauri runs in production mode 39 | # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL 40 | default = ["custom-protocol"] 41 | # this feature is used used for production builds where `devPath` points to the filesystem 42 | # DO NOT remove this 43 | custom-protocol = ["tauri/custom-protocol"] 44 | -------------------------------------------------------------------------------- /src/lib/components/ServiceListItem.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
  • 13 | changeSelection(service.id)} 20 | > 21 |
    24 | 25 |
    26 |
    27 |
    {service.name}
    28 |
    {service.type}
    29 |
    30 | 42 |
    43 |
    44 |
    45 |
  • 46 | -------------------------------------------------------------------------------- /static/svelte.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/publish-macos.yml: -------------------------------------------------------------------------------- 1 | name: 'publish' 2 | on: 3 | push: 4 | branches: 5 | - deploy-macos 6 | tags: 7 | - 'v*' 8 | 9 | env: 10 | TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} 11 | TAURI_KEY_PASSWORD: '' 12 | 13 | jobs: 14 | publish-tauri: 15 | permissions: 16 | contents: write 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | platform: [macos-latest] 21 | runs-on: ${{ matrix.platform }} 22 | 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v3 26 | 27 | - name: Rust setup 28 | uses: dtolnay/rust-toolchain@stable 29 | 30 | - name: Rust cache 31 | uses: swatinem/rust-cache@v2 32 | with: 33 | workspaces: './src-tauri -> target' 34 | 35 | - name: Sync node version and setup cache 36 | uses: actions/setup-node@v3 37 | with: 38 | node-version: 'lts/*' 39 | cache: 'npm' # Set this to npm, npm or pnpm. 40 | 41 | - name: Install frontend dependencies 42 | # If you don't have `beforeBuildCommand` configured you may want to build your frontend here too. 43 | run: npm install # Change this to npm, yarn or pnpm. 44 | 45 | - name: Build the app 46 | uses: tauri-apps/tauri-action@v0 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} 50 | APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} 51 | APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} 52 | APPLE_ID: ${{ secrets.APPLE_ID }} 53 | APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} 54 | APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} 55 | with: 56 | tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version 57 | releaseName: 'EzUp v__VERSION__' 58 | releaseBody: 'See the assets to download this version and install.' 59 | releaseDraft: true 60 | prerelease: false 61 | -------------------------------------------------------------------------------- /src/routes/about/+page.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 |
    26 | 27 | 28 | Key 29 | Value 30 | 31 | 32 | 33 | Source Code 34 | 35 | https://github.com/HuakunShen/ezup 40 | 41 | 42 | 43 | EzUp Version 44 | {appVersion} 45 | 46 | 47 | Author 48 | 49 | Huakun 54 | 55 | 56 | 57 |
    58 |
    59 |
    60 |

    Instructions

    61 |

    Imgur Service

    62 |

    63 | See API Docs for how to create an application. 64 | Put the client id in this application. 65 |

    66 |

    AWS S3 Service

    67 |

    68 | Make a AWS S3 bucket and put the bucket name and region in this 69 | application. 70 |

    71 |

    Get Access Key and Secret Access Key with access to your S3 bucket.

    72 |
    73 |
    74 | -------------------------------------------------------------------------------- /src/lib/components/DropUpload.svelte: -------------------------------------------------------------------------------- 1 | 36 | 37 | 42 | 43 | 57 |

    58 | Click to upload or drag and drop 59 |

    60 |

    61 | SVG, PNG, JPG or GIF (MAX. 800x400px) 62 |

    63 |
    64 |
    65 | 66 | 76 | -------------------------------------------------------------------------------- /src/lib/components/ServiceSettings/S3Setting.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 |
    24 |
    25 | 26 | 33 |
    34 |
    35 | 36 | 43 |
    44 |
    45 | 46 | 53 |
    54 |
    55 | 56 | 63 |
    64 |
    65 | 66 | 73 |
    74 | 79 |
    80 | -------------------------------------------------------------------------------- /src/routes/service/+page.svelte: -------------------------------------------------------------------------------- 1 | 50 | 51 |
    52 |
    53 | 54 | {#each $services as service, i} 55 | 56 | {/each} 57 | 58 | 59 | 66 |
    67 |
    68 | 69 |
    70 |
    71 | -------------------------------------------------------------------------------- /static/tauri.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: 'build' 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | workflow_dispatch: 7 | 8 | env: 9 | TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} 10 | TAURI_KEY_PASSWORD: "" 11 | 12 | jobs: 13 | publish-tauri: 14 | permissions: 15 | contents: write 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | platform: [ubuntu-20.04, windows-latest, macos-latest] 20 | 21 | runs-on: ${{ matrix.platform }} 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: setup node 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: 20 28 | cache: 'npm' 29 | - name: Rust setup 30 | uses: dtolnay/rust-toolchain@stable 31 | 32 | - name: Rust cache 33 | uses: swatinem/rust-cache@v2 34 | with: 35 | workspaces: './src-tauri -> target' 36 | - name: install webkit2gtk (ubuntu only) 37 | if: matrix.platform == 'ubuntu-20.04' 38 | run: | 39 | sudo apt-get update 40 | sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf 41 | # libayatana-appindicator3-dev is for fixing an error https://github.com/tauri-apps/tauri/issues/5175 42 | - name: install app dependencies 43 | run: npm i 44 | - name: Tauri Build and Release (Non MacOS) 45 | uses: tauri-apps/tauri-action@v0 46 | if: matrix.platform != 'macos-latest' 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | with: 50 | tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version 51 | releaseName: 'EzUp v__VERSION__' 52 | releaseBody: 'See the assets to download this version and install.' 53 | releaseDraft: true 54 | prerelease: false 55 | - name: Add Mac Universal Target 56 | if: matrix.platform == 'macos-latest' 57 | run: | 58 | rustup target add x86_64-apple-darwin 59 | rustup target add aarch64-apple-darwin 60 | - name: Tauri Build and Release (MacOS Universal Build) 61 | uses: tauri-apps/tauri-action@v0 62 | if: matrix.platform == 'macos-latest' 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 65 | # ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }} 66 | APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} 67 | APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} 68 | APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} 69 | APPLE_ID: ${{ secrets.APPLE_ID }} 70 | APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} 71 | APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} 72 | with: 73 | tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version 74 | releaseName: 'EzUp v__VERSION__' 75 | args: '--target universal-apple-darwin' # build universal app for mac, tauri is small any ways. 6MB -> 13MB. No difference 76 | releaseBody: 'See the assets to download this version and install.' 77 | releaseDraft: true 78 | prerelease: false -------------------------------------------------------------------------------- /src/lib/components/ServiceSetting.svelte: -------------------------------------------------------------------------------- 1 | 35 | 36 | {#if !$curService} 37 | 38 | 52 | Warning 53 | No Service Selected, please select one or create one. 54 | 55 | {:else} 56 |
    57 | Service Name: {$curService.name} 58 |
    59 | Service Type: {$curService.type} 60 |
    61 |
    62 | 63 | 70 |
    71 | {#if $curService.type === 'imgur'} 72 | 73 | {:else if $curService.type === 's3'} 74 | 75 | {:else} 76 | 77 | Error Service Type {$curService.type} 78 | Not Supported 79 | 80 | {/if} 81 |
    82 |
    83 | 84 | 85 |
    86 |
    87 | {/if} 88 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 54 | 55 |
    56 |
    57 |

    58 | Current Service:: {$curService?.name} ({$curService?.type}) 59 |

    60 |
    61 | Upload by Drag and Drop 62 |
    63 | 68 |
    69 |

    OR

    70 |
    71 | 72 |
    73 | 76 |
    77 |
    78 |
    79 |

    OR

    80 |
    81 |
    82 | File URL To Upload 83 | 84 |
    85 |
    86 |
    87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EzUp 2 | 3 | 4 | 5 | 6 | 7 | - [EzUp](#ezup) 8 | - [Installation](#installation) 9 | - [Usage](#usage) 10 | - [Services](#services) 11 | - [Imgur](#imgur) 12 | - [AWS S3](#aws-s3) 13 | - [Updater](#updater) 14 | - [Development](#development) 15 | - [Building](#building) 16 | - [MacOS](#macos) 17 | 18 | 19 | 20 | Eazy Uploader is for easy image/file uploader. 21 | 22 | Here is some demo screenshots. 23 | 24 | 25 |
    26 | More Images 27 |
    28 | 29 | 30 |
    31 |
    32 | 33 | ## Installation 34 | 35 | Download from the latest release based on your platform. 36 | 37 | For Mac you should download the `*.app.gz` file for now. Uncompress it and run `xattr -cr EzUp.app` to unlock. This is because the author didn't pay apple tax. 38 | 39 | ## Usage 40 | 41 | 1. Go to **Service** tab, choose a service and fill in the parameters 42 | 43 | ## Services 44 | 45 | ### Imgur 46 | 47 | Register an application from [here](https://api.imgur.com/oauth2/addclient). Get the application client id and fill into EzUp service. 48 | 49 | ### AWS S3 50 | 51 | Create a public S3 bucket, and create a pair of access key and secret key with S3 access and enter into EzUp. 52 | 53 | 54 | ## Updater 55 | 56 | ```bash 57 | tauri signer generate -w ~/.tauri/ezup.key # generate a pair of keys 58 | ``` 59 | 60 | - [vercel/hazel](https://github.com/vercel/hazel) (updater server) 61 | - [Vercel Console](https://vercel.com/huakunshen/tauri-ezup-updater) 62 | - [Deployed Updater Server](https://tauri-ezup-updater.vercel.app/) 63 | 64 | Update Server for this app: https://tauri-ezup-updater.vercel.app/ 65 | 66 | How to deploy an update server for Tauri App in one click: https://github.com/HuakunShen/tauri-update-server 67 | 68 | ## Development 69 | 70 | The data are stored on disk with [tauri-plugin-store](https://github.com/tauri-apps/tauri-plugin-store). Files are stored in [appDataDir](https://tauri.app/v1/api/js/path/#appdatadir). See [This issue](https://github.com/tauri-apps/plugins-workspace/issues/298). 71 | 72 | ```bash 73 | npm i 74 | npm run tauri dev 75 | ``` 76 | 77 | ## Building 78 | 79 | Set environment variables first 80 | 81 | ```bash 82 | tauri signer generate -w ~/.tauri/ezup.key 83 | export TAURI_PRIVATE_KEY="" 84 | export TAURI_PRIVATE_KEY=$(cat ~/.tauri/ezup.key) # if the private key is stored on disk 85 | export TAURI_KEY_PASSWORD="" 86 | ``` 87 | 88 | ### MacOS 89 | 90 | For universal build on darwin, the following command is required. 91 | 92 | Choose one of the following depending on the Mac CPU. If you use apple silicon, then add x86 target, and vice versa. 93 | 94 | ```bash 95 | rustup target add x86_64-apple-darwin 96 | rustup target add aarch64-apple-darwin 97 | ``` 98 | 99 | Use `npm run build:mac:universal` to build a darwin universal app. 100 | 101 | -------------------------------------------------------------------------------- /src-tauri/src/putobj.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0. 4 | */ 5 | 6 | // TODO: Check python example to understand the process, then come back to rust 7 | // https://github.com/awsdocs/aws-doc-sdk-examples/blob/main/python/example_code/s3/s3_basics/object_wrapper.py 8 | 9 | use aws_config::meta::region::RegionProviderChain; 10 | use aws_sdk_s3::presigning::config::PresigningConfig; 11 | use aws_sdk_s3::{Client, Region, PKG_VERSION}; 12 | use std::error::Error; 13 | use std::time::Duration; 14 | use structopt::StructOpt; 15 | 16 | #[derive(Debug, StructOpt)] 17 | struct Opt { 18 | /// The AWS Region. 19 | #[structopt(short, long)] 20 | region: Option, 21 | 22 | /// The name of the bucket. 23 | #[structopt(short, long)] 24 | bucket: String, 25 | 26 | /// The object key. 27 | #[structopt(short, long)] 28 | object: String, 29 | 30 | /// How long in seconds before the presigned request should expire. 31 | #[structopt(short, long)] 32 | expires_in: Option, 33 | 34 | /// Whether to display additional information. 35 | #[structopt(short, long)] 36 | verbose: bool, 37 | } 38 | 39 | // Adds an object to a bucket and returns a public URI. 40 | // snippet-start:[s3.rust.put-object-presigned] 41 | async fn put_object( 42 | client: &Client, 43 | bucket: &str, 44 | object: &str, 45 | expires_in: u64, 46 | ) -> Result<(), Box> { 47 | let expires_in = Duration::from_secs(expires_in); 48 | 49 | let presigned_request = client 50 | .put_object() 51 | .bucket(bucket) 52 | .key(object) 53 | .presigned(PresigningConfig::expires_in(expires_in)?) 54 | .await?; 55 | 56 | println!("Object URI: {}", presigned_request.uri()); 57 | 58 | Ok(()) 59 | } 60 | // snippet-end:[s3.rust.put-object-presigned] 61 | 62 | /// Adds an object to a bucket and returns a public URI. 63 | /// # Arguments 64 | /// 65 | /// * `[-r REGION]` - The Region in which the client is created. 66 | /// If not supplied, uses the value of the **AWS_REGION** environment variable. 67 | /// * `-b BUCKET` - The bucket where the object is uploaded. 68 | /// * `-o OBJECT` - The name of the file to upload to the bucket. 69 | /// If the environment variable is not set, defaults to **us-west-2**. 70 | /// * `[-e EXPIRES_IN]` - The amount of time the presigned request should be valid for. 71 | /// If not given, this defaults to 15 minutes. 72 | /// * `[-v]` - Whether to display additional information. 73 | #[tokio::main] 74 | async fn main() -> Result<(), Box> { 75 | tracing_subscriber::fmt::init(); 76 | 77 | let Opt { 78 | region, 79 | bucket, 80 | object, 81 | expires_in, 82 | verbose, 83 | } = Opt::from_args(); 84 | 85 | let region_provider = RegionProviderChain::first_try(region.map(Region::new)) 86 | .or_default_provider() 87 | .or_else(Region::new("us-west-2")); 88 | let shared_config = aws_config::from_env().region(region_provider).load().await; 89 | let client = Client::new(&shared_config); 90 | 91 | println!(); 92 | 93 | if verbose { 94 | println!("S3 client version: {}", PKG_VERSION); 95 | println!("Region: {}", shared_config.region().unwrap()); 96 | println!("Bucket: {}", &bucket); 97 | println!("Object: {}", &object); 98 | println!("Expires in: {} seconds", expires_in.unwrap_or(900)); 99 | println!(); 100 | } 101 | 102 | put_object(&client, &bucket, &object, expires_in.unwrap_or(900)).await 103 | } -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "beforeDevCommand": "npm run dev", 4 | "beforeBuildCommand": "npm run build", 5 | "devPath": "http://localhost:1420", 6 | "distDir": "../build", 7 | "withGlobalTauri": true 8 | }, 9 | "package": { 10 | "productName": "EzUp", 11 | "version": "0.1.15" 12 | }, 13 | "tauri": { 14 | "systemTray": { 15 | "iconPath": "icons/icon.ico", 16 | "iconAsTemplate": true 17 | }, 18 | "allowlist": { 19 | "app": { 20 | "hide": true, 21 | "show": true 22 | }, 23 | "dialog": { 24 | "open": true, 25 | "save": true 26 | }, 27 | "notification": { 28 | "all": true 29 | }, 30 | "clipboard": { 31 | "all": true 32 | }, 33 | "globalShortcut": { 34 | "all": true 35 | }, 36 | "path": { 37 | "all": true 38 | }, 39 | "os": { 40 | "all": true 41 | }, 42 | "fs": { 43 | "createDir": true, 44 | "removeFile": true, 45 | "writeFile": true, 46 | "scope": ["$APPDATA/*", "$CACHE/**"] 47 | }, 48 | "shell": { 49 | "all": false, 50 | "open": true 51 | }, 52 | "window": { 53 | "all": true, 54 | "center": true, 55 | "close": true, 56 | "create": true, 57 | "hide": true, 58 | "maximize": false, 59 | "minimize": false, 60 | "print": false, 61 | "requestUserAttention": false, 62 | "setAlwaysOnTop": false, 63 | "setCursorGrab": false, 64 | "setCursorIcon": false, 65 | "setCursorPosition": false, 66 | "setCursorVisible": false, 67 | "setDecorations": false, 68 | "setFocus": true, 69 | "setFullscreen": false, 70 | "setIcon": false, 71 | "setIgnoreCursorEvents": false, 72 | "setMaxSize": false, 73 | "setMinSize": false, 74 | "setPosition": false, 75 | "setResizable": false, 76 | "setSize": false, 77 | "setSkipTaskbar": false, 78 | "setTitle": false, 79 | "show": true, 80 | "startDragging": true, 81 | "unmaximize": false, 82 | "unminimize": false 83 | } 84 | }, 85 | "bundle": { 86 | "active": true, 87 | "category": "DeveloperTool", 88 | "copyright": "", 89 | "deb": { 90 | "depends": [] 91 | }, 92 | "externalBin": [], 93 | "icon": [ 94 | "icons/32x32.png", 95 | "icons/128x128.png", 96 | "icons/128x128@2x.png", 97 | "icons/icon.icns", 98 | "icons/icon.ico" 99 | ], 100 | "identifier": "tech.huakun.ezup", 101 | "longDescription": "", 102 | "macOS": { 103 | "entitlements": null, 104 | "exceptionDomain": "", 105 | "frameworks": [], 106 | "providerShortName": null, 107 | "signingIdentity": null 108 | }, 109 | "resources": [], 110 | "shortDescription": "", 111 | "targets": "all", 112 | "windows": { 113 | "certificateThumbprint": null, 114 | "digestAlgorithm": "sha256", 115 | "timestampUrl": "" 116 | } 117 | }, 118 | "security": { 119 | "csp": null 120 | }, 121 | "updater": { 122 | "active": true, 123 | "endpoints": [ 124 | "https://github.com/HuakunShen/ezup/releases/latest/download/latest.json" 125 | ], 126 | "dialog": true, 127 | "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDY3OTFCOTBGMzAwNDAwMQpSV1FCUUFEemtCdDVCcE5GTFY4NGxCZW44NjBWSy95elY3dGFGSUJRR2VJdDY4cFNqcUYvYkJMRQo=" 128 | }, 129 | "windows": [ 130 | { 131 | "fullscreen": false, 132 | "height": 1000, 133 | "width": 800, 134 | "resizable": true, 135 | "decorations": false, 136 | "title": "EzUp" 137 | } 138 | ] 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/lib/store.ts: -------------------------------------------------------------------------------- 1 | import { writable, derived } from 'svelte/store'; 2 | import type { Writable } from 'svelte/store'; 3 | import type { Service, Toast, ServiceType, ShortcutsMap } from './types'; 4 | import { ServiceSchema, ServicesSchema, ShortcutsSchema } from './types'; 5 | import { Store } from 'tauri-plugin-store-api'; 6 | import { 7 | KeyServices, 8 | KeyCurSerivce, 9 | KeySelectedServiceId, 10 | KeyShortcuts, 11 | KeyFormatter, 12 | } from './constants'; 13 | import { z, type Writeable } from 'zod'; 14 | import { defaultShortcutsMap, type FormatType, FormatEnum } from '$lib/util'; 15 | 16 | // these stores are tauri stores, not svelte stores 17 | export const settingStore = new Store('settings.json'); 18 | export const dataStore = new Store('data.json'); 19 | 20 | // define writable stores 21 | export const services: Writable = writable([]); 22 | export const toasts: Writable = writable([]); 23 | export const logImagesUrls: Writable = writable([]); 24 | export const uploading: Writable = writable(false); 25 | export const formatter: Writable = writable('plainlink'); 26 | // export const formatter: Writable = writable(FormatEnum.enum.plainLink); 27 | export const selectedServiceId: Writable = 28 | writable(undefined); 29 | export const shortcutsMap: Writable = writable(); 30 | export const serviceMap = derived(services, ($services) => { 31 | const _map = new Map(); 32 | for (const service of $services) { 33 | _map.set(service.id, service); 34 | } 35 | return _map; 36 | }); 37 | export const curService = derived( 38 | [selectedServiceId, serviceMap], 39 | ([$selectedServiceId, $serviceMap]) => { 40 | return !!$selectedServiceId 41 | ? $serviceMap.get($selectedServiceId) 42 | : undefined; 43 | } 44 | ); 45 | 46 | export async function init() { 47 | await settingStore.set('last-access-time', Date.now()); 48 | await dataStore.set('last-access-time', Date.now()); 49 | const rawKeyShortcuts = await settingStore.get(KeyShortcuts); 50 | const loadedShortcutsMap = rawKeyShortcuts 51 | ? ShortcutsSchema.parse(rawKeyShortcuts) 52 | : defaultShortcutsMap(); 53 | shortcutsMap.set(loadedShortcutsMap); 54 | shortcutsMap.subscribe(async (value) => { 55 | await settingStore.set(KeyShortcuts, value); 56 | await settingStore.save(); 57 | }); 58 | const rawServices = await settingStore.get(KeyServices); 59 | const rawSelectedServiceId = await settingStore.get( 60 | KeySelectedServiceId 61 | ); 62 | const loadedServices = !!rawServices 63 | ? ServicesSchema.parse(rawServices) 64 | : ([] as Service[]); 65 | services.set(loadedServices); 66 | services.subscribe(async (value) => { 67 | // console.log(value); 68 | // localStorage.setItem(KeyServices, JSON.stringify(value)); 69 | await settingStore.set(KeyServices, value); 70 | await settingStore.save(); 71 | }); 72 | 73 | const loadedServiceId = rawSelectedServiceId 74 | ? z.string().parse(rawSelectedServiceId) 75 | : undefined; 76 | selectedServiceId?.set(loadedServiceId); 77 | selectedServiceId.subscribe(async (value) => { 78 | // console.log(value); 79 | // localStorage.setItem(KeySelectedServiceId, JSON.stringify(value)); 80 | await settingStore.set(KeySelectedServiceId, value || null); 81 | await settingStore.save(); 82 | }); 83 | 84 | // formatter init 85 | const rawFormatter = await settingStore.get(KeyFormatter); 86 | // console.log(rawFormatter); 87 | let format: FormatType = FormatEnum.enum.plainlink; 88 | try { 89 | format = FormatEnum.parse(rawFormatter); 90 | } catch (error) { 91 | await settingStore.set(KeyFormatter, FormatEnum.enum.plainlink); 92 | await settingStore.save(); 93 | } 94 | formatter.set(format); 95 | formatter.subscribe(async (value) => { 96 | await settingStore.set(KeyFormatter, value); 97 | await settingStore.save(); 98 | }); 99 | } 100 | 101 | export default { 102 | services, 103 | selectedServiceId, 104 | curService, 105 | shortcutsMap, 106 | init, 107 | }; 108 | -------------------------------------------------------------------------------- /src/lib/util.ts: -------------------------------------------------------------------------------- 1 | import { ServiceTypes } from './constants'; 2 | import {z} from 'zod'; 3 | import type { 4 | ImgurSetting, 5 | S3Setting, 6 | ServiceSetting, 7 | ServiceType, 8 | ToastType, 9 | ShortcutsMap, 10 | } from '$lib/types'; 11 | import { 12 | settingStore, 13 | dataStore, 14 | services, 15 | selectedServiceId, 16 | logImagesUrls, 17 | } from '$lib/store'; 18 | import { toasts } from '$lib/store'; 19 | import { v4 as uuidv4 } from 'uuid'; 20 | import _ from 'lodash'; 21 | 22 | export const emptyS3Setting = (): S3Setting => ({ 23 | region: '', 24 | bucket: '', 25 | accessKey: '', 26 | secretKey: '', 27 | prefix: '', 28 | }); 29 | 30 | export const defaultShortcutsMap = (): ShortcutsMap => ({ 31 | toggleWindow: 'Control+Command+U', 32 | upload: 'Command+Alt+U', 33 | }); 34 | 35 | export const emptyImgurSetting = (): ImgurSetting => ({ 36 | clientId: '', 37 | }); 38 | 39 | export const getEmptySetting = (serviceType: ServiceType) => { 40 | switch (serviceType) { 41 | case ServiceTypes.S3: 42 | return emptyS3Setting(); 43 | case ServiceTypes.Imgur: 44 | return emptyImgurSetting(); 45 | default: 46 | throw new Error('Service Type Not Handled'); 47 | } 48 | }; 49 | 50 | export function updateServiceName(serviceId: string, name: string) { 51 | services.update((x) => { 52 | const servicesClone = _.cloneDeep(x); 53 | 54 | for (const service of servicesClone) { 55 | if (service.id === serviceId) { 56 | service.name = name; 57 | } 58 | } 59 | return servicesClone; 60 | }); 61 | } 62 | 63 | export function updateServiceSetting( 64 | serviceId: string, 65 | setting: ServiceSetting 66 | ) { 67 | services.update((x) => { 68 | const servicesClone = _.cloneDeep(x); 69 | 70 | for (const service of servicesClone) { 71 | if (service.id === serviceId) { 72 | service.setting = setting; 73 | } 74 | } 75 | return servicesClone; 76 | }); 77 | } 78 | 79 | export function addToast( 80 | toastType: ToastType, 81 | msg: string, 82 | duration: number = 3000 83 | ) { 84 | const toastId = uuidv4(); 85 | toasts.update((t) => [...t, { type: toastType, msg, id: toastId }]); 86 | setTimeout(() => { 87 | toasts.update((t) => t.filter((x) => x.id != toastId)); 88 | }, duration); 89 | } 90 | 91 | export function addImageUrlToDisplay(url: string) { 92 | logImagesUrls.update((urls) => [url, ...urls]); 93 | } 94 | 95 | export async function clearAllData() { 96 | await settingStore.clear(); 97 | await dataStore.clear(); 98 | await services.set([]); 99 | await selectedServiceId.set(undefined); 100 | } 101 | 102 | export function isDev(): boolean { 103 | return process.env.NODE_ENV == 'development'; 104 | } 105 | 106 | export function isLetter(letter: string): boolean { 107 | if (letter.length != 1) return false; 108 | return letter.match(/[a-zA-Z]/) ? true : false; 109 | } 110 | 111 | export function isShortcut(letters: string[]): boolean { 112 | if (letters.length <= 1 || letters.length > 3) return false; 113 | return letters.filter((letter) => isLetter(letter)).length == 1; 114 | } 115 | 116 | export const FormatTypeArr = ['markdown', 'html', 'plainlink'] as const; 117 | export const FormatEnum = z.enum(FormatTypeArr); 118 | export type FormatType = z.infer; 119 | // export enum FormatTypeEnum { 120 | // Markdown = 'markdown', 121 | // HTML = 'html', 122 | // PlainLink = 'plainLink', 123 | // } 124 | 125 | // export type FormatType = FormatTypeEnum.Markdown | FormatTypeEnum.HTML | FormatTypeEnum.PlainLink; 126 | 127 | export class ImageLinkFormatter { 128 | static md_format(url: string): string { 129 | return `![image](${url})`; 130 | } 131 | 132 | static html_format(url: string): string { 133 | return ``; 134 | } 135 | 136 | static format(format: FormatType, url: string): string { 137 | switch (format) { 138 | case FormatEnum.enum.markdown: 139 | return this.md_format(url); 140 | case FormatEnum.enum.html: 141 | return this.html_format(url); 142 | case FormatEnum.enum.plainlink: 143 | return url; 144 | default: 145 | throw new Error('Unknown Format Type'); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src-tauri/s3/main.rs: -------------------------------------------------------------------------------- 1 | pub async fn upload_object( 2 | client: &Client, 3 | bucket_name: &str, 4 | file_name: &str, 5 | key: &str, 6 | ) -> Result<(), Error> { 7 | let body = ByteStream::from_path(Path::new(file_name)).await; 8 | client 9 | .put_object() 10 | .bucket(bucket_name) 11 | .key(key) 12 | .body(body.unwrap()) 13 | .send() 14 | .await?; 15 | 16 | println!("Uploaded file: {}", file_name); 17 | Ok(()) 18 | } 19 | 20 | // fn main() { 21 | 22 | // upload_image() 23 | // } 24 | 25 | /* 26 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 27 | * SPDX-License-Identifier: Apache-2.0. 28 | */ 29 | 30 | use aws_config::meta::region::RegionProviderChain; 31 | use aws_sdk_s3::types::ByteStream; 32 | use aws_sdk_s3::{Client, Error, Region, PKG_VERSION}; 33 | use std::path::Path; 34 | use std::process; 35 | use structopt::StructOpt; 36 | 37 | #[derive(Debug, StructOpt)] 38 | struct Opt { 39 | /// The AWS Region. 40 | #[structopt(short, long)] 41 | region: Option, 42 | 43 | /// The name of the bucket. 44 | #[structopt(short, long)] 45 | bucket: String, 46 | 47 | /// The name of the file to upload. 48 | #[structopt(short, long)] 49 | filename: String, 50 | 51 | /// The name of the object in the bucket. 52 | #[structopt(short, long)] 53 | key: String, 54 | 55 | /// Whether to display additional information. 56 | #[structopt(short, long)] 57 | verbose: bool, 58 | } 59 | 60 | // Upload a file to a bucket. 61 | // snippet-start:[s3.rust.s3-helloworld] 62 | async fn upload_object( 63 | client: &Client, 64 | bucket: &str, 65 | filename: &str, 66 | key: &str, 67 | ) -> Result<(), Error> { 68 | let resp = client.list_buckets().send().await?; 69 | 70 | for bucket in resp.buckets().unwrap_or_default() { 71 | println!("bucket: {:?}", bucket.name().unwrap_or_default()) 72 | } 73 | 74 | println!(); 75 | 76 | let body = ByteStream::from_path(Path::new(filename)).await; 77 | 78 | match body { 79 | Ok(b) => { 80 | let resp = client 81 | .put_object() 82 | .bucket(bucket) 83 | .key(key) 84 | .body(b) 85 | .send() 86 | .await?; 87 | 88 | println!("Upload success. Version: {:?}", resp.version_id); 89 | 90 | let resp = client.get_object().bucket(bucket).key(key).send().await?; 91 | let data = resp.body.collect().await; 92 | println!("data: {:?}", data.unwrap().into_bytes()); 93 | } 94 | Err(e) => { 95 | println!("Got an error uploading object:"); 96 | println!("{}", e); 97 | process::exit(1); 98 | } 99 | } 100 | 101 | Ok(()) 102 | } 103 | // snippet-end:[s3.rust.s3-helloworld] 104 | 105 | /// Lists your buckets and uploads a file to a bucket. 106 | /// # Arguments 107 | /// 108 | /// * `-b BUCKET` - The bucket to which the file is uploaded. 109 | /// * `-k KEY` - The name of the file to upload to the bucket. 110 | /// * `[-r REGION]` - The Region in which the client is created. 111 | /// If not supplied, uses the value of the **AWS_REGION** environment variable. 112 | /// If the environment variable is not set, defaults to **us-west-2**. 113 | /// * `[-v]` - Whether to display additional information. 114 | #[tokio::main] 115 | async fn main() -> Result<(), Error> { 116 | tracing_subscriber::fmt::init(); 117 | 118 | let Opt { 119 | bucket, 120 | filename, 121 | key, 122 | region, 123 | verbose, 124 | } = Opt::from_args(); 125 | 126 | let region_provider = RegionProviderChain::first_try(region.map(Region::new)) 127 | .or_default_provider() 128 | .or_else(Region::new("us-west-2")); 129 | 130 | println!(); 131 | 132 | if verbose { 133 | println!("S3 client version: {}", PKG_VERSION); 134 | println!( 135 | "Region: {}", 136 | region_provider.region().await.unwrap().as_ref() 137 | ); 138 | println!("Bucket: {}", &bucket); 139 | println!("Filename: {}", &filename); 140 | println!("Key: {}", &key); 141 | println!(); 142 | } 143 | 144 | let shared_config = aws_config::from_env().region(region_provider).load().await; 145 | let client = Client::new(&shared_config); 146 | 147 | upload_object(&client, &bucket, &filename, &key).await 148 | } 149 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 57 | 58 |
    59 | 60 | 61 |
    62 |
    63 | {#each $toasts as toast, i} 64 | {#if toast.type === ToastType.Success} 65 | 66 | 67 | 79 | Check icon 80 | 81 | {toast.msg} 82 | 83 | {:else if toast.type === ToastType.Warning} 84 | 85 | 86 | 98 | Warning icon 99 | 100 | {toast.msg} 101 | 102 | {:else if toast.type === ToastType.Error} 103 | 104 | 105 | 117 | Error icon 118 | 119 | {toast.msg} 120 | 121 | {:else}{/if} 122 | {/each} 123 |
    124 |
    125 | 126 | 127 |
    128 | -------------------------------------------------------------------------------- /src/routes/preference/+page.svelte: -------------------------------------------------------------------------------- 1 | 78 | 79 |
    80 |

    Preference

    81 | 82 | 83 | Command 84 | Shortcut 85 | 86 | 87 | 88 | Toggle Window 89 | 90 | 96 | {toggleHotkeyValue} 97 | 104 | {toggleWarning} 105 | 106 | 107 | 108 | Upload 109 | 110 | 116 | {uploadHotkeyValue} 117 | 124 | {uploadWarning} 125 | 126 | 127 | 128 |
    129 |

    Formatter

    130 |
      133 |
    • 134 | Plain Link 137 |
    • 138 |
    • 139 | Markdown 142 |
    • 143 |
    • 144 | HTML 147 |
    • 148 |
    149 |
    150 | -------------------------------------------------------------------------------- /src/lib/uploader.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ServiceTypesEnum, 3 | type ImgurSetting, 4 | type S3Setting, 5 | type Service, 6 | ToastType, 7 | } from './types'; 8 | import { invoke } from '@tauri-apps/api'; 9 | import { uploading, formatter } from '$lib/store'; 10 | import { writeText, readText } from '@tauri-apps/api/clipboard'; 11 | import { 12 | addToast, 13 | addImageUrlToDisplay, 14 | ImageLinkFormatter, 15 | type FormatType, 16 | } from '$lib/util'; 17 | import path from 'path-browserify'; 18 | import { cacheDir } from '@tauri-apps/api/path'; 19 | import { BaseDirectory, createDir, removeFile } from '@tauri-apps/api/fs'; 20 | import { v4 as uuid } from 'uuid'; 21 | import { notify } from '$lib/notify'; 22 | 23 | export interface Uploader { 24 | uploadUrl: (url: string) => Promise; 25 | uploadFile: (filepath: string) => Promise; 26 | } 27 | 28 | export class S3Uploader implements Uploader { 29 | setting: S3Setting; 30 | 31 | constructor(setting: S3Setting) { 32 | this.setting = setting; 33 | } 34 | uploadUrl(url: string) { 35 | return cacheDir() 36 | .then((cacheDir) => { 37 | const destDir = path.join(cacheDir, 'ezup', 'download_url'); 38 | return createDir(destDir, { 39 | dir: BaseDirectory.Cache, 40 | recursive: true, 41 | }).then(() => invoke('download_file', { url: url, destDir })); 42 | }) 43 | .then((filePath) => { 44 | if (filePath instanceof String || typeof filePath === 'string') { 45 | return this.uploadFile(filePath.toString()).then((_url) => { 46 | return removeFile(filePath as string, { 47 | dir: BaseDirectory.Cache, 48 | }).then(() => { 49 | return _url; 50 | }); 51 | }); 52 | } else { 53 | throw new Error( 54 | 'Unexpected File Path Type from invoke("download_file")' 55 | ); 56 | } 57 | }); 58 | } 59 | uploadFile(filepath: string) { 60 | const now = new Date(); 61 | const year = now.getUTCFullYear(), 62 | month = now.getUTCMonth() + 1, 63 | date = now.getUTCDate(); 64 | let key = `${this.setting.prefix.replace( 65 | /^\/|\/$/g, 66 | '' 67 | )}/${year}/${month}/${date}/${path.basename(filepath)}`; 68 | if (key.length > 0 && key[0] === '/') { 69 | key = key.substring(1); 70 | } 71 | return invoke('upload_s3', { 72 | region: this.setting.region, 73 | bucket: this.setting.bucket, 74 | filename: filepath, 75 | key: key, 76 | accessKeyId: this.setting.accessKey, 77 | secretAccessKey: this.setting.secretKey, 78 | }).then((url) => { 79 | return url as string; 80 | }); 81 | } 82 | } 83 | 84 | export class ImgurUploader implements Uploader { 85 | setting: ImgurSetting; 86 | 87 | constructor(setting: ImgurSetting) { 88 | this.setting = setting; 89 | } 90 | 91 | uploadUrl(url: string) { 92 | return this.uploadFile(url); 93 | } 94 | 95 | uploadFile(filepath: string) { 96 | return invoke('upload_imgur_from_url', { 97 | url: filepath, 98 | clientId: this.setting.clientId, 99 | }).then((response: any) => { 100 | if (response) { 101 | const url = response.data.link as string; 102 | return url; 103 | } else { 104 | throw new Error('Unhandled Error'); 105 | } 106 | }); 107 | } 108 | } 109 | 110 | export class UploadManager { 111 | uploader: Uploader; 112 | service: Service | undefined; 113 | format: FormatType; 114 | 115 | constructor(service: Service | undefined, format: FormatType) { 116 | this.service = service; 117 | this.format = format; 118 | if (service?.type === ServiceTypesEnum.Enum.imgur) { 119 | this.uploader = new ImgurUploader(service.setting as ImgurSetting); 120 | } else if (service?.type === ServiceTypesEnum.Enum.s3) { 121 | this.uploader = new S3Uploader(service.setting as S3Setting); 122 | } else { 123 | throw new Error('Unsupported Service Type'); 124 | } 125 | } 126 | _upload(url_or_path: string) { 127 | if (this.service === undefined) { 128 | throw new Error('No Service Selected'); 129 | } 130 | if (url_or_path.match(/^https?:\/\/.+/)) { 131 | return this.uploader.uploadUrl(url_or_path); 132 | } else { 133 | return this.uploader.uploadFile(url_or_path); 134 | } 135 | } 136 | upload(url_or_path: string): Promise { 137 | if (this.service === undefined) { 138 | throw new Error('No Service Selected'); 139 | } 140 | uploading.set(true); 141 | return this._upload(url_or_path) 142 | .then((url: string) => { 143 | addImageUrlToDisplay(url); 144 | uploading.set(false); 145 | return writeText(ImageLinkFormatter.format(this.format, url)); 146 | }) 147 | .then(() => { 148 | return addToast(ToastType.Success, 'Image URL Written to Clipboard'); 149 | }) 150 | .catch((err) => { 151 | return addToast(ToastType.Error, err); 152 | }); 153 | } 154 | } 155 | 156 | export async function uploadFromClipboard(uploadManager: UploadManager) { 157 | const clipboardText = await readText(); 158 | uploading.set(true); 159 | 160 | if (!!clipboardText) { 161 | // if clipboard text is non empty, it must be a link or file path 162 | return uploadManager.upload(clipboardText); 163 | // return uploadImg(clipboardText, format, service); 164 | } else { 165 | const filename = `${uuid()}.png`; 166 | const cachePath = await cacheDir(); 167 | // console.log(cachePath); 168 | const clipboardImgPath = path.join( 169 | cachePath, 170 | 'ezup', 171 | 'clipboard-images', 172 | filename 173 | ); 174 | await createDir('ezup/clipboard-images', { 175 | dir: BaseDirectory.Cache, 176 | recursive: true, 177 | }); 178 | // read image from clipboard and save to temp dir 179 | return invoke('image_to_file', { 180 | filename: clipboardImgPath, 181 | }) 182 | .then(() => { 183 | // addToast(ToastType.Success, 'Clipboard Image Saved To FS'); 184 | // uploading = false; 185 | return uploadManager.upload(clipboardImgPath); 186 | // return uploadImg(clipboardImgPath, format, service); 187 | }) 188 | .then(() => { 189 | // Remove Cached File 190 | return removeFile(clipboardImgPath, { 191 | dir: BaseDirectory.Cache, 192 | }); 193 | }) 194 | .catch((err) => { 195 | console.error(err); 196 | addToast(ToastType.Error, err); 197 | return notify('Error', err); 198 | }); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr( 2 | all(not(debug_assertions), target_os = "windows"), 3 | windows_subsystem = "windows" 4 | )] 5 | use arboard; 6 | use aws_sdk_s3::types::ByteStream; 7 | use aws_sdk_s3::Config; 8 | use aws_sdk_s3::{Client, Region}; 9 | use aws_types::Credentials; 10 | use image::{DynamicImage, ImageBuffer, RgbaImage}; 11 | use imgurs::{ImageInfo, ImgurClient}; 12 | use std::path::{Path, PathBuf}; 13 | use tauri; 14 | use tauri::Manager; 15 | use tauri::{CustomMenuItem, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem}; 16 | // use uuid::{uuid, Uuid}; 17 | // use error_chain::error_chain; 18 | use std::fs::File; 19 | use std::io::copy; 20 | // use tempfile::Builder; 21 | // use std::borrow::Cow; 22 | use std::io::Cursor; 23 | use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial}; 24 | 25 | // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command 26 | #[tauri::command] 27 | fn greet(name: &str) -> String { 28 | format!("Hello, {}! You've been greeted from Rust!", name) 29 | } 30 | 31 | #[tauri::command] 32 | async fn upload_s3( 33 | region: String, 34 | bucket: String, 35 | filename: String, 36 | key: String, 37 | access_key_id: String, 38 | secret_access_key: String, 39 | ) -> Result { 40 | let bucket2 = &bucket; 41 | let creds = Credentials::from_keys(access_key_id, secret_access_key, None); 42 | let region2 = region.clone(); 43 | let region3 = region.clone(); 44 | let conf = Config::builder() 45 | .credentials_provider(creds) 46 | .region(Region::new(region2)) 47 | .build(); 48 | let client = Client::from_conf(conf); 49 | let body = ByteStream::from_path(Path::new(&filename)).await.unwrap(); 50 | let key2 = key.clone(); 51 | 52 | let key_path = Path::new(&key); 53 | let ext = key_path.extension().unwrap().to_str().unwrap(); 54 | let content_type = match ext { 55 | "png" => "image/png", 56 | "jpeg" => "image/jpeg", 57 | "jpg" => "image/jpeg", 58 | "gif" => "image/gif", 59 | "bmp" => "image/bmp", 60 | "tiff" => "image/tiff", 61 | "svg" => "image/svg+xml", 62 | _ => "application/octet-stream", 63 | }; 64 | 65 | let upload_result = client 66 | .put_object() 67 | .bucket(bucket2) 68 | .key(key) 69 | .body(body) 70 | .content_type(content_type) 71 | .send() 72 | .await; 73 | match upload_result { 74 | Ok(_response) => Ok(format!( 75 | "https://{}.s3.{}.amazonaws.com/{}", 76 | bucket2, region3, key2 77 | )), 78 | Err(_error) => Err("Upload Error".to_string()), 79 | } 80 | // upload_result.map_err(|err| err.to_string()) 81 | // let url = format!("https://{}.s3.{}.amazonaws.com/{}", bucket, region, key); 82 | // Ok(url) 83 | // let upload_response = upload::upload_s3_object(&client, &bucket, &filename, &key); 84 | // format!("Hello, {}! You've been greeted from Rust!", "huakun") 85 | } 86 | 87 | #[tauri::command] 88 | async fn upload_imgur_from_url(client_id: String, url: String) -> Result { 89 | let client = ImgurClient::new(&client_id); 90 | let upload_result = client.upload_image(&url).await; 91 | println!("{:?}", upload_result); 92 | // format!("Hello, {}! You've been greeted from Rust!", url) 93 | upload_result.map_err(|err| err.to_string()) 94 | } 95 | 96 | /// Download a file from given url and save to given location 97 | #[tauri::command] 98 | async fn download_file(url: String, dest_dir: String) -> Result { 99 | // let tmp_dir = Builder::new().prefix("ezup").tempdir().unwrap(); 100 | let dest_dir_path = Path::new(&dest_dir); 101 | let response = reqwest::get(url).await.unwrap(); 102 | let dest_file_path: PathBuf; 103 | let mut dest = { 104 | let fname = response 105 | .url() 106 | .path_segments() 107 | .and_then(|segments| segments.last()) 108 | .and_then(|name| if name.is_empty() { None } else { Some(name) }) 109 | .unwrap_or("tmp.bin"); 110 | println!("file to download: '{}'", fname); 111 | dest_file_path = dest_dir_path.join(fname); 112 | let dest_file_path_clone = dest_file_path.clone(); 113 | // println!("will be located under: '{:?}'", dest_file_path); 114 | File::create(dest_file_path_clone).unwrap() 115 | }; 116 | // let content = response.text().await.unwrap(); 117 | // let _copyResult = copy(&mut content.as_bytes(), &mut dest).unwrap(); 118 | let bytes = response.bytes().await.map_err(|err| err.to_string())?; 119 | let mut content = Cursor::new(bytes); 120 | copy(&mut content, &mut dest).map_err(|err| err.to_string())?; 121 | 122 | // let metadata = dest.metadata().map_err(|err| err.to_string())?; 123 | // metadata. 124 | let dest_file_path_str = dest_file_path.into_os_string().into_string().unwrap(); 125 | Ok(dest_file_path_str) 126 | } 127 | 128 | /// read image from clipboard and save to given location 129 | #[tauri::command] 130 | fn image_to_file(filename: String) -> Result { 131 | let mut clipboard = arboard::Clipboard::new().unwrap(); 132 | // let runtime_dir = tauri::api::path::runtime_dir().unwrap(); 133 | // println!("{}", runtime_dir); 134 | // let runtime_d = tauri::api::path::cache_dir().unwrap(); 135 | match clipboard.get_image() { 136 | Ok(img) => { 137 | // eprintln!("getting {}x{} image", img.width, img.height); 138 | let img2: RgbaImage = ImageBuffer::from_raw( 139 | img.width.try_into().unwrap(), 140 | img.height.try_into().unwrap(), 141 | img.bytes.into_owned(), 142 | ) 143 | .unwrap(); 144 | let img3 = DynamicImage::ImageRgba8(img2); 145 | let filename2 = filename.clone(); 146 | img3.save(filename).unwrap(); 147 | Ok(filename2) 148 | } 149 | Err(_e) => Err("failed to save image".to_string()), 150 | } 151 | } 152 | 153 | // pub async fn upload_object( 154 | // client: &Client, 155 | // bucket_name: &str, 156 | // file_name: &str, 157 | // key: &str, 158 | // ) -> Result<(), Error> { 159 | // let body = ByteStream::from_path(Path::new(file_name)).await; 160 | // client 161 | // .put_object() 162 | // .bucket(bucket_name) 163 | // .key(key) 164 | // .body(body.unwrap()) 165 | // .send() 166 | // .await?; 167 | 168 | // println!("Uploaded file: {}", file_name); 169 | // Ok(()) 170 | // } 171 | 172 | fn main() { 173 | let quit = CustomMenuItem::new("quit".to_string(), "Quit"); 174 | let hide = CustomMenuItem::new("hide".to_string(), "Hide"); 175 | let show = CustomMenuItem::new("show".to_string(), "Show"); 176 | let tray_menu = SystemTrayMenu::new() 177 | .add_item(quit) 178 | .add_native_item(SystemTrayMenuItem::Separator) 179 | .add_item(hide) 180 | .add_native_item(SystemTrayMenuItem::Separator) 181 | .add_item(show); 182 | 183 | let system_tray = SystemTray::new().with_menu(tray_menu); 184 | tauri::Builder::default() 185 | // .plugin( 186 | // tauri_plugin_stronghold::Builder::new(|password| { 187 | // // TODO: hash the password here with e.g. argon2, blake2b or any other secure algorithm 188 | // todo!() 189 | // }) 190 | // .build() 191 | // ) 192 | .invoke_handler(tauri::generate_handler![ 193 | greet, 194 | upload_imgur_from_url, 195 | upload_s3, 196 | image_to_file, 197 | download_file 198 | ]) 199 | .plugin(tauri_plugin_store::Builder::default().build()) 200 | .plugin(tauri_plugin_clipboard::init()) 201 | .setup(|app| { 202 | let window = app.get_window("main").unwrap(); 203 | #[cfg(target_os = "macos")] 204 | apply_vibrancy(&window, NSVisualEffectMaterial::HudWindow, None, None) 205 | .expect("Unsupported platform! 'apply_vibrancy' is only supported on macOS"); 206 | Ok(()) 207 | }) 208 | .system_tray(system_tray) 209 | .on_system_tray_event(|app, event| match event { 210 | // SystemTrayEvent::LeftClick { 211 | // position: _, 212 | // size: _, 213 | // .. 214 | // } => { 215 | // println!("system tray received a left click"); 216 | // } 217 | // SystemTrayEvent::RightClick { 218 | // position: _, 219 | // size: _, 220 | // .. 221 | // } => { 222 | // println!("system tray received a right click"); 223 | // } 224 | // SystemTrayEvent::DoubleClick { 225 | // position: _, 226 | // size: _, 227 | // .. 228 | // } => { 229 | // println!("system tray received a double click"); 230 | // } 231 | SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() { 232 | "quit" => { 233 | std::process::exit(0); 234 | } 235 | "hide" => { 236 | let window = app.get_window("main").unwrap(); 237 | window.hide().unwrap(); 238 | } 239 | "show" => { 240 | let window = app.get_window("main").unwrap(); 241 | window.show().unwrap(); 242 | } 243 | _ => {} 244 | }, 245 | _ => {} 246 | }) 247 | .build(tauri::generate_context!()) 248 | // .run(tauri::generate_context!()) 249 | .expect("error while running tauri application") 250 | .run(|_app_handle, event| match event { 251 | tauri::RunEvent::ExitRequested { api, .. } => { 252 | api.prevent_exit(); 253 | } 254 | _ => {} 255 | }); 256 | } 257 | --------------------------------------------------------------------------------