-
+
+
-
-
+
-
+
Last checked: {repo.lastCheck}
@@ -136,10 +122,23 @@ const RepoCard = ({ repo, onToggle, onPull }: { repo: Repo, onToggle: () => void
);
};
-const StatusBadge = ({ status, count }: { status: string, count?: number }) => {
- switch (status) {
+const ActionButton = ({ repo, onPull, onPush }: { repo: Repo; onPull: () => void; onPush: () => void }) => {
+ const isProcessing = repo.status === 'pulling' || repo.status === 'checking';
+
+ if (isProcessing) return
;
+ if (repo.status === 'behind') return
;
+ if (repo.status === 'ahead') return
;
+
+ return
;
+};
+
+const StatusBadge = ({ repo }: { repo: Repo }) => {
+ switch (repo.status) {
case 'clean': return
Up to date;
- case 'behind': return
{count} commits behind;
+ case 'behind': return
{repo.commitsBehind} behind;
+ case 'ahead': return
{repo.commitsAhead} ahead;
+ case 'diverged': return
{repo.commitsAhead}↑ {repo.commitsBehind}↓;
+ case 'dirty': return
Uncommitted Changes;
case 'error': return
Connection Error;
case 'checking': return
Checking...;
case 'pulling': return
Pulling...;
diff --git a/src/Settings.tsx b/src/Settings.tsx
index c19dbc2..ce5a5f2 100644
--- a/src/Settings.tsx
+++ b/src/Settings.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from 'react';
+import { useState, useEffect } from 'react';
export const Settings = () => {
const [githubToken, setGithubToken] = useState('');
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx
new file mode 100644
index 0000000..adbec46
--- /dev/null
+++ b/src/components/Footer.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+
+export const Footer = () => {
+ return (
+
+
+
Service Running
+
v1.0.2
+
+ );
+};
diff --git a/src/index.css b/src/index.css
index 4fc70f2..60e686c 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,158 +1,335 @@
-
-/* Reset & Base */
:root {
- --bg-app: #202020;
- --bg-panel: #2b2b2b;
- --border-color: #383838;
- --accent: #60cdff; /* Windows 11 Blue */
- --text-primary: #ffffff;
+ --font-family-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
+ --background: #1a1a1a;
+ --background-light: #252526;
+ --background-lighter: #2d2d30;
+ --border-color: #333333;
+ --text-primary: #e0e0e0;
--text-secondary: #a0a0a0;
- --success: #6cc970;
- --warning: #eac54f;
- --error: #ff6b6b;
-}
-
-* { box-sizing: border-box; user-select: none; }
-body { margin: 0; font-family: 'Segoe UI Variable', 'Segoe UI', sans-serif; background: var(--bg-app); color: var(--text-primary); overflow: hidden; }
-
-/* Layout */
-.app-container { display: flex; flex-direction: column; height: 100vh; }
-.main-layout { display: flex; flex: 1; overflow: hidden; }
-
-/* Title Bar */
-.title-bar {
- height: 38px;
- background: var(--bg-app);
- display: flex; justify-content: space-between;
- align-items: center;
- -webkit-app-region: drag;
- border-bottom: 1px solid var(--border-color);
-}
-.title-drag-region { display: flex; align-items: center; padding-left: 16px; gap: 10px; font-size: 13px; font-weight: 500; }
-.app-icon { font-size: 16px; }
-.window-controls { display: flex; -webkit-app-region: no-drag; height: 100%; }
-.control { width: 46px; display: flex; justify-content: center; align-items: center; cursor: pointer; font-size: 10px; transition: 0.1s; }
-.control:hover { background: #3a3a3a; }
-.control.close:hover { background: #c42b1c; color: white; }
-
-/* Sidebar (Glassmorphismish) */
-.sidebar {
- width: 240px;
- background: rgba(32, 32, 32, 0.95);
- display: flex; flex-direction: column;
- border-right: 1px solid var(--border-color);
- padding-top: 10px;
-}
-.sidebar-menu { padding: 10px; display: flex; flex-direction: column; gap: 4px; }
-.sidebar-footer { margin-top: auto; padding: 20px; font-size: 11px; color: var(--text-secondary); border-top: 1px solid var(--border-color); }
-.status-indicator { display: flex; align-items: center; gap: 8px; color: var(--success); margin-bottom: 4px; font-weight: 500; }
-.dot { width: 8px; height: 8px; background: currentColor; border-radius: 50%; box-shadow: 0 0 8px rgba(108, 201, 112, 0.4); }
-
-/* Content Area */
-.content-area { flex: 1; display: flex; flex-direction: column; background: #191919; overflow: hidden; position: relative; }
-
-/* Dashboard */
-.dashboard-container { display: flex; flex-direction: column; height: 100%; padding: 24px; gap: 24px; }
-.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
-.section-title { margin: 0; font-size: 16px; font-weight: 600; }
-
-.repo-grid { display: flex; flex-direction: column; gap: 12px; overflow-y: auto; padding-right: 8px; flex: 1; }
-
-/* Repo Card */
+ --accent-primary: #3b82f6; /* Blue */
+ --accent-secondary: #10b981; /* Green */
+ --accent-warn: #f97316; /* Orange */
+ --accent-error: #ef4444; /* Red */
+}
+
+body {
+ margin: 0;
+ font-family: var(--font-family-sans);
+ background-color: var(--background);
+ color: var(--text-primary);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ overflow: hidden;
+}
+
+.app-container {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+}
+
+/* --- Custom Title Bar --- */
+.title-bar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ height: 32px;
+ background-color: var(--background);
+ border-bottom: 1px solid var(--border-color);
+ user-select: none;
+}
+
+.title-drag-region {
+ flex-grow: 1;
+ height: 100%;
+ -webkit-app-region: drag;
+ display: flex;
+ align-items: center;
+ padding-left: 12px;
+ gap: 8px;
+ font-weight: 600;
+ font-size: 13px;
+}
+
+.window-controls {
+ display: flex;
+ height: 100%;
+}
+
+.control {
+ width: 46px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: 16px;
+ transition: background-color 0.2s;
+}
+.control:hover { background-color: var(--background-lighter); }
+.close:hover { background-color: #e81123; }
+
+/* --- Main Layout --- */
+.main-layout {
+ display: flex;
+ flex-grow: 1;
+ height: calc(100vh - 32px);
+}
+
+.sidebar {
+ width: 200px;
+ background-color: var(--background);
+ padding: 16px;
+ border-right: 1px solid var(--border-color);
+ display: flex;
+ flex-direction: column;
+}
+
+.sidebar-item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 10px 14px;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 500;
+ transition: background-color 0.2s, color 0.2s;
+ color: var(--text-secondary);
+}
+
+.sidebar-item:hover {
+ background-color: var(--background-light);
+ color: var(--text-primary);
+}
+
+.sidebar-item.active {
+ background-color: var(--accent-primary);
+ color: white;
+}
+
+.content-area {
+ flex: 1;
+ padding: 24px;
+ overflow-y: auto;
+}
+
+/* --- Dashboard --- */
+.dashboard-container {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+}
+
+.section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 16px;
+}
+.section-title {
+ font-size: 20px;
+ font-weight: 600;
+ margin: 0;
+}
+.btn-primary {
+ background-color: var(--accent-primary);
+ color: white;
+ border: none;
+ padding: 8px 14px;
+ border-radius: 6px;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background-color 0.2s;
+}
+.btn-primary:hover { background-color: #2563eb; }
+
+.repo-grid {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 16px;
+}
+
+/* --- Repo Card --- */
.repo-card {
- background: var(--bg-panel);
+ background-color: var(--background-light);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 16px;
display: flex;
justify-content: space-between;
align-items: center;
- transition: border-color 0.2s, transform 0.1s;
-}
-.repo-card:hover { border-color: #555; }
-.repo-info h4 { margin: 0 0 6px 0; font-size: 15px; font-weight: 600; display: flex; align-items: center; gap: 10px; }
-.repo-path { font-size: 12px; color: var(--text-secondary); font-family: 'Consolas', monospace; opacity: 0.8; cursor: help; }
-
-/* Badges */
-.badge { font-size: 11px; padding: 2px 8px; border-radius: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
-.badge.clean { background: rgba(108, 201, 112, 0.15); color: var(--success); border: 1px solid rgba(108, 201, 112, 0.3); }
-.badge.behind { background: rgba(234, 197, 79, 0.15); color: var(--warning); border: 1px solid rgba(234, 197, 79, 0.3); }
-.badge.error { background: rgba(255, 107, 107, 0.15); color: var(--error); border: 1px solid rgba(255, 107, 107, 0.3); }
-.badge.process { background: rgba(96, 205, 255, 0.15); color: var(--accent); border: 1px solid rgba(96, 205, 255, 0.3); animation: pulse 2s infinite; }
-
-@keyframes pulse { 0% { opacity: 0.7; } 50% { opacity: 1; } 100% { opacity: 0.7; } }
-
-/* Quick Actions */
-.quick-actions { display: flex; gap: 6px; margin-top: 10px; }
-.action-btn {
- background: rgba(255,255,255,0.05);
- border: 1px solid transparent;
- color: var(--text-secondary);
- border-radius: 4px;
- padding: 4px 8px;
- font-size: 11px;
- cursor: pointer;
- display: flex; align-items: center; gap: 6px;
-}
-.action-btn:hover { background: rgba(255,255,255,0.1); color: white; border-color: #444; }
-
-/* Timeline Logs */
-.logs-container {
- background: #151515;
- border-top: 1px solid var(--border-color);
- display: flex; flex-direction: column;
- height: 35%; /* Fixed height for log area */
- min-height: 200px;
-}
-.logs-header { background: #1f1f1f; padding: 8px 16px; font-size: 12px; font-weight: 600; color: var(--text-secondary); border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; }
-.logs-scroll-area { overflow-y: auto; padding: 16px; flex: 1; font-family: 'Segoe UI', sans-serif; }
-
-.timeline-item { display: flex; gap: 14px; position: relative; padding-bottom: 16px; }
-.timeline-item::before {
- content: ''; position: absolute; left: 9px; top: 24px; bottom: 0; width: 2px; background: #333;
-}
-.timeline-item:last-child::before { display: none; }
-
-.timeline-icon {
- width: 20px; height: 20px; border-radius: 50%;
- display: flex; align-items: center; justify-content: center;
- font-size: 10px; z-index: 2; margin-top: 2px;
- flex-shrink: 0;
-}
-.timeline-icon.info { background: #2b2b2b; color: #aaa; border: 2px solid #333; }
-.timeline-icon.success { background: rgba(108, 201, 112, 0.2); color: var(--success); border: 2px solid var(--success); }
-.timeline-icon.error { background: rgba(255, 107, 107, 0.2); color: var(--error); border: 2px solid var(--error); }
-.timeline-icon.warn { background: rgba(234, 197, 79, 0.2); color: var(--warning); border: 2px solid var(--warning); }
-
-.timeline-content { display: flex; flex-direction: column; gap: 2px; }
-.timeline-msg { font-size: 13px; color: #eee; line-height: 1.4; }
-.timeline-meta { font-size: 11px; color: #666; display: flex; gap: 10px; }
-.timeline-repo { color: var(--accent); font-weight: 500; }
-
-/* Buttons */
-.btn-primary {
- background: var(--accent); color: #000;
- border: none; padding: 6px 14px; border-radius: 6px;
- cursor: pointer; font-weight: 600; font-size: 13px;
- transition: opacity 0.2s;
-}
-.btn-primary:hover { opacity: 0.9; }
-.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
-
-.pull-btn {
- background: transparent;
+ position: relative;
+}
+
+.repo-info h4 {
+ margin: 0 0 4px 0;
+ font-size: 16px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.repo-path {
+ font-size: 12px;
+ color: var(--text-secondary);
+ font-family: monospace;
+}
+
+.quick-actions {
+ display: flex;
+ gap: 8px;
+ margin-top: 12px;
+}
+
+.action-btn {
+ background: var(--background-lighter);
border: 1px solid var(--border-color);
- color: white;
- padding: 6px 16px;
+ color: var(--text-secondary);
+ padding: 4px 10px;
border-radius: 6px;
cursor: pointer;
+ font-size: 12px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ transition: all 0.2s;
+}
+.action-btn:hover {
+ background: #3a3a3d;
+ color: var(--text-primary);
+ border-color: #555;
+}
+
+.pull-btn, .push-btn {
+ padding: 6px 12px;
+ border-radius: 6px;
font-size: 13px;
+ font-weight: 500;
+ border: 1px solid transparent;
+ cursor: pointer;
+ transition: background-color 0.2s;
+}
+.pull-btn { background-color: #2da44e; color: white; border-color: rgba(255,255,255,0.1); }
+.pull-btn:hover { background-color: #238636; }
+.push-btn { background-color: var(--accent-primary); color: white; }
+.push-btn:hover { background-color: #2563eb; }
+.pull-btn:disabled { background-color: #333; color: #777; cursor: not-allowed; border-color: #444;}
+
+.delete-btn {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ background: none;
+ border: none;
+ color: var(--text-secondary);
+ cursor: pointer;
+ padding: 4px;
+ border-radius: 50%;
+ width: 28px;
+ height: 28px;
+ font-size: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
transition: all 0.2s;
}
-.pull-btn:hover:not(:disabled) { background: #333; border-color: #666; }
-.pull-btn:disabled { opacity: 0.5; cursor: default; }
+.delete-btn:hover { background-color: var(--background-lighter); color: var(--accent-error); }
+
-/* Settings */
-.settings-container { padding: 30px; max-width: 600px; overflow-y: auto; height: 100%; }
-.settings-group { margin-bottom: 30px; }
-.settings-group h4 { border-bottom: 1px solid #333; padding-bottom: 10px; margin-bottom: 15px; color: var(--accent); font-weight: 600; }
+/* --- Status Badges --- */
+.badge {
+ font-size: 10px;
+ padding: 3px 8px;
+ border-radius: 12px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+.badge.clean { background-color: #238636; color: white; }
+.badge.behind { background-color: var(--accent-warn); color: white; }
+.badge.ahead { background-color: var(--accent-primary); color: white; }
+.badge.diverged { background-color: #6b21a8; color: white; }
+.badge.dirty { background-color: #9ca3af; color: #1a1a1a; }
+.badge.error { background-color: var(--accent-error); color: white; }
+.badge.process { background-color: #444; color: #ccc; }
+
+/* --- Logs --- */
+.logs-container {
+ background-color: var(--background-light);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ display: flex;
+ flex-direction: column;
+ max-height: 250px;
+}
+.logs-header {
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--border-color);
+ font-weight: 600;
+ display: flex;
+ justify-content: space-between;
+}
+.logs-scroll-area {
+ flex: 1;
+ overflow-y: auto;
+ padding: 8px 16px;
+}
+
+.timeline-item {
+ display: flex;
+ gap: 12px;
+ padding: 8px 0;
+}
+.timeline-icon {
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ font-weight: bold;
+ color: white;
+}
+.timeline-icon.info { background-color: #555; }
+.timeline-icon.success { background-color: var(--accent-secondary); }
+.timeline-icon.warn { background-color: var(--accent-warn); }
+.timeline-icon.error { background-color: var(--accent-error); }
+
+.timeline-content {
+ flex: 1;
+}
+.timeline-msg { font-size: 13px; }
+.timeline-meta { font-size: 11px; color: var(--text-secondary); margin-top: 2px; }
+
+/* --- Settings Page --- */
+.settings-container {
+ max-width: 600px;
+}
+.settings-group {
+ margin-bottom: 24px;
+ padding-bottom: 24px;
+ border-bottom: 1px solid var(--border-color);
+}
+.settings-group h4 {
+ margin-top: 0;
+ margin-bottom: 16px;
+ font-size: 16px;
+}
+
+/* --- Footer --- */
+.footer {
+ border-top: 1px solid var(--border-color);
+ padding: 8px 16px;
+ font-size: 12px;
+ color: var(--text-secondary);
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.status-indicator {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background-color: var(--accent-secondary);
+}
diff --git a/src/renderer.d.ts b/src/renderer.d.ts
new file mode 100644
index 0000000..49e46a0
--- /dev/null
+++ b/src/renderer.d.ts
@@ -0,0 +1,27 @@
+export interface IElectronAPI {
+ getRepos: () => Promise
;
+ addRepo: (repo: any) => Promise;
+ toggleAutoPull: (id: string) => Promise;
+ saveGitHubToken: (token: string) => Promise;
+ getSettings: () => Promise;
+ saveSettings: (settings: any) => Promise;
+ startSmartAuth: () => Promise;
+ onSmartAuthTokenFound: (callback: (token: string) => void) => () => void;
+ onRepoUpdate: (callback: (value: any) => void) => () => void;
+ selectFolder: () => Promise<{ path: string; name: string; isRepo: boolean } | null>;
+ openRepoFolder: (path: string) => Promise;
+ openRepoUrl: (id: string) => Promise;
+ openInVsCode: (path: string) => Promise;
+ gitPull: (id: string) => Promise;
+ gitPush: (id: string) => Promise;
+ deleteRepo: (id: string) => Promise;
+ minimize: () => void;
+ maximize: () => void;
+ close: () => void;
+}
+
+declare global {
+ interface Window {
+ electronAPI: IElectronAPI;
+ }
+}
diff --git a/src/types.ts b/src/types.ts
index bb56052..c40e62b 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -4,9 +4,10 @@ export interface Repo {
path: string;
name: string;
autoPull: boolean;
- status: 'clean' | 'behind' | 'error' | 'pulling' | 'checking';
+ status: 'clean' | 'behind' | 'ahead' | 'diverged' | 'dirty' | 'error' | 'pulling' | 'checking';
lastCheck: string;
commitsBehind?: number;
+ commitsAhead?: number;
githubUrl?: string; // For the "Open in Browser" quick action
}
diff --git a/tsconfig.json b/tsconfig.json
index 05a07d3..e4bfee2 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -21,6 +21,6 @@
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
- "include": ["src", "electron"],
+ "include": ["src", "src/renderer.d.ts"],
"references": [{ "path": "./tsconfig.node.json" }]
}
\ No newline at end of file