├── .eslintrc.json ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── app-icon.png ├── components.json ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── app-icon.svg ├── next.svg └── vercel.svg ├── src-tauri ├── .env.example ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── build.rs ├── bundle │ └── templates │ │ ├── default.nginx.conf.template │ │ ├── docker-compose.yml.template │ │ └── server.conf.template ├── capabilities │ ├── default.json │ ├── desktop.json │ └── migrated.json ├── gen │ └── schemas │ │ ├── acl-manifests.json │ │ ├── capabilities.json │ │ ├── desktop-schema.json │ │ └── macOS-schema.json ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── android │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ └── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ └── ios │ │ ├── AppIcon-20x20@1x.png │ │ ├── AppIcon-20x20@2x-1.png │ │ ├── AppIcon-20x20@2x.png │ │ ├── AppIcon-20x20@3x.png │ │ ├── AppIcon-29x29@1x.png │ │ ├── AppIcon-29x29@2x-1.png │ │ ├── AppIcon-29x29@2x.png │ │ ├── AppIcon-29x29@3x.png │ │ ├── AppIcon-40x40@1x.png │ │ ├── AppIcon-40x40@2x-1.png │ │ ├── AppIcon-40x40@2x.png │ │ ├── AppIcon-40x40@3x.png │ │ ├── AppIcon-512@2x.png │ │ ├── AppIcon-60x60@2x.png │ │ ├── AppIcon-60x60@3x.png │ │ ├── AppIcon-76x76@1x.png │ │ ├── AppIcon-76x76@2x.png │ │ └── AppIcon-83.5x83.5@2x.png ├── src │ ├── commands │ │ └── hosts.rs │ ├── keychainmgr │ │ ├── keychain_passwords.rs │ │ └── mod.rs │ └── main.rs └── tauri.conf.json ├── src ├── app │ ├── favicon.ico │ ├── global-error.tsx │ ├── globals.css │ ├── layout.tsx │ ├── page.tsx │ ├── settings │ │ └── page.tsx │ ├── status │ │ └── page.tsx │ └── test-page │ │ └── page.tsx ├── components │ ├── app-sidebar.tsx │ ├── icons │ │ ├── discord.tsx │ │ └── docker.tsx │ ├── page-components │ │ ├── add-proxy │ │ │ └── index.tsx │ │ ├── certificate-dialogs │ │ │ └── cert-buttons.tsx │ │ ├── docker-control │ │ │ └── index.tsx │ │ ├── home │ │ │ └── index.tsx │ │ ├── icons.tsx │ │ ├── proxy-list │ │ │ ├── add-new │ │ │ │ ├── group.tsx │ │ │ │ └── proxy-to-group.tsx │ │ │ ├── delete │ │ │ │ └── delete-proxy-dialog.tsx │ │ │ ├── docker-log │ │ │ │ └── index.tsx │ │ │ ├── edit │ │ │ │ └── group.tsx │ │ │ ├── index.tsx │ │ │ ├── prepare │ │ │ │ └── prepare-proxy-dialog.tsx │ │ │ └── table │ │ │ │ └── index.tsx │ │ ├── setup-provider │ │ │ └── index.tsx │ │ ├── theme-toggle.tsx │ │ └── updater │ │ │ └── index.tsx │ ├── theme-provider.tsx │ ├── tools │ │ └── ga.tsx │ └── ui │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── code.tsx │ │ ├── command.tsx │ │ ├── copy-command-button.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── multi-state-button.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── sonner.tsx │ │ ├── stepper.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── toast.tsx │ │ └── tooltip.tsx ├── helpers │ ├── certificate-manager │ │ └── index.ts │ ├── file-manager │ │ └── index.ts │ ├── proxy-manager │ │ ├── constants.ts │ │ ├── index.ts │ │ ├── interfaces.ts │ │ └── migration │ │ │ ├── 001-create-group.ts │ │ │ └── 002-add-proxy-created-at.ts │ └── system │ │ └── index.ts ├── hooks │ ├── use-integer-height.ts │ └── use-mobile.ts ├── lib │ ├── constants.ts │ └── utils.ts └── stores │ ├── cert-keychain-store.ts │ ├── hosts-store.ts │ ├── proxy-list.ts │ └── system-status.ts ├── tailwind.config.ts ├── tsconfig.json └── update-check-v1 └── latest.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | @cheeselemon @code-xhyun @apilylabs -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: 28 | - OS Version: 29 | - App Version: 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the feature** 10 | A clear and concise description of what feature you'd like to see implemented. 11 | 12 | **Problem it solves** 13 | A clear explanation of the problem or pain point this feature would address. 14 | 15 | **Describe your proposed solution** 16 | A detailed description of how you envision this feature working. 17 | 18 | **Describe alternatives you've considered** 19 | A description of any alternative solutions or features you've thought about. 20 | 21 | **Implementation suggestions** 22 | If applicable, add any technical suggestions or considerations for implementing this feature. 23 | 24 | **Mockups or examples** 25 | If applicable, add mockups, screenshots, or examples from other products to help illustrate the feature. 26 | 27 | **Additional context** 28 | Add any other context, use cases, or screenshots about the feature request here. 29 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: "publish" 2 | on: 3 | push: 4 | branches: 5 | - release 6 | 7 | jobs: 8 | publish-tauri: 9 | permissions: 10 | contents: write 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | platform: [macos-latest] 15 | runs-on: ${{ matrix.platform }} 16 | environment: Release CI 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v3 21 | 22 | - name: Rust setup 23 | uses: dtolnay/rust-toolchain@stable 24 | 25 | - name: Rust cache 26 | uses: swatinem/rust-cache@v2 27 | with: 28 | workspaces: "./src-tauri -> target" 29 | 30 | - name: Sync node version and setup cache 31 | uses: actions/setup-node@v3 32 | with: 33 | node-version: "lts/*" 34 | cache: "npm" # Set this to npm, yarn or pnpm. 35 | 36 | - name: Install frontend dependencies 37 | # If you don't have `beforeBuildCommand` configured you may want to build your frontend here too. 38 | run: npm install # Change this to npm, yarn or pnpm. 39 | 40 | - name: Build the app 41 | uses: tauri-apps/tauri-action@v0 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} 45 | APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} 46 | APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} 47 | APPLE_ID: ${{ secrets.APPLE_ID }} 48 | APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} 49 | APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} 50 | with: 51 | tagName: app-v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version 52 | releaseName: "App v__VERSION__" 53 | releaseBody: "See the assets to download this version and install." 54 | releaseDraft: true 55 | prerelease: false 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | # IDE folders 38 | .idea 39 | .vscode 40 | 41 | .env 42 | .cursor/rules/workflow-rule.mdc 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 @cheeselemon @apilylabs 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Ophiuchi - Setup Localhost SSL Proxy in 5 Seconds! | Product Hunt 2 | 3 | 4 | # Ophiuchi - Localhost SSL Proxy Server Manager 5 | 6 | 7 | ![Screenshot 2024-06-10 at 11 56 57 AM](https://github.com/apilylabs/ophiuchi-desktop/assets/5467111/a5b465b6-065e-43c4-ac66-bf8a502d5bae) 8 | 9 | 10 | 11 | 12 | # Download the Built App: 13 | 14 | The latest built app is available at the website: 15 | 16 | 🚀 [Ophiuchi Official Website](https://www.ophiuchi.dev/) 17 | 18 | Screenshot 2025-05-15 at 2 26 24 PM 19 | 20 | --- 21 | 22 | ## Related Links 23 | 24 | ### Join Discord for Support: 25 | 26 | 💪 [Discord Channel](https://discord.gg/fpp8kNyPtz) 27 | 28 | ### Where Ophiuchi was Showcased 29 | 30 | [dev.to Article](https://dev.to/cheeselemon/ssl-in-localhost-takes-5-seconds-now-460i) 31 | 32 | [daily.dev Article](https://app.daily.dev/posts/ssl-in-localhost-takes-5-seconds-now--zhuzgvwxh) 33 | 34 | [GeekNews Article(ko)](https://news.hada.io/topic?id=20888) 35 | 36 | [RaqibNur's Bookmarks](https://www.raqibnur.com/bookmark) 37 | 38 | --- 39 | 40 | # Build from Source & Run Locally 41 | 42 | 1. See [Tauri's Docs](https://v2.tauri.app/start/prerequisites/) to prepare for tauri app development. 43 | 44 | 2. Clone the repo and run: 45 | 46 | ``` 47 | npm install 48 | ``` 49 | 50 | 3. To build and run the app: 51 | 52 | ``` 53 | npm run tauri dev 54 | ``` 55 | 56 | ### Building the App 57 | 58 | ``` 59 | CI=true npm run tauri build 60 | ``` 61 | 62 | ### Debuggable build 63 | 64 | ``` 65 | npm run tauri build -- --debug 66 | ``` 67 | 68 | ### Build macOS App Bundles 69 | 70 | Tauri v2 build command for macOS app bundles 71 | 72 | 73 | ``` 74 | npm run tauri build -- --bundles app 75 | ``` 76 | 77 | 78 | --- 79 | # Troubleshooting 80 | 81 | ## Resolving Tauri Build Error: "No such file or directory (os error 2)" 82 | 83 | The error you're encountering indicates that Tauri is trying to use Rust's Cargo build system, but it cannot find the necessary Rust setup to compile the project. This is likely because either Rust is not installed, or the environment isn’t configured properly for Tauri's Rust dependencies. 84 | 85 | ### Steps to Resolve 86 | 87 | ### 1. Install Rust 88 | Tauri uses Rust, so ensure you have the Rust toolchain installed. You can install Rust using `rustup`: 89 | 90 | ```bash 91 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 92 | ``` 93 | ## Set Up Tauri’s Rust Targets 94 | 95 | #### Since you are building for universal-apple-darwin, you will need to install the necessary Rust targets. Use: 96 | ```bash 97 | rustup target add aarch64-apple-darwin x86_64-apple-darwin 98 | ``` 99 | ## Run the Build Command Again 100 | #### After setting up Rust and the necessary targets, try running the build command again: 101 | 102 | ```bash 103 | npm run tauri build -- --debug 104 | ``` 105 | 106 | ## Build Errors 107 | 108 | ### Can't find crate for 'core' error... 109 | 110 | ```bash 111 | error[E0463]: can't find crate for `core` 112 | | 113 | = note: the `aarch64-apple-darwin` target may not be installed 114 | = help: consider downloading the target with `rustup target add aarch64-apple-darwin` 115 | 116 | For more information about this error, try `rustc --explain E0463`. 117 | error: could not compile `cfg-if` (lib) due to 1 previous error 118 | warning: build failed, waiting for other jobs to finish... 119 | failed to build aarch64-apple-darwin binary: failed to build app 120 | Error [tauri_cli_node] failed to build aarch64-apple-darwin binary: failed to build app 121 | ``` 122 | 123 | In this case, update rustup to the latest version: 124 | 125 | ```bash 126 | rustup update 127 | ``` 128 | 129 | 130 | 131 | 132 | ### notes 133 | 134 | https://discord.com/channels/616186924390023171/1096449326672248903/1096449326672248903 135 | 136 | -------------------------------------------------------------------------------- /app-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/app-icon.png -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "gray", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | swcMinify: false, 4 | output: 'export', 5 | } 6 | 7 | module.exports = nextConfig 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ophiuchi", 3 | "version": "0.6.1", 4 | "license": "MIT", 5 | "scripts": { 6 | "tauri": "tauri", 7 | "dev": "next dev -p 8899", 8 | "build": "next build", 9 | "start": "next start -p 8899", 10 | "serve": "serve out -p 8899", 11 | "lint": "next lint", 12 | "build:universal": "tauri build --target universal-apple-darwin", 13 | "build:appbundles": "tauri build -- --bundles app", 14 | "build:universal:verbose": "tauri build --target universal-apple-darwin --verbose", 15 | "build:clean": "tauri build --target clean", 16 | "cargo:clean": "cd src-tauri && cargo clean", 17 | "tauri:icon": "tauri icon ./app-icon.png" 18 | }, 19 | "dependencies": { 20 | "@headlessui/react": "^1.7.17", 21 | "@heroicons/react": "^2.0.18", 22 | "@hookform/resolvers": "^3.9.1", 23 | "@radix-ui/react-checkbox": "^1.1.4", 24 | "@radix-ui/react-dialog": "^1.1.2", 25 | "@radix-ui/react-dismissable-layer": "1.1.5", 26 | "@radix-ui/react-dropdown-menu": "^2.1.2", 27 | "@radix-ui/react-icons": "^1.3.0", 28 | "@radix-ui/react-label": "^2.1.1", 29 | "@radix-ui/react-popover": "^1.1.2", 30 | "@radix-ui/react-progress": "^1.1.4", 31 | "@radix-ui/react-separator": "^1.1.0", 32 | "@radix-ui/react-slot": "^1.1.2", 33 | "@radix-ui/react-tabs": "^1.1.2", 34 | "@radix-ui/react-toast": "^1.2.6", 35 | "@radix-ui/react-tooltip": "^1.1.4", 36 | "@tauri-apps/api": "^2.5.0", 37 | "@tauri-apps/plugin-dialog": "^2.2.0", 38 | "@tauri-apps/plugin-fs": "^2.2.0", 39 | "@tauri-apps/plugin-process": "^2.2.1", 40 | "@tauri-apps/plugin-shell": "^2.2.0", 41 | "@tauri-apps/plugin-updater": "^2.6.0", 42 | "@types/node": "20.5.0", 43 | "@types/react": "18.3.12", 44 | "@types/react-dom": "18.2.7", 45 | "autoprefixer": "10.4.15", 46 | "class-variance-authority": "^0.7.1", 47 | "clsx": "^2.1.1", 48 | "cmdk": "^1.0.0", 49 | "eslint": "8.47.0", 50 | "eslint-config-next": "13.4.16", 51 | "lucide-react": "^0.399.0", 52 | "next": "13.4.16", 53 | "next-themes": "^0.4.6", 54 | "postcss": "8.4.28", 55 | "react": "18.2.0", 56 | "react-dom": "18.2.0", 57 | "react-hook-form": "^7.54.1", 58 | "selfsigned": "^2.1.1", 59 | "sonner": "^2.0.1", 60 | "tailwind-merge": "^2.3.0", 61 | "tailwindcss": "3.3.3", 62 | "tailwindcss-animate": "^1.0.7", 63 | "typescript": "5.1.6", 64 | "use-mask-input": "^3.4.2", 65 | "usehooks-ts": "^3.1.0", 66 | "zod": "^3.24.1", 67 | "zustand": "^4.5.2" 68 | }, 69 | "devDependencies": { 70 | "@tauri-apps/cli": "^2.5.0", 71 | "serve": "^14.2.4" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/app-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src-tauri/.env.example: -------------------------------------------------------------------------------- 1 | SENTRY_DSN=YOUR_SENTRY_DSN_URL_HERE -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "app" 3 | version = "0.6.1" 4 | description = "Ophiuchi - Developers Toolkit" 5 | authors = ["Jaeha Kim"] 6 | license = "Private" 7 | repository = "" 8 | default-run = "app" 9 | edition = "2021" 10 | rust-version = "1.60" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [build-dependencies] 15 | tauri-build = { version = "2.2", features = [] } 16 | 17 | [dependencies] 18 | serde_json = "1.0" 19 | serde = { version = "1.0", features = ["derive"] } 20 | tauri = { version = "=2.5", features = [] } 21 | tauri-plugin-sentry = "0.2" 22 | tauri-plugin-fs = { version = "=2.2.0", features = ["watch"] } 23 | tauri-plugin-dialog = "=2.2" 24 | tauri-plugin-opener = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } 25 | tauri-plugin-shell = "=2.2" 26 | tauri-plugin-process = "=2.2" 27 | chrono = "0.4.38" 28 | security-framework = "2.11.0" 29 | syn = "=2.0.90" 30 | dotenv = "0.15.0" 31 | time = "0.3.36" 32 | # 0.5.11 and later require Rust 1.81. see https://github.com/delta-io/delta-rs/issues/3065 33 | home = "=0.5.9" 34 | regex = "1.10.3" 35 | 36 | 37 | [dependencies.fix-path-env] 38 | git = "https://github.com/tauri-apps/fix-path-env-rs" 39 | #tag = "fix-path-env-v0.1.0" 40 | #branch = "dev" 41 | 42 | [features] 43 | # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. 44 | # If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes. 45 | # DO NOT REMOVE!! 46 | custom-protocol = [ "tauri/custom-protocol" ] 47 | 48 | [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] 49 | tauri-plugin-updater = "=2.6" 50 | 51 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/bundle/templates/default.nginx.conf.template: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | error_log /var/log/nginx/error.log warn; 4 | pid /var/run/nginx.pid; 5 | events { 6 | worker_connections 20000; 7 | } 8 | 9 | http { 10 | log_format compression '$remote_addr - $remote_user [$time_local] ' 11 | '"$request" $status $body_bytes_sent ' 12 | '"$http_referer" "$http_user_agent" "$gzip_ratio"'; 13 | 14 | include /etc/nginx/mime.types; 15 | default_type application/octet-stream; 16 | 17 | access_log /var/log/nginx/access.log compression; 18 | 19 | sendfile off; 20 | keepalive_timeout 300; 21 | include /etc/nginx/conf.d/*.conf; 22 | } -------------------------------------------------------------------------------- /src-tauri/bundle/templates/docker-compose.yml.template: -------------------------------------------------------------------------------- 1 | # auto generated docker-compose.yml 2 | version: '3' 3 | name: "devophiuchidesktop" 4 | services: 5 | nginx: 6 | image: nginx:1.23.2-alpine 7 | ports: 8 | - 443:443 9 | extra_hosts: 10 | - "host.docker.internal:host-gateway" 11 | volumes: 12 | - ./conf/nginx.conf:/etc/nginx/nginx.conf 13 | - ./conf/conf.d:/etc/nginx/conf.d 14 | - ./cert:/usr/local/ssl/cert 15 | container_name: ophiuchi-nginx -------------------------------------------------------------------------------- /src-tauri/bundle/templates/server.conf.template: -------------------------------------------------------------------------------- 1 | upstream internal_{UPSTREAM_SUFFIX} { 2 | server host.docker.internal:{PORT}; 3 | } 4 | 5 | # Websocket connection upgrade mapping 6 | map $http_upgrade $connection_upgrade { 7 | default upgrade; 8 | '' close; 9 | } 10 | 11 | server { 12 | # domain name 13 | server_name {DOMAIN_NAME}; 14 | 15 | listen 443 ssl; 16 | listen [::]:443 ssl; 17 | 18 | # cert path 19 | ssl_certificate /usr/local/ssl/cert/{DOMAIN_NAME}/cert.pem; 20 | ssl_certificate_key /usr/local/ssl/cert/{DOMAIN_NAME}/private.key; 21 | 22 | 23 | location / { 24 | proxy_buffers 16 4k; 25 | proxy_buffer_size 2k; 26 | proxy_http_version 1.1; 27 | proxy_set_header Host $host; 28 | proxy_set_header X-Real-IP $remote_addr; 29 | 30 | # Websocket Support 31 | proxy_set_header Upgrade $http_upgrade; 32 | proxy_set_header Connection $connection_upgrade; 33 | 34 | # Additional Headers 35 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 36 | proxy_set_header X-Forwarded-Proto $scheme; 37 | 38 | proxy_pass http://internal_{UPSTREAM_SUFFIX}; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src-tauri/capabilities/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "main-capability", 4 | "description": "Capability for the main window", 5 | "windows": [ 6 | "main" 7 | ], 8 | "permissions": [ 9 | "core:path:default", 10 | "core:event:default", 11 | "core:window:default", 12 | "core:app:default", 13 | "core:resources:default", 14 | "core:menu:default", 15 | "core:tray:default", 16 | "core:window:allow-set-title", 17 | "sentry:default" 18 | ] 19 | } -------------------------------------------------------------------------------- /src-tauri/capabilities/desktop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "desktop-capability", 4 | "windows": [ 5 | "main" 6 | ], 7 | "platforms": [ 8 | "linux", 9 | "macOS", 10 | "windows" 11 | ], 12 | "permissions": [ 13 | "fs:default", 14 | "fs:allow-watch", 15 | "fs:allow-app-meta-recursive", 16 | "fs:allow-app-read-recursive", 17 | "fs:allow-app-write-recursive", 18 | "fs:scope-appdata-recursive", 19 | { 20 | "identifier": "fs:scope", 21 | "allow": [ 22 | { 23 | "path": "$RESOURCE/bundle/templates/docker-compose.yml.template" 24 | }, 25 | { 26 | "path": "$RESOURCE/bundle/templates/default.nginx.conf.template" 27 | }, 28 | { 29 | "path": "$RESOURCE/bundle/templates/server.conf.template" 30 | } 31 | ] 32 | }, 33 | "dialog:default", 34 | "dialog:allow-confirm", 35 | { 36 | "identifier": "shell:allow-spawn", 37 | "allow": [ 38 | { 39 | "name": "run-docker-compose", 40 | "cmd": "docker", 41 | "args": true 42 | }, 43 | { 44 | "name": "stop-docker-compose", 45 | "cmd": "docker", 46 | "args": true 47 | }, 48 | { 49 | "name": "check-docker-container-exists", 50 | "cmd": "docker", 51 | "args": true 52 | }, 53 | { 54 | "name": "docker-version", 55 | "cmd": "docker", 56 | "args": [ 57 | "--version" 58 | ] 59 | }, 60 | { 61 | "name": "open-docker-app", 62 | "cmd": "open", 63 | "args": [ 64 | "-a", 65 | "Docker Desktop" 66 | ] 67 | } 68 | ] 69 | }, 70 | { 71 | "identifier": "shell:allow-execute", 72 | "allow": [ 73 | { 74 | "name": "check-docker-container", 75 | "cmd": "docker", 76 | "args": true 77 | } 78 | ] 79 | }, 80 | "shell:allow-open", 81 | "updater:default", 82 | "process:default" 83 | ] 84 | } -------------------------------------------------------------------------------- /src-tauri/capabilities/migrated.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "migrated", 3 | "description": "permissions that were migrated from v1", 4 | "local": true, 5 | "windows": [ 6 | "main" 7 | ], 8 | "permissions": [ 9 | "core:default" 10 | ] 11 | } -------------------------------------------------------------------------------- /src-tauri/gen/schemas/capabilities.json: -------------------------------------------------------------------------------- 1 | {"desktop-capability":{"identifier":"desktop-capability","description":"","local":true,"windows":["main"],"permissions":["fs:default","fs:allow-watch","fs:allow-app-meta-recursive","fs:allow-app-read-recursive","fs:allow-app-write-recursive","fs:scope-appdata-recursive",{"identifier":"fs:scope","allow":[{"path":"$RESOURCE/bundle/templates/docker-compose.yml.template"},{"path":"$RESOURCE/bundle/templates/default.nginx.conf.template"},{"path":"$RESOURCE/bundle/templates/server.conf.template"}]},"dialog:default","dialog:allow-confirm",{"identifier":"shell:allow-spawn","allow":[{"args":true,"cmd":"docker","name":"run-docker-compose"},{"args":true,"cmd":"docker","name":"stop-docker-compose"},{"args":true,"cmd":"docker","name":"check-docker-container-exists"},{"args":["--version"],"cmd":"docker","name":"docker-version"},{"args":["-a","Docker Desktop"],"cmd":"open","name":"open-docker-app"}]},{"identifier":"shell:allow-execute","allow":[{"args":true,"cmd":"docker","name":"check-docker-container"}]},"shell:allow-open","updater:default","process:default"],"platforms":["linux","macOS","windows"]},"main-capability":{"identifier":"main-capability","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","core:window:allow-set-title","sentry:default"]},"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main"],"permissions":["core:default"]}} -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/ios/AppIcon-20x20@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/ios/AppIcon-20x20@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/ios/AppIcon-20x20@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/ios/AppIcon-20x20@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/ios/AppIcon-29x29@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/ios/AppIcon-29x29@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/ios/AppIcon-29x29@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/ios/AppIcon-29x29@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/ios/AppIcon-40x40@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/ios/AppIcon-40x40@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/ios/AppIcon-40x40@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/ios/AppIcon-40x40@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/ios/AppIcon-512@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/ios/AppIcon-60x60@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/ios/AppIcon-60x60@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/ios/AppIcon-76x76@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/ios/AppIcon-76x76@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png -------------------------------------------------------------------------------- /src-tauri/src/commands/hosts.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::fs::File; 3 | use std::io::{self, BufRead, BufReader}; 4 | 5 | #[derive(Serialize, Deserialize)] 6 | pub struct HostsFileContext { 7 | line_number: usize, 8 | surrounding_lines: Vec, 9 | } 10 | 11 | #[tauri::command] 12 | pub async fn get_hosts_file_context(hostname: String) -> Result { 13 | let file = File::open("/etc/hosts").map_err(|e| e.to_string())?; 14 | let reader = BufReader::new(file); 15 | let lines: Vec = reader.lines().collect::>().map_err(|e| e.to_string())?; 16 | 17 | let target_line = lines.iter() 18 | .enumerate() 19 | .find(|(_, line)| line.contains(&hostname)) 20 | .ok_or_else(|| "Hostname not found in hosts file".to_string())?; 21 | 22 | let start_idx = target_line.0.saturating_sub(2); 23 | let end_idx = (target_line.0 + 3).min(lines.len()); 24 | 25 | let context = HostsFileContext { 26 | line_number: target_line.0 + 1, // Convert to 1-based line number 27 | surrounding_lines: lines[start_idx..end_idx].to_vec(), 28 | }; 29 | 30 | Ok(context) 31 | } -------------------------------------------------------------------------------- /src-tauri/src/keychainmgr/keychain_passwords.rs: -------------------------------------------------------------------------------- 1 | // use security_framework::passwords::{ 2 | // delete_generic_password, get_generic_password, set_generic_password, 3 | // }; 4 | // use tauri::ipc::InvokeError; 5 | 6 | // /** 7 | // * Note: 8 | // * pub fn set_generic_password( 9 | // service: &str, 10 | // account: &str, 11 | // password: &[u8] 12 | // ) -> Result<()> 13 | 14 | // pub fn get_generic_password(service: &str, account: &str) -> Result> 15 | // Get the generic password for the given service and account. If no matching keychain entry exists, fails with error code errSecItemNotFound. 16 | // */ 17 | // const PREFIX: &str = "dev.ophiuchi.app: "; 18 | // const SERVICE_NAME_PREFIX: &str = "dev.ophiuchi.app: "; 19 | 20 | // #[tauri::command(rename_all = "snake_case")] 21 | // pub fn save_password(app_name: &str, item_name: &str, password: &str) -> Result<(), InvokeError> { 22 | // let prefixed_service_name = format!("{}{}", SERVICE_NAME_PREFIX, item_name); 23 | // let prefixed_item_name = format!("{}_{}", app_name, item_name); 24 | // set_generic_password( 25 | // &prefixed_service_name, 26 | // &prefixed_item_name, 27 | // password.as_bytes(), 28 | // ) 29 | // .map_err(|e| InvokeError::from(e.to_string()))?; 30 | // Ok(()) 31 | // } 32 | 33 | // #[tauri::command(rename_all = "snake_case")] 34 | // pub fn delete_password(app_name: &str, item_name: &str) -> Result<(), InvokeError> { 35 | // let prefixed_service_name = format!("{}{}", SERVICE_NAME_PREFIX, item_name); 36 | // let prefixed_item_name = format!("{}_{}", app_name, item_name); 37 | // delete_generic_password(&prefixed_service_name, &prefixed_item_name) 38 | // .map_err(|e| InvokeError::from(e.to_string()))?; 39 | 40 | // Ok(()) 41 | // } 42 | 43 | // #[tauri::command(rename_all = "snake_case")] 44 | // pub fn get_password(app_name: &str, item_name: &str) -> Result { 45 | // let prefixed_service_name = format!("{}{}", SERVICE_NAME_PREFIX, item_name); 46 | // let prefixed_item_name = format!("{}_{}", app_name, item_name); 47 | // let password = get_generic_password(&prefixed_service_name, &prefixed_item_name) 48 | // .map_err(|e| InvokeError::from(e.to_string()))?; 49 | 50 | // let password = String::from_utf8(password).map_err(|e| InvokeError::from(e.to_string()))?; 51 | 52 | // Ok(password) 53 | // } 54 | -------------------------------------------------------------------------------- /src-tauri/src/keychainmgr/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod keychain_passwords; 2 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../node_modules/@tauri-apps/cli/schema.json", 3 | "build": { 4 | "beforeBuildCommand": "npm run build", 5 | "beforeDevCommand": "npm run dev", 6 | "frontendDist": "../out", 7 | "devUrl": "http://localhost:8899" 8 | }, 9 | "bundle": { 10 | "active": true, 11 | "category": "DeveloperTool", 12 | "copyright": "", 13 | "targets": "all", 14 | "externalBin": [], 15 | "icon": [ 16 | "icons/32x32.png", 17 | "icons/128x128.png", 18 | "icons/128x128@2x.png", 19 | "icons/icon.icns", 20 | "icons/icon.ico" 21 | ], 22 | "windows": { 23 | "certificateThumbprint": null, 24 | "digestAlgorithm": "sha256", 25 | "timestampUrl": "" 26 | }, 27 | "longDescription": "", 28 | "macOS": { 29 | "entitlements": null, 30 | "exceptionDomain": "", 31 | "frameworks": [], 32 | "providerShortName": null, 33 | "signingIdentity": null 34 | }, 35 | "resources": [ 36 | "bundle/templates/**/*" 37 | ], 38 | "shortDescription": "", 39 | "linux": { 40 | "deb": { 41 | "depends": [] 42 | } 43 | }, 44 | "createUpdaterArtifacts": true 45 | }, 46 | "productName": "Ophiuchi", 47 | "mainBinaryName": "Ophiuchi", 48 | "version": "0.6.1", 49 | "identifier": "dev.ophiuchi.desktop", 50 | "plugins": { 51 | "updater": { 52 | "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDM3NjRGMUVBRkVDMUMxMApSV1FRSE95dkhrOTJBejRlcVRBMzV0ajFkR1UvVnpPRHI1ekNYVHp6VUdHYjZOYUhYa2p2UVVhRgo=", 53 | "endpoints": [ 54 | "https://ophiuchi.dev/stable/update-check-v2/latest" 55 | ] 56 | } 57 | }, 58 | "app": { 59 | "windows": [ 60 | { 61 | "fullscreen": false, 62 | "height": 768, 63 | "resizable": true, 64 | "title": "Ophiuchi", 65 | "width": 1280, 66 | "minWidth": 1024, 67 | "minHeight": 640, 68 | "useHttpsScheme": true 69 | } 70 | ], 71 | "security": { 72 | "csp": null 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/global-error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; // Error boundaries must be Client Components 2 | 3 | export default function GlobalError({ 4 | error, 5 | reset, 6 | }: { 7 | error: Error & { digest?: string }; 8 | reset: () => void; 9 | }) { 10 | return ( 11 | // global-error must include html and body tags 12 | 13 | 14 |

