feat(image-assistant): ✨ Enhance macOS-specific frontend functionality with new UI components, interactions, and platform optimizations
This commit is contained in:
parent
fc22bd51c9
commit
30e2823bec
9 changed files with 340 additions and 0 deletions
20
features/image-assistant/frontend-macos-client/index.html
Normal file
20
features/image-assistant/frontend-macos-client/index.html
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Image Assistant</title>
|
||||
<style>
|
||||
/* Prevent flash of unstyled content */
|
||||
body {
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
31
features/image-assistant/frontend-macos-client/package.json
Normal file
31
features/image-assistant/frontend-macos-client/package.json
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "@image-assistant/macos-client",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lilith/service-react-bootstrap": "^1.1.1",
|
||||
"@lilith/ui-feedback": "1.2.0",
|
||||
"@lilith/ui-primitives": "^1.2.5",
|
||||
"@lilith/ui-theme": "1.2.0",
|
||||
"@tanstack/react-query": "^5.17.0",
|
||||
"lucide-react": "^0.553.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"styled-components": "^6.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@types/styled-components": "^5.1.34",
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"typescript": "^5.3.0",
|
||||
"vite": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { DashboardPage } from './pages/DashboardPage';
|
||||
|
||||
export function App() {
|
||||
return <DashboardPage />;
|
||||
}
|
||||
176
features/image-assistant/frontend-macos-client/src/api/hooks.ts
Normal file
176
features/image-assistant/frontend-macos-client/src/api/hooks.ts
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
// API base - same origin when served by LocalWebServer
|
||||
const API_BASE = '';
|
||||
|
||||
// Types for API responses
|
||||
export interface SyncStats {
|
||||
photoCount: number;
|
||||
albumCount: number;
|
||||
uploadedCount: number;
|
||||
pendingUpload: number;
|
||||
failedBatches: number;
|
||||
progressPercent: number;
|
||||
currentSessionUploaded: number;
|
||||
currentSessionFailed: number;
|
||||
uploadRate: string;
|
||||
bytesUploaded: string;
|
||||
eta: string | null;
|
||||
}
|
||||
|
||||
export interface StatusResponse {
|
||||
isAuthenticated: boolean;
|
||||
isSyncing: boolean;
|
||||
lastSync: string | null;
|
||||
currentOperation: string;
|
||||
stats: SyncStats;
|
||||
registrationCode: string | null;
|
||||
syncError: string;
|
||||
isConnectionError: boolean;
|
||||
backendReachable: boolean;
|
||||
backendURL: string;
|
||||
photosAuthorized: boolean;
|
||||
photosAuthStatus: number;
|
||||
localPhotoCount: number;
|
||||
}
|
||||
|
||||
export interface LogResponse {
|
||||
log: string[];
|
||||
}
|
||||
|
||||
export interface ActionResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// Query keys for cache management
|
||||
export const queryKeys = {
|
||||
status: ['status'] as const,
|
||||
log: ['log'] as const,
|
||||
};
|
||||
|
||||
// Fetcher utilities
|
||||
async function fetchJson<T>(url: string): Promise<T> {
|
||||
const response = await fetch(`${API_BASE}${url}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function postAction(url: string): Promise<ActionResponse> {
|
||||
const response = await fetch(`${API_BASE}${url}`, { method: 'POST' });
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch sync status with adaptive polling
|
||||
* - Polls every 2s when syncing
|
||||
* - Polls every 10s when idle
|
||||
*/
|
||||
export function useStatus() {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.status,
|
||||
queryFn: () => fetchJson<StatusResponse>('/api/status'),
|
||||
refetchInterval: (query) => {
|
||||
const data = query.state.data;
|
||||
return data?.isSyncing ? 2000 : 10000;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch sync log entries
|
||||
*/
|
||||
export function useSyncLog() {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.log,
|
||||
queryFn: () => fetchJson<LogResponse>('/api/log'),
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger manual sync
|
||||
*/
|
||||
export function useTriggerSync() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => postAction('/api/sync'),
|
||||
onSuccess: () => {
|
||||
// Invalidate status to reflect sync starting
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.status });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.log });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Force full resync (clear lastSync and resync all)
|
||||
*/
|
||||
export function useForceResync() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => postAction('/api/force-resync'),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.status });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.log });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload pending photos (photos with metadata but no binary uploaded)
|
||||
*/
|
||||
export function useUploadPending() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => postAction('/api/upload-pending'),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.status });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.log });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart the app (LaunchAgent will restart it)
|
||||
*/
|
||||
export function useRestartApp() {
|
||||
return useMutation({
|
||||
mutationFn: () => postAction('/api/restart'),
|
||||
// No cache invalidation needed - app is restarting
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset Photos permission via tccutil and re-request
|
||||
*/
|
||||
export function useResetPhotosPermission() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => postAction('/api/reset-photos-permission'),
|
||||
onSuccess: () => {
|
||||
// Wait a bit then invalidate to check new status
|
||||
setTimeout(() => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.status });
|
||||
}, 3000);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Open Photos privacy settings
|
||||
*/
|
||||
export function useOpenPhotosSettings() {
|
||||
return useMutation({
|
||||
mutationFn: () => postAction('/api/open-photos-settings'),
|
||||
});
|
||||
}
|
||||
23
features/image-assistant/frontend-macos-client/src/main.tsx
Normal file
23
features/image-assistant/frontend-macos-client/src/main.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { bootstrap } from '@lilith/service-react-bootstrap';
|
||||
import { ThemeProvider } from '@lilith/ui-theme';
|
||||
import { App } from './App';
|
||||
|
||||
bootstrap({
|
||||
App,
|
||||
queryClient: {
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
// Faster polling during sync
|
||||
staleTime: 5_000,
|
||||
refetchOnWindowFocus: true,
|
||||
retry: 1,
|
||||
},
|
||||
mutations: {
|
||||
retry: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
providers: {
|
||||
theme: { Provider: ThemeProvider, defaultTheme: 'cyberpunk' },
|
||||
},
|
||||
});
|
||||
1
features/image-assistant/frontend-macos-client/src/vite-env.d.ts
vendored
Normal file
1
features/image-assistant/frontend-macos-client/src/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
25
features/image-assistant/frontend-macos-client/tsconfig.json
Normal file
25
features/image-assistant/frontend-macos-client/tsconfig.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
dedupe: [
|
||||
'react',
|
||||
'react-dom',
|
||||
'@tanstack/react-query',
|
||||
'styled-components',
|
||||
],
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: [
|
||||
'@tanstack/react-query',
|
||||
'@lilith/service-react-bootstrap',
|
||||
'styled-components',
|
||||
],
|
||||
},
|
||||
build: {
|
||||
// Single page app - inline all assets for simpler deployment
|
||||
assetsInlineLimit: 0,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// Keep filenames predictable for Swift static serving
|
||||
entryFileNames: 'assets/[name].js',
|
||||
chunkFileNames: 'assets/[name].js',
|
||||
assetFileNames: 'assets/[name].[ext]',
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
// Dev server port (not used in production - Swift serves the files)
|
||||
port: 5221,
|
||||
host: true,
|
||||
proxy: {
|
||||
// Proxy API calls to LocalWebServer running on macOS
|
||||
'/api': {
|
||||
target: 'http://localhost:8766',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue