Compare commits
	
		
			10 Commits
		
	
	
		
			32d6e0c875
			...
			d2a854490f
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					d2a854490f | ||
| 
						 | 
					d465951f31 | ||
| 
						 | 
					55b10f5a5c | ||
| 
						 | 
					edf4163e38 | ||
| 
						 | 
					09cba205a4 | ||
| 
						 | 
					eea7446346 | ||
| 
						 | 
					429d6451c4 | ||
| 
						 | 
					128a45ad77 | ||
| 
						 | 
					66a85f29f9 | ||
| 
						 | 
					d1d8def602 | 
@@ -143,3 +143,17 @@ pub async fn list_patterns(
 | 
			
		||||
        .map_err(|e| errors::AppError::StorageFailure(e.to_string()))?;
 | 
			
		||||
    Ok(patterns)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[tauri::command]
 | 
			
		||||
pub async fn save_pattern(
 | 
			
		||||
    app_state: State<'_, Arc<RwLock<AppState>>>,
 | 
			
		||||
    pattern: Pattern,
 | 
			
		||||
) -> Result<(), errors::AppError> {
 | 
			
		||||
    let state = app_state.read().await;
 | 
			
		||||
    state
 | 
			
		||||
        .db
 | 
			
		||||
        .store_pattern(&pattern)
 | 
			
		||||
        .await
 | 
			
		||||
        .map_err(|e| errors::AppError::StorageFailure(e.to_string()))?;
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -65,7 +65,8 @@ pub fn run() {
 | 
			
		||||
            cmd::activate_central_adapter,
 | 
			
		||||
            cmd::start_scan_devices,
 | 
			
		||||
            cmd::stop_scan_devices,
 | 
			
		||||
            cmd::list_patterns
 | 
			
		||||
            cmd::list_patterns,
 | 
			
		||||
            cmd::save_pattern
 | 
			
		||||
        ])
 | 
			
		||||
        .run(tauri::generate_context!())
 | 
			
		||||
        .expect("error while running tauri application");
 | 
			
		||||
 
 | 
			
		||||
@@ -227,10 +227,12 @@
 | 
			
		||||
        --button-text: var(--color-info);
 | 
			
		||||
      }
 | 
			
		||||
      &:hover {
 | 
			
		||||
        color: var(--button-text);
 | 
			
		||||
        background-color: color-mix(in oklch, var(--button-text) 8%, transparent);
 | 
			
		||||
        box-shadow: var(--elevation-0);
 | 
			
		||||
      }
 | 
			
		||||
      &:active {
 | 
			
		||||
        color: var(--button-text);
 | 
			
		||||
        background-color: color-mix(in oklch, var(--button-text) 18%, transparent);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
@@ -260,11 +262,13 @@
 | 
			
		||||
        --button-text: var(--color-info);
 | 
			
		||||
      }
 | 
			
		||||
      &:hover {
 | 
			
		||||
        --button-surface: color-mix(in oklch, var(--button-text) 8%, transparent);
 | 
			
		||||
        color: var(--button-text);
 | 
			
		||||
        background-color: color-mix(in oklch, var(--button-text) 8%, transparent);
 | 
			
		||||
        box-shadow: var(--elevation-0);
 | 
			
		||||
      }
 | 
			
		||||
      &:active {
 | 
			
		||||
        --button-surface: color-mix(in oklch, var(--button-text) 18%, transparent);
 | 
			
		||||
        color: var(--button-text);
 | 
			
		||||
        background-color: color-mix(in oklch, var(--button-text) 18%, transparent);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    &.text:disabled {
 | 
			
		||||
@@ -274,6 +278,7 @@
 | 
			
		||||
    &.icon:not(:disabled) {
 | 
			
		||||
      --button-text: var(--color-on-surface-variant);
 | 
			
		||||
      --button-surface: transparent;
 | 
			
		||||
      padding: calc(var(--spacing) * 1);
 | 
			
		||||
      &.selected {
 | 
			
		||||
        --button-text: var(--color-primary);
 | 
			
		||||
        &.secondary {
 | 
			
		||||
@@ -296,20 +301,24 @@
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      &:hover {
 | 
			
		||||
        --button-surface: color-mix(in oklch, var(--button-text) 8%, transparent);
 | 
			
		||||
        color: var(--button-text);
 | 
			
		||||
        background-color: color-mix(in oklch, var(--button-text) 8%, transparent);
 | 
			
		||||
        box-shadow: var(--elevation-0);
 | 
			
		||||
      }
 | 
			
		||||
      &:active {
 | 
			
		||||
        --button-surface: color-mix(in oklch, var(--button-text) 18%, transparent);
 | 
			
		||||
        color: var(--button-text);
 | 
			
		||||
        background-color: color-mix(in oklch, var(--button-text) 18%, transparent);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    &.icon:disabled {
 | 
			
		||||
      --button-text: color-mix(in oklch, var(--color-on-surface) 38%, transparent);
 | 
			
		||||
      --button-surface: transparent;
 | 
			
		||||
      padding: calc(var(--spacing) * 1);
 | 
			
		||||
    }
 | 
			
		||||
    &.filled_icon:not(:disabled) {
 | 
			
		||||
      --button-text: var(--color-on-primary);
 | 
			
		||||
      --button-surface: var(--color-primary);
 | 
			
		||||
      padding: calc(var(--spacing) * 1);
 | 
			
		||||
      &.secondary {
 | 
			
		||||
        --button-text: var(--color-on-secondary);
 | 
			
		||||
        --button-surface: var(--color-secondary);
 | 
			
		||||
@@ -342,10 +351,12 @@
 | 
			
		||||
    &.filled_icon:disabled {
 | 
			
		||||
      --button-text: color-mix(in oklch, var(--color-on-surface) 38%, transparent);
 | 
			
		||||
      --button-surface: color-mix(in oklch, var(--color-on-surface) 12%, transparent);
 | 
			
		||||
      padding: calc(var(--spacing) * 1);
 | 
			
		||||
    }
 | 
			
		||||
    &.tonal_icon:not(:disabled) {
 | 
			
		||||
      --button-text: var(--color-on-primary-container);
 | 
			
		||||
      --button-surface: var(--color-primary-container);
 | 
			
		||||
      padding: calc(var(--spacing) * 1);
 | 
			
		||||
      &.secondary {
 | 
			
		||||
        --button-text: var(--color-on-secondary-container);
 | 
			
		||||
        --button-surface: var(--color-secondary-container);
 | 
			
		||||
@@ -360,52 +371,59 @@
 | 
			
		||||
      }
 | 
			
		||||
      &.warn {
 | 
			
		||||
        --button-text: var(--color-on-warning-container);
 | 
			
		||||
        --button-surface: var(--color-warning);
 | 
			
		||||
        --button-surface: var(--color-warning-container);
 | 
			
		||||
      }
 | 
			
		||||
      &.success {
 | 
			
		||||
        --button-text: var(--color-on-success-container);
 | 
			
		||||
        --button-surface: var(--color-success-container);
 | 
			
		||||
      }
 | 
			
		||||
      &.info {
 | 
			
		||||
        --button-text: var(--color-on-info);
 | 
			
		||||
        --button-surface: var(--color-info);
 | 
			
		||||
        --button-text: var(--color-on-info-container);
 | 
			
		||||
        --button-surface: var(--color-info-container);
 | 
			
		||||
      }
 | 
			
		||||
      &.unselected {
 | 
			
		||||
        --button-text: var(--color-on-surface-variant);
 | 
			
		||||
        --button-surface: var(--color-surface-container-highest);
 | 
			
		||||
      }
 | 
			
		||||
      &:hover {
 | 
			
		||||
        --button-surface: color-mix(in oklch, var(--button-text) 8%, transparent);
 | 
			
		||||
        color: var(--button-surface);
 | 
			
		||||
        background-color: color-mix(in oklch, var(--button-surface) 8%, transparent);
 | 
			
		||||
        box-shadow: var(--elevation-0);
 | 
			
		||||
      }
 | 
			
		||||
      &:active {
 | 
			
		||||
        --button-surface: color-mix(in oklch, var(--button-text) 18%, transparent);
 | 
			
		||||
        color: var(--button-surface);
 | 
			
		||||
        background-color: color-mix(in oklch, var(--button-surface) 18%, transparent);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    &.tonal_icon:disabled {
 | 
			
		||||
      --button-text: color-mix(in oklch, var(--color-on-surface) 38%, transparent);
 | 
			
		||||
      --button-surface: color-mix(in oklch, var(--color-on-surface) 12%, transparent);
 | 
			
		||||
      padding: calc(var(--spacing) * 1);
 | 
			
		||||
    }
 | 
			
		||||
    &.outlined_icon:not(:disabled) {
 | 
			
		||||
      --button-text: var(--color-on-surface-variant);
 | 
			
		||||
      --button-surface: transparent;
 | 
			
		||||
      border: 1px solid var(--button-outline);
 | 
			
		||||
      padding: calc(var(--spacing) * 1);
 | 
			
		||||
      &.selected {
 | 
			
		||||
        --button-text: var(--color-inverse-on-surface);
 | 
			
		||||
        --button-surface: var(--color-inverse-surface);
 | 
			
		||||
      }
 | 
			
		||||
      &:hover {
 | 
			
		||||
        --button-surface: color-mix(in oklch, var(--button-text) 8%, transparent);
 | 
			
		||||
        color: var(--button-text);
 | 
			
		||||
        background-color: color-mix(in oklch, var(--button-text) 8%, transparent);
 | 
			
		||||
        box-shadow: var(--elevation-0);
 | 
			
		||||
      }
 | 
			
		||||
      &:active {
 | 
			
		||||
        --button-surface: color-mix(in oklch, var(--button-text) 18%, transparent);
 | 
			
		||||
        color: var(--button-text);
 | 
			
		||||
        background-color: color-mix(in oklch, var(--button-text) 18%, transparent);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    &.outlined_icon:disabled {
 | 
			
		||||
      --button-text: color-mix(in oklch, var(--color-on-surface) 38%, transparent);
 | 
			
		||||
      --button-surface: color-mix(in oklch, var(--color-on-surface) 12%, transparent);
 | 
			
		||||
      border: 1px solid color-mix(in oklch, var(--color-on-surface) 12%, transparent);
 | 
			
		||||
      padding: calc(var(--spacing) * 1);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										47
									
								
								src/components/Switch.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/components/Switch.module.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
@layer components {
 | 
			
		||||
  .switch {
 | 
			
		||||
    display: inline-flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    gap: 1em;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    &[aria-disabled='true'] {
 | 
			
		||||
      cursor: not-allowed;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  .switch_handle {
 | 
			
		||||
    position: relative;
 | 
			
		||||
    width: calc(var(--spacing) * 9);
 | 
			
		||||
    height: calc(var(--spacing) * 5);
 | 
			
		||||
    border: 1px solid var(--color-primary);
 | 
			
		||||
    border-radius: calc(var(--border-radius) * 2);
 | 
			
		||||
    background-color: color-mix(in oklch, var(--color-surface-container-high) 38%, transparent);
 | 
			
		||||
    &::before {
 | 
			
		||||
      content: '';
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      transform: translate(2px, 3px);
 | 
			
		||||
      width: calc(var(--spacing) * 3);
 | 
			
		||||
      height: calc(var(--spacing) * 3);
 | 
			
		||||
      border-radius: calc(var(--border-radius) * 2);
 | 
			
		||||
      background-color: var(--color-primary);
 | 
			
		||||
      transition: transform 0.2s ease-in-out;
 | 
			
		||||
    }
 | 
			
		||||
    &.checked {
 | 
			
		||||
      background-color: color-mix(in oklch, var(--color-primary) 38%, transparent);
 | 
			
		||||
      &::before {
 | 
			
		||||
        background-color: var(--color-primary);
 | 
			
		||||
        transform: translate(calc(var(--spacing) * 5 - 2px), 3px);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    [aria-disabled='true'] & {
 | 
			
		||||
      --disabled-color: var(--color-on-surface);
 | 
			
		||||
      background-color: color-mix(in oklch, var(--disabled-color) 8%, transparent);
 | 
			
		||||
      border-color: var(--disabled-color);
 | 
			
		||||
      &.checked {
 | 
			
		||||
        background-color: color-mix(in oklch, var(--disabled-color) 20%, transparent);
 | 
			
		||||
      }
 | 
			
		||||
      &::before {
 | 
			
		||||
        background-color: color-mix(in oklch, var(--disabled-color) 38%, transparent);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										40
									
								
								src/components/Switch.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/components/Switch.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
import cx from 'clsx';
 | 
			
		||||
import { FC, useCallback, useEffect, useState } from 'react';
 | 
			
		||||
import styles from './Switch.module.css';
 | 
			
		||||
 | 
			
		||||
type SwitchProps = {
 | 
			
		||||
  name?: string;
 | 
			
		||||
  checked?: boolean;
 | 
			
		||||
  disabled?: boolean;
 | 
			
		||||
  onChange?: (checked: boolean) => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const Switch: FC<SwitchProps> = ({ name, checked = false, disabled = false, onChange }) => {
 | 
			
		||||
  const [isChecked, setChecked] = useState(checked);
 | 
			
		||||
  const handleChange = useCallback(() => {
 | 
			
		||||
    if (!disabled) {
 | 
			
		||||
      setChecked((prev) => !prev);
 | 
			
		||||
      onChange?.(!isChecked);
 | 
			
		||||
    }
 | 
			
		||||
  }, [disabled, onChange, isChecked]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (checked !== isChecked) {
 | 
			
		||||
      setChecked(checked);
 | 
			
		||||
    }
 | 
			
		||||
  }, [checked]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div aria-disabled={disabled} className={styles.switch}>
 | 
			
		||||
      <div
 | 
			
		||||
        className={cx(styles.switch_handle, isChecked && styles.checked)}
 | 
			
		||||
        onClick={handleChange}
 | 
			
		||||
      />
 | 
			
		||||
      {name !== undefined && (
 | 
			
		||||
        <input type="hidden" name={name} value={isChecked ? 'true' : 'false'} />
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Switch;
 | 
			
		||||
@@ -9,5 +9,8 @@
 | 
			
		||||
    border-radius: calc(var(--border-radius) * 2);
 | 
			
		||||
    color: var(--color-light-on-surface);
 | 
			
		||||
    background-color: var(--color-surface-container);
 | 
			
		||||
    .content {
 | 
			
		||||
      max-width: 300px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										19
									
								
								src/page-components/pattern-editor/PatternHeader.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/page-components/pattern-editor/PatternHeader.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
import { Icon } from '@iconify/react/dist/iconify.js';
 | 
			
		||||
import { FC } from 'react';
 | 
			
		||||
import EditableContent from '../../components/EditableContent';
 | 
			
		||||
import styles from './PatternHeader.module.css';
 | 
			
		||||
 | 
			
		||||
const PatternHeader: FC = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={styles.pattern_header}>
 | 
			
		||||
      <EditableContent as="h3" placeholder="Pattern Name" additionalClassName={styles.content} />
 | 
			
		||||
      <div className="spacer" />
 | 
			
		||||
      <button className="tonal">
 | 
			
		||||
        <Icon icon="material-symbols-light:close" />
 | 
			
		||||
        Close Edit
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default PatternHeader;
 | 
			
		||||
@@ -0,0 +1,19 @@
 | 
			
		||||
@layer pages {
 | 
			
		||||
  .pattern_overview {
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    align-items: stretch;
 | 
			
		||||
    gap: calc(var(--spacing));
 | 
			
		||||
    padding: calc(var(--spacing) * 2) calc(var(--spacing));
 | 
			
		||||
    border-radius: calc(var(--border-radius) * 2);
 | 
			
		||||
    color: var(--color-on-surface);
 | 
			
		||||
    background-color: var(--color-surface-container);
 | 
			
		||||
  }
 | 
			
		||||
  .attribute_row {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    gap: calc(var(--spacing) * 2);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								src/page-components/pattern-editor/PatternOverview.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/page-components/pattern-editor/PatternOverview.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
import { FC } from 'react';
 | 
			
		||||
import Switch from '../../components/Switch';
 | 
			
		||||
import styles from './PatternOverview.module.css';
 | 
			
		||||
import PulseList from './PulseList';
 | 
			
		||||
 | 
			
		||||
const PatternOverview: FC = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={styles.pattern_overview}>
 | 
			
		||||
      <div className={styles.attribute_row}>
 | 
			
		||||
        <label>Smooth Repeat</label>
 | 
			
		||||
        <Switch />
 | 
			
		||||
      </div>
 | 
			
		||||
      <PulseList />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default PatternOverview;
 | 
			
		||||
@@ -0,0 +1,13 @@
 | 
			
		||||
@layer pages {
 | 
			
		||||
  .pulse_attributes {
 | 
			
		||||
    flex: 2;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    align-items: stretch;
 | 
			
		||||
    gap: calc(var(--spacing) * 2);
 | 
			
		||||
    padding: calc(var(--spacing) * 2) calc(var(--spacing));
 | 
			
		||||
    border-radius: calc(var(--border-radius) * 2);
 | 
			
		||||
    color: var(--color-on-surface);
 | 
			
		||||
    background-color: var(--color-surface-container);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										8
									
								
								src/page-components/pattern-editor/PulseAttributes.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/page-components/pattern-editor/PulseAttributes.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
import { FC } from 'react';
 | 
			
		||||
import styles from './PulseAttributes.module.css';
 | 
			
		||||
 | 
			
		||||
const PulseAttributes: FC = () => {
 | 
			
		||||
  return <div className={styles.pulse_attributes}></div>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default PulseAttributes;
 | 
			
		||||
							
								
								
									
										20
									
								
								src/page-components/pattern-editor/PulseList.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/page-components/pattern-editor/PulseList.module.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
@layer page {
 | 
			
		||||
  .pulse_tools {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
    justify-content: flex-end;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    gap: calc(var(--spacing) * 2);
 | 
			
		||||
  }
 | 
			
		||||
  .pulses {
 | 
			
		||||
    min-width: 0;
 | 
			
		||||
    min-height: 0;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    padding-inline: calc(var(--spacing) * 2);
 | 
			
		||||
    padding-block-end: calc(var(--spacing));
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    align-items: stretch;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								src/page-components/pattern-editor/PulseList.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/page-components/pattern-editor/PulseList.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
import { Icon } from '@iconify/react/dist/iconify.js';
 | 
			
		||||
import { FC } from 'react';
 | 
			
		||||
import { ScrollArea } from '../../components/ScrollArea';
 | 
			
		||||
import styles from './PulseList.module.css';
 | 
			
		||||
 | 
			
		||||
const PulseList: FC = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <div className={styles.pulse_tools}>
 | 
			
		||||
        <button className="text">
 | 
			
		||||
          <Icon icon="material-symbols-light:add" />
 | 
			
		||||
          <span>Add Pulse</span>
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className={styles.pulses}>
 | 
			
		||||
        <ScrollArea enableY></ScrollArea>
 | 
			
		||||
      </div>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default PulseList;
 | 
			
		||||
		Reference in New Issue
	
	Block a user