Something went wrong!

15 |
16 |

{error.message}

17 | 18 |
19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss/base'; 2 | @import 'tailwindcss/components'; 3 | @import 'tailwindcss/utilities'; 4 | 5 | 6 | @layer base { 7 | :root { 8 | --background: 0 0% 100%; 9 | --foreground: 224 71.4% 4.1%; 10 | --card: 0 0% 100%; 11 | --card-foreground: 224 71.4% 4.1%; 12 | --popover: 0 0% 100%; 13 | --popover-foreground: 224 71.4% 4.1%; 14 | --primary: 34 100% 48%; 15 | --primary-foreground: 210 20% 98%; 16 | --secondary: 220 14.3% 90%; 17 | --secondary-foreground: 220.9 39.3% 11%; 18 | --muted: 220 14.3% 95.9%; 19 | --muted-foreground: 220 8.9% 46.1%; 20 | --accent: 220 14.3% 95.9%; 21 | --accent-foreground: 220.9 39.3% 11%; 22 | --destructive: 0 72.2% 50.6%; 23 | --destructive-foreground: 210 20% 98%; 24 | --border: 220 13% 91%; 25 | --input: 220 13% 91%; 26 | --ring: 34 100% 48%; 27 | --radius: 0.5rem; 28 | --chart-1: 12 76% 61%; 29 | --chart-2: 173 58% 39%; 30 | --chart-3: 197 37% 24%; 31 | --chart-4: 43 74% 66%; 32 | --chart-5: 27 87% 67%; 33 | 34 | --sidebar-background: 0 0% 92%; 35 | --sidebar-foreground: 240 5.3% 26.1%; 36 | --sidebar-primary: 240 5.9% 10%; 37 | --sidebar-primary-foreground: 0 0% 98%; 38 | --sidebar-accent: 240 4.8% 95.9%; 39 | --sidebar-accent-foreground: 240 5.9% 10%; 40 | --sidebar-border: 220 13% 91%; 41 | --sidebar-ring: 217.2 91.2% 59.8%; 42 | } 43 | .dark { 44 | --background: 240 5.9% 10%; 45 | --foreground: 210 20% 98%; 46 | --card: 224 71.4% 4.1%; 47 | --card-foreground: 210 20% 98%; 48 | --popover: 240 5.9% 10%; 49 | --popover-foreground: 210 20% 98%; 50 | --primary: 34 100% 48%; 51 | --primary-foreground: 210 20% 98%; 52 | --secondary: 240 5.9% 18.0%; 53 | --secondary-foreground: 210 20% 98%; 54 | --muted: 240 5.9% 12%; 55 | --muted-foreground: 217.9 10.6% 64.9%; 56 | --accent: 240 5.9% 12%; 57 | --accent-foreground: 210 20% 98%; 58 | --destructive: 0 73.7% 41.8%; 59 | --destructive-foreground: 210 20% 98%; 60 | --border: 240 3.7% 15.9%; 61 | --input: 240 3.7% 15.9%; 62 | --ring: 34 100% 48%; 63 | --chart-1: 220 70% 50%; 64 | --chart-2: 160 60% 45%; 65 | --chart-3: 30 80% 55%; 66 | --chart-4: 280 65% 60%; 67 | --chart-5: 340 75% 55%; 68 | 69 | --sidebar-background: 240 5.9% 10%; 70 | --sidebar-foreground: 240 4.8% 95.9%; 71 | --sidebar-primary: 224.3 76.3% 48%; 72 | --sidebar-primary-foreground: 0 0% 100%; 73 | --sidebar-accent: 240 3.7% 15.9%; 74 | --sidebar-accent-foreground: 240 4.8% 95.9%; 75 | --sidebar-border: 240 3.7% 15.9%; 76 | --sidebar-ring: 217.2 91.2% 59.8%; 77 | } 78 | } 79 | 80 | 81 | @layer base { 82 | * { 83 | @apply border-border; 84 | } 85 | body { 86 | @apply bg-background text-foreground; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { AppSidebar } from "@/components/app-sidebar"; 4 | import { SystemSetupProvider } from "@/components/page-components/setup-provider"; 5 | import { ThemeProvider } from "@/components/theme-provider"; 6 | import GoogleAnalytics from "@/components/tools/ga"; 7 | import { SidebarProvider } from "@/components/ui/sidebar"; 8 | import { Toaster } from "@/components/ui/sonner"; 9 | import { cn } from "@/lib/utils"; 10 | import { Noto_Sans } from "next/font/google"; 11 | import "./globals.css"; 12 | 13 | const notoSans = Noto_Sans({ 14 | subsets: ["latin"], 15 | weight: ["100", "300", "400", "500", "600", "700", "800"], 16 | }); 17 | 18 | export default function RootLayout({ 19 | children, 20 | }: { 21 | children: React.ReactNode; 22 | }) { 23 | return ( 24 | 25 | 28 | 29 | 30 | 36 | 37 | 38 |
39 | {children} 40 |
41 |
42 | 43 |
44 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import ProxyListComponent from "@/components/page-components/proxy-list"; 2 | import dynamic from "next/dynamic"; 3 | 4 | function Home() { 5 | return ; 6 | } 7 | 8 | export default dynamic(() => Promise.resolve(Home), { 9 | ssr: false, 10 | }); 11 | -------------------------------------------------------------------------------- /src/app/settings/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ModeToggle } from "@/components/page-components/theme-toggle"; 4 | import { 5 | Card, 6 | CardContent, 7 | CardDescription, 8 | CardHeader, 9 | CardTitle, 10 | } from "@/components/ui/card"; 11 | import Code from "@/components/ui/code"; 12 | import { invoke } from "@tauri-apps/api/core"; 13 | import { appDataDir, homeDir } from "@tauri-apps/api/path"; 14 | import dynamic from "next/dynamic"; 15 | import { useCallback, useEffect, useState } from "react"; 16 | 17 | function SettingsPage() { 18 | const [appDataDirPath, setAppDataDirPath] = useState(null); 19 | const [homeDirPath, setHomeDirPath] = useState(null); 20 | 21 | useEffect(() => { 22 | appDataDir().then(setAppDataDirPath); 23 | homeDir().then(setHomeDirPath); 24 | }, []); 25 | 26 | const onOpenFinder = useCallback(async () => { 27 | invoke("open_finder_or_explorer", { 28 | path: appDataDirPath, 29 | }); 30 | }, [appDataDirPath]); 31 | 32 | const onOpenBackupFiles = useCallback(async () => { 33 | invoke("open_finder_or_explorer", { 34 | path: `${homeDirPath}/ophiuchi.hosts.bak`, 35 | }); 36 | }, [homeDirPath]); 37 | 38 | // if(!appDataDirPath) { 39 | // return null; 40 | // } 41 | 42 | return ( 43 | 44 | 45 | Settings 46 | 47 | 48 | 49 | 50 | Generated Files 51 | 52 |
53 | Required files to run nginx proxy localhost servers, such as 54 | self-signed certificates, nginx configuration files and 55 | docker-compose.yml files can be found at: 56 |
57 |
58 | 59 | {appDataDirPath} 60 | 61 |
62 |
63 |
64 | 65 |

{ 68 | onOpenFinder(); 69 | }} 70 | > 71 | Show in Finder.... 72 |

