completely refactor patterns atom workflow.

This commit is contained in:
Vixalie 2025-03-11 16:41:22 +08:00
parent 1c48bb36d3
commit 8b0ddcecec
7 changed files with 53 additions and 36 deletions

View File

@ -1,7 +1,7 @@
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { atom, useSetAtom } from 'jotai'; import { atom, useSetAtom } from 'jotai';
import { atomWithDefault, atomWithRefresh } from 'jotai/utils'; import { atomWithRefresh } from 'jotai/utils';
import { get, reduce } from 'lodash-es'; import { get, reduce } from 'lodash-es';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { NotificationType, ToastDuration, useNotification } from '../components/Notifications'; import { NotificationType, ToastDuration, useNotification } from '../components/Notifications';
@ -125,9 +125,6 @@ export const SearchKeywordAtom = atom<string | null>(null);
export const PatternsAtom = atomWithRefresh(async (get) => { export const PatternsAtom = atomWithRefresh(async (get) => {
try { try {
const keyword = get(SearchKeywordAtom); const keyword = get(SearchKeywordAtom);
if (keyword === null) {
return [];
}
const patterns = await invoke<Pattern[]>('list_patterns', { keyword }); const patterns = await invoke<Pattern[]>('list_patterns', { keyword });
return patterns; return patterns;
} catch (e) { } catch (e) {
@ -135,7 +132,7 @@ export const PatternsAtom = atomWithRefresh(async (get) => {
} }
return []; return [];
}); });
export const SelectedPatternIdAtom = atomWithDefault<string | null>(null); export const SelectedPatternIdAtom = atom<string | null>(null);
export const CurrentPatternAtom = atom<Pattern | null>(async (get) => { export const CurrentPatternAtom = atom<Pattern | null>(async (get) => {
try { try {
const patternId = get(SelectedPatternIdAtom); const patternId = get(SelectedPatternIdAtom);
@ -181,6 +178,7 @@ export function useSavePattern() {
try { try {
await invoke('save_pattern', { pattern }); await invoke('save_pattern', { pattern });
refreshPatterns(); refreshPatterns();
return true;
} catch (error) { } catch (error) {
console.error('[save pattern]', error); console.error('[save pattern]', error);
showToast( showToast(
@ -190,6 +188,7 @@ export function useSavePattern() {
ToastDuration.MEDIUM, ToastDuration.MEDIUM,
); );
} }
return false;
}; };
return savePattern; return savePattern;

View File

@ -1,18 +1,19 @@
import { Icon } from '@iconify/react/dist/iconify.js'; import { Icon } from '@iconify/react/dist/iconify.js';
import { useAtom } from 'jotai'; import { useAtomValue, useSetAtom } from 'jotai';
import { FC, useCallback } from 'react'; import { FC, useCallback } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import EditableContent from '../../components/EditableContent'; import EditableContent from '../../components/EditableContent';
import { CurrentPatternAtom } from '../../context/Patterns'; import { CurrentPatternAtom, SelectedPatternIdAtom } from '../../context/Patterns';
import styles from './PatternHeader.module.css'; import styles from './PatternHeader.module.css';
const PatternHeader: FC = () => { const PatternHeader: FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [currentPattern, setPattern] = useAtom(CurrentPatternAtom); const currentPattern = useAtomValue(CurrentPatternAtom);
const setActivePattern = useSetAtom(SelectedPatternIdAtom);
const handleClosePattern = useCallback(() => { const handleClosePattern = useCallback(() => {
setPattern(null); setActivePattern(null);
navigate('/library'); navigate('/library');
}, [currentPattern]); }, []);
return ( return (
<div className={styles.pattern_header}> <div className={styles.pattern_header}>

View File

@ -9,7 +9,9 @@
gap: calc(var(--spacing) * 2); gap: calc(var(--spacing) * 2);
} }
.empty_promption { .empty_promption {
flex: 1; flex: 2;
border-radius: calc(var(--border-radius) * 2);
background-color: var(--color-surface-container);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;

View File

@ -1,6 +1,6 @@
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { FC } from 'react'; import { FC } from 'react';
import { CurrentPatternAtom } from '../../context/Patterns'; import { CurrentPatternAtom, Pattern } from '../../context/Patterns';
import styles from './PatternDetail.module.css'; import styles from './PatternDetail.module.css';
const EmptyPromption: FC = () => { const EmptyPromption: FC = () => {
@ -14,10 +14,14 @@ const EmptyPromption: FC = () => {
); );
}; };
const Detail: FC<{ pattern: Pattern }> = ({ pattern }) => {
return <div className={styles.pattern_detail}></div>;
};
const PatternDetail: FC = () => { const PatternDetail: FC = () => {
const currentPattern = useAtomValue(CurrentPatternAtom); const currentPattern = useAtomValue(CurrentPatternAtom);
return <div className={styles.pattern_detail}>{!currentPattern && <EmptyPromption />}</div>; return !currentPattern ? <EmptyPromption /> : <Detail pattern={currentPattern} />;
}; };
export default PatternDetail; export default PatternDetail;

View File

@ -1,29 +1,35 @@
import { Icon } from '@iconify/react/dist/iconify.js'; import { Icon } from '@iconify/react/dist/iconify.js';
import cx from 'clsx'; import cx from 'clsx';
import { useAtom, useAtomValue } from 'jotai'; import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { FC, useCallback, useMemo, useState } from 'react'; import { FC, useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useDebounce } from 'react-use'; import { useDebounce } from 'react-use';
import { ScrollArea } from '../../components/ScrollArea'; import { ScrollArea } from '../../components/ScrollArea';
import { CurrentPatternAtom, Pattern, PatternsAtom, totalDuration } from '../../context/Patterns'; import {
Pattern,
PatternsAtom,
SearchKeywordAtom,
SelectedPatternIdAtom,
totalDuration,
} from '../../context/Patterns';
import styles from './Patterns.module.css'; import styles from './Patterns.module.css';
const PatternCard: FC<{ pattern: Pattern }> = ({ pattern }) => { const PatternCard: FC<{ pattern: Pattern }> = ({ pattern }) => {
const [currentPattern, setCurrentPattern] = useAtom(CurrentPatternAtom); const [selectedId, setSelectedId] = useAtom(SelectedPatternIdAtom);
const navigate = useNavigate(); const navigate = useNavigate();
const duration = useMemo(() => { const duration = useMemo(() => {
return totalDuration(pattern) / 1000; return totalDuration(pattern) / 1000;
}, [pattern]); }, [pattern]);
const selected = useMemo(() => currentPattern?.id === pattern.id, [currentPattern, pattern]); const selected = useMemo(() => selectedId === pattern.id, [selectedId, pattern]);
const handleSingleClick = useCallback(() => { const handleSingleClick = useCallback(() => {
if (currentPattern?.id === pattern.id) { if (selectedId === pattern.id) {
setCurrentPattern(null); setSelectedId(null);
} else { } else {
setCurrentPattern(pattern); setSelectedId(pattern.id);
} }
}, [pattern, currentPattern]); }, [pattern, selectedId]);
const handleDblClick = useCallback(() => { const handleDblClick = useCallback(() => {
setCurrentPattern(pattern); setSelectedId(pattern.id);
navigate('/pattern-editor/edit'); navigate('/pattern-editor/edit');
}, [pattern]); }, [pattern]);
@ -40,8 +46,14 @@ const PatternCard: FC<{ pattern: Pattern }> = ({ pattern }) => {
const Patterns: FC = () => { const Patterns: FC = () => {
const [rawKeyword, setRawKeyword] = useState<string>(''); const [rawKeyword, setRawKeyword] = useState<string>('');
const [keyword, setKeyword] = useState<string | null>(null); const [keyword, setKeyword] = useAtom(SearchKeywordAtom);
const patterns = useAtomValue(PatternsAtom(keyword)); const patterns = useAtomValue(PatternsAtom);
const setPatternSelection = useSetAtom(SelectedPatternIdAtom);
const navigate = useNavigate();
const createNewAction = useCallback(() => {
setPatternSelection(null);
navigate('/pattern-editor/new');
}, []);
useDebounce( useDebounce(
() => { () => {
@ -67,7 +79,7 @@ const Patterns: FC = () => {
onChange={(evt) => setRawKeyword(evt.currentTarget.value)} onChange={(evt) => setRawKeyword(evt.currentTarget.value)}
/> />
</div> </div>
<button className="tonal secondary"> <button className="tonal secondary" onClick={createNewAction}>
<Icon icon="material-symbols-light:add" /> <Icon icon="material-symbols-light:add" />
New Pattern New Pattern
</button> </button>

View File

@ -1,16 +1,16 @@
import { invoke } from '@tauri-apps/api/core';
import cx from 'clsx'; import cx from 'clsx';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { FC, useActionState } from 'react'; import { FC, useActionState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { NotificationType, ToastDuration, useNotification } from '../components/Notifications'; import { NotificationType, ToastDuration, useNotification } from '../components/Notifications';
import { CurrentPatternAtom, Pattern } from '../context/Patterns'; import { Pattern, SelectedPatternIdAtom, useSavePattern } from '../context/Patterns';
import styles from './CreatePattern.module.css'; import styles from './CreatePattern.module.css';
const CreatePattern: FC = () => { const CreatePattern: FC = () => {
const { showToast } = useNotification(); const { showToast } = useNotification();
const navigate = useNavigate(); const navigate = useNavigate();
const loadPattern = useSetAtom(CurrentPatternAtom); const loadPattern = useSetAtom(SelectedPatternIdAtom);
const savePattern = useSavePattern();
const [errState, handleFormSubmit] = useActionState(async (state, formData) => { const [errState, handleFormSubmit] = useActionState(async (state, formData) => {
const patternName = formData.get('pattern_name') as string | null; const patternName = formData.get('pattern_name') as string | null;
@ -29,9 +29,8 @@ const CreatePattern: FC = () => {
newPattern.name = patternName; newPattern.name = patternName;
try { try {
await invoke('save_pattern', { pattern: newPattern }); const updated = await savePattern(newPattern);
const reloadedPattern = await invoke('get_pattern', { patternId: newPattern.id }); if (!updated) {
if (!reloadedPattern) {
showToast( showToast(
NotificationType.ERROR, NotificationType.ERROR,
'Failed to reload the created pattern. Please try again.', 'Failed to reload the created pattern. Please try again.',
@ -42,8 +41,8 @@ const CreatePattern: FC = () => {
navigate('/library'); navigate('/library');
return true; return true;
} }
loadPattern(reloadedPattern); loadPattern(newPattern.id);
navigate('./edit'); navigate('/pattern-editor/edit');
} catch (e) { } catch (e) {
console.error('[save pattern]', e); console.error('[save pattern]', e);
loadPattern(null); loadPattern(null);

View File

@ -1,12 +1,12 @@
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { FC } from 'react'; import { FC } from 'react';
import { Navigate } from 'react-router-dom'; import { Navigate } from 'react-router-dom';
import { CurrentPatternAtom } from '../context/Patterns'; import { SelectedPatternIdAtom } from '../context/Patterns';
const PatternNavigator: FC = () => { const PatternNavigator: FC = () => {
const currentPattern = useAtomValue(CurrentPatternAtom); const selected = useAtomValue(SelectedPatternIdAtom);
return currentPattern === null ? <Navigate to="new" /> : <Navigate to="edit" />; return selected === null ? <Navigate to="new" /> : <Navigate to="edit" />;
}; };
export default PatternNavigator; export default PatternNavigator;