Skip to content

Commit 92c61aa

Browse files
committed
feat(webui): complete Phase 3 - add Phase 1 services and headless adapters
Add all Phase 1 core infrastructure services required for WebUI operation: - AutoConnectService: automatic connection decision logic - ConnectionEstablishmentService: low-level connection setup and validation - ConnectionStateManager: multi-context connection state tracking - PrinterDiscoveryService: network scanning and printer detection - SavedPrinterService: persistent printer storage and last connected tracking - ThumbnailRequestQueue: thumbnail caching and request queue management - validation.utils: Zod validation schemas and IP address validation Add minimal headless adapters for UI-specific components: - LoadingManager: logs loading states to console instead of showing UI - DialogIntegrationService: returns sensible defaults for all dialog requests Fix WebUIManager: - Remove unused getRtspStreamService import All Phase 1 services are full implementations from FlashForgeUI-Electron reference to maintain 1:1 functionality match. Only UI-specific components use minimal adapters. Type check: 0 errors Phase 3 WebUI Server: Complete
1 parent 1bdf08f commit 92c61aa

10 files changed

Lines changed: 2426 additions & 1 deletion

src/managers/LoadingManager.ts

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
/**
2+
* @fileoverview Headless LoadingManager for standalone WebUI mode
3+
*
4+
* Provides a no-op implementation of LoadingManager for headless Node.js operation.
5+
* All loading states are logged to console instead of displaying UI overlays.
6+
*
7+
* This adapter allows PrinterBackendManager and ConnectionFlowManager to work
8+
* without requiring Electron-specific UI components.
9+
*/
10+
11+
import { EventEmitter } from 'events';
12+
13+
/**
14+
* Loading state types for different UI states
15+
*/
16+
export type LoadingState = 'hidden' | 'loading' | 'success' | 'error';
17+
18+
/**
19+
* Loading operation options for customizing behavior
20+
*/
21+
export interface LoadingOptions {
22+
message: string;
23+
canCancel?: boolean;
24+
showProgress?: boolean;
25+
autoHideAfter?: number; // milliseconds
26+
}
27+
28+
/**
29+
* Loading event data sent to renderer
30+
*/
31+
export interface LoadingEventData {
32+
state: LoadingState;
33+
message?: string;
34+
progress?: number;
35+
canCancel?: boolean;
36+
autoHideAfter?: number;
37+
}
38+
39+
/**
40+
* Branded type for LoadingManager singleton
41+
*/
42+
type LoadingManagerBrand = { readonly __brand: 'LoadingManager' };
43+
type LoadingManagerInstance = LoadingManager & LoadingManagerBrand;
44+
45+
/**
46+
* Headless LoadingManager - logs loading states instead of showing UI
47+
*/
48+
export class LoadingManager extends EventEmitter {
49+
private static instance: LoadingManagerInstance | null = null;
50+
51+
private currentState: LoadingState = 'hidden';
52+
private currentMessage: string = '';
53+
private currentProgress: number = 0;
54+
private canCancelFlag: boolean = false;
55+
56+
private constructor() {
57+
super();
58+
}
59+
60+
/**
61+
* Get singleton instance
62+
*/
63+
public static getInstance(): LoadingManagerInstance {
64+
if (!LoadingManager.instance) {
65+
LoadingManager.instance = new LoadingManager() as LoadingManagerInstance;
66+
}
67+
return LoadingManager.instance;
68+
}
69+
70+
/**
71+
* Show loading overlay with message (headless: just logs)
72+
*/
73+
public show(options: LoadingOptions): void {
74+
this.currentState = 'loading';
75+
this.currentMessage = options.message;
76+
this.currentProgress = 0;
77+
this.canCancelFlag = options.canCancel || false;
78+
79+
console.log(`[Loading] ${this.currentMessage}`);
80+
81+
const eventData: LoadingEventData = {
82+
state: this.currentState,
83+
message: this.currentMessage,
84+
progress: this.currentProgress,
85+
canCancel: this.canCancelFlag
86+
};
87+
88+
this.emit('loading-state-changed', eventData);
89+
this.emit('loadingStateChanged', eventData.state);
90+
}
91+
92+
/**
93+
* Hide loading overlay (headless: just logs)
94+
*/
95+
public hide(): void {
96+
this.currentState = 'hidden';
97+
this.currentMessage = '';
98+
this.currentProgress = 0;
99+
this.canCancelFlag = false;
100+
101+
const eventData: LoadingEventData = {
102+
state: this.currentState
103+
};
104+
105+
this.emit('loading-state-changed', eventData);
106+
this.emit('loadingStateChanged', eventData.state);
107+
}
108+
109+
/**
110+
* Show success message (headless: just logs)
111+
*/
112+
public showSuccess(message: string, _autoHideAfter: number = 4000): void {
113+
this.currentState = 'success';
114+
this.currentMessage = message;
115+
116+
console.log(`[Loading] ✓ ${message}`);
117+
118+
const eventData: LoadingEventData = {
119+
state: this.currentState,
120+
message: this.currentMessage,
121+
autoHideAfter: _autoHideAfter
122+
};
123+
124+
this.emit('loading-state-changed', eventData);
125+
this.emit('loadingStateChanged', eventData.state);
126+
127+
// Auto-hide after timeout
128+
setTimeout(() => this.hide(), _autoHideAfter);
129+
}
130+
131+
/**
132+
* Show error message (headless: just logs)
133+
*/
134+
public showError(message: string, _autoHideAfter: number = 5000): void {
135+
this.currentState = 'error';
136+
this.currentMessage = message;
137+
138+
console.error(`[Loading] ✗ ${message}`);
139+
140+
const eventData: LoadingEventData = {
141+
state: this.currentState,
142+
message: this.currentMessage,
143+
autoHideAfter: _autoHideAfter
144+
};
145+
146+
this.emit('loading-state-changed', eventData);
147+
this.emit('loadingStateChanged', eventData.state);
148+
149+
// Auto-hide after timeout
150+
setTimeout(() => this.hide(), _autoHideAfter);
151+
}
152+
153+
/**
154+
* Set progress percentage
155+
*/
156+
public setProgress(progress: number): void {
157+
this.currentProgress = Math.min(100, Math.max(0, progress));
158+
159+
const eventData: LoadingEventData = {
160+
state: this.currentState,
161+
message: this.currentMessage,
162+
progress: this.currentProgress,
163+
canCancel: this.canCancelFlag
164+
};
165+
166+
this.emit('loading-state-changed', eventData);
167+
}
168+
169+
/**
170+
* Update loading message
171+
*/
172+
public updateMessage(message: string): void {
173+
this.currentMessage = message;
174+
175+
console.log(`[Loading] ${message}`);
176+
177+
const eventData: LoadingEventData = {
178+
state: this.currentState,
179+
message: this.currentMessage,
180+
progress: this.currentProgress,
181+
canCancel: this.canCancelFlag
182+
};
183+
184+
this.emit('loading-state-changed', eventData);
185+
}
186+
187+
/**
188+
* Handle cancel request (headless: always returns false - no cancellation)
189+
*/
190+
public handleCancelRequest(): boolean {
191+
return false;
192+
}
193+
194+
/**
195+
* Get current state
196+
*/
197+
public getState(): LoadingState {
198+
return this.currentState;
199+
}
200+
201+
/**
202+
* Get current message
203+
*/
204+
public getMessage(): string {
205+
return this.currentMessage;
206+
}
207+
208+
/**
209+
* Get current progress
210+
*/
211+
public getProgress(): number {
212+
return this.currentProgress;
213+
}
214+
215+
/**
216+
* Check if loading is visible
217+
*/
218+
public isVisible(): boolean {
219+
return this.currentState !== 'hidden';
220+
}
221+
222+
/**
223+
* Check if operation is cancellable
224+
*/
225+
public isCancellable(): boolean {
226+
return this.canCancelFlag;
227+
}
228+
229+
/**
230+
* Cleanup and dispose
231+
*/
232+
public dispose(): void {
233+
this.hide();
234+
this.removeAllListeners();
235+
LoadingManager.instance = null;
236+
}
237+
}
238+
239+
/**
240+
* Get singleton instance
241+
*/
242+
export function getLoadingManager(): LoadingManagerInstance {
243+
return LoadingManager.getInstance();
244+
}