73 |
74 |
75 | 76 | 77 | Backup Files 78 | 79 |
80 | Whenever Ophiuchi makes changes to the /etc/hosts file, a backup 81 | is created at: 82 |
83 |
84 | {`${homeDirPath}/ophiuchi.hosts.bak`} 88 |
89 |
90 |
91 | 92 |

{ 95 | onOpenBackupFiles(); 96 | }} 97 | > 98 | Show in Finder.... 99 |

100 |
101 |
102 | 103 | 104 | Dark/Light Mode 105 | 106 | Toggle between dark and light mode, or auto-detect system theme. 107 | 108 | 109 | 110 |
111 | 112 |
113 |
114 |
115 |
116 |
117 | ); 118 | } 119 | 120 | export default dynamic(() => Promise.resolve(SettingsPage), { 121 | ssr: false, 122 | }); 123 | -------------------------------------------------------------------------------- /src/app/status/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Card, 5 | CardContent, 6 | CardDescription, 7 | CardHeader, 8 | CardTitle, 9 | } from "@/components/ui/card"; 10 | import Code from "@/components/ui/code"; 11 | import systemStatusStore from "@/stores/system-status"; 12 | import dynamic from "next/dynamic"; 13 | 14 | function SettingsPage() { 15 | const { isDockerInstalled, isDockerContainerRunning, runningContainerInfo } = 16 | systemStatusStore(); 17 | 18 | return ( 19 | 20 | 21 | Status 22 | 23 | 24 | 25 | 26 | Docker Intallation 27 | 28 | Ophiuchi uses Docker + nginx to run the proxy server. If you 29 | don't have Docker installed, you can install it from the 30 | official website: 31 | 37 | Get Docker 38 | 39 | 40 | 41 | 42 |
43 | {isDockerInstalled ? ( 44 | 45 | ) : ( 46 | 47 | )} 48 |
49 |

50 | {isDockerInstalled 51 | ? "Docker found on system." 52 | : "Docker is not found on system."} 53 |

54 |
55 |
56 |
57 |
58 | 59 | 60 | Docker Container 61 | 62 | Ophiuchi creates and runs a Docker container to run the proxy 63 | server. 64 | 65 | 66 | 67 |
68 | {isDockerContainerRunning ? ( 69 | 70 | ) : ( 71 | 72 | )} 73 |
74 |

75 | {isDockerContainerRunning 76 | ? "Container is running." 77 | : "Container is not running."} 78 |

79 |
80 | {isDockerContainerRunning ? ( 81 | <> 82 |

83 | Project name:{" "} 84 | {runningContainerInfo?.Project} 85 |

86 |

87 | Container name:{" "} 88 | {runningContainerInfo?.Name} 89 |

90 | 91 | ) : ( 92 |

93 | Container name will be shown here after you start the 94 | container. 95 |

96 | )} 97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | ); 105 | } 106 | 107 | export default dynamic(() => Promise.resolve(SettingsPage), { 108 | ssr: false, 109 | }); 110 | -------------------------------------------------------------------------------- /src/app/test-page/page.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | 3 | function EndpointListPage() { 4 | return ( 5 |

This is a test page

6 | ); 7 | } 8 | 9 | export default dynamic(() => Promise.resolve(EndpointListPage), { 10 | ssr: false, 11 | }); 12 | -------------------------------------------------------------------------------- /src/components/app-sidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | /* eslint-disable @next/next/no-img-element */ 4 | 5 | import { 6 | Sidebar, 7 | SidebarContent, 8 | SidebarFooter, 9 | SidebarGroup, 10 | SidebarGroupContent, 11 | SidebarGroupLabel, 12 | SidebarHeader, 13 | SidebarMenu, 14 | SidebarMenuBadge, 15 | SidebarMenuButton, 16 | SidebarMenuItem, 17 | SidebarMenuSub, 18 | SidebarMenuSubItem, 19 | } from "@/components/ui/sidebar"; 20 | import { ICON_SIZE, ICON_STROKE_WIDTH } from "@/lib/constants"; 21 | import { cn } from "@/lib/utils"; 22 | import proxyListStore from "@/stores/proxy-list"; 23 | import systemStatusStore from "@/stores/system-status"; 24 | import { 25 | CheckCircle, 26 | CircleAlert, 27 | Computer, 28 | ExternalLinkIcon, 29 | HelpCircle, 30 | List, 31 | LoaderCircle, 32 | Settings, 33 | } from "lucide-react"; 34 | import Link from "next/link"; 35 | import { usePathname } from "next/navigation"; 36 | import packageJson from "../../package.json"; 37 | import DiscordIcon from "./icons/discord"; 38 | import { AddProxyGroupDialog } from "./page-components/proxy-list/add-new/group"; 39 | import UpdaterInterface from "./page-components/updater"; 40 | import { Badge } from "./ui/badge"; 41 | 42 | // Menu items. 43 | const appItems = [ 44 | // { 45 | // title: "Status", 46 | // url: "/test-page", 47 | // icon: () => , 48 | // }, 49 | // { 50 | // title: "Calendar", 51 | // url: "#", 52 | // icon: () => , 53 | // }, 54 | // { 55 | // title: "Search", 56 | // url: "#", 57 | // icon: () => , 58 | // }, 59 | { 60 | title: "Settings", 61 | url: "/settings", 62 | icon: () => , 63 | }, 64 | ]; 65 | 66 | const helpItems = [ 67 | { 68 | title: "Help", 69 | url: "https://heavenly-tent-fff.notion.site/Ophiuchi-Developers-Toolkit-734dc4f766fe40aebfe0da3cbbc304f5?pvs=4", 70 | isBlank: true, 71 | icon: () => , 72 | badgeText: , 73 | }, 74 | { 75 | title: "Discord", 76 | url: "https://discord.gg/fpp8kNyPtz", 77 | isBlank: true, 78 | icon: () => , 79 | badgeText: "Feedback", 80 | }, 81 | ]; 82 | 83 | function DockerStatus() { 84 | const { isCheckDone, isDockerInstalled, isDockerContainerRunning } = 85 | systemStatusStore(); 86 | 87 | if (!isCheckDone) { 88 | return ; 89 | } 90 | 91 | if (!isDockerInstalled) { 92 | return ; 93 | } 94 | 95 | if (!isDockerContainerRunning) { 96 | return ; 97 | } 98 | 99 | return ; 100 | } 101 | 102 | export function AppSidebar() { 103 | const pathname = usePathname(); 104 | const { selectedGroup, groupList, setSelectedGroup, totalProxyList } = 105 | proxyListStore(); 106 | 107 | return ( 108 | 109 | {/*
110 |
111 | 112 |
113 |
*/} 114 | 115 |
116 | 117 |

