Compare commits
2 Commits
cc91868c63
...
cbaf999692
Author | SHA1 | Date | |
---|---|---|---|
|
cbaf999692 | ||
|
b3ddf3710e |
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue
Block a user