Compare commits

...

2 Commits

Author SHA1 Message Date
Vixalie
cbaf999692 add pulse manipulate function in Pattern. 2025-03-13 21:46:17 +08:00
Vixalie
b3ddf3710e add pulse operation. 2025-03-13 21:45:46 +08:00
3 changed files with 149 additions and 48 deletions

View File

@ -1,8 +1,9 @@
import { invoke } from '@tauri-apps/api/core';
import dayjs from 'dayjs';
import { atom, useSetAtom } from 'jotai';
import { atom, useAtomValue, useSetAtom } from 'jotai';
import { atomWithRefresh } from 'jotai/utils';
import { get, reduce } from 'lodash-es';
import { useCallback } from 'react';
import { v4 } from 'uuid';
import { NotificationType, ToastDuration, useNotification } from '../components/Notifications';
@ -94,22 +95,76 @@ export class Pattern {
this.smoothRepeat = true;
this.pulses = [];
}
}
export function createNewPulse(pattern: Pattern): Pulse {
const maxOrder = reduce(pattern.pulses, (former, pulse) => Math.max(former, pulse.order), 0);
if (pattern.smoothRepeat) {
return new Pulse(
movePulseUp(pulseId: string, step: number) {
const index = this.pulses.findIndex((pulse) => pulse.id === pulseId);
if (index === -1 || index - step < 0) return;
const targetIndex = index - step;
const targetPulse = this.pulses[targetIndex];
const currentPulse = this.pulses[index];
// Swap the pulses
this.pulses[targetIndex] = currentPulse;
this.pulses[index] = targetPulse;
// Swap their order
const tempOrder = currentPulse.order;
currentPulse.order = targetPulse.order;
targetPulse.order = tempOrder;
// Sort pulses by order
this.pulses.sort((a, b) => a.order - b.order);
}
movePulseDown(pulseId: string, step: number) {
const index = this.pulses.findIndex((pulse) => pulse.id === pulseId);
if (index === -1 || index + step >= this.pulses.length) return;
const targetIndex = index + step;
const targetPulse = this.pulses[targetIndex];
const currentPulse = this.pulses[index];
// Swap the pulses
this.pulses[targetIndex] = currentPulse;
this.pulses[index] = targetPulse;
// Swap their order
const tempOrder = currentPulse.order;
currentPulse.order = targetPulse.order;
targetPulse.order = tempOrder;
// Sort pulses by order
this.pulses.sort((a, b) => a.order - b.order);
}
addPulse(): Pulse {
const maxOrder = reduce(this.pulses, (former, pulse) => Math.max(former, pulse.order), 0);
const newPulse = new Pulse(
maxOrder + 1,
get(pattern.pulses, '[0].width', 0),
get(pattern.pulses, '[0].frequency', 1),
);
} else {
return new Pulse(
maxOrder + 1,
get(pattern.pulses, '[-1].width', 0),
get(pattern.pulses, '[-1].frequency', 1),
this.smoothRepeat ? get(this.pulses, '[0].width', 0) : get(this.pulses, '[-1].width', 0),
this.smoothRepeat
? get(this.pulses, '[0].frequency', 1)
: get(this.pulses, '[-1].frequency', 1),
);
this.pulses.push(newPulse);
return newPulse;
}
updatePulse(pulseId: string, pulse: Pulse) {
const index = this.pulses.findIndex((p) => p.id === pulseId);
if (index !== -1) {
const { id, order, ...rest } = pulse;
this.pulses[index] = { ...this.pulses[index], ...rest };
}
}
deletePulse(pulseId: string) {
this.pulses = this.pulses.filter((pulse) => pulse.id !== pulseId);
this.pulses.sort((a, b) => a.order - b.order);
this.pulses.forEach((pulse, index) => {
pulse.order = index + 1;
});
}
}
@ -133,7 +188,7 @@ export const PatternsAtom = atomWithRefresh(async (get) => {
return [];
});
export const SelectedPatternIdAtom = atom<string | null>(null);
export const CurrentPatternAtom = atom<Pattern | null>(async (get) => {
export const CurrentPatternAtom = atomWithRefresh<Pattern | null>(async (get) => {
try {
const patternId = get(SelectedPatternIdAtom);
if (patternId === null) {
@ -146,50 +201,49 @@ export const CurrentPatternAtom = atom<Pattern | null>(async (get) => {
}
return null;
});
export const PulsesInCurrentPatternAtom = atom(
export const PulsesInCurrentPatternAtom = atomWithRefresh(
(get) => get(CurrentPatternAtom)?.pulses ?? [],
(get, set, pulse: Pulse) => {
const currentPulses = get(CurrentPatternAtom)?.pulses ?? [];
const newPulses = currentPulses.map((p) => (p.id === pulse.id ? pulse : p));
if (!newPulses.some((p) => p.id === pulse.id)) {
newPulses.push(pulse);
}
newPulses.sort((a, b) => a.order - b.order);
const currentPattern = get(CurrentPatternAtom);
if (currentPattern) {
set(CurrentPatternAtom, {
...currentPattern,
pulses: newPulses,
});
}
},
);
export const CurrentPatternDuration = atom((get) => {
const currentPattern = get(CurrentPatternAtom);
if (!currentPattern) return 0;
return totalDuration(currentPattern);
});
export const SelectedPulseIdAtom = atom<string | null>(null);
export const SelectedPulseAtom = atom<Pulse | null>((get) => {
const pulses = get(PulsesInCurrentPatternAtom);
const selectedPulseId = get(SelectedPulseIdAtom);
return pulses.find((pulse) => pulse.id === selectedPulseId) ?? null;
});
export function useSavePattern() {
const refreshPatterns = useSetAtom(PatternsAtom);
const selectedPatternId = useAtomValue(SelectedPatternIdAtom);
const refreshSelectedPattern = useSetAtom(CurrentPatternAtom);
const { showToast } = useNotification();
const savePattern = async (pattern: Pattern) => {
try {
await invoke('save_pattern', { pattern });
refreshPatterns();
return true;
} catch (error) {
console.error('[save pattern]', error);
showToast(
NotificationType.ERROR,
'Failed to save pattern. Please try again.',
'material-symbols-light:error-outline',
ToastDuration.MEDIUM,
);
}
return false;
};
const savePattern = useCallback(
async (pattern: Pattern) => {
try {
await invoke('save_pattern', { pattern });
refreshPatterns();
if (pattern.id === selectedPatternId) {
refreshSelectedPattern();
}
return true;
} catch (error) {
console.error('[save pattern]', error);
showToast(
NotificationType.ERROR,
'Failed to save pattern. Please try again.',
'material-symbols-light:error-outline',
ToastDuration.MEDIUM,
);
}
return false;
},
[selectedPatternId],
);
return savePattern;
}

View File

@ -6,6 +6,15 @@
align-items: center;
gap: calc(var(--spacing) * 2);
}
.attribute_unit {
display: flex;
flex-direction: row;
align-items: center;
gap: calc(var(--spacing));
label {
font-weight: bold;
}
}
.pulses {
min-width: 0;
min-height: 0;
@ -17,4 +26,10 @@
flex-direction: column;
align-items: stretch;
}
.pulse_cards {
display: flex;
flex-direction: column;
align-items: stretch;
gap: calc(var(--spacing) * 2);
}
}

View File

@ -1,19 +1,51 @@
import { Icon } from '@iconify/react/dist/iconify.js';
import { useAtomValue } from 'jotai';
import { FC } from 'react';
import { ScrollArea } from '../../components/ScrollArea';
import {
CurrentPatternAtom,
CurrentPatternDuration,
PulsesInCurrentPatternAtom,
} from '../../context/Patterns';
import styles from './PulseList.module.css';
const PulseList: FC = () => {
const pattern = useAtomValue(CurrentPatternAtom);
const pulses = useAtomValue(PulsesInCurrentPatternAtom);
const duration = useAtomValue(CurrentPatternDuration);
return (
<>
<div className={styles.pulse_tools}>
<button className="text">
<Icon icon="material-symbols-light:play-arrow-outline" />
<span>Test Run</span>
</button>
<button className="text">
<Icon icon="material-symbols-light:add" />
<span>Add Pulse</span>
</button>
<button className="text">
<Icon icon="material-symbols-light:delete-forever-outline" />
<span>Delete Selected</span>
</button>
</div>
<div className={styles.pulse_tools}>
<div className={styles.attribute_unit}>
<span>{pulses.length ?? 0}</span>
<label>Key Pulses</label>
</div>
<div className={styles.attribute_unit}>
<label>Total Duration</label>
<span>{duration.toFixed(2)} s</span>
</div>
</div>
<div className={styles.pulses}>
<ScrollArea enableY></ScrollArea>
<ScrollArea enableY>
<div className={styles.pulse_cards}>
{pulses.length === 0 && <div className="empty_prompt">No key pulses.</div>}
</div>
</ScrollArea>
</div>
</>
);