Compare commits

..

No commits in common. "cbaf999692e54a0eb711fb9d53b70c7a7577bb72" and "cc91868c63be8f530c8f5bb172a96692970c50a8" have entirely different histories.

3 changed files with 48 additions and 149 deletions

View File

@ -1,9 +1,8 @@
import { invoke } from '@tauri-apps/api/core';
import dayjs from 'dayjs';
import { atom, useAtomValue, useSetAtom } from 'jotai';
import { atom, 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';
@ -95,76 +94,22 @@ export class Pattern {
this.smoothRepeat = true;
this.pulses = [];
}
}
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(
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(
maxOrder + 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),
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.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;
});
}
}
@ -188,7 +133,7 @@ export const PatternsAtom = atomWithRefresh(async (get) => {
return [];
});
export const SelectedPatternIdAtom = atom<string | null>(null);
export const CurrentPatternAtom = atomWithRefresh<Pattern | null>(async (get) => {
export const CurrentPatternAtom = atom<Pattern | null>(async (get) => {
try {
const patternId = get(SelectedPatternIdAtom);
if (patternId === null) {
@ -201,49 +146,50 @@ export const CurrentPatternAtom = atomWithRefresh<Pattern | null>(async (get) =>
}
return null;
});
export const PulsesInCurrentPatternAtom = atomWithRefresh(
export const PulsesInCurrentPatternAtom = atom(
(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 = 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],
);
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;
};
return savePattern;
}

View File

@ -6,15 +6,6 @@
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;
@ -26,10 +17,4 @@
flex-direction: column;
align-items: stretch;
}
.pulse_cards {
display: flex;
flex-direction: column;
align-items: stretch;
gap: calc(var(--spacing) * 2);
}
}

View File

@ -1,51 +1,19 @@
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>
<div className={styles.pulse_cards}>
{pulses.length === 0 && <div className="empty_prompt">No key pulses.</div>}
</div>
</ScrollArea>
<ScrollArea enableY></ScrollArea>
</div>
</>
);