Initial commit: Gitpub Desktop scaffold

Add complete project scaffold for Gitpub Desktop (Tauri + Rust backend and vanilla HTML/CSS/JS frontend). Includes frontend entry (index.html), styles (base/components CSS), app logic and modules (app.js, gitea-api.js, tauri-api.js, state.js, storage.js), static assets and component READMEs, README.md, VSCode recommendations, and project config (package.json, package-lock.json). Also adds src-tauri skeleton (Cargo.toml, main.rs, lib.rs, build.rs, tauri.conf.json), application icons, and .gitignore files. Provides MVP plumbing for server setup and management, repository dashboard, local repo opening, and Git operations via Tauri invoke (clone/pull/push/status/branch).
This commit is contained in:
2026-05-07 14:41:15 +12:00
commit 6b245c628c
44 changed files with 7105 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
# 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?
+3
View File
@@ -0,0 +1,3 @@
{
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
}
+38
View File
@@ -0,0 +1,38 @@
# Gitpub Desktop
Gitpub Desktop is a lightweight, cross-platform Git client built with Tauri, Rust, and plain HTML/CSS/JavaScript.
It connects to any compatible self-hosted Gitea instance (including Gitpub) without hardcoding a single backend.
## Stack
- Tauri + Rust backend commands
- Vanilla HTML/CSS/JS frontend (no framework)
- System-installed Git for repository operations
- Gitea REST API for server-side repository listing and account context
## Project Structure
- `src-tauri/` Rust backend and Tauri application setup
- `frontend/` vanilla frontend shell
- `frontend/assets/` static assets
- `frontend/css/` UI styles
- `frontend/js/` application modules
- `frontend/components/` shared component/template area
## MVP Included
- First-launch setup flow for adding first Gitea server
- Multi-server backend management (add/edit/remove/switch/default)
- Dynamic API base URL generation (`<server>/api/v1`)
- Connection testing against Gitea API
- Repository dashboard with search and mock cards fallback
- Local repository open + recent repositories
- Rust Git commands: clone, pull, push, status, branch
- Settings for git path, clone directory, and theme placeholder
## Run
```bash
npm install
npm run tauri:dev
```
+3
View File
@@ -0,0 +1,3 @@
# Assets
Place Gitpub Desktop icons, logos, and static image assets in this directory.
+5
View File
@@ -0,0 +1,5 @@
# Frontend Components
This directory is reserved for reusable HTML component templates and small render helpers.
For the MVP, the app renders component markup from JavaScript modules in `frontend/js`.
+78
View File
@@ -0,0 +1,78 @@
:root {
--bg-app: #0b1018;
--bg-panel: #111a27;
--bg-panel-alt: #172234;
--bg-hover: #1c2a3f;
--border: #253349;
--text-main: #e5edf7;
--text-muted: #94a7c0;
--accent: #4f9dff;
--accent-strong: #2f85f4;
--success: #41c48f;
--danger: #ef6a6a;
--radius-md: 10px;
--radius-lg: 14px;
--shadow: 0 14px 40px rgba(5, 9, 16, 0.45);
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle at top right, #172a47 0%, var(--bg-app) 55%);
color: var(--text-main);
font-family: "Inter", "Segoe UI", system-ui, -apple-system, sans-serif;
}
button,
input,
select,
textarea {
font: inherit;
}
button {
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--bg-panel-alt);
color: var(--text-main);
padding: 8px 12px;
cursor: pointer;
transition: all 0.15s ease;
}
button:hover {
transform: translateY(-1px);
border-color: #3b4f6e;
}
button.primary {
background: linear-gradient(180deg, #5ca7ff 0%, var(--accent-strong) 100%);
border-color: #2670d6;
}
button.danger {
border-color: #813434;
color: #ffcaca;
}
input,
select,
textarea {
width: 100%;
padding: 10px 12px;
border-radius: var(--radius-md);
border: 1px solid var(--border);
background: #0f1724;
color: var(--text-main);
}
textarea {
min-height: 96px;
resize: vertical;
}
+142
View File
@@ -0,0 +1,142 @@
#app {
width: 100%;
height: 100%;
}
.layout {
display: grid;
grid-template-columns: 260px 1fr 320px;
width: 100%;
height: 100%;
gap: 0;
}
.sidebar,
.rightbar {
background: rgba(13, 21, 34, 0.88);
border-right: 1px solid var(--border);
padding: 14px;
}
.rightbar {
border-right: 0;
border-left: 1px solid var(--border);
}
.main {
padding: 16px;
overflow: auto;
}
.panel {
border: 1px solid var(--border);
border-radius: var(--radius-lg);
background: rgba(16, 26, 41, 0.92);
box-shadow: var(--shadow);
padding: 14px;
}
.stack {
display: flex;
flex-direction: column;
gap: 12px;
}
.row {
display: flex;
gap: 8px;
}
.row.wrap {
flex-wrap: wrap;
}
.row.end {
justify-content: flex-end;
}
.server-chip,
.repo-card {
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 10px;
background: rgba(23, 34, 53, 0.75);
}
.repo-grid {
display: grid;
gap: 10px;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
.muted {
color: var(--text-muted);
}
.label {
font-size: 12px;
color: var(--text-muted);
margin-bottom: 4px;
}
.title {
margin: 0;
font-size: 18px;
}
.subtitle {
margin: 0;
font-size: 13px;
color: var(--text-muted);
}
.hidden {
display: none !important;
}
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.list {
margin: 0;
padding-left: 16px;
}
.welcome-wrap {
min-height: 100%;
display: grid;
place-items: center;
padding: 24px;
}
.welcome-card {
width: min(680px, 100%);
}
.status-ok {
color: var(--success);
}
.status-error {
color: var(--danger);
}
@media (max-width: 1180px) {
.layout {
grid-template-columns: 220px 1fr;
}
.rightbar {
display: none;
}
}
+14
View File
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Gitpub Desktop</title>
<link rel="stylesheet" href="/css/base.css" />
<link rel="stylesheet" href="/css/components.css" />
<script type="module" src="/js/app.js" defer></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
+488
View File
@@ -0,0 +1,488 @@
import { fetchRepositories } from "./gitea-api.js";
import { getActiveServer, getState, setSettings, updateSettings, addRecentRepo } from "./state.js";
import {
runGitBranch,
runGitClone,
runGitPull,
runGitPush,
runGitStatus,
testGiteaConnection,
} from "./tauri-api.js";
const appRoot = document.getElementById("app");
const mockRepos = [
{ id: 1, full_name: "gitpub-desktop/client-ui", private: false, clone_url: "https://example.com/client-ui.git", updated_at: "today" },
{ id: 2, full_name: "gitpub-desktop/git-core", private: true, clone_url: "https://example.com/git-core.git", updated_at: "yesterday" },
{ id: 3, full_name: "gitpub-desktop/docs", private: false, clone_url: "https://example.com/docs.git", updated_at: "2 days ago" },
];
let repositories = [...mockRepos];
let serverTestResult = "";
let settingsNotice = "";
let gitOutput = "";
function uid() {
return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
}
function escapeHtml(value = "") {
return value
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
function serverFormTemplate(server = null) {
// Reused in first-launch setup and in Settings server management.
const defaults = {
id: "",
displayName: "",
serverUrl: "",
authMethod: "token",
token: "",
username: "",
password: "",
};
const config = { ...defaults, ...(server || {}) };
return `
<div class="stack panel" id="server-form-card">
<h3 class="title">${server ? "Edit Server" : "Add Server"}</h3>
<input type="hidden" name="id" value="${escapeHtml(config.id)}" />
<div>
<div class="label">Display name</div>
<input name="displayName" value="${escapeHtml(config.displayName)}" placeholder="Gitpub Main" />
</div>
<div>
<div class="label">Gitea server URL</div>
<input name="serverUrl" value="${escapeHtml(config.serverUrl)}" placeholder="https://git.example.com" />
</div>
<div>
<div class="label">Auth method</div>
<select name="authMethod">
<option value="token" ${config.authMethod === "token" ? "selected" : ""}>Access Token</option>
<option value="password" ${config.authMethod === "password" ? "selected" : ""}>Username / Password</option>
</select>
</div>
<div>
<div class="label">Access token</div>
<input name="token" value="${escapeHtml(config.token)}" placeholder="gitea token" />
</div>
<div class="row">
<div style="flex: 1">
<div class="label">Username</div>
<input name="username" value="${escapeHtml(config.username)}" placeholder="username" />
</div>
<div style="flex: 1">
<div class="label">Password</div>
<input type="password" name="password" value="${escapeHtml(config.password)}" placeholder="password" />
</div>
</div>
<div class="row wrap">
<button id="test-server-btn" type="button">Test connection</button>
<button id="save-server-btn" class="primary" type="button">Save server</button>
</div>
<div class="muted">${escapeHtml(serverTestResult || settingsNotice)}</div>
</div>
`;
}
function welcomeView() {
appRoot.innerHTML = `
<div class="welcome-wrap">
<div class="welcome-card stack">
<div class="panel stack">
<h1 class="title">Welcome to Gitpub Desktop</h1>
<p class="subtitle">
Connect your first Gitea backend. Gitpub Desktop works with any compatible Gitea server.
</p>
${serverFormTemplate(null)}
</div>
</div>
</div>
`;
bindServerFormEvents();
}
function filteredRepositories() {
const searchTerm = getState().repoSearch.trim().toLowerCase();
if (!searchTerm) return repositories;
return repositories.filter((repo) => repo.full_name.toLowerCase().includes(searchTerm));
}
function dashboardView() {
const state = getState();
const activeServer = getActiveServer();
const visibleRepos = filteredRepositories();
appRoot.innerHTML = `
<div class="layout">
<aside class="sidebar stack">
<div>
<h2 class="title">Gitpub Desktop</h2>
<p class="subtitle">Lightweight Gitea Git client</p>
</div>
<div class="server-chip">
<div class="label">Active backend</div>
<div>${escapeHtml(activeServer?.displayName || "None")}</div>
<div class="muted">${escapeHtml(activeServer?.serverUrl || "Not configured")}</div>
</div>
<button id="refresh-repos-btn" type="button">Refresh repositories</button>
<button id="open-settings-btn" type="button">Settings</button>
<div class="panel">
<div class="label">Recent local repositories</div>
<ul class="list">
${state.settings.recentRepositories.length
? state.settings.recentRepositories.map((path) => `<li>${escapeHtml(path)}</li>`).join("")
: "<li class='muted'>No repositories opened yet.</li>"}
</ul>
</div>
</aside>
<main class="main stack">
<div class="app-header">
<div>
<h2 class="title">Repository Dashboard</h2>
<p class="subtitle">Browse, clone, and open repositories across multiple Gitea backends.</p>
</div>
<div class="row">
<input id="repo-search-input" placeholder="Search repositories..." value="${escapeHtml(state.repoSearch)}" />
</div>
</div>
<section class="panel stack">
<div class="section-header">
<h3 class="title">Repositories</h3>
<span class="muted">${visibleRepos.length} shown</span>
</div>
<div class="repo-grid">
${visibleRepos
.map(
(repo) => `
<article class="repo-card stack">
<div><strong>${escapeHtml(repo.full_name)}</strong></div>
<div class="muted">${repo.private ? "Private" : "Public"} • Updated ${escapeHtml(repo.updated_at || "recently")}</div>
<div class="row">
<button class="clone-repo-btn" data-clone-url="${escapeHtml(repo.clone_url || "")}" type="button">Clone</button>
<button class="open-repo-btn" data-repo-name="${escapeHtml(repo.full_name)}" type="button">Open</button>
</div>
</article>
`
)
.join("")}
</div>
</section>
<section class="panel stack">
<h3 class="title">Repository View</h3>
<div class="row">
<input id="local-repo-path-input" placeholder="Local repository path" value="${escapeHtml(state.localRepoPathInput)}" />
<button id="open-local-repo-btn" type="button">Open Local Repo</button>
</div>
<div class="muted">Current repository: ${escapeHtml(state.selectedRepoName || "None selected")}</div>
<div class="row wrap">
<button id="git-status-btn" type="button">Status</button>
<button id="git-branch-btn" type="button">Branch</button>
<button id="git-pull-btn" type="button">Pull</button>
<button id="git-push-btn" type="button">Push</button>
</div>
<div>
<div class="label">Commit message (MVP placeholder)</div>
<textarea id="commit-message-input" placeholder="Write commit message...">${escapeHtml(state.commitMessage)}</textarea>
<button type="button" disabled title="Commit command will be added in next milestone">Commit (next milestone)</button>
</div>
<pre class="repo-card">${escapeHtml(gitOutput || "Run a git action to see output.")}</pre>
</section>
</main>
<aside class="rightbar stack">
<section class="panel stack">
<h3 class="title">Clone Repository</h3>
<input id="clone-url-input" placeholder="https://git.example.com/org/repo.git" value="${escapeHtml(state.cloneUrlInput)}" />
<input id="clone-destination-input" placeholder="/Users/me/code/repo" value="${escapeHtml(state.cloneDestinationInput || state.settings.defaultCloneDirectory)}" />
<button id="clone-btn" class="primary" type="button">Clone</button>
<span class="muted">Uses system Git executable from settings.</span>
</section>
<section class="panel stack">
<h3 class="title">Settings</h3>
<div class="label">Theme</div>
<select id="theme-select">
<option value="dark" ${state.settings.theme === "dark" ? "selected" : ""}>Dark (default)</option>
</select>
<div class="label">Git executable path</div>
<input id="git-path-input" value="${escapeHtml(state.settings.gitExecutablePath)}" placeholder="git or /usr/bin/git" />
<div class="label">Default clone directory</div>
<input id="default-clone-dir-input" value="${escapeHtml(state.settings.defaultCloneDirectory)}" placeholder="/Users/me/code" />
<button id="save-basic-settings-btn" type="button">Save basic settings</button>
</section>
<section class="panel stack">
<h3 class="title">Server Management</h3>
<div class="stack">
${state.settings.servers
.map(
(server) => `
<div class="server-chip stack">
<strong>${escapeHtml(server.displayName)}</strong>
<span class="muted">${escapeHtml(server.serverUrl)}</span>
<div class="row wrap">
<button class="set-default-server-btn" data-id="${server.id}" type="button">Set Default</button>
<button class="edit-server-btn" data-id="${server.id}" type="button">Edit</button>
<button class="delete-server-btn danger" data-id="${server.id}" type="button">Remove</button>
</div>
</div>
`
)
.join("")}
</div>
<button id="add-server-btn" type="button">Add new server</button>
<div id="server-form-slot" class="stack"></div>
<div class="muted">${escapeHtml(settingsNotice)}</div>
</section>
</aside>
</div>
`;
bindDashboardEvents();
}
function bindServerFormEvents(existingServer = null) {
const testButton = document.getElementById("test-server-btn");
const saveButton = document.getElementById("save-server-btn");
const formCard = document.getElementById("server-form-card");
if (!testButton || !saveButton || !formCard) return;
const collectPayload = () => {
const get = (name) => formCard.querySelector(`[name="${name}"]`)?.value?.trim() || "";
return {
id: get("id"),
displayName: get("displayName"),
serverUrl: get("serverUrl"),
authMethod: get("authMethod") || "token",
token: get("token"),
username: get("username"),
password: get("password"),
};
};
testButton.addEventListener("click", async () => {
const payload = collectPayload();
try {
const result = await testGiteaConnection(payload);
serverTestResult = `${result.ok ? "Connected" : "Failed"}: ${result.message} (${result.apiBaseUrl})`;
render();
if (existingServer) {
openServerForm(existingServer.id);
}
} catch (error) {
serverTestResult = `Connection test failed: ${error.message}`;
render();
if (existingServer) {
openServerForm(existingServer.id);
}
}
});
saveButton.addEventListener("click", () => {
const payload = collectPayload();
if (!payload.displayName || !payload.serverUrl) {
settingsNotice = "Display name and server URL are required.";
render();
if (existingServer) openServerForm(existingServer.id);
return;
}
const state = getState();
let nextServers = [...state.settings.servers];
const nextId = payload.id || uid();
const serverData = { ...payload, id: nextId };
const existingIndex = nextServers.findIndex((item) => item.id === nextId);
if (existingIndex >= 0) {
nextServers[existingIndex] = serverData;
settingsNotice = `Updated server ${payload.displayName}.`;
} else {
nextServers.push(serverData);
settingsNotice = `Added server ${payload.displayName}.`;
}
const activeServerId = state.settings.activeServerId || nextId;
setSettings({ ...state.settings, servers: nextServers, activeServerId });
serverTestResult = "";
render();
});
}
function openServerForm(serverId = null) {
const slot = document.getElementById("server-form-slot");
if (!slot) return;
const server = getState().settings.servers.find((item) => item.id === serverId) || null;
slot.innerHTML = serverFormTemplate(server);
bindServerFormEvents(server);
}
async function loadRepositories() {
const activeServer = getActiveServer();
if (!activeServer) {
// Empty setup defaults to mock cards so UI can be tested immediately.
repositories = [...mockRepos];
return;
}
try {
repositories = await fetchRepositories(activeServer);
} catch (error) {
settingsNotice = `Using mock repositories. API fetch failed: ${error.message}`;
repositories = [...mockRepos];
}
}
async function runRepoCommand(actionName, runner) {
const state = getState();
if (!state.selectedRepoPath) {
gitOutput = "Select or enter a local repository path first.";
render();
return;
}
try {
const result = await runner();
gitOutput = `${actionName}: ${result.command}\n\n${result.stdout || "(no stdout)"}\n${result.stderr ? `\n${result.stderr}` : ""}`;
} catch (error) {
gitOutput = `${actionName} failed: ${error.message}`;
}
render();
}
function bindDashboardEvents() {
const state = getState();
document.getElementById("repo-search-input")?.addEventListener("input", (event) => {
state.repoSearch = event.target.value;
render();
});
document.getElementById("refresh-repos-btn")?.addEventListener("click", async () => {
await loadRepositories();
render();
});
document.getElementById("open-settings-btn")?.addEventListener("click", () => {
const panel = document.getElementById("server-form-slot");
if (panel) panel.scrollIntoView({ behavior: "smooth", block: "start" });
});
document.querySelectorAll(".open-repo-btn").forEach((button) => {
button.addEventListener("click", () => {
state.selectedRepoName = button.dataset.repoName || "Repository";
render();
});
});
document.querySelectorAll(".clone-repo-btn").forEach((button) => {
button.addEventListener("click", () => {
state.cloneUrlInput = button.dataset.cloneUrl || "";
render();
});
});
document.getElementById("open-local-repo-btn")?.addEventListener("click", () => {
// MVP local detection is user-driven path entry; folder picker can be added next.
const value = document.getElementById("local-repo-path-input")?.value?.trim() || "";
state.localRepoPathInput = value;
state.selectedRepoPath = value;
state.selectedRepoName = value.split("/").filter(Boolean).pop() || value;
addRecentRepo(value);
render();
});
document.getElementById("clone-btn")?.addEventListener("click", async () => {
state.cloneUrlInput = document.getElementById("clone-url-input")?.value?.trim() || "";
state.cloneDestinationInput = document.getElementById("clone-destination-input")?.value?.trim() || "";
if (!state.cloneUrlInput || !state.cloneDestinationInput) {
gitOutput = "Clone URL and destination path are required.";
render();
return;
}
try {
const result = await runGitClone(
state.cloneUrlInput,
state.cloneDestinationInput,
state.settings.gitExecutablePath
);
gitOutput = `${result.command}\n\n${result.stdout || "Clone completed."}\n${result.stderr || ""}`;
addRecentRepo(state.cloneDestinationInput);
} catch (error) {
gitOutput = `Clone failed: ${error.message}`;
}
render();
});
document.getElementById("git-status-btn")?.addEventListener("click", () => {
runRepoCommand("Status", () => runGitStatus(state.selectedRepoPath, state.settings.gitExecutablePath));
});
document.getElementById("git-branch-btn")?.addEventListener("click", () => {
runRepoCommand("Branch", () => runGitBranch(state.selectedRepoPath, state.settings.gitExecutablePath));
});
document.getElementById("git-pull-btn")?.addEventListener("click", () => {
runRepoCommand("Pull", () => runGitPull(state.selectedRepoPath, state.settings.gitExecutablePath));
});
document.getElementById("git-push-btn")?.addEventListener("click", () => {
runRepoCommand("Push", () => runGitPush(state.selectedRepoPath, state.settings.gitExecutablePath));
});
document.getElementById("save-basic-settings-btn")?.addEventListener("click", () => {
updateSettings({
theme: document.getElementById("theme-select")?.value || "dark",
gitExecutablePath: document.getElementById("git-path-input")?.value?.trim() || "",
defaultCloneDirectory: document.getElementById("default-clone-dir-input")?.value?.trim() || "",
});
settingsNotice = "Saved basic settings.";
render();
});
document.getElementById("add-server-btn")?.addEventListener("click", () => openServerForm());
document.querySelectorAll(".set-default-server-btn").forEach((button) => {
button.addEventListener("click", async () => {
updateSettings({ activeServerId: button.dataset.id });
settingsNotice = "Default server updated.";
await loadRepositories();
render();
});
});
document.querySelectorAll(".delete-server-btn").forEach((button) => {
button.addEventListener("click", () => {
const nextServers = getState().settings.servers.filter((server) => server.id !== button.dataset.id);
const nextActive =
getState().settings.activeServerId === button.dataset.id ? nextServers[0]?.id || null : getState().settings.activeServerId;
setSettings({ ...getState().settings, servers: nextServers, activeServerId: nextActive });
settingsNotice = "Server removed.";
render();
});
});
document.querySelectorAll(".edit-server-btn").forEach((button) => {
button.addEventListener("click", () => openServerForm(button.dataset.id));
});
}
function render() {
if (!getState().settings.servers.length) {
// First launch always lands in setup until at least one backend is saved.
welcomeView();
return;
}
dashboardView();
}
window.addEventListener("DOMContentLoaded", async () => {
await loadRepositories();
render();
});
+41
View File
@@ -0,0 +1,41 @@
function normalizeServerUrl(serverUrl) {
return serverUrl.trim().replace(/\/+$/, "");
}
export function buildApiBaseUrl(serverUrl) {
const normalized = normalizeServerUrl(serverUrl);
return normalized.endsWith("/api/v1") ? normalized : `${normalized}/api/v1`;
}
export function buildHeaders(serverConfig) {
const headers = {
Accept: "application/json",
};
if (serverConfig.authMethod === "token" && serverConfig.token) {
headers.Authorization = `token ${serverConfig.token}`;
} else if (
serverConfig.authMethod === "password" &&
serverConfig.username &&
serverConfig.password
) {
headers.Authorization = `Basic ${btoa(
`${serverConfig.username}:${serverConfig.password}`
)}`;
}
return headers;
}
export async function fetchRepositories(serverConfig, page = 1, limit = 50) {
const apiBase = buildApiBaseUrl(serverConfig.serverUrl);
const url = `${apiBase}/user/repos?page=${page}&limit=${limit}&sort=updated`;
const response = await fetch(url, {
headers: buildHeaders(serverConfig),
});
if (!response.ok) {
throw new Error(`Gitea API error: ${response.status}`);
}
return response.json();
}
+37
View File
@@ -0,0 +1,37 @@
import { loadSettings, saveSettings } from "./storage.js";
const state = {
settings: loadSettings(),
selectedRepoPath: "",
selectedRepoName: "",
repoSearch: "",
localRepoPathInput: "",
cloneUrlInput: "",
cloneDestinationInput: "",
commitMessage: "",
};
export function getState() {
return state;
}
export function setSettings(nextSettings) {
state.settings = nextSettings;
saveSettings(state.settings);
}
export function updateSettings(patch) {
setSettings({ ...state.settings, ...patch });
}
export function addRecentRepo(path) {
if (!path) return;
const current = state.settings.recentRepositories.filter((item) => item !== path);
const next = [path, ...current].slice(0, 15);
updateSettings({ recentRepositories: next });
}
export function getActiveServer() {
const { activeServerId, servers } = state.settings;
return servers.find((server) => server.id === activeServerId) ?? null;
}
+29
View File
@@ -0,0 +1,29 @@
const STORAGE_KEY = "gitpub-desktop-settings-v1";
export function getDefaultSettings() {
return {
theme: "dark",
gitExecutablePath: "",
defaultCloneDirectory: "",
activeServerId: null,
servers: [],
recentRepositories: [],
};
}
export function loadSettings() {
try {
const rawValue = localStorage.getItem(STORAGE_KEY);
if (!rawValue) return getDefaultSettings();
const parsed = JSON.parse(rawValue);
return { ...getDefaultSettings(), ...parsed };
} catch (error) {
console.error("Failed to load settings:", error);
return getDefaultSettings();
}
}
export function saveSettings(settings) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
}
+41
View File
@@ -0,0 +1,41 @@
const invoke = window.__TAURI__?.core?.invoke;
function ensureInvoke() {
if (!invoke) {
throw new Error("Tauri invoke API is not available.");
}
}
export async function runGitClone(repoUrl, destinationPath, gitPath) {
ensureInvoke();
return invoke("git_clone", {
repoUrl,
destinationPath,
gitPath: gitPath || null,
});
}
export async function runGitPull(repoPath, gitPath) {
ensureInvoke();
return invoke("git_pull", { repoPath, gitPath: gitPath || null });
}
export async function runGitPush(repoPath, gitPath) {
ensureInvoke();
return invoke("git_push", { repoPath, gitPath: gitPath || null });
}
export async function runGitStatus(repoPath, gitPath) {
ensureInvoke();
return invoke("git_status", { repoPath, gitPath: gitPath || null });
}
export async function runGitBranch(repoPath, gitPath) {
ensureInvoke();
return invoke("git_branch", { repoPath, gitPath: gitPath || null });
}
export async function testGiteaConnection(payload) {
ensureInvoke();
return invoke("test_gitea_connection", payload);
}
+232
View File
@@ -0,0 +1,232 @@
{
"name": "gitpub-desktop",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gitpub-desktop",
"version": "0.1.0",
"devDependencies": {
"@tauri-apps/cli": "^2"
}
},
"node_modules/@tauri-apps/cli": {
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.1.tgz",
"integrity": "sha512-rpEbaJ/HzNb6fwsquwoAbq29/Vt4gADhS423A8fdkwL4edJ0wZmoB8ar7O6JPDL834MUKOCm/rrJ7c9oAaEaYQ==",
"dev": true,
"license": "Apache-2.0 OR MIT",
"bin": {
"tauri": "tauri.js"
},
"engines": {
"node": ">= 10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/tauri"
},
"optionalDependencies": {
"@tauri-apps/cli-darwin-arm64": "2.11.1",
"@tauri-apps/cli-darwin-x64": "2.11.1",
"@tauri-apps/cli-linux-arm-gnueabihf": "2.11.1",
"@tauri-apps/cli-linux-arm64-gnu": "2.11.1",
"@tauri-apps/cli-linux-arm64-musl": "2.11.1",
"@tauri-apps/cli-linux-riscv64-gnu": "2.11.1",
"@tauri-apps/cli-linux-x64-gnu": "2.11.1",
"@tauri-apps/cli-linux-x64-musl": "2.11.1",
"@tauri-apps/cli-win32-arm64-msvc": "2.11.1",
"@tauri-apps/cli-win32-ia32-msvc": "2.11.1",
"@tauri-apps/cli-win32-x64-msvc": "2.11.1"
}
},
"node_modules/@tauri-apps/cli-darwin-arm64": {
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.1.tgz",
"integrity": "sha512-6eEKMBXsQPCuM1EmvrjT2+aBuxWQuFdKdW8pzNuNQtpq45nEEpBlD5gr8pUeAyOU1DQKlkFaEc/MPBxb/Pfjtg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-darwin-x64": {
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.1.tgz",
"integrity": "sha512-LQUO7exfRWjWALNhetph5guWpMeHphRpokOLk0OIbTTExaNwJNFu3I4vb+CCM/4G/QGoZe/5XikZOJdNEFP1ig==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.1.tgz",
"integrity": "sha512-5i/awiBCRRhOUG8yjn0fMHXIWD5Ez8eEk5LtvOxyQrKuJkRaZDvnbIjZbE183blAwkoA4xN3aO/prJiqscl02Q==",
"cpu": [
"arm"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.1.tgz",
"integrity": "sha512-9LrwDw3S9Fygtw/Q6WDhOP+3svJRGAsejeE+GKrc0eO1ThMVhwi2LL6hw4dlKw93IfS7VY1G19sWGxJ/NcU4nA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.1.tgz",
"integrity": "sha512-mNA5dbbqPqDUdTIwdUYYuhO2GvIe9UnB2r0VU2njxBOS3Opbx4gKNC5yP0Iu4rYmEmqdlwry9VzGZQ3wq9dyFg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.1.tgz",
"integrity": "sha512-fZj3Gwq+6fUs305T5WQiD5iSGJw+j/4w/HGmk4sHDAcy+rp9zU5eaxB7nOyz5/I/nkNAuKPqfp6uIbiUBXkBCw==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.1.tgz",
"integrity": "sha512-XFxGxOvHM7jjeD6ozCKdGfhzJ7lERYDGZl1/Kb4fsvchaJsfLJ981TlyTG8Qy/gFq+f5GitH3bfrX9JAkjPEyw==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-x64-musl": {
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.1.tgz",
"integrity": "sha512-d5C2/Zm+68v7R9wTuTCjRQEVrWjcdMkJBZ1+rXse+QdMMlTB9+u9PDNDLw9PQflWxYLaYZ7tjxxL9Nb9II6PbA==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.1.tgz",
"integrity": "sha512-YdeVWFAR1pTXzUU6NLstPq4G6OLxuDrXCXEBdmBH+5EZIDXUx0D2kJlz3+YjpazkKvAzYpgziTsyRagls0OfRQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.1.tgz",
"integrity": "sha512-VBGkuH0eB9K9LLSMv361Gzr5Ou72sCS4+ztpmkWEQ+wd/amhcYOsf3X6qn1RJZDzIhiOYHJEOysZUC3baD01rA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.1.tgz",
"integrity": "sha512-b3ORhIAKgp9ZYY+zBt7b7r0kLU2kjvyGF0+MS2SBym3emsweGPybEqocJcmtMuxyBhkOKHP4CiuEJEDuAlTx6A==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
}
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"name": "gitpub-desktop",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"tauri": "tauri",
"tauri:dev": "tauri dev",
"tauri:build": "tauri build"
},
"devDependencies": {
"@tauri-apps/cli": "^2"
}
}
+7
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
+5456
View File
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
[package]
name = "gitpub-desktop"
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 = "gitpub_desktop_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"
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
+3
View File
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
+10
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"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

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

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+197
View File
@@ -0,0 +1,197 @@
use reqwest::blocking::Client;
use reqwest::header::{ACCEPT, AUTHORIZATION};
use serde::Serialize;
use std::path::Path;
use std::process::Command;
#[derive(Serialize)]
struct GitCommandResult {
command: String,
stdout: String,
stderr: String,
success: bool,
}
#[derive(Serialize)]
struct ServerConnectionResult {
ok: bool,
message: String,
api_base_url: String,
version: Option<String>,
}
fn normalize_api_base_url(server_url: &str) -> Result<String, String> {
// Normalize user input so every backend consistently resolves to /api/v1.
let trimmed = server_url.trim().trim_end_matches('/');
if !(trimmed.starts_with("http://") || trimmed.starts_with("https://")) {
return Err("Server URL must start with http:// or https://".to_string());
}
if trimmed.ends_with("/api/v1") {
Ok(trimmed.to_string())
} else {
Ok(format!("{trimmed}/api/v1"))
}
}
fn resolve_git_binary(git_path: Option<String>) -> String {
git_path
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("git")
.to_string()
}
fn run_git_command(
repo_path: Option<&str>,
git_path: Option<String>,
args: Vec<String>,
) -> Result<GitCommandResult, String> {
// Central command executor keeps Git command behavior consistent.
let git_binary = resolve_git_binary(git_path);
let mut command = Command::new(&git_binary);
if let Some(path) = repo_path {
if !Path::new(path).exists() {
return Err(format!("Repository path does not exist: {path}"));
}
command.current_dir(path);
}
command.args(&args);
let output = command
.output()
.map_err(|err| format!("Failed to run git command: {err}"))?;
Ok(GitCommandResult {
command: format!("{git_binary} {}", args.join(" ")),
stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
stderr: String::from_utf8_lossy(&output.stderr).trim().to_string(),
success: output.status.success(),
})
}
#[tauri::command]
fn git_clone(
repo_url: String,
destination_path: String,
git_path: Option<String>,
) -> Result<GitCommandResult, String> {
if repo_url.trim().is_empty() {
return Err("Repository URL is required".to_string());
}
if destination_path.trim().is_empty() {
return Err("Destination path is required".to_string());
}
run_git_command(
None,
git_path,
vec![
"clone".to_string(),
repo_url.trim().to_string(),
destination_path.trim().to_string(),
],
)
}
#[tauri::command]
fn git_pull(repo_path: String, git_path: Option<String>) -> Result<GitCommandResult, String> {
run_git_command(Some(repo_path.trim()), git_path, vec!["pull".to_string()])
}
#[tauri::command]
fn git_push(repo_path: String, git_path: Option<String>) -> Result<GitCommandResult, String> {
run_git_command(Some(repo_path.trim()), git_path, vec!["push".to_string()])
}
#[tauri::command]
fn git_status(repo_path: String, git_path: Option<String>) -> Result<GitCommandResult, String> {
run_git_command(
Some(repo_path.trim()),
git_path,
vec!["status".to_string(), "--short".to_string(), "--branch".to_string()],
)
}
#[tauri::command]
fn git_branch(repo_path: String, git_path: Option<String>) -> Result<GitCommandResult, String> {
run_git_command(
Some(repo_path.trim()),
git_path,
vec!["branch".to_string(), "--all".to_string()],
)
}
#[tauri::command]
fn test_gitea_connection(
server_url: String,
auth_method: String,
token: Option<String>,
username: Option<String>,
password: Option<String>,
) -> Result<ServerConnectionResult, String> {
// A lightweight compatibility check against the canonical Gitea version endpoint.
let api_base_url = normalize_api_base_url(&server_url)?;
let version_url = format!("{api_base_url}/version");
let mut request = Client::new()
.get(&version_url)
.header(ACCEPT, "application/json");
if auth_method == "token" {
if let Some(value) = token.as_deref() {
if !value.trim().is_empty() {
request = request.header(AUTHORIZATION, format!("token {}", value.trim()));
}
}
} else if auth_method == "password" {
request = request.basic_auth(username.unwrap_or_default(), password);
}
let response = request
.send()
.map_err(|err| format!("Failed to connect to Gitea server: {err}"))?;
if !response.status().is_success() {
return Ok(ServerConnectionResult {
ok: false,
message: format!("Server responded with status {}", response.status()),
api_base_url,
version: None,
});
}
let response_json: serde_json::Value = response
.json()
.map_err(|err| format!("Failed to parse Gitea response: {err}"))?;
let version = response_json
.get("version")
.and_then(|value| value.as_str())
.map(ToString::to_string);
Ok(ServerConnectionResult {
ok: true,
message: "Gitea API is reachable and compatible.".to_string(),
api_base_url,
version,
})
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![
git_clone,
git_pull,
git_push,
git_status,
git_branch,
test_gitea_connection
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
+6
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() {
gitpub_desktop_lib::run()
}
+35
View File
@@ -0,0 +1,35 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "gitpub-desktop",
"version": "0.1.0",
"identifier": "com.andrewzambazos.gitpub-desktop",
"build": {
"frontendDist": "../frontend"
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"title": "Gitpub Desktop",
"width": 1320,
"height": 860,
"minWidth": 1024,
"minHeight": 680
}
],
"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"
]
}
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#F7DF1E" d="M0 0h256v256H0V0Z"></path><path d="m67.312 213.932l19.59-11.856c3.78 6.701 7.218 12.371 15.465 12.371c7.905 0 12.89-3.092 12.89-15.12v-81.798h24.057v82.138c0 24.917-14.606 36.259-35.916 36.259c-19.245 0-30.416-9.967-36.087-21.996m85.07-2.576l19.588-11.341c5.157 8.421 11.859 14.607 23.715 14.607c9.969 0 16.325-4.984 16.325-11.858c0-8.248-6.53-11.17-17.528-15.98l-6.013-2.58c-17.357-7.387-28.87-16.667-28.87-36.257c0-18.044 13.747-31.792 35.228-31.792c15.294 0 26.292 5.328 34.196 19.247l-18.732 12.03c-4.125-7.389-8.591-10.31-15.465-10.31c-7.046 0-11.514 4.468-11.514 10.31c0 7.217 4.468 10.14 14.778 14.608l6.014 2.577c20.45 8.765 31.963 17.7 31.963 37.804c0 21.654-17.012 33.51-39.867 33.51c-22.339 0-36.774-10.654-43.819-24.574"></path></svg>

After

Width:  |  Height:  |  Size: 995 B

+6
View File
@@ -0,0 +1,6 @@
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+39
View File
@@ -0,0 +1,39 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="styles.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri App</title>
<script type="module" src="/main.js" defer></script>
</head>
<body>
<main class="container">
<h1>Welcome to Tauri</h1>
<div class="row">
<a href="https://tauri.app" target="_blank">
<img src="/assets/tauri.svg" class="logo tauri" alt="Tauri logo" />
</a>
<a
href="https://developer.mozilla.org/en-US/docs/Web/JavaScript"
target="_blank"
>
<img
src="/assets/javascript.svg"
class="logo vanilla"
alt="JavaScript logo"
/>
</a>
</div>
<p>Click on the Tauri logo to learn more about the framework</p>
<form class="row" id="greet-form">
<input id="greet-input" placeholder="Enter a name..." />
<button type="submit">Greet</button>
</form>
<p id="greet-msg"></p>
</main>
</body>
</html>
+18
View File
@@ -0,0 +1,18 @@
const { invoke } = window.__TAURI__.core;
let greetInputEl;
let greetMsgEl;
async function greet() {
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
greetMsgEl.textContent = await invoke("greet", { name: greetInputEl.value });
}
window.addEventListener("DOMContentLoaded", () => {
greetInputEl = document.querySelector("#greet-input");
greetMsgEl = document.querySelector("#greet-msg");
document.querySelector("#greet-form").addEventListener("submit", (e) => {
e.preventDefault();
greet();
});
});
+112
View File
@@ -0,0 +1,112 @@
.logo.vanilla:hover {
filter: drop-shadow(0 0 2em #ffe21c);
}
: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;
}
}