Compare commits
No commits in common. "0aba9bb15ef4ff5b071df8021890ec0e953a0b25" and "c6f0b2a8fccc6e8d934a4e42eaf802b38e1f9ca4" have entirely different histories.
0aba9bb15e
...
c6f0b2a8fc
|
@ -1,11 +1,9 @@
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
|
||||||
|
|
||||||
use crate::fraction::Fraction;
|
use crate::fraction::Fraction;
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Serialize_repr, Deserialize_repr)]
|
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
|
||||||
#[repr(u32)]
|
|
||||||
pub enum FrequencyShifting {
|
pub enum FrequencyShifting {
|
||||||
Linear,
|
Linear,
|
||||||
Quadratic,
|
Quadratic,
|
||||||
|
|
|
@ -1,9 +1,180 @@
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import { atom, useAtomValue, useSetAtom } from 'jotai';
|
import { atom, useAtomValue, useSetAtom } from 'jotai';
|
||||||
import { atomWithRefresh } from 'jotai/utils';
|
import { atomWithRefresh } from 'jotai/utils';
|
||||||
|
import { get, reduce } from 'lodash-es';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
import { NotificationType, ToastDuration, useNotification } from '../components/Notifications';
|
import { NotificationType, ToastDuration, useNotification } from '../components/Notifications';
|
||||||
import { Pattern, Pulse, totalDuration } from './pattern-model';
|
|
||||||
|
export enum FrequencyShifting {
|
||||||
|
/**
|
||||||
|
* Change frequency undergoes a linear transformation from previous pulse to current one.
|
||||||
|
*/
|
||||||
|
Linear,
|
||||||
|
/**
|
||||||
|
* Change frequency undergoes a quadratic transformation from previous pulse to current one.
|
||||||
|
*/
|
||||||
|
Quadratic,
|
||||||
|
/**
|
||||||
|
* Change frequency undergoes a cubic transformation from previous pulse to current one.
|
||||||
|
*/
|
||||||
|
Cubic,
|
||||||
|
/**
|
||||||
|
* Change frequency with quick fade in and fade out.
|
||||||
|
*/
|
||||||
|
Ease,
|
||||||
|
/**
|
||||||
|
* Change frequency with spiking within range from previous pulse and current one.
|
||||||
|
*/
|
||||||
|
Pulsating,
|
||||||
|
/**
|
||||||
|
* Based on frequency of previous pulse, take the twice of frequency of current pulse as the peak frequency. Follow the maximum frequency limitation.
|
||||||
|
*/
|
||||||
|
Spiking,
|
||||||
|
/**
|
||||||
|
* Randomize frequency within range from previous frequency and current one.
|
||||||
|
*/
|
||||||
|
Randomize,
|
||||||
|
/**
|
||||||
|
* Randomize frequency within minium and maximum frequency.
|
||||||
|
*/
|
||||||
|
Maniac,
|
||||||
|
/**
|
||||||
|
* Synchronize changes of frequency with pulse width changes.
|
||||||
|
*/
|
||||||
|
Synchronized,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ControlPoint {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Pulse {
|
||||||
|
order: number;
|
||||||
|
id: number;
|
||||||
|
offset: number;
|
||||||
|
width: number;
|
||||||
|
maniac: boolean;
|
||||||
|
frequency: number;
|
||||||
|
frequencyShifting: FrequencyShifting;
|
||||||
|
controlPoint1: ControlPoint;
|
||||||
|
controlPoint2: ControlPoint;
|
||||||
|
|
||||||
|
constructor(order: number, width: number, frequency: number) {
|
||||||
|
this.id = v4();
|
||||||
|
this.order = order;
|
||||||
|
this.offset = 0;
|
||||||
|
this.width = width;
|
||||||
|
this.maniac = false;
|
||||||
|
this.frequency = frequency;
|
||||||
|
this.frequencyShifting = FrequencyShifting.Linear;
|
||||||
|
this.controlPoint1 = { x: 0, y: 0 };
|
||||||
|
this.controlPoint2 = { x: 0, y: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
equals(other: Pulse): boolean {
|
||||||
|
return this.id === other.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Pattern {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
createdAt: number;
|
||||||
|
lastModifiedAt: number | null;
|
||||||
|
smoothRepeat: boolean;
|
||||||
|
pulses: Pulse[];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.id = v4();
|
||||||
|
this.name = '';
|
||||||
|
this.createdAt = dayjs().valueOf();
|
||||||
|
this.lastModifiedAt = null;
|
||||||
|
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(
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function totalDuration(pattern: Pattern): number {
|
||||||
|
return reduce(
|
||||||
|
pattern.pulses,
|
||||||
|
(former, pulse) => former + pulse.offset,
|
||||||
|
pattern.smoothRepeat && pattern.pulses.length > 1 ? 100 : 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const SearchKeywordAtom = atom<string | null>(null);
|
export const SearchKeywordAtom = atom<string | null>(null);
|
||||||
export const PatternsAtom = atomWithRefresh(async (get) => {
|
export const PatternsAtom = atomWithRefresh(async (get) => {
|
||||||
|
@ -30,22 +201,25 @@ export const CurrentPatternAtom = atomWithRefresh<Pattern | null>(async (get) =>
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
export const PulsesInCurrentPatternAtom = atomWithRefresh(
|
||||||
|
(get) => get(CurrentPatternAtom)?.pulses ?? [],
|
||||||
|
);
|
||||||
export const CurrentPatternDuration = atom((get) => {
|
export const CurrentPatternDuration = atom((get) => {
|
||||||
const currentPattern = get(CurrentPatternAtom);
|
const currentPattern = get(CurrentPatternAtom);
|
||||||
if (!currentPattern) return 0;
|
if (!currentPattern) return 0;
|
||||||
return totalDuration(currentPattern);
|
return totalDuration(currentPattern);
|
||||||
});
|
});
|
||||||
export const SelectedPulseIdAtom = atom<string | null>(null);
|
export const SelectedPulseIdAtom = atom<string | null>(null);
|
||||||
export const SelectedPulseAtom = atom<Pulse | null>(async (get) => {
|
export const SelectedPulseAtom = atom<Pulse | null>((get) => {
|
||||||
const pattern = await get(CurrentPatternAtom);
|
const pulses = get(PulsesInCurrentPatternAtom);
|
||||||
const selectedPulseId = get(SelectedPulseIdAtom);
|
const selectedPulseId = get(SelectedPulseIdAtom);
|
||||||
console.debug('[refresh selected pulse]', selectedPulseId, pattern);
|
return pulses.find((pulse) => pulse.id === selectedPulseId) ?? null;
|
||||||
return pattern?.pulses?.find((pulse) => pulse.id === selectedPulseId) ?? null;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export function useSavePattern() {
|
export function useSavePattern() {
|
||||||
const refreshPatterns = useSetAtom(PatternsAtom);
|
const refreshPatterns = useSetAtom(PatternsAtom);
|
||||||
const selectedPatternId = useAtomValue(SelectedPatternIdAtom);
|
const selectedPatternId = useAtomValue(SelectedPatternIdAtom);
|
||||||
|
const refreshSelectedPattern = useSetAtom(CurrentPatternAtom);
|
||||||
const { showToast } = useNotification();
|
const { showToast } = useNotification();
|
||||||
|
|
||||||
const savePattern = useCallback(
|
const savePattern = useCallback(
|
||||||
|
@ -53,6 +227,9 @@ export function useSavePattern() {
|
||||||
try {
|
try {
|
||||||
await invoke('save_pattern', { pattern });
|
await invoke('save_pattern', { pattern });
|
||||||
refreshPatterns();
|
refreshPatterns();
|
||||||
|
if (pattern.id === selectedPatternId) {
|
||||||
|
refreshSelectedPattern();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[save pattern]', error);
|
console.error('[save pattern]', error);
|
||||||
|
|
|
@ -1,190 +0,0 @@
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import { get, reduce } from 'lodash-es';
|
|
||||||
import { v4 } from 'uuid';
|
|
||||||
|
|
||||||
export enum FrequencyShifting {
|
|
||||||
/**
|
|
||||||
* Change frequency undergoes a linear transformation from previous pulse to current one.
|
|
||||||
*/
|
|
||||||
Linear = 0,
|
|
||||||
/**
|
|
||||||
* Change frequency undergoes a quadratic transformation from previous pulse to current one.
|
|
||||||
*/
|
|
||||||
Quadratic = 1,
|
|
||||||
/**
|
|
||||||
* Change frequency undergoes a cubic transformation from previous pulse to current one.
|
|
||||||
*/
|
|
||||||
Cubic = 2,
|
|
||||||
/**
|
|
||||||
* Change frequency with quick fade in and fade out.
|
|
||||||
*/
|
|
||||||
Ease = 3,
|
|
||||||
/**
|
|
||||||
* Change frequency with spiking within range from previous pulse and current one.
|
|
||||||
*/
|
|
||||||
Pulsating = 4,
|
|
||||||
/**
|
|
||||||
* Based on frequency of previous pulse, take the twice of frequency of current pulse as the peak frequency. Follow the maximum frequency limitation.
|
|
||||||
*/
|
|
||||||
Spiking = 5,
|
|
||||||
/**
|
|
||||||
* Randomize frequency within range from previous frequency and current one.
|
|
||||||
*/
|
|
||||||
Randomize = 6,
|
|
||||||
/**
|
|
||||||
* Randomize frequency within minium and maximum frequency.
|
|
||||||
*/
|
|
||||||
Maniac = 7,
|
|
||||||
/**
|
|
||||||
* Synchronize changes of frequency with pulse width changes.
|
|
||||||
*/
|
|
||||||
Synchronized = 8,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ControlPoint {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Pulse {
|
|
||||||
order: number;
|
|
||||||
id: number;
|
|
||||||
offset: number;
|
|
||||||
width: number;
|
|
||||||
maniac: boolean;
|
|
||||||
frequency: number;
|
|
||||||
frequencyShifting: FrequencyShifting;
|
|
||||||
controlPoint1: ControlPoint;
|
|
||||||
controlPoint2: ControlPoint;
|
|
||||||
}
|
|
||||||
export function createPulse(order: number, width: number, frequency: number) {
|
|
||||||
return {
|
|
||||||
id: v4(),
|
|
||||||
order,
|
|
||||||
offset: 0,
|
|
||||||
width,
|
|
||||||
maniac: false,
|
|
||||||
frequency,
|
|
||||||
frequencyShifting: FrequencyShifting.Linear,
|
|
||||||
controlPoint1: { x: 0, y: 0 },
|
|
||||||
controlPoint2: { x: 0, y: 0 },
|
|
||||||
} as Pulse;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Pattern {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
createdAt: number;
|
|
||||||
lastModifiedAt: number | null;
|
|
||||||
smoothRepeat: boolean;
|
|
||||||
pulses: Pulse[];
|
|
||||||
}
|
|
||||||
export function createPattern() {
|
|
||||||
return {
|
|
||||||
id: v4(),
|
|
||||||
name: '',
|
|
||||||
createdAt: dayjs().valueOf(),
|
|
||||||
lastModifiedAt: null,
|
|
||||||
smoothRepeat: true,
|
|
||||||
pulses: [],
|
|
||||||
} as Pattern;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function movePulseUp(pattern: Pattern, pulseId: string, step: number) {
|
|
||||||
const index = pattern.pulses.findIndex((pulse) => pulse.id === pulseId);
|
|
||||||
if (index === -1 || index - step < 0) return;
|
|
||||||
|
|
||||||
const targetIndex = index - step;
|
|
||||||
const targetPulse = pattern.pulses[targetIndex];
|
|
||||||
const currentPulse = pattern.pulses[index];
|
|
||||||
|
|
||||||
// Swap the pulses
|
|
||||||
pattern.pulses[targetIndex] = currentPulse;
|
|
||||||
pattern.pulses[index] = targetPulse;
|
|
||||||
|
|
||||||
// If the target pulse's order is 1, swap their offsets
|
|
||||||
if (targetPulse.order === 1) {
|
|
||||||
const tempOffset = currentPulse.offset;
|
|
||||||
currentPulse.offset = targetPulse.offset;
|
|
||||||
targetPulse.offset = tempOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Swap their order
|
|
||||||
const tempOrder = currentPulse.order;
|
|
||||||
currentPulse.order = targetPulse.order;
|
|
||||||
targetPulse.order = tempOrder;
|
|
||||||
|
|
||||||
// Sort pulses by order
|
|
||||||
pattern.pulses.sort((a, b) => a.order - b.order);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function movePulseDown(pattern: Pattern, pulseId: string, step: number) {
|
|
||||||
const index = pattern.pulses.findIndex((pulse) => pulse.id === pulseId);
|
|
||||||
if (index === -1 || index + step >= pattern.pulses.length) return;
|
|
||||||
|
|
||||||
const targetIndex = index + step;
|
|
||||||
const targetPulse = pattern.pulses[targetIndex];
|
|
||||||
const currentPulse = pattern.pulses[index];
|
|
||||||
|
|
||||||
// Swap the pulses
|
|
||||||
pattern.pulses[targetIndex] = currentPulse;
|
|
||||||
pattern.pulses[index] = targetPulse;
|
|
||||||
|
|
||||||
// If the current pulse's order is 1, swap their offsets
|
|
||||||
if (currentPulse.order === 1) {
|
|
||||||
const tempOffset = currentPulse.offset;
|
|
||||||
currentPulse.offset = targetPulse.offset;
|
|
||||||
targetPulse.offset = tempOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Swap their order
|
|
||||||
const tempOrder = currentPulse.order;
|
|
||||||
currentPulse.order = targetPulse.order;
|
|
||||||
targetPulse.order = tempOrder;
|
|
||||||
|
|
||||||
// Sort pulses by order
|
|
||||||
pattern.pulses.sort((a, b) => a.order - b.order);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addPulse(pattern: Pattern): Pulse {
|
|
||||||
const maxOrder = reduce(pattern.pulses, (former, pulse) => Math.max(former, pulse.order), 0);
|
|
||||||
const newPulse = createPulse(
|
|
||||||
maxOrder + 1,
|
|
||||||
pattern.smoothRepeat
|
|
||||||
? get(pattern.pulses, '[0].width', 0)
|
|
||||||
: get(pattern.pulses, '[-1].width', 0),
|
|
||||||
pattern.smoothRepeat
|
|
||||||
? get(pattern.pulses, '[0].frequency', 1)
|
|
||||||
: get(pattern.pulses, '[-1].frequency', 1),
|
|
||||||
);
|
|
||||||
pattern.pulses.push(newPulse);
|
|
||||||
return newPulse;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updatePulse(pattern: Pattern, pulseId: string, pulse: Pulse) {
|
|
||||||
const index = pattern.pulses.findIndex((p) => p.id === pulseId);
|
|
||||||
if (index !== -1) {
|
|
||||||
const { id, order, ...rest } = pulse;
|
|
||||||
pattern.pulses[index] = { ...pattern.pulses[index], ...rest };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deletePulse(pattern: Pattern, pulseId: string) {
|
|
||||||
pattern.pulses = pattern.pulses.filter((pulse) => pulse.id !== pulseId);
|
|
||||||
pattern.pulses.sort((a, b) => a.order - b.order);
|
|
||||||
pattern.pulses.forEach((pulse, index) => {
|
|
||||||
pulse.order = index + 1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function durationRemains(pattern: Pattern): number {
|
|
||||||
return 20 * 1000 - totalDuration(pattern) - (pattern.smoothRepeat ? 100 : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function totalDuration(pattern: Pattern): number {
|
|
||||||
return reduce(
|
|
||||||
pattern.pulses,
|
|
||||||
(former, pulse) => former + pulse.offset,
|
|
||||||
pattern.smoothRepeat && pattern.pulses.length > 1 ? 100 : 0,
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
@layer pages {
|
|
||||||
.pulse_card {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: calc(var(--spacing) * 2);
|
|
||||||
color: var(--color-on-surface);
|
|
||||||
background-color: var(--color-surface);
|
|
||||||
border-radius: calc(var(--border-radius) * 2);
|
|
||||||
padding: 0;
|
|
||||||
.order {
|
|
||||||
padding: calc(var(--spacing) * 2) calc(var(--spacing) * 3);
|
|
||||||
background-color: var(--color-secondary-container);
|
|
||||||
color: var(--color-on-secondary-container);
|
|
||||||
border-top-left-radius: calc(var(--border-radius) * 2);
|
|
||||||
border-bottom-left-radius: calc(var(--border-radius) * 2);
|
|
||||||
}
|
|
||||||
.offset {
|
|
||||||
padding: calc(var(--spacing) * 2) calc(var(--spacing) * 3);
|
|
||||||
}
|
|
||||||
&.selected {
|
|
||||||
background-color: color-mix(in oklch, var(--color-on-surface) 18%, transparent);
|
|
||||||
.order {
|
|
||||||
background-color: color-mix(
|
|
||||||
in oklch,
|
|
||||||
var(--color-secondary-container) 88%,
|
|
||||||
var(--color-white)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
background-color: color-mix(in oklch, var(--color-on-surface) 8%, transparent);
|
|
||||||
box-shadow: var(--elevation-2-ambient), var(--elevation-2-umbra);
|
|
||||||
.order {
|
|
||||||
background-color: color-mix(
|
|
||||||
in oklch,
|
|
||||||
var(--color-secondary-container) 60%,
|
|
||||||
var(--color-white)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
background-color: color-mix(in oklch, var(--color-on-surface) 18%, transparent);
|
|
||||||
.order {
|
|
||||||
background-color: color-mix(
|
|
||||||
in oklch,
|
|
||||||
var(--color-secondary-container) 68%,
|
|
||||||
var(--color-white)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
import cx from 'clsx';
|
|
||||||
import { useAtom } from 'jotai';
|
|
||||||
import { FC, useCallback } from 'react';
|
|
||||||
import { Pulse } from '../../context/pattern-model';
|
|
||||||
import { SelectedPulseIdAtom } from '../../context/Patterns';
|
|
||||||
import styles from './PulseCard.module.css';
|
|
||||||
|
|
||||||
type PulseCardProps = {
|
|
||||||
pulse: Pulse;
|
|
||||||
};
|
|
||||||
|
|
||||||
const PulseCard: FC<PulseCardProps> = ({ pulse }) => {
|
|
||||||
const [selected, setSelected] = useAtom(SelectedPulseIdAtom);
|
|
||||||
|
|
||||||
const handleSelect = useCallback(() => {
|
|
||||||
if (selected === pulse.id) {
|
|
||||||
setSelected(null);
|
|
||||||
} else {
|
|
||||||
setSelected(pulse.id);
|
|
||||||
}
|
|
||||||
}, [pulse, selected]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cx(styles.pulse_card, selected === pulse.id && styles.selected)}
|
|
||||||
onClick={handleSelect}>
|
|
||||||
<div className={styles.order}>{pulse.order.toString().padStart(3, '0')}</div>
|
|
||||||
<div className="spacer" />
|
|
||||||
<div className={styles.offset}>+ {pulse.offset} ms</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PulseCard;
|
|
|
@ -5,17 +5,14 @@
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: calc(var(--spacing) * 2);
|
gap: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
.attribute_unit {
|
.attribute_unit {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: calc(var(--spacing));
|
gap: calc(var(--spacing));
|
||||||
font-size: var(--body-small-font-size);
|
|
||||||
line-height: var(--body-small-line-height);
|
|
||||||
font-weight: var(--body-small-font-weight);
|
|
||||||
label {
|
label {
|
||||||
font-weight: 500;
|
font-weight: bold;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.pulses {
|
.pulses {
|
||||||
|
|
|
@ -1,46 +1,39 @@
|
||||||
|
import { DndContext } from '@dnd-kit/core';
|
||||||
import { Icon } from '@iconify/react/dist/iconify.js';
|
import { Icon } from '@iconify/react/dist/iconify.js';
|
||||||
import { useAtom, useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { max } from 'lodash-es';
|
import { FC } from 'react';
|
||||||
import { FC, useCallback, useMemo } from 'react';
|
|
||||||
import { ScrollArea } from '../../components/ScrollArea';
|
import { ScrollArea } from '../../components/ScrollArea';
|
||||||
import { addPulse, deletePulse } from '../../context/pattern-model';
|
|
||||||
import {
|
import {
|
||||||
CurrentPatternAtom,
|
CurrentPatternAtom,
|
||||||
CurrentPatternDuration,
|
CurrentPatternDuration,
|
||||||
SelectedPulseAtom,
|
PulsesInCurrentPatternAtom,
|
||||||
useSavePattern,
|
|
||||||
} from '../../context/Patterns';
|
} from '../../context/Patterns';
|
||||||
import PulseCard from './PulseCard';
|
|
||||||
import styles from './PulseList.module.css';
|
import styles from './PulseList.module.css';
|
||||||
|
|
||||||
const PulseList: FC = () => {
|
const PulseList: FC = () => {
|
||||||
const [pattern, refreshPattern] = useAtom(CurrentPatternAtom);
|
const pattern = useAtomValue(CurrentPatternAtom);
|
||||||
|
const pulses = useAtomValue(PulsesInCurrentPatternAtom);
|
||||||
const duration = useAtomValue(CurrentPatternDuration);
|
const duration = useAtomValue(CurrentPatternDuration);
|
||||||
const selectedPulse = useAtomValue(SelectedPulseAtom);
|
|
||||||
const maxPulseOrder = useMemo(
|
|
||||||
() => max(pattern?.pulses.map((pulse) => pulse.order) ?? []),
|
|
||||||
[pattern],
|
|
||||||
);
|
|
||||||
const savePattern = useSavePattern();
|
|
||||||
|
|
||||||
const handleAddPulseAction = useCallback(async () => {
|
|
||||||
if (!pattern) return;
|
|
||||||
addPulse(pattern);
|
|
||||||
await savePattern(pattern);
|
|
||||||
refreshPattern();
|
|
||||||
}, [pattern]);
|
|
||||||
const handleDeletePulseAction = useCallback(async () => {
|
|
||||||
if (!pattern || !selectedPulse) return;
|
|
||||||
deletePulse(pattern, selectedPulse.id);
|
|
||||||
await savePattern(pattern);
|
|
||||||
refreshPattern();
|
|
||||||
}, [pattern, selectedPulse]);
|
|
||||||
|
|
||||||
return (
|
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.pulse_tools}>
|
||||||
<div className={styles.attribute_unit}>
|
<div className={styles.attribute_unit}>
|
||||||
<span>{pattern?.pulses.length ?? 0}</span>
|
<span>{pulses.length ?? 0}</span>
|
||||||
<label>Key Pulses</label>
|
<label>Key Pulses</label>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.attribute_unit}>
|
<div className={styles.attribute_unit}>
|
||||||
|
@ -48,43 +41,13 @@ const PulseList: FC = () => {
|
||||||
<span>{duration.toFixed(2)} s</span>
|
<span>{duration.toFixed(2)} s</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.pulse_tools}>
|
|
||||||
<button className="text" disabled>
|
|
||||||
<Icon icon="material-symbols-light:play-arrow-outline" />
|
|
||||||
<span>Test Run</span>
|
|
||||||
</button>
|
|
||||||
<button className="text" onClick={handleAddPulseAction}>
|
|
||||||
<Icon icon="material-symbols-light:add" />
|
|
||||||
<span>Add Pulse</span>
|
|
||||||
</button>
|
|
||||||
<button className="text" onClick={handleDeletePulseAction} disabled={!selectedPulse}>
|
|
||||||
<Icon icon="material-symbols-light:delete-forever-outline" />
|
|
||||||
<span>Delete Selected</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className={styles.pulse_tools}>
|
|
||||||
<button className="text" disabled={!selectedPulse || selectedPulse.order === 1}>
|
|
||||||
<Icon icon="material-symbols-light:arrow-upward" />
|
|
||||||
<span>Move Up</span>
|
|
||||||
</button>
|
|
||||||
<button className="text" disabled={!selectedPulse || selectedPulse.order === maxPulseOrder}>
|
|
||||||
<Icon icon="material-symbols-light:arrow-downward" />
|
|
||||||
<span>Move Down</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className={styles.pulses}>
|
<div className={styles.pulses}>
|
||||||
<ScrollArea enableY>
|
<ScrollArea enableY>
|
||||||
|
<DndContext>
|
||||||
<div className={styles.pulse_cards}>
|
<div className={styles.pulse_cards}>
|
||||||
{pattern?.pulses.length === 0 ? (
|
{pulses.length === 0 && <div className="empty_prompt">No key pulses.</div>}
|
||||||
<div className="empty_prompt">No key pulses.</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{pattern?.pulses.map((pulse) => (
|
|
||||||
<PulseCard key={pulse.id} pulse={pulse} />
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
</DndContext>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -6,8 +6,13 @@ import { FC, useCallback, useMemo } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { NotificationType, useNotification } from '../../components/Notifications';
|
import { NotificationType, useNotification } from '../../components/Notifications';
|
||||||
import PatternPreview from '../../components/PatternPreview';
|
import PatternPreview from '../../components/PatternPreview';
|
||||||
import { CurrentPatternAtom, PatternsAtom, SelectedPatternIdAtom } from '../../context/Patterns';
|
import {
|
||||||
import { Pattern, totalDuration } from '../../context/pattern-model';
|
CurrentPatternAtom,
|
||||||
|
Pattern,
|
||||||
|
PatternsAtom,
|
||||||
|
SelectedPatternIdAtom,
|
||||||
|
totalDuration,
|
||||||
|
} from '../../context/Patterns';
|
||||||
import styles from './PatternDetail.module.css';
|
import styles from './PatternDetail.module.css';
|
||||||
|
|
||||||
const EmptyPromption: FC = () => {
|
const EmptyPromption: FC = () => {
|
||||||
|
|
|
@ -5,8 +5,13 @@ 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 { PatternsAtom, SearchKeywordAtom, SelectedPatternIdAtom } from '../../context/Patterns';
|
import {
|
||||||
import { Pattern, totalDuration } from '../../context/pattern-model';
|
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 }) => {
|
||||||
|
|
|
@ -3,7 +3,7 @@ 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 { SelectedPatternIdAtom, useSavePattern } 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 = () => {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user