This commit is contained in:
GW_MC
2026-05-25 03:43:34 +00:00
commit e5cac44ce5
59 changed files with 14461 additions and 0 deletions

78
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1,78 @@
FROM mcr.microsoft.com/devcontainers/typescript-node:24-bookworm
# Arguments for the non-root user (provided by VS Code remote extensions)
ARG USERNAME=node
# Install system packages required by Tauri on Linux and packages for VNC/noVNC
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
git \
ca-certificates \
pkg-config \
libgtk-3-dev \
libgdk-pixbuf2.0-dev \
libwebkit2gtk-4.1-dev \
build-essential \
curl \
wget \
file \
libxdo-dev \
libssl-dev \
libayatana-appindicator3-dev \
libgdk-pixbuf2.0-dev \
librsvg2-dev \
xvfb \
xauth \
dbus-x11 \
xfce4 \
xfce4-goodies \
x11vnc \
websockify \
python3 \
python3-pip \
&& rm -rf /var/lib/apt/lists/*
# Install rustup and the stable toolchain into the non-root user's home
ENV RUSTUP_HOME=/home/${USERNAME}/.rustup \
CARGO_HOME=/home/${USERNAME}/.cargo \
PATH=/home/${USERNAME}/.cargo/bin:/usr/local/cargo/bin:$PATH
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o /tmp/rustup-init.sh \
&& chmod +x /tmp/rustup-init.sh \
&& su - ${USERNAME} -c "/tmp/rustup-init.sh -y" \
&& rm /tmp/rustup-init.sh
# Enable Corepack (for pnpm) and install latest pnpm shim
RUN corepack enable \
&& corepack prepare pnpm@latest --activate
# Install the JS Tauri CLI globally for convenience
RUN npm install -g @tauri-apps/cli
# Switch to the non-root user for user-local installs
USER ${USERNAME}
# Install `just` (task runner) into the user's cargo bin
RUN cargo install --locked just || true
WORKDIR /workspace
# Temporarily switch back to root to install noVNC into /opt (root-owned)
USER root
# clone noVNC so we can proxy VNC over WebSockets (browser access)
RUN git clone https://github.com/novnc/noVNC.git /opt/noVNC \
&& git clone https://github.com/novnc/websockify.git /opt/noVNC/utils/websockify \
&& chown -R ${USERNAME}:${USERNAME} /opt/noVNC
# Ensure novnc scripts are executable
RUN chmod +x /opt/noVNC/utils/novnc_proxy || true
# Return to the non-root user for the rest of the build
USER ${USERNAME}
# copy the VNC start script (added in devcontainer) and make it executable
COPY --chown=${USERNAME}:${USERNAME} start-vnc.sh /workspace/.devcontainer/start-vnc.sh
RUN chmod +x /workspace/.devcontainer/start-vnc.sh
EXPOSE 5900 6080

32
.devcontainer/README.md Normal file
View File

@@ -0,0 +1,32 @@
This devcontainer provides a development environment for the Otter Tauri app.
What it includes
- Node 24 (from base image)
- pnpm (via Corepack)
- Rust (installed with rustup, stable toolchain)
- @tauri-apps/cli (global npm install)
- `just` (installed via `cargo install just`)
- Linux libraries required by Tauri: libwebkit2gtk, libssl, GTK, appindicator, etc.
How to use
1. Open this repository in VS Code.
2. When prompted, reopen in container. If not prompted: Command Palette → Remote-Containers: Reopen in Container.
3. After the container builds, the `postCreateCommand` will run: it enables Corepack, activates `pnpm`, runs `pnpm install` and `cargo fetch`.
Notes
- If you need additional system libraries for your distribution, edit `Dockerfile`.
- Forwarded ports:
- `5173` (Vite dev server)
- `5900` (VNC server - direct VNC connection)
- `6080` (noVNC web interface - access via browser)
- `just` is available in the container; run `just` to execute repository tasks.
VNC / noVNC (Remote Desktop)
- This devcontainer includes a headless X11 environment with XFCE4 desktop.
- To start VNC: Run `just start-vnc` in the terminal
- To stop VNC: Run `just stop-vnc`
- Access the desktop via:
1. **Browser (recommended)**: http://localhost:6080/vnc.html
2. **VNC client**: Connect to `localhost:5900` with password `devpass`
- Default VNC password: `devpass` (set via `VNC_PASSWORD` environment variable)
- If you see "Port 6080 already in use", the noVNC service is already running.

View File

@@ -0,0 +1,41 @@
{
"name": "Otter — Tauri Devcontainer",
"build": {
"dockerfile": "Dockerfile"
},
"features": {
// Add Docker-in-Docker to allow act to run containers
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
// Add act
"ghcr.io/devcontainers-extra/features/act:1": {}
},
"customizations": {
"settings": {
"terminal.integrated.shell.linux": "/bin/bash"
},
"extensions": ["rust-lang.rust-analyzer", "esbenp.prettier-vscode", "dbaeumer.vscode-eslint", "nefrob.vscode-just-syntax"]
},
"postCreateCommand": "corepack enable && corepack prepare pnpm@latest --activate && pnpm install && cd src-tauri && cargo fetch && cd -",
"forwardPorts": [5173, 5900, 6080],
"portsAttributes": {
"5173": {
"label": "Vite Dev Server",
"onAutoForward": "notify"
},
"5900": {
"label": "VNC Server",
"onAutoForward": "silent"
},
"6080": {
"label": "noVNC Web (VNC via Browser)",
"protocol": "http"
}
},
"remoteEnv": { "VNC_PASSWORD": "devpass", "DISPLAY": "${localEnv:DISPLAY}" },
"runArgs": ["-e", "DISPLAY=${localEnv:DISPLAY}"],
"mounts": ["source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached"],
"workspaceFolder": "/workspace",
"remoteUser": "node"
}

View File

@@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -euo pipefail
# Manual wrapper to start the VNC services on demand.
# Use this instead of automatic postStartCommand.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="${SCRIPT_DIR%/.*}/.."
LOGDIR=/tmp
OUT_LOG="$LOGDIR/start-vnc.manual.log"
echo "Starting VNC services (logs -> $OUT_LOG)"
# Ensure underlying start script exists
if [ ! -x "$SCRIPT_DIR/start-vnc.sh" ] && [ -f "$SCRIPT_DIR/start-vnc.sh" ]; then
chmod +x "$SCRIPT_DIR/start-vnc.sh" || true
fi
if [ ! -f "$SCRIPT_DIR/start-vnc.sh" ]; then
echo "Error: start-vnc.sh not found in $SCRIPT_DIR"
exit 1
fi
# Run the original script in the background and stream logs to the terminal
nohup bash "$SCRIPT_DIR/start-vnc.sh" >"$OUT_LOG" 2>&1 &
PID=$!
echo "start-vnc.sh started with PID $PID"
echo "Tailing logs (press Ctrl-C to stop tailing):"
sleep 1
tail -n +1 -f "$OUT_LOG"

View File

@@ -0,0 +1,27 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>noVNC (local-scaling wrapper)</title>
<script>
// Try several possible setting keys used by different noVNC versions
try {
localStorage.setItem('resize', 'scale');
localStorage.setItem('scaleViewport', 'true');
localStorage.setItem('scalingMode', 'local');
localStorage.setItem('preferLocalScaling', 'true');
localStorage.setItem('viewOnly', localStorage.getItem('viewOnly') || 'false');
} catch (e) {
// ignore
}
// Load the original noVNC UI (we will rename the original to vnc.orig.html)
var orig = 'vnc.orig.html' + window.location.search + window.location.hash;
// Use replace so back button doesn't loop
window.location.replace(orig);
</script>
</head>
<body>
<p>Applying local-scaling defaults and loading noVNC...</p>
</body>
</html>

138
.devcontainer/start-vnc.sh Executable file
View File

@@ -0,0 +1,138 @@
#!/usr/bin/env bash
set -euo pipefail
# Start a headless X server, a lightweight desktop, x11vnc and noVNC proxy.
# Runs as the container user (configured via devcontainer.json remoteUser).
VNC_PASS=${VNC_PASSWORD:-devpass}
VNC_DIR="$HOME/.vnc"
mkdir -p "$VNC_DIR"
if [ ! -f "$VNC_DIR/passwd" ]; then
echo "Creating VNC password"
x11vnc -storepasswd "$VNC_PASS" "$VNC_DIR/passwd"
fi
# Prefer using an existing X server (host) when available. Set USE_EXISTING_DISPLAY=true
# to force using the existing DISPLAY; otherwise, if a host DISPLAY is propagated into the
# container (via devcontainer runArgs / mounts), prefer that instead of starting Xvfb.
if [ "${USE_EXISTING_DISPLAY:-}" = "true" ] || { [ -n "${DISPLAY:-}" ] && [ -d "/tmp/.X11-unix" ]; }; then
echo "Using existing X server on ${DISPLAY:-<unset>} (X11 forwarding to host)"
export USE_EXISTING_DISPLAY=true
else
echo "Prefer Xvfb — starting Xvfb :1"
if command -v Xvfb >/dev/null 2>&1; then
# Define virtual screen size once; will try to confirm at runtime
# using xdpyinfo and fall back to these values if xdpyinfo isn't available.
SCREEN_W=1280
SCREEN_H=800
# Use a slightly taller virtual screen so desktop panels/toolbars
# don't cause the noVNC client to show an unexpected area below
# the visible display; use variables so we can clip x11vnc to
# exactly the same region and avoid framebuffer artifacts.
Xvfb :1 -screen 0 "${SCREEN_W}x${SCREEN_H}x24" >/tmp/xvfb.log 2>&1 &
export DISPLAY=:1
else
echo "Warning: Xvfb not available; expecting an X server on DISPLAY"
fi
fi
sleep 1
echo "Starting desktop session (xfce)"
# startxfce4 backgrounds itself; if not available fall back to openbox
if command -v startxfce4 >/dev/null 2>&1; then
startxfce4 >/tmp/xfce4.log 2>&1 &
else
if command -v openbox-session >/dev/null 2>&1; then
openbox-session >/tmp/openbox.log 2>&1 &
fi
fi
sleep 1
echo "Starting x11vnc on ${DISPLAY:-:1}"
if [ "${USE_EXISTING_DISPLAY:-}" != "true" ]; then
# Query the actual display size (xdpyinfo) to compute a precise clip region
# and avoid sending framebuffer rows outside the visible desktop. Disable
# client-side caching which can sometimes produce visual artifacts.
GEOM=""
if command -v xdpyinfo >/dev/null 2>&1; then
GEOM=$(xdpyinfo -display "${DISPLAY:-:1}" 2>/dev/null | awk '/dimensions:/ {print $2}') || true
fi
if [ -n "$GEOM" ]; then
# GEOM looks like WxH (e.g. 1280x800)
W=${GEOM%x*}
H=${GEOM#*x}
else
W=${SCREEN_W}
H=${SCREEN_H}
fi
echo "Starting x11vnc with clip ${W}x${H}+0+0 (display ${DISPLAY:-:1})"
x11vnc -display "${DISPLAY:-:1}" -rfbauth "$VNC_DIR/passwd" -forever -shared -rfbport 5900 -clip ${W}x${H}+0+0 -bg || true
else
echo "USE_EXISTING_DISPLAY=true — skipping x11vnc (display forwarded to host)"
fi
sleep 1
# Locate noVNC installation. Prefer /opt/noVNC, then workspace/noVNC.
NOVNC_DIR=""
if [ -d /opt/noVNC ]; then
NOVNC_DIR="/opt/noVNC"
elif [ -d "$PWD/noVNC" ]; then
NOVNC_DIR="$PWD/noVNC"
# Ensure it's available under /opt for tools that expect it there
if [ ! -e /opt/noVNC ]; then
mkdir -p /opt
ln -s "$NOVNC_DIR" /opt/noVNC || true
fi
fi
# Start the noVNC proxy only when we are running an internal VNC server on port 5900
if [ -n "$NOVNC_DIR" ] && [ "${USE_EXISTING_DISPLAY:-}" != "true" ]; then
echo "Found noVNC at $NOVNC_DIR"
# If a wrapper exists in the devcontainer, install it into the noVNC web
# root so we can set localStorage defaults (e.g. prefer local scaling)
WRAPPER="${SCRIPT_DIR:-$(pwd)}/novnc/vnc-wrapper.html"
if [ -f "$WRAPPER" ]; then
# Backup original vnc.html once and replace with wrapper
if [ -f "$NOVNC_DIR/vnc.html" ] && [ ! -f "$NOVNC_DIR/vnc.orig.html" ]; then
echo "Backing up original noVNC vnc.html to vnc.orig.html and installing wrapper"
cp "$NOVNC_DIR/vnc.html" "$NOVNC_DIR/vnc.orig.html" || true
cp "$WRAPPER" "$NOVNC_DIR/vnc.html" || true
fi
fi
echo "Waiting for VNC server on localhost:5900..."
ATTEMPTS=0
until ss -ltnp | grep -q ':5900' || [ $ATTEMPTS -ge 10 ]; do
ATTEMPTS=$((ATTEMPTS+1))
sleep 1
done
if ss -ltnp | grep -q ':5900'; then
# If port 6080 is already bound, report the owner and skip starting a new proxy.
if ss -ltnp | grep -q ':6080'; then
echo "Port 6080 already in use. Current listener:"
ss -ltnp | egrep ':6080' || true
echo "Skipping starting another noVNC proxy to avoid address-in-use errors."
else
echo "Starting noVNC proxy on port 6080"
# Prefer the bundled novnc_proxy script; pass explicit --web and bind address
NOVNC_PROXY="$NOVNC_DIR/utils/novnc_proxy"
if [ -x "$NOVNC_PROXY" ]; then
nohup "$NOVNC_PROXY" --vnc localhost:5900 --listen 0.0.0.0:6080 --web "$NOVNC_DIR" >/tmp/novnc.log 2>&1 &
else
nohup python3 "$NOVNC_DIR/utils/novnc_proxy" --vnc localhost:5900 --listen 0.0.0.0:6080 --web "$NOVNC_DIR" >/tmp/novnc.log 2>&1 &
fi
fi
else
echo "VNC server not listening on :5900, skipping noVNC startup. See logs for details."
fi
else
if [ "${USE_EXISTING_DISPLAY:-}" = "true" ]; then
echo "Using host X11 forwarding; skipping noVNC/websockify startup."
fi
fi
echo "VNC service started. Connect to :5900 or open http://<host>:6080/vnc.html"

37
.github/Dockerfile vendored Normal file
View File

@@ -0,0 +1,37 @@
# This Dockerfile sets up the environment for ci.
# requires at least bookworm for the required versions of webkit2gtk and gtk3
FROM node:24-bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
ca-certificates \
pkg-config \
libgtk-3-dev \
libgdk-pixbuf2.0-dev \
libwebkit2gtk-4.1-dev \
build-essential \
curl \
wget \
file \
libxdo-dev \
libssl-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
libgdk-pixbuf2.0-dev \
xfce4 \
xfce4-goodies \
&& rm -rf /var/lib/apt/lists/*
# Install rustup and the stable toolchain
ENV RUSTUP_HOME=/root/.rustup \
CARGO_HOME=/root/.cargo \
PATH=/root/.cargo/bin:/usr/local/cargo/bin:$PATH
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o /tmp/rustup-init.sh \
&& chmod +x /tmp/rustup-init.sh \
&& /tmp/rustup-init.sh -y \
&& rm /tmp/rustup-init.sh
# Enable Corepack (for pnpm) and install latest pnpm shim
RUN corepack enable \
&& corepack prepare pnpm@latest --activate

66
.github/actions/setup-rust/action.yml vendored Normal file
View File

@@ -0,0 +1,66 @@
name: 'Setup Rust environment'
description: 'Composite action to checkout the repo, restore cargo caches and set up the Rust toolchain. Use this from job steps to keep setup DRY across jobs.'
inputs:
toolchain:
description: 'Rust toolchain to install'
required: false
default: 'stable'
override:
description: 'Whether to override the default toolchain'
required: false
default: 'true'
components:
description: 'Comma-separated list of additional rust components to install'
required: false
default: 'clippy, rustfmt'
runs:
using: 'composite'
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Cache cargo registry
uses: actions/cache@v4
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v4
with:
path: ~/.cargo/index
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Sanitize components input
shell: bash
run: echo "SANITIZED_COMPONENTS=${{ inputs.components }}" | sed -E 's/, ?| /-/g' >> $GITHUB_ENV
- name: Cache Rust toolchain
uses: actions/cache@v4
with:
path: ~/.rustup
# Key includes the OS and the toolchain version (e.g., 'stable')
key: ${{ runner.os }}-rustup-${{ hashFiles('rust-toolchain.toml') }}-v1-${{ inputs.toolchain }}-${{ env.SANITIZED_COMPONENTS }}
restore-keys: |
${{ runner.os }}-rustup-
- name: Cache cargo build (target)
uses: actions/cache@v4
with:
path: target
key: ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }}
- name: Set up rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ inputs.toolchain }}
override: ${{ inputs.override }}
components: ${{ inputs.components }}

61
.github/workflows/lint.yml vendored Normal file
View File

@@ -0,0 +1,61 @@
name: Lint
on:
push:
branches:
- master
pull_request:
branches:
- master
workflow_dispatch:
jobs:
lint-crates:
runs-on: ubuntu-latest
container:
image: gitea.gwmc.dev/otter/otter-ci:latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Rust, checkout and restore caches
uses: ./.github/actions/setup-rust
with:
components: clippy, rustfmt
- name: Run clippy
run: cargo clippy --all-features
- name: Check code formatting
run: |
cd src-tauri
cargo fmt -- --check
lint-frontend:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- uses: pnpm/action-setup@v4
with:
version: 10
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: 'pnpm'
cache-dependency-path: pnpm-lock.yaml
- name: Install dependencies
run: |
pnpm install
- name: Run frontend linter
run: |
pnpm lint

30
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Test
on:
push:
branches:
- master
pull_request:
branches:
- master
workflow_dispatch:
jobs:
test-crates:
runs-on: ubuntu-latest
container:
image: gitea.gwmc.dev/otter/otter-ci:latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Rust, checkout and restore caches
uses: ./.github/actions/setup-rust
- name: Run tests
run: |
cd src-tauri
cargo test --all-features

30
.gitignore vendored Normal file
View File

@@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
#pnpm
.pnpm-store
# target
target/

3
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
}

7037
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

15
Cargo.toml Normal file
View File

@@ -0,0 +1,15 @@
[workspace]
members = ["crates/*", "src-tauri"]
resolver = "2"
[workspace.package]
edition = "2021"
rust-version = "1.85.0"
[workspace.dependencies]
sea-orm = { version = "^2.0.0-rc", features = ["sqlx-sqlite", "runtime-tokio-native-tls"] }
sea-orm-migration = { version = "^2.0.0-rc", features = ["runtime-tokio-native-tls", "sqlx-sqlite"] }
tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] }
chrono = { version = "0.4.43", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

7
README.md Normal file
View File

@@ -0,0 +1,7 @@
# Tauri + React + Typescript
This template should help get you started developing with Tauri, React and Typescript in Vite.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)

View File

@@ -0,0 +1,18 @@
[package]
name = "migration"
version = "0.1.0"
edition = "2021"
rust-version = "1.85.0"
publish = false
[lib]
name = "migration"
path = "src/lib.rs"
[[bin]]
name = "migration-cli"
path = "src/main.rs"
[dependencies]
tokio = { workspace = true }
sea-orm-migration = { workspace = true }

View File

@@ -0,0 +1,11 @@
pub use sea_orm_migration::prelude::*;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![]
}
}

View File

@@ -0,0 +1,6 @@
use sea_orm_migration::prelude::*;
#[tokio::main]
async fn main() {
cli::run_cli(migration::Migrator).await;
}

14
index.html Normal file
View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri + React + Typescript</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

57
justfile Normal file
View File

@@ -0,0 +1,57 @@
default:
@just --list
act:
act --pull=false
init:
cargo install sea-orm-cli@^2.0.0-rc
db-migrate:
cd crates && \
rm temp.db || true && \
touch temp.db && \
sea-orm-cli migrate -u sqlite://temp.db && \
sea-orm-cli generate entity \
-u sqlite://temp.db \
-o ../src-tauri/src/db/entities \
--with-serde both \
--date-time-crate chrono \
--with-prelude all-allow-unused-imports \
--model-extra-attributes 'allow(dead_code)' \
--enum-extra-attributes 'allow(dead_code)'
rm crates/temp.db
start-vnc:
@echo "Run the manual VNC start script"
bash .devcontainer/manual-start-vnc.sh
stop-vnc:
@echo "Stop x11vnc and novnc proxy"
pkill -f x11vnc || true
pkill -f novnc_proxy || true
dev DISPLAY='1':
# Note: this assumes that the VNC server is running and accessible at DISPLAY=:1
# Check the start-vnc output for the correct DISPLAY value if you have multiple VNC sessions running
DISPLAY=:{{DISPLAY}} pnpm tauri dev
test-cargo-integration:
cargo test \
--package otter integration_tests \
-- --ignored
# docker images for ci
DOCKER_IMAGE_NAME := 'gitea.gwmc.dev/otter/otter-ci:latest'
build-ci-image:
docker build -t {{DOCKER_IMAGE_NAME}} -f .github/Dockerfile .
push-ci-image:
docker push {{DOCKER_IMAGE_NAME}}
update-ci-image: build-ci-image push-ci-image
pull-ci-image:
docker pull {{DOCKER_IMAGE_NAME}}

26
package.json Normal file
View File

@@ -0,0 +1,26 @@
{
"name": "otter",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2"
},
"devDependencies": {
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.6.0",
"typescript": "~5.8.3",
"vite": "^7.0.4",
"@tauri-apps/cli": "^2"
}
}

1260
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

4
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,4 @@
allowBuilds:
esbuild: true
onlyBuiltDependencies:
- esbuild

0
public/.gitkeep Normal file
View File

7
src-tauri/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas

5082
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

25
src-tauri/Cargo.toml Normal file
View File

@@ -0,0 +1,25 @@
[package]
name = "otter"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "otter_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

3
src-tauri/build.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@@ -0,0 +1,10 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"opener:default"
]
}

0
src-tauri/icons/.gitkeep Normal file
View File

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,3 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0
pub mod prelude;

View File

@@ -0,0 +1,3 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0
#![allow(unused_imports)]

14
src-tauri/src/lib.rs Normal file
View File

@@ -0,0 +1,14 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

6
src-tauri/src/main.rs Normal file
View File

@@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
otter_lib::run()
}

35
src-tauri/tauri.conf.json Normal file
View File

@@ -0,0 +1,35 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "otter",
"version": "0.1.0",
"identifier": "dev.gwmc.otter",
"build": {
"beforeDevCommand": "pnpm dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "pnpm build",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "otter",
"width": 800,
"height": 600
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}

116
src/App.css Normal file
View File

@@ -0,0 +1,116 @@
.logo.vite:hover {
filter: drop-shadow(0 0 2em #747bff);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafb);
}
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color: #0f0f0f;
background-color: #f6f6f6;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
.container {
margin: 0;
padding-top: 10vh;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: 0.75s;
}
.logo.tauri:hover {
filter: drop-shadow(0 0 2em #24c8db);
}
.row {
display: flex;
justify-content: center;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
h1 {
text-align: center;
}
input,
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
color: #0f0f0f;
background-color: #ffffff;
transition: border-color 0.25s;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
}
button {
cursor: pointer;
}
button:hover {
border-color: #396cd8;
}
button:active {
border-color: #396cd8;
background-color: #e8e8e8;
}
input,
button {
outline: none;
}
#greet-input {
margin-right: 5px;
}
@media (prefers-color-scheme: dark) {
:root {
color: #f6f6f6;
background-color: #2f2f2f;
}
a:hover {
color: #24c8db;
}
input,
button {
color: #ffffff;
background-color: #0f0f0f98;
}
button:active {
background-color: #0f0f0f69;
}
}

51
src/App.tsx Normal file
View File

@@ -0,0 +1,51 @@
import { useState } from "react";
// import reactLogo from "./assets/react.svg";
import { invoke } from "@tauri-apps/api/core";
import "./App.css";
function App() {
const [greetMsg, setGreetMsg] = useState("");
const [name, setName] = useState("");
async function greet() {
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
setGreetMsg(await invoke("greet", { name }));
}
return (
<main className="container">
<h1>Welcome to Tauri + React</h1>
<div className="row">
<a href="https://vite.dev" target="_blank">
<img src="/vite.svg" className="logo vite" alt="Vite logo" />
</a>
<a href="https://tauri.app" target="_blank">
<img src="/tauri.svg" className="logo tauri" alt="Tauri logo" />
</a>
<a href="https://react.dev" target="_blank">
{/* <img src={reactLogo} className="logo react" alt="React logo" /> */}
</a>
</div>
<p>Click on the Tauri, Vite, and React logos to learn more.</p>
<form
className="row"
onSubmit={(e) => {
e.preventDefault();
greet();
}}
>
<input
id="greet-input"
onChange={(e) => setName(e.currentTarget.value)}
placeholder="Enter a name..."
/>
<button type="submit">Greet</button>
</form>
<p>{greetMsg}</p>
</main>
);
}
export default App;

0
src/assets/.gitkeep Normal file
View File

9
src/main.tsx Normal file
View File

@@ -0,0 +1,9 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);

1
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

25
tsconfig.json Normal file
View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

10
tsconfig.node.json Normal file
View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

32
vite.config.ts Normal file
View File

@@ -0,0 +1,32 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// @ts-expect-error process is a nodejs global
const host = process.env.TAURI_DEV_HOST;
// https://vite.dev/config/
export default defineConfig(async () => ({
plugins: [react()],
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
//
// 1. prevent Vite from obscuring rust errors
clearScreen: false,
// 2. tauri expects a fixed port, fail if that port is not available
server: {
port: 1420,
strictPort: true,
host: host || false,
hmr: host
? {
protocol: "ws",
host,
port: 1421,
}
: undefined,
watch: {
// 3. tell Vite to ignore watching `src-tauri`
ignored: ["**/src-tauri/**"],
},
},
}));