Ophiuchi

118 |
119 |
120 | 121 | 122 | Application 123 | 124 | 125 | 126 | 127 | 128 | 129 | Proxies 130 | 131 | 132 | 133 | {groupList.map((group) => { 134 | const isMainPage = pathname === "/"; 135 | return ( 136 | 144 | 145 | {/* Purposely used div here because SidebarMenuSubItem is an component and it will produce an error. */} 146 |
setSelectedGroup(group)} 151 | > 152 | {group.name} 153 | {/* {selectedGroup?.id === group.id && ( 154 | 158 | )} */} 159 | {!group.isNoGroup ? ( 160 | 161 | {group.includedHosts.length} 162 | 163 | ) : ( 164 | 165 | {totalProxyList.length} 166 | 167 | )} 168 |
169 | 170 |
171 | ); 172 | })} 173 | 174 | {}} /> 175 | 176 |
177 |
178 | 179 | 180 | 181 | 182 | Status 183 | 184 | 185 | 186 | 187 | 188 | 189 | {appItems.map((item) => { 190 | const isActive = item.url === pathname; 191 | return ( 192 | 193 | 194 | 195 | 196 | {item.title} 197 | 198 | 199 | 200 | ); 201 | })} 202 |
203 |
204 | Help 205 | 206 | 207 | {helpItems.map((item) => { 208 | return ( 209 | 210 | 211 | 215 | 216 | {item.title} 217 | {item.badgeText && ( 218 | 222 | {item.badgeText} 223 | 224 | )} 225 | 226 | 227 | 228 | ); 229 | })} 230 | 231 | 232 | 233 |
234 | 235 |
236 |
237 | v{packageJson.version} 238 |
239 |
240 |
241 |
242 | ); 243 | } 244 | -------------------------------------------------------------------------------- /src/components/icons/discord.tsx: -------------------------------------------------------------------------------- 1 | export default function DiscordIcon({ className }: { className?: string }) { 2 | return ( 3 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/components/icons/docker.tsx: -------------------------------------------------------------------------------- 1 | export default function DockerIcon({ className }: { className?: string }) { 2 | return ( 3 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/components/page-components/add-proxy/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { 3 | Dialog, 4 | DialogContent, 5 | DialogDescription, 6 | DialogFooter, 7 | DialogHeader, 8 | DialogTitle, 9 | DialogTrigger, 10 | } from "@/components/ui/dialog"; 11 | import { 12 | Form, 13 | FormControl, 14 | FormDescription, 15 | FormField, 16 | FormItem, 17 | FormLabel, 18 | FormMessage, 19 | } from "@/components/ui/form"; 20 | import { Input } from "@/components/ui/input"; 21 | import { IProxyData } from "@/helpers/proxy-manager/interfaces"; 22 | import proxyListStore from "@/stores/proxy-list"; 23 | import { zodResolver } from "@hookform/resolvers/zod"; 24 | import { PlusIcon } from "lucide-react"; 25 | import React, { useCallback } from "react"; 26 | import { useForm } from "react-hook-form"; 27 | import { withMask } from "use-mask-input"; 28 | import { z } from "zod"; 29 | 30 | const formSchema = z.object({ 31 | port: z.preprocess( 32 | (arg) => parseInt(z.string().parse(arg), 10), 33 | z 34 | .number({ 35 | required_error: "Port is required", 36 | invalid_type_error: "Port must be a number", 37 | }) 38 | .int() 39 | .min(1) 40 | .max(65535) 41 | ), 42 | hostname: z.string().min(3).max(64), 43 | }); 44 | 45 | export function AddProxyDialog({ onDone }: { onDone: () => void }) { 46 | const { addProxyItem, totalProxyList } = proxyListStore(); 47 | const [open, setOpen] = React.useState(false); 48 | const [hostnameExists, setHostnameExists] = React.useState(false); 49 | const [canSubmit, setCanSubmit] = React.useState(false); 50 | const [hostErrorMessage, setHostErrorMessage] = React.useState(""); 51 | 52 | const form = useForm>({ 53 | resolver: zodResolver(formSchema), 54 | defaultValues: { 55 | port: 0, 56 | hostname: "", 57 | }, 58 | }); 59 | 60 | const fixHostname = useCallback((hostname: string) => { 61 | return hostname.replace(/[^a-z0-9\-\.]/g, ""); 62 | }, []); 63 | 64 | const checkHostnameExists = useCallback( 65 | (hostname: string) => { 66 | setHostnameExists( 67 | totalProxyList.some((endpoint) => endpoint.hostname === hostname) 68 | ); 69 | }, 70 | [totalProxyList] 71 | ); 72 | 73 | const updateCanSubmit = useCallback(() => { 74 | const values = form.getValues(); 75 | setCanSubmit(false); 76 | const hostname = fixHostname(values.hostname); 77 | checkHostnameExists(hostname); 78 | if (hostnameExists) { 79 | setHostErrorMessage("Hostname already exists"); 80 | return; 81 | } 82 | if (hostname.length < 4) { 83 | setHostErrorMessage("Hostname must be at least 4 characters long"); 84 | return; 85 | } 86 | if (hostname.endsWith(".app")) { 87 | setHostErrorMessage("Hostname cannot end with .app"); 88 | return; 89 | } 90 | if (hostname.endsWith(".")) { 91 | setHostErrorMessage("Hostname cannot end with a dot"); 92 | return; 93 | } 94 | // if hostname ends with dot + number, return 95 | if (/\.\d+$/.test(hostname)) { 96 | setHostErrorMessage("Hostname cannot end with a number"); 97 | return; 98 | } 99 | if (values.port <= 0 || values.port > 65535) { 100 | setHostErrorMessage("Port must be between 1 and 65535"); 101 | return; 102 | } 103 | setHostErrorMessage(""); 104 | setCanSubmit(true); 105 | }, [form, fixHostname, checkHostnameExists, hostnameExists]); 106 | 107 | function onSubmit(values: z.infer) { 108 | const hostname = fixHostname(values.hostname); 109 | checkHostnameExists(hostname); 110 | if (hostnameExists) return; 111 | if (values.port <= 0 || values.port > 65535) return; 112 | const data: IProxyData = { 113 | nickname: `Proxy for ${hostname}`, 114 | hostname: hostname, 115 | port: values.port, 116 | createdAt: new Date().toISOString(), 117 | }; 118 | addProxyItem(data); 119 | onDone(); 120 | form.reset(); 121 | setOpen(false); 122 | } 123 | 124 | return ( 125 | { 128 | form.reset(); 129 | setOpen(open); 130 | }} 131 | > 132 | 133 | 137 | 138 | 139 | 140 | Create Proxy 141 | 142 | Create a new proxy to turn your local web app to support custom 143 | hostname and https. 144 | 145 | 146 |
147 |
148 | 149 | ( 153 | 154 | Application port 155 | 156 | { 165 | if (e.target.value.startsWith("0")) { 166 | e.target.value = e.target.value.slice(1); 167 | } 168 | field.onChange(e); 169 | updateCanSubmit(); 170 | }} 171 | // value={portState} 172 | // onChange={(e) => { 173 | // const port = e.target.value; 174 | // setPortState(port); 175 | // }} 176 | /> 177 | 178 | 179 | Your local application's port number. 180 | 181 | 182 | 183 | )} 184 | /> 185 | ( 189 | 190 | Hostname 191 | 192 | { 195 | const sanitizedHostname = e.target.value.replace( 196 | /[^a-z0-9\-\.]/g, 197 | "" 198 | ); 199 | field.onChange(sanitizedHostname); 200 | updateCanSubmit(); 201 | }} 202 | /> 203 | 204 | 205 | Any hostname you want to use locally.
206 | - Needs to be at least 4 characters long. 207 |
- Cannot end with .app (MacOS) 208 |
209 | 210 |
211 | )} 212 | /> 213 | 214 | 215 | {hostErrorMessage ? ( 216 |
{hostErrorMessage}
217 | ) : ( 218 |
219 | . 220 |
221 | )} 222 |
223 | 224 | 233 | 234 |
235 |
236 | ); 237 | } 238 | -------------------------------------------------------------------------------- /src/components/page-components/certificate-dialogs/cert-buttons.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { CertificateManager } from "@/helpers/certificate-manager"; 4 | import { IProxyData } from "@/helpers/proxy-manager/interfaces"; 5 | import { ICON_SIZE, ICON_STROKE_WIDTH } from "@/lib/constants"; 6 | import { certKeychainStore } from "@/stores/cert-keychain-store"; 7 | import { appDataDir } from "@tauri-apps/api/path"; 8 | import { open as shellOpen } from "@tauri-apps/plugin-shell"; 9 | import { LoaderCircle } from "lucide-react"; 10 | import { useCallback, useEffect, useState } from "react"; 11 | import { PrepareProxyDialog } from "../proxy-list/prepare/prepare-proxy-dialog"; 12 | 13 | export default function PrepareButtons({ item }: { item: IProxyData }) { 14 | const [certExist, setCertExist] = useState(undefined); 15 | const { certOnKeychain } = certKeychainStore(); 16 | 17 | const openCert = useCallback(async (data: IProxyData) => { 18 | const appDataDirPath = await appDataDir(); 19 | const certPath = `${appDataDirPath}/cert/${data.hostname}`; 20 | shellOpen(certPath); 21 | }, []); 22 | 23 | async function checkExist(hostname: string) { 24 | const configHelper = CertificateManager.shared(); 25 | const exists = await configHelper.checkCertificateExists(hostname); 26 | setCertExist(exists); 27 | } 28 | 29 | useEffect(() => { 30 | checkExist(item.hostname); 31 | }, [item.hostname]); 32 | 33 | if (certExist === undefined) { 34 | return ( 35 |
36 | 41 |
42 | ); 43 | } 44 | 45 | return ( 46 |
47 | { 50 | checkExist(item.hostname); 51 | }} 52 | /> 53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/components/page-components/home/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | // When using the Tauri API npm package: 4 | import { BaseDirectory, writeTextFile } from "@tauri-apps/plugin-fs"; 5 | import { useCallback, useEffect } from "react"; 6 | // Write a text file to the `$APPCONFIG/app.conf` path 7 | 8 | const onStartServer = () => { 9 | // Invoke the command 10 | }; 11 | const loadTest = async () => { 12 | const res = await fetch("http://localhost:8899/api/test"); 13 | const data = await res.json(); 14 | console.log(data); 15 | return data; 16 | }; 17 | 18 | export function HomeComponent() { 19 | useEffect(() => { 20 | loadTest(); 21 | }, []); 22 | 23 | const onStartServer = useCallback(async () => { 24 | const res = await writeTextFile("app.conf", "file contents", { 25 | baseDir: BaseDirectory.AppData, 26 | }); 27 | console.log(res); 28 | }, []); 29 | 30 | return ( 31 |
32 |
33 |

34 | Ophiuchi 35 |

36 |

37 | Start your local HTTPS proxy server with docker. 38 |

39 |
40 |
41 |
42 |
43 | 44 | 49 |
50 |
51 | 52 | 57 |
58 |
59 | 67 |
68 |
69 |
70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/components/page-components/icons.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeselemon/ophiuchi-desktop/9e4f8dfac16ae922f0537a484e3192430e77f97a/src/components/page-components/icons.tsx -------------------------------------------------------------------------------- /src/components/page-components/proxy-list/add-new/group.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { 3 | Dialog, 4 | DialogContent, 5 | DialogDescription, 6 | DialogFooter, 7 | DialogHeader, 8 | DialogTitle, 9 | DialogTrigger, 10 | } from "@/components/ui/dialog"; 11 | import { Input } from "@/components/ui/input"; 12 | import proxyListStore from "@/stores/proxy-list"; 13 | import { Label } from "@radix-ui/react-label"; 14 | import { PlusIcon } from "lucide-react"; 15 | import React from "react"; 16 | 17 | export function AddProxyGroupDialog({ onDone }: { onDone: () => void }) { 18 | const [groupName, setGroupName] = React.useState(""); 19 | const [open, setOpen] = React.useState(false); 20 | const { addGroup } = proxyListStore(); 21 | 22 | return ( 23 | 24 | 25 | 29 | 30 | 31 | 32 | Create Group 33 | 34 | Groups are useful when you have separate projects and want to launch 35 | them with different configurations. 36 | 37 | 38 |
39 |
40 | 43 | { 48 | setGroupName(e.target.value); 49 | }} 50 | /> 51 |
52 |
53 | 54 | 66 | 67 |
68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/components/page-components/proxy-list/add-new/proxy-to-group.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { 3 | Dialog, 4 | DialogContent, 5 | DialogDescription, 6 | DialogFooter, 7 | DialogHeader, 8 | DialogTitle, 9 | DialogTrigger, 10 | } from "@/components/ui/dialog"; 11 | import { 12 | Table, 13 | TableBody, 14 | TableCaption, 15 | TableCell, 16 | TableHead, 17 | TableHeader, 18 | TableRow, 19 | } from "@/components/ui/table"; 20 | import proxyListStore from "@/stores/proxy-list"; 21 | import { DialogClose } from "@radix-ui/react-dialog"; 22 | import { Label } from "@radix-ui/react-label"; 23 | import { BookmarkPlusIcon } from "lucide-react"; 24 | import React from "react"; 25 | 26 | export function AddProxyToGroupDialog({ onDone }: { onDone: () => void }) { 27 | const { 28 | addGroup, 29 | totalProxyList, 30 | proxyList, 31 | addProxyToGroup, 32 | selectedGroup, 33 | } = proxyListStore(); 34 | const [open, setOpen] = React.useState(false); 35 | 36 | return ( 37 | 38 | 39 | 42 | 43 | 44 | 45 | Add Proxy to Group 46 | Select a proxy to add to group. 47 | 48 |
49 |
50 | 53 | 54 | Select a proxy to add to group. 55 | 56 | 57 | Hostname 58 | Port 59 | 60 | 61 | 62 | 63 | {totalProxyList.map((proxyItem) => { 64 | const isAlreadyAdded = proxyList.find( 65 | (item) => item.hostname === proxyItem.hostname 66 | ); 67 | return ( 68 | 69 | 70 | {proxyItem.hostname} 71 | 72 | {proxyItem.port} 73 | 74 | {isAlreadyAdded ? ( 75 | 78 | ) : ( 79 | 88 | )} 89 | 90 | 91 | ); 92 | })} 93 | 94 |
95 |
96 |
97 | 98 | 99 | 100 | 101 | 102 |
103 |
104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /src/components/page-components/proxy-list/docker-log/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { Checkbox } from "@/components/ui/checkbox"; 3 | import { 4 | Dialog, 5 | DialogContent, 6 | DialogDescription, 7 | DialogFooter, 8 | DialogHeader, 9 | DialogTitle, 10 | } from "@/components/ui/dialog"; 11 | 12 | import { useEffect, useState } from "react"; 13 | 14 | export default function DockerLogModal({ 15 | stream, 16 | detailedStream, 17 | isOpen, 18 | onClosed, 19 | onClearLogs, 20 | }: { 21 | stream: any; 22 | detailedStream: any; 23 | isOpen: boolean; 24 | onClosed?: () => void; 25 | onClearLogs?: () => void; 26 | }) { 27 | const [_isOpen, setIsOpen] = useState(true); 28 | const [showDetailedLog, setShowDetailedLog] = useState(true); 29 | 30 | useEffect(() => { 31 | setIsOpen(isOpen); 32 | }, [isOpen]); 33 | 34 | function closeModal() { 35 | setIsOpen(false); 36 | onClosed && onClosed(); 37 | } 38 | 39 | return ( 40 | <> 41 | 42 | { 45 | // fix for https://github.com/radix-ui/primitives/issues/1241 46 | event.preventDefault(); 47 | document.body.style.pointerEvents = ""; 48 | }} 49 | > 50 | 51 | Docker Command Log 52 | 53 | This dialog shows the logs of the docker command that was 54 | executed. 55 |
56 | To see detailed logs of the docker command, check the detailed logs 57 | checkbox. 58 |
59 | You can close the dialog anytime. 60 |
61 |
62 |
63 |
64 |
65 |
66 | 67 | {showDetailedLog ? detailedStream : stream} 68 | 69 |
70 |
71 |
72 |
73 |
74 | 78 | setShowDetailedLog(checked as boolean) 79 | } 80 | /> 81 | 87 |
88 |
89 |
90 | 91 | 99 | 102 | 103 |
104 |
105 | 106 | ); 107 | } 108 | -------------------------------------------------------------------------------- /src/components/page-components/proxy-list/edit/group.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { 3 | Dialog, 4 | DialogContent, 5 | DialogDescription, 6 | DialogFooter, 7 | DialogHeader, 8 | DialogTitle, 9 | DialogTrigger, 10 | } from "@/components/ui/dialog"; 11 | import { Input } from "@/components/ui/input"; 12 | import proxyListStore from "@/stores/proxy-list"; 13 | import { Label } from "@radix-ui/react-label"; 14 | 15 | import React, { useEffect } from "react"; 16 | 17 | export function EditGroupDialog() { 18 | const { selectedGroup, updateGroup, deleteGroup } = proxyListStore(); 19 | const [groupName, setGroupName] = React.useState(""); 20 | const [open, setOpen] = React.useState(false); 21 | 22 | useEffect(() => { 23 | if (!selectedGroup) return; 24 | setGroupName(selectedGroup.name); 25 | }, [selectedGroup]); 26 | 27 | if (!selectedGroup) return null; 28 | 29 | return ( 30 | 31 | 32 | 35 | {/* */} 39 | 40 | 41 | 42 | Edit Group 43 | Edit the name of the group. 44 | 45 |
46 |
47 | 50 | { 55 | setGroupName(e.target.value); 56 | }} 57 | /> 58 |
59 |
60 | 61 | 62 | 74 | 75 |
76 |
77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /src/components/page-components/proxy-list/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import ProxyListTable from "./table"; 4 | 5 | export default function ProxyListComponent() { 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/page-components/setup-provider/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | "use client"; 3 | 4 | import { SystemHelper } from "@/helpers/system"; 5 | import { certKeychainStore } from "@/stores/cert-keychain-store"; 6 | import proxyListStore from "@/stores/proxy-list"; 7 | import systemStatusStore from "@/stores/system-status"; 8 | // When using the Tauri API npm package: 9 | import { invoke } from "@tauri-apps/api/core"; 10 | import { useCallback, useEffect } from "react"; 11 | import { useInterval } from "usehooks-ts"; 12 | 13 | export function SystemSetupProvider(props: any) { 14 | const { 15 | isDockerInstalled, 16 | setIsDockerInstalled, 17 | setIsCheckDone, 18 | setIsDockerContainerRunning, 19 | updateDockerContainerStatus, 20 | } = systemStatusStore(); 21 | const { init: initCertKeychainStore } = certKeychainStore(); 22 | const { load } = proxyListStore(); 23 | 24 | useInterval( 25 | () => { 26 | updateDockerContainerStatus(); 27 | }, 28 | 29 | isDockerInstalled ? 3000 : null 30 | ); 31 | 32 | useEffect(() => { 33 | initCertKeychainStore(); 34 | updateDockerContainerStatus(); 35 | }, []); 36 | 37 | async function checkDocker() { 38 | try { 39 | const isInstalled = (await invoke("check_docker_installed")) as boolean; 40 | setIsDockerInstalled(isInstalled); 41 | return isInstalled; 42 | } catch (error) { 43 | console.error("Error checking Docker installation:", error); 44 | } 45 | } 46 | 47 | const onStartApp = useCallback(async () => { 48 | const systemHelper = new SystemHelper(); 49 | await systemHelper.boot(); 50 | await checkDocker(); 51 | load(); 52 | setIsCheckDone(true); 53 | }, []); 54 | 55 | useEffect(() => { 56 | onStartApp(); 57 | }, []); 58 | 59 | return props.children; 60 | } 61 | -------------------------------------------------------------------------------- /src/components/page-components/theme-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Moon, Sun } from "lucide-react"; 4 | import { useTheme } from "next-themes"; 5 | 6 | import { Button } from "@/components/ui/button"; 7 | import { 8 | DropdownMenu, 9 | DropdownMenuContent, 10 | DropdownMenuItem, 11 | DropdownMenuTrigger, 12 | } from "@/components/ui/dropdown-menu"; 13 | 14 | export function ModeToggle() { 15 | const { setTheme } = useTheme(); 16 | 17 | return ( 18 | 19 | 20 | 24 | 25 | 26 | setTheme("light")}> 27 | Light 28 | 29 | setTheme("dark")}> 30 | Dark 31 | 32 | setTheme("system")}> 33 | System 34 | 35 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/components/page-components/updater/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { 5 | Card, 6 | CardContent, 7 | CardDescription, 8 | CardHeader, 9 | CardTitle, 10 | } from "@/components/ui/card"; 11 | import { Progress } from "@/components/ui/progress"; 12 | import { relaunch } from "@tauri-apps/plugin-process"; 13 | import { check, Update } from "@tauri-apps/plugin-updater"; 14 | import { XIcon } from "lucide-react"; 15 | import { useEffect, useState } from "react"; 16 | 17 | export default function UpdaterInterface() { 18 | const [theUpdate, setTheUpdate] = useState(null); 19 | const [canInstall, setCanInstall] = useState(false); 20 | const [isDownloading, setIsDownloading] = useState(false); 21 | const [downloaded, setDownloaded] = useState(0); 22 | const [contentLength, setContentLength] = useState(0); 23 | 24 | useEffect(() => { 25 | const checkUpdate = async () => { 26 | try { 27 | const update = await check(); 28 | setTheUpdate(update); 29 | } catch (error) { 30 | console.error(`error`, error); 31 | } 32 | }; 33 | checkUpdate(); 34 | }, []); 35 | 36 | const handleInstall = async () => { 37 | setIsDownloading(true); 38 | let downloaded = 0; 39 | let contentLength = 0; 40 | await theUpdate?.downloadAndInstall((event) => { 41 | switch (event.event) { 42 | case "Started": 43 | contentLength = event.data.contentLength ?? 0; 44 | setContentLength(contentLength); 45 | console.log(`started downloading ${event.data.contentLength} bytes`); 46 | break; 47 | case "Progress": 48 | downloaded += event.data.chunkLength; 49 | setDownloaded(downloaded); 50 | console.log(`downloaded ${downloaded} from ${contentLength}`); 51 | break; 52 | case "Finished": 53 | console.log("download finished"); 54 | setIsDownloading(false); 55 | setCanInstall(true); 56 | break; 57 | } 58 | }); 59 | await relaunch(); 60 | }; 61 | 62 | const buttonInterface = () => { 63 | if (isDownloading) { 64 | return ; 65 | } 66 | return ( 67 | 75 | ); 76 | }; 77 | 78 | if (!theUpdate) { 79 | return null; 80 | } 81 | 82 | return ( 83 | 84 | 85 | Update available! 86 | v{theUpdate.version} 87 |
88 | 95 |
96 |
97 | {buttonInterface()} 98 |
99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /src/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ThemeProvider as NextThemesProvider } from "next-themes" 4 | import * as React from "react" 5 | 6 | export function ThemeProvider({ 7 | children, 8 | ...props 9 | }: React.ComponentProps) { 10 | return {children} 11 | } 12 | -------------------------------------------------------------------------------- /src/components/tools/ga.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { invoke } from "@tauri-apps/api/core"; 4 | import { usePathname, useRouter } from "next/navigation"; 5 | import Script from "next/script"; 6 | import { useEffect, useState } from "react"; 7 | import version from "../../../package.json"; 8 | 9 | declare global { 10 | interface Window { 11 | gtag: any; 12 | } 13 | } 14 | 15 | const GoogleAnalytics = () => { 16 | const pathname = usePathname(); 17 | const router = useRouter(); 18 | 19 | const [userInstallDate, setUserInstallDate] = useState(null); 20 | const [gtagLoaded, setGtagLoaded] = useState(false); 21 | const [ga_id, setGaId] = useState(null); 22 | 23 | // const ga_id = useMemo(async () => { 24 | // return await invoke("get_env", { name: "GTAG_ID" }); 25 | // }, []); 26 | 27 | useEffect(() => { 28 | const fetchGaId = async () => { 29 | const id = await invoke("get_env", { name: "GTAG_ID" }); 30 | setGaId(id as string); 31 | console.log(`id`, id); 32 | }; 33 | fetchGaId(); 34 | }, []); 35 | 36 | useEffect(() => { 37 | const installDate = window.localStorage.getItem("userInstallDate"); 38 | if (installDate) { 39 | setUserInstallDate(installDate); 40 | } else { 41 | window.localStorage.setItem("userInstallDate", new Date().toISOString()); 42 | setUserInstallDate(new Date().toISOString()); 43 | } 44 | }, []); 45 | 46 | useEffect(() => { 47 | if (!userInstallDate) return; 48 | if (!gtagLoaded) return; 49 | window.gtag("set", "user_install_date", userInstallDate); 50 | window.gtag("set", "version", version); 51 | console.log(`userInstallDate`, userInstallDate); 52 | console.log(`version`, version.version); 53 | }, [userInstallDate, gtagLoaded]); 54 | 55 | // when pathname changes, send a pageview to Google Analytics 56 | useEffect(() => { 57 | // when pathname changes, send a pageview to Google Analytics 58 | if (typeof window === "undefined") return; 59 | if (!ga_id) return; 60 | if (!window.gtag) return; 61 | if (!pathname) return; 62 | console.log(`pathname`, pathname); 63 | window.gtag("config", ga_id, { 64 | page_path: pathname, 65 | }); 66 | }, [ga_id, pathname]); 67 | 68 | if (!ga_id) return null; 69 | 70 | return ( 71 | <> 72 | 77 | 93 | 94 | ); 95 | }; 96 | 97 | export default GoogleAnalytics; 98 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from "class-variance-authority" 2 | import * as React from "react" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground border-foreground/10", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from "@radix-ui/react-slot"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | import * as React from "react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | "destructive-outline": 23 | "border border-destructive text-destructive shadow-sm hover:bg-destructive/10", 24 | }, 25 | size: { 26 | default: "h-9 px-4 py-2", 27 | sm: "h-8 rounded-md px-3 text-xs", 28 | lg: "h-10 rounded-md px-8", 29 | icon: "h-9 w-9", 30 | xs: "h-6 px-2 text-xs", 31 | "icon-sm": "h-8 w-8", 32 | }, 33 | }, 34 | defaultVariants: { 35 | variant: "default", 36 | size: "default", 37 | }, 38 | } 39 | ); 40 | 41 | export interface ButtonProps 42 | extends React.ButtonHTMLAttributes, 43 | VariantProps { 44 | asChild?: boolean; 45 | } 46 | 47 | const Button = React.forwardRef( 48 | ({ className, variant, size = "sm", asChild = false, ...props }, ref) => { 49 | const Comp = asChild ? Slot : "button"; 50 | return ( 51 | 56 | ); 57 | } 58 | ); 59 | Button.displayName = "Button"; 60 | 61 | export { Button, buttonVariants }; 62 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )); 18 | Card.displayName = "Card"; 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )); 30 | CardHeader.displayName = "CardHeader"; 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
44 | )); 45 | CardTitle.displayName = "CardTitle"; 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLDivElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )); 57 | CardDescription.displayName = "CardDescription"; 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |
64 | )); 65 | CardContent.displayName = "CardContent"; 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )); 77 | CardFooter.displayName = "CardFooter"; 78 | 79 | export { 80 | Card, 81 | CardContent, 82 | CardDescription, 83 | CardFooter, 84 | CardHeader, 85 | CardTitle 86 | }; 87 | 88 | -------------------------------------------------------------------------------- /src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" 3 | import { cn } from "@/lib/utils" 4 | import { CheckIcon } from "@radix-ui/react-icons" 5 | 6 | const Checkbox = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | 21 | 22 | 23 | 24 | )) 25 | Checkbox.displayName = CheckboxPrimitive.Root.displayName 26 | 27 | export { Checkbox } 28 | -------------------------------------------------------------------------------- /src/components/ui/code.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | export default function Code({ 4 | children, 5 | style = "normal", 6 | type = "normal", 7 | className, 8 | }: { 9 | children: React.ReactNode; 10 | style?: "normal" | "sudo"; 11 | type?: "normal" | "block"; 12 | className?: string; 13 | }) { 14 | function fontColor() { 15 | return style === "normal" ? " " : "text-red-500"; 16 | } 17 | 18 | function bgColor() { 19 | return style === "normal" ? " " : "bg-red-950"; 20 | } 21 | 22 | function blockStyle() { 23 | return type === "block" 24 | ? "w-full border border-muted-foreground/20 rounded-md font-mono p-1.5 break-all" 25 | : "px-1.5 py-0.5 rounded-md whitespace-break-spaces border-muted-foreground/20 border"; 26 | } 27 | 28 | if (type === "block") { 29 | return ( 30 |
31 | {children} 32 |
33 | ); 34 | } else { 35 | return ( 36 | 37 | {children} 38 | 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/ui/command.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { type DialogProps } from "@radix-ui/react-dialog" 3 | import { Command as CommandPrimitive } from "cmdk" 4 | import { cn } from "@/lib/utils" 5 | import { Dialog, DialogContent } from "@/components/ui/dialog" 6 | import { MagnifyingGlassIcon } from "@radix-ui/react-icons" 7 | 8 | const Command = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Command.displayName = CommandPrimitive.displayName 22 | 23 | const CommandDialog = ({ children, ...props }: DialogProps) => { 24 | return ( 25 | 26 | 27 | 28 | {children} 29 | 30 | 31 | 32 | ) 33 | } 34 | 35 | const CommandInput = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 |
40 | 41 | 49 |
50 | )) 51 | 52 | CommandInput.displayName = CommandPrimitive.Input.displayName 53 | 54 | const CommandList = React.forwardRef< 55 | React.ElementRef, 56 | React.ComponentPropsWithoutRef 57 | >(({ className, ...props }, ref) => ( 58 | 63 | )) 64 | 65 | CommandList.displayName = CommandPrimitive.List.displayName 66 | 67 | const CommandEmpty = React.forwardRef< 68 | React.ElementRef, 69 | React.ComponentPropsWithoutRef 70 | >((props, ref) => ( 71 | 76 | )) 77 | 78 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName 79 | 80 | const CommandGroup = React.forwardRef< 81 | React.ElementRef, 82 | React.ComponentPropsWithoutRef 83 | >(({ className, ...props }, ref) => ( 84 | 92 | )) 93 | 94 | CommandGroup.displayName = CommandPrimitive.Group.displayName 95 | 96 | const CommandSeparator = React.forwardRef< 97 | React.ElementRef, 98 | React.ComponentPropsWithoutRef 99 | >(({ className, ...props }, ref) => ( 100 | 105 | )) 106 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName 107 | 108 | const CommandItem = React.forwardRef< 109 | React.ElementRef, 110 | React.ComponentPropsWithoutRef 111 | >(({ className, ...props }, ref) => ( 112 | 120 | )) 121 | 122 | CommandItem.displayName = CommandPrimitive.Item.displayName 123 | 124 | const CommandShortcut = ({ 125 | className, 126 | ...props 127 | }: React.HTMLAttributes) => { 128 | return ( 129 | 136 | ) 137 | } 138 | CommandShortcut.displayName = "CommandShortcut" 139 | 140 | export { 141 | Command, 142 | CommandDialog, 143 | CommandInput, 144 | CommandList, 145 | CommandEmpty, 146 | CommandGroup, 147 | CommandItem, 148 | CommandShortcut, 149 | CommandSeparator, 150 | } 151 | -------------------------------------------------------------------------------- /src/components/ui/copy-command-button.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { Copy } from "lucide-react"; 3 | import { toast } from "sonner"; 4 | 5 | interface CopyCommandButtonProps { 6 | command: string; 7 | className?: string; 8 | } 9 | 10 | export function CopyCommandButton({ 11 | command, 12 | className, 13 | }: CopyCommandButtonProps) { 14 | 15 | return ( 16 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { cn } from "@/lib/utils" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { Cross2Icon } from "@radix-ui/react-icons" 6 | import * as React from "react" 7 | 8 | const Dialog = DialogPrimitive.Root 9 | 10 | const DialogTrigger = DialogPrimitive.Trigger 11 | 12 | const DialogPortal = DialogPrimitive.Portal 13 | 14 | const DialogClose = DialogPrimitive.Close 15 | 16 | const DialogOverlay = React.forwardRef< 17 | React.ElementRef, 18 | React.ComponentPropsWithoutRef 19 | >(({ className, ...props }, ref) => ( 20 | 28 | )) 29 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 30 | 31 | const DialogContent = React.forwardRef< 32 | React.ElementRef, 33 | React.ComponentPropsWithoutRef 34 | >(({ className, children, ...props }, ref) => ( 35 | 36 | 37 | 45 | {children} 46 | 47 | 48 | Close 49 | 50 | 51 | 52 | )) 53 | DialogContent.displayName = DialogPrimitive.Content.displayName 54 | 55 | const DialogHeader = ({ 56 | className, 57 | ...props 58 | }: React.HTMLAttributes) => ( 59 |
66 | ) 67 | DialogHeader.displayName = "DialogHeader" 68 | 69 | const DialogFooter = ({ 70 | className, 71 | ...props 72 | }: React.HTMLAttributes) => ( 73 |
80 | ) 81 | DialogFooter.displayName = "DialogFooter" 82 | 83 | const DialogTitle = React.forwardRef< 84 | React.ElementRef, 85 | React.ComponentPropsWithoutRef 86 | >(({ className, ...props }, ref) => ( 87 | 95 | )) 96 | DialogTitle.displayName = DialogPrimitive.Title.displayName 97 | 98 | const DialogDescription = React.forwardRef< 99 | React.ElementRef, 100 | React.ComponentPropsWithoutRef 101 | >(({ className, ...props }, ref) => ( 102 | 107 | )) 108 | DialogDescription.displayName = DialogPrimitive.Description.displayName 109 | 110 | export { 111 | Dialog, DialogClose, 112 | DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger 113 | } 114 | 115 | -------------------------------------------------------------------------------- /src/components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" 3 | import { CheckIcon, ChevronRightIcon, DotFilledIcon } from "@radix-ui/react-icons" 4 | import * as React from "react" 5 | 6 | const DropdownMenu = DropdownMenuPrimitive.Root 7 | 8 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger 9 | 10 | const DropdownMenuGroup = DropdownMenuPrimitive.Group 11 | 12 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal 13 | 14 | const DropdownMenuSub = DropdownMenuPrimitive.Sub 15 | 16 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup 17 | 18 | const DropdownMenuSubTrigger = React.forwardRef< 19 | React.ElementRef, 20 | React.ComponentPropsWithoutRef & { 21 | inset?: boolean 22 | } 23 | >(({ className, inset, children, ...props }, ref) => ( 24 | 33 | {children} 34 | 35 | 36 | )) 37 | DropdownMenuSubTrigger.displayName = 38 | DropdownMenuPrimitive.SubTrigger.displayName 39 | 40 | const DropdownMenuSubContent = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | )) 53 | DropdownMenuSubContent.displayName = 54 | DropdownMenuPrimitive.SubContent.displayName 55 | 56 | const DropdownMenuContent = React.forwardRef< 57 | React.ElementRef, 58 | React.ComponentPropsWithoutRef 59 | >(({ className, sideOffset = 4, ...props }, ref) => ( 60 | 61 | 71 | 72 | )) 73 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName 74 | 75 | const DropdownMenuItem = React.forwardRef< 76 | React.ElementRef, 77 | React.ComponentPropsWithoutRef & { 78 | inset?: boolean 79 | } 80 | >(({ className, inset, ...props }, ref) => ( 81 | svg]:size-4 [&>svg]:shrink-0", 85 | inset && "pl-8", 86 | className 87 | )} 88 | {...props} 89 | /> 90 | )) 91 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName 92 | 93 | const DropdownMenuCheckboxItem = React.forwardRef< 94 | React.ElementRef, 95 | React.ComponentPropsWithoutRef 96 | >(({ className, children, checked, ...props }, ref) => ( 97 | 106 | 107 | 108 | 109 | 110 | 111 | {children} 112 | 113 | )) 114 | DropdownMenuCheckboxItem.displayName = 115 | DropdownMenuPrimitive.CheckboxItem.displayName 116 | 117 | const DropdownMenuRadioItem = React.forwardRef< 118 | React.ElementRef, 119 | React.ComponentPropsWithoutRef 120 | >(({ className, children, ...props }, ref) => ( 121 | 129 | 130 | 131 | 132 | 133 | 134 | {children} 135 | 136 | )) 137 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName 138 | 139 | const DropdownMenuLabel = React.forwardRef< 140 | React.ElementRef, 141 | React.ComponentPropsWithoutRef & { 142 | inset?: boolean 143 | } 144 | >(({ className, inset, ...props }, ref) => ( 145 | 154 | )) 155 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName 156 | 157 | const DropdownMenuSeparator = React.forwardRef< 158 | React.ElementRef, 159 | React.ComponentPropsWithoutRef 160 | >(({ className, ...props }, ref) => ( 161 | 166 | )) 167 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName 168 | 169 | const DropdownMenuShortcut = ({ 170 | className, 171 | ...props 172 | }: React.HTMLAttributes) => { 173 | return ( 174 | 178 | ) 179 | } 180 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut" 181 | 182 | export { 183 | DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, 184 | DropdownMenuShortcut, DropdownMenuSub, 185 | DropdownMenuSubContent, 186 | DropdownMenuSubTrigger, DropdownMenuTrigger 187 | } 188 | 189 | -------------------------------------------------------------------------------- /src/components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | import * as LabelPrimitive from "@radix-ui/react-label" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import * as React from "react" 4 | import { 5 | Controller, 6 | ControllerProps, 7 | FieldPath, 8 | FieldValues, 9 | FormProvider, 10 | useFormContext, 11 | } from "react-hook-form" 12 | 13 | import { Label } from "@/components/ui/label" 14 | import { cn } from "@/lib/utils" 15 | 16 | const Form = FormProvider 17 | 18 | type FormFieldContextValue< 19 | TFieldValues extends FieldValues = FieldValues, 20 | TName extends FieldPath = FieldPath 21 | > = { 22 | name: TName 23 | } 24 | 25 | const FormFieldContext = React.createContext( 26 | {} as FormFieldContextValue 27 | ) 28 | 29 | const FormField = < 30 | TFieldValues extends FieldValues = FieldValues, 31 | TName extends FieldPath = FieldPath 32 | >({ 33 | ...props 34 | }: ControllerProps) => { 35 | return ( 36 | 37 | 38 | 39 | ) 40 | } 41 | 42 | const useFormField = () => { 43 | const fieldContext = React.useContext(FormFieldContext) 44 | const itemContext = React.useContext(FormItemContext) 45 | const { getFieldState, formState } = useFormContext() 46 | 47 | const fieldState = getFieldState(fieldContext.name, formState) 48 | 49 | if (!fieldContext) { 50 | throw new Error("useFormField should be used within ") 51 | } 52 | 53 | const { id } = itemContext 54 | 55 | return { 56 | id, 57 | name: fieldContext.name, 58 | formItemId: `${id}-form-item`, 59 | formDescriptionId: `${id}-form-item-description`, 60 | formMessageId: `${id}-form-item-message`, 61 | ...fieldState, 62 | } 63 | } 64 | 65 | type FormItemContextValue = { 66 | id: string 67 | } 68 | 69 | const FormItemContext = React.createContext( 70 | {} as FormItemContextValue 71 | ) 72 | 73 | const FormItem = React.forwardRef< 74 | HTMLDivElement, 75 | React.HTMLAttributes 76 | >(({ className, ...props }, ref) => { 77 | const id = React.useId() 78 | 79 | return ( 80 | 81 |
82 | 83 | ) 84 | }) 85 | FormItem.displayName = "FormItem" 86 | 87 | const FormLabel = React.forwardRef< 88 | React.ElementRef, 89 | React.ComponentPropsWithoutRef 90 | >(({ className, ...props }, ref) => { 91 | const { error, formItemId } = useFormField() 92 | 93 | return ( 94 |