fix: phase 9+10 refactoring

This commit is contained in:
2026-02-16 07:04:15 +01:00
parent e7c395e1bd
commit 4051fa9333
8 changed files with 295 additions and 128 deletions

View File

@@ -13,6 +13,19 @@ export interface CurrentValues {
caption: string;
}
type SuggestionFieldKey = 'title' | 'alt' | 'caption';
interface SuggestionFieldConfig {
key: SuggestionFieldKey;
label: string;
}
const SUGGESTION_FIELDS: SuggestionFieldConfig[] = [
{ key: 'title', label: 'Title' },
{ key: 'alt', label: 'Alt Text' },
{ key: 'caption', label: 'Caption' },
];
interface AISuggestionsModalProps {
isOpen: boolean;
isLoading: boolean;
@@ -65,6 +78,51 @@ export const AISuggestionsModal: React.FC<AISuggestionsModalProps> = ({
const hasAnySuggestion = suggestions && (suggestions.title || suggestions.alt || suggestions.caption);
const hasAnySelected = useTitle || useAlt || useCaption;
const fieldSelection: Record<SuggestionFieldKey, [boolean, (checked: boolean) => void]> = {
title: [useTitle, setUseTitle],
alt: [useAlt, setUseAlt],
caption: [useCaption, setUseCaption],
};
const renderSuggestionField = (field: SuggestionFieldConfig) => {
if (!suggestions?.[field.key]) {
return null;
}
const [isChecked, setChecked] = fieldSelection[field.key];
const currentValue = currentValues[field.key];
const suggestedValue = suggestions[field.key];
return (
<div key={field.key} className="ai-suggestion-item">
<label className="ai-suggestion-checkbox">
<input
type="checkbox"
checked={isChecked}
onChange={(e) => setChecked(e.target.checked)}
/>
<span className="checkmark"></span>
</label>
<div className="ai-suggestion-content">
<div className="ai-suggestion-label">
{field.label}
{currentValue && (
<span className="ai-suggestion-has-value" title="This field already has a value">
(has existing value)
</span>
)}
</div>
<div className="ai-suggestion-value">{suggestedValue}</div>
{currentValue && (
<div className="ai-suggestion-current">
Current: <em>{currentValue}</em>
</div>
)}
</div>
</div>
);
};
return (
<div className="ai-suggestions-modal-backdrop" onClick={handleBackdropClick}>
<div className="ai-suggestions-modal">
@@ -97,93 +155,7 @@ export const AISuggestionsModal: React.FC<AISuggestionsModalProps> = ({
<p className="ai-suggestions-intro">
Select which AI-generated values to apply. Existing values are preserved by default.
</p>
{suggestions?.title && (
<div className="ai-suggestion-item">
<label className="ai-suggestion-checkbox">
<input
type="checkbox"
checked={useTitle}
onChange={(e) => setUseTitle(e.target.checked)}
/>
<span className="checkmark"></span>
</label>
<div className="ai-suggestion-content">
<div className="ai-suggestion-label">
Title
{currentValues.title && (
<span className="ai-suggestion-has-value" title="This field already has a value">
(has existing value)
</span>
)}
</div>
<div className="ai-suggestion-value">{suggestions.title}</div>
{currentValues.title && (
<div className="ai-suggestion-current">
Current: <em>{currentValues.title}</em>
</div>
)}
</div>
</div>
)}
{suggestions?.alt && (
<div className="ai-suggestion-item">
<label className="ai-suggestion-checkbox">
<input
type="checkbox"
checked={useAlt}
onChange={(e) => setUseAlt(e.target.checked)}
/>
<span className="checkmark"></span>
</label>
<div className="ai-suggestion-content">
<div className="ai-suggestion-label">
Alt Text
{currentValues.alt && (
<span className="ai-suggestion-has-value" title="This field already has a value">
(has existing value)
</span>
)}
</div>
<div className="ai-suggestion-value">{suggestions.alt}</div>
{currentValues.alt && (
<div className="ai-suggestion-current">
Current: <em>{currentValues.alt}</em>
</div>
)}
</div>
</div>
)}
{suggestions?.caption && (
<div className="ai-suggestion-item">
<label className="ai-suggestion-checkbox">
<input
type="checkbox"
checked={useCaption}
onChange={(e) => setUseCaption(e.target.checked)}
/>
<span className="checkmark"></span>
</label>
<div className="ai-suggestion-content">
<div className="ai-suggestion-label">
Caption
{currentValues.caption && (
<span className="ai-suggestion-has-value" title="This field already has a value">
(has existing value)
</span>
)}
</div>
<div className="ai-suggestion-value">{suggestions.caption}</div>
{currentValues.caption && (
<div className="ai-suggestion-current">
Current: <em>{currentValues.caption}</em>
</div>
)}
</div>
</div>
)}
{SUGGESTION_FIELDS.map(renderSuggestionField)}
</div>
)}

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { showToast } from '../Toast';
import { getContrastColor } from '../../utils/color';
import { subscribeToTagEvents } from '../../utils/tagEventSubscriptions';
import './TagInput.css';
interface TagData {
@@ -54,24 +55,7 @@ export const TagInput: React.FC<TagInputProps> = ({
// Listen for tag changes
useEffect(() => {
const unsubscribers: Array<() => void> = [];
unsubscribers.push(
window.electronAPI?.on('tag:created', () => loadTags()) || (() => {})
);
unsubscribers.push(
window.electronAPI?.on('tag:deleted', () => loadTags()) || (() => {})
);
unsubscribers.push(
window.electronAPI?.on('tag:renamed', () => loadTags()) || (() => {})
);
unsubscribers.push(
window.electronAPI?.on('tags:merged', () => loadTags()) || (() => {})
);
return () => {
unsubscribers.forEach(unsub => unsub());
};
return subscribeToTagEvents(window.electronAPI?.on, loadTags);
}, [loadTags]);
// Filter suggestions based on input

View File

@@ -2,6 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react';
import { useAppStore } from '../../store';
import { showToast } from '../Toast';
import { getContrastColor } from '../../utils/color';
import { subscribeToTagEvents } from '../../utils/tagEventSubscriptions';
import './TagsView.css';
// Types
@@ -179,27 +180,9 @@ export const TagsView: React.FC = () => {
// Listen for tag events
useEffect(() => {
const unsubscribers: Array<() => void> = [];
unsubscribers.push(
window.electronAPI?.on('tag:created', () => loadTags()) || (() => {})
);
unsubscribers.push(
window.electronAPI?.on('tag:updated', () => loadTags()) || (() => {})
);
unsubscribers.push(
window.electronAPI?.on('tag:deleted', () => loadTags()) || (() => {})
);
unsubscribers.push(
window.electronAPI?.on('tag:renamed', () => loadTags()) || (() => {})
);
unsubscribers.push(
window.electronAPI?.on('tags:merged', () => loadTags()) || (() => {})
);
return () => {
unsubscribers.forEach(unsub => unsub());
};
return subscribeToTagEvents(window.electronAPI?.on, loadTags, {
includeUpdated: true,
});
}, [loadTags]);
// Handle tag selection

View File

@@ -0,0 +1,27 @@
type ElectronOn = ((channel: string, callback: (...args: unknown[]) => void) => (() => void) | void) | undefined;
interface SubscribeTagEventsOptions {
includeUpdated?: boolean;
}
const BASE_TAG_EVENTS = ['tag:created', 'tag:deleted', 'tag:renamed', 'tags:merged'] as const;
export function subscribeToTagEvents(
on: ElectronOn,
callback: () => void,
options: SubscribeTagEventsOptions = {}
): () => void {
if (!on) {
return () => {};
}
const channels = options.includeUpdated
? [...BASE_TAG_EVENTS, 'tag:updated']
: BASE_TAG_EVENTS;
const unsubscribers = channels.map((channel) => on(channel, callback) || (() => {}));
return () => {
unsubscribers.forEach((unsubscribe) => unsubscribe());
};
}