src/services/AutoConnectService.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/**
2+
* @fileoverview
3+
* AutoConnectService.ts
4+
*
5+
* Provides automated printer connection functionality for the FlashForgeUI-Electron application.
6+
* This service handles the logic for determining when and how to automatically connect to
7+
* previously saved printers based on network discovery results. It implements decision-making
8+
* algorithms for selecting the appropriate printer when multiple matches are found, and manages
9+
* auto-connect preferences and retry logic. The service follows a singleton pattern and extends
10+
* EventEmitter to provide event-based communication with other components.
11+
*
12+
* Key responsibilities:
13+
* - Determine when auto-connection should be attempted
14+
* - Make decisions about which printer to connect to when multiple options exist
15+
* - Manage auto-connect preferences and configuration
16+
* - Handle auto-connect retry logic and logging
17+
*/
18+
19+
import { EventEmitter } from 'events';
20+
import { getConfigManager } from '../managers/ConfigManager';
21+
import { SavedPrinterMatch, AutoConnectDecision } from '../types/printer';
22+
export class AutoConnectService extends EventEmitter {
23+
private static instance: AutoConnectService | null = null;
24+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
25+
private readonly _configManager = getConfigManager();
26+
27+
private constructor() {
28+
super();
29+
}
30+
31+
/**
32+
* Get singleton instance of AutoConnectService
33+
*/
34+
public static getInstance(): AutoConnectService {
35+
if (!AutoConnectService.instance) {
36+
AutoConnectService.instance = new AutoConnectService();
37+
}
38+
return AutoConnectService.instance;
39+
}
40+
41+
/**
42+
* Determine if auto-connect should be attempted
43+
* Based on configuration settings and saved printer availability
44+
*/
45+
public shouldAutoConnect(): boolean {
46+
// Auto-connect is always enabled by default
47+
// We could add a config option later if needed
48+
return true;
49+
}
50+
51+
/**
52+
* Determine the auto-connect choice based on available matches
53+
* @param matches - Array of saved printer matches found on network
54+
* @returns The auto-connect choice and selected match if applicable
55+
*/
56+
public determineAutoConnectChoice(matches: SavedPrinterMatch[]): AutoConnectDecision {
57+
if (matches.length === 0) {
58+
// No saved printers found on network
59+
return {
60+
action: 'none',
61+
reason: 'No saved printers found on network'
62+
};
63+
} else if (matches.length === 1) {
64+
// Single saved printer found - auto-connect
65+
return {
66+
action: 'connect',
67+
selectedMatch: matches[0],
68+
reason: 'Single saved printer found'
69+
};
70+
} else {
71+
// Multiple saved printers found - need user selection
72+
return {
73+
action: 'select',
74+
matches,
75+
reason: 'Multiple saved printers found'
76+
};
77+
}
78+
}
79+
80+
/**
81+
* Get the preferred printer for auto-connect
82+
* Returns the last used printer if available
83+
*/
84+
public getPreferredPrinter(matches: SavedPrinterMatch[]): SavedPrinterMatch | null {
85+
if (matches.length === 0) {
86+
return null;
87+
}
88+
89+
// For now, return null to let the UI handle selection
90+
// We could add last used printer tracking later
91+
return null;
92+
}
93+
94+
/**
95+
* Check if a specific printer should be auto-connected
96+
* Can be used for direct connection attempts
97+
*/
98+
public shouldAutoConnectToPrinter(_serialNumber: string): boolean {
99+
// For now, always return false
100+
// We could add last used printer tracking later
101+
return false;
102+
}
103+
104+
/**
105+
* Update auto-connect preferences after successful connection
106+
*/
107+
public updateAutoConnectPreferences(serialNumber: string): void {
108+
// This might update config settings for future auto-connect
109+
this.emit('auto-connect-preferences-updated', serialNumber);
110+
}
111+
112+
/**
113+
* Get auto-connect delay in milliseconds
114+
* Allows for a brief delay before attempting auto-connect
115+
*/
116+
public getAutoConnectDelay(): number {
117+
// Return default delay of 100ms
118+
return 100;
119+
}
120+
121+
/**
122+
* Check if auto-connect should be retried after failure
123+
*/
124+
public shouldRetryAutoConnect(_attemptCount: number): boolean {
125+
// No retries by default
126+
return false;
127+
}
128+
129+
/**
130+
* Log auto-connect attempt for debugging
131+
*/
132+
public logAutoConnectAttempt(
133+
action: 'started' | 'succeeded' | 'failed' | 'cancelled',
134+
details?: unknown
135+
): void {
136+
const timestamp = new Date().toISOString();
137+
console.log(`[AutoConnect] ${timestamp} - ${action}`, details);
138+
this.emit('auto-connect-logged', { action, details, timestamp });
139+
}
140+
}
141+
142+
// Export singleton getter function
143+
export const getAutoConnectService = (): AutoConnectService => {
144+
return AutoConnectService.getInstance();
145+
};
146+

0 commit comments

Comments
 (0)