mirror of
https://github.com/jhbruhn/respira.git
synced 2026-03-13 18:28:41 +00:00
Compare commits
13 commits
7d42435056
...
60762d1526
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60762d1526 | ||
| 356e850147 | |||
| e07d6b9a6f | |||
| a6d9d266f8 | |||
| 0e504c3069 | |||
|
|
9f13d49487 | ||
| dac772713c | |||
| abe8dde159 | |||
| f301269efd | |||
| 5fea462a71 | |||
| 6cf491d921 | |||
| 1517c69595 | |||
| 1cf3d231fe |
11 changed files with 1637 additions and 3155 deletions
22
.github/workflows/autolabel.yaml
vendored
Normal file
22
.github/workflows/autolabel.yaml
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
name: Autolabel
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
autolabel:
|
||||
name: Autolabel PR
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Label PR
|
||||
id: drafter
|
||||
uses: release-drafter/release-drafter@v6
|
||||
with:
|
||||
disable-releaser: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
|
@ -32,4 +32,4 @@ jobs:
|
|||
run: npm run test:run
|
||||
|
||||
- name: Build application
|
||||
run: npm run build
|
||||
run: npm run web:build
|
||||
|
|
|
|||
9
.github/workflows/draft-release.yml
vendored
9
.github/workflows/draft-release.yml
vendored
|
|
@ -4,9 +4,6 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- main
|
||||
# pull_request event is required only for autolabeler
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
|
@ -24,6 +21,8 @@ jobs:
|
|||
- name: Draft release
|
||||
id: drafter
|
||||
uses: release-drafter/release-drafter@v6
|
||||
with:
|
||||
disable-autolabeler: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
|
@ -51,7 +50,7 @@ jobs:
|
|||
run: npm ci
|
||||
|
||||
- name: Build web app
|
||||
run: npm run build -- --base=/respira/
|
||||
run: npm run web:build -- --base=/respira/
|
||||
|
||||
- name: Create web artifact zip
|
||||
run: |
|
||||
|
|
@ -92,7 +91,7 @@ jobs:
|
|||
run: npm ci
|
||||
|
||||
- name: Package and make
|
||||
run: npm run make
|
||||
run: npm run electron:make
|
||||
|
||||
- name: Upload Windows artifacts
|
||||
if: matrix.os == 'windows-latest'
|
||||
|
|
|
|||
4390
package-lock.json
generated
4390
package-lock.json
generated
File diff suppressed because it is too large
Load diff
18
package.json
18
package.json
|
|
@ -8,18 +8,18 @@
|
|||
"license": "Apache-2.0",
|
||||
"main": ".vite/build/main.js",
|
||||
"scripts": {
|
||||
"dev": "vite --host 0.0.0.0",
|
||||
"dev:electron": "vite --mode electron",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"web:dev": "vite --host 0.0.0.0",
|
||||
"web:preview": "vite preview",
|
||||
"web:build": "tsc -b && vite build",
|
||||
"test": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:run": "vitest run",
|
||||
"start": "electron-forge start",
|
||||
"package": "electron-forge package",
|
||||
"make": "electron-forge make",
|
||||
"icons": "electron-icon-builder --input=./public/icons/icon.png --output=./public -f"
|
||||
"electron:dev": "vite --mode electron",
|
||||
"electron:start": "electron-forge start",
|
||||
"electron:package": "electron-forge package",
|
||||
"electron:make": "electron-forge make",
|
||||
"electron:icons": "electron-icon-builder --input=./public/icons/icon.png --output=./public -f"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.2.0",
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
"electron-squirrel-startup": "^1.0.1",
|
||||
"electron-store": "^10.0.0",
|
||||
"konva": "^10.0.12",
|
||||
"pyodide": "^0.27.4",
|
||||
"pyodide": "^0.29.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-konva": "^19.2.1",
|
||||
|
|
|
|||
70
src/App.tsx
70
src/App.tsx
|
|
@ -12,6 +12,7 @@ import { BluetoothDevicePicker } from './components/BluetoothDevicePicker';
|
|||
import { getErrorDetails } from './utils/errorCodeHelpers';
|
||||
import { getStateVisualInfo } from './utils/machineStateHelpers';
|
||||
import { CheckCircleIcon, BoltIcon, PauseCircleIcon, ExclamationTriangleIcon, ArrowPathIcon, XMarkIcon, InformationCircleIcon } from '@heroicons/react/24/solid';
|
||||
import { isBluetoothSupported } from './utils/bluetoothSupport';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
|
|
@ -333,27 +334,60 @@ function App() {
|
|||
<div className="flex-1 grid grid-cols-1 lg:grid-cols-[480px_1fr] gap-4 md:gap-5 lg:gap-6 lg:overflow-hidden">
|
||||
{/* Left Column - Controls */}
|
||||
<div className="flex flex-col gap-4 md:gap-5 lg:gap-6 lg:overflow-hidden">
|
||||
{/* Connect Button - Show when disconnected */}
|
||||
{/* Connect Button or Browser Hint - Show when disconnected */}
|
||||
{!isConnected && (
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md border-l-4 border-gray-400 dark:border-gray-600">
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
<div className="w-6 h-6 text-gray-600 dark:text-gray-400 flex-shrink-0 mt-0.5">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0" />
|
||||
</svg>
|
||||
<>
|
||||
{isBluetoothSupported() ? (
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md border-l-4 border-gray-400 dark:border-gray-600">
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
<div className="w-6 h-6 text-gray-600 dark:text-gray-400 flex-shrink-0 mt-0.5">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-1">Get Started</h3>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">Connect to your embroidery machine</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={connect}
|
||||
className="w-full flex items-center justify-center gap-2 px-3 py-2.5 sm:py-2 bg-blue-600 dark:bg-blue-700 text-white rounded font-semibold text-sm hover:bg-blue-700 dark:hover:bg-blue-600 active:bg-blue-800 dark:active:bg-blue-500 transition-colors cursor-pointer"
|
||||
>
|
||||
Connect to Machine
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-1">Get Started</h3>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">Connect to your embroidery machine</p>
|
||||
) : (
|
||||
<div className="bg-amber-50 dark:bg-amber-900/20 p-4 rounded-lg shadow-md border-l-4 border-amber-500 dark:border-amber-600">
|
||||
<div className="flex items-start gap-3">
|
||||
<ExclamationTriangleIcon className="w-6 h-6 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-base font-semibold text-amber-900 dark:text-amber-100 mb-2">Browser Not Supported</h3>
|
||||
<p className="text-sm text-amber-800 dark:text-amber-200 mb-3">
|
||||
Your browser doesn't support Web Bluetooth, which is required to connect to your embroidery machine.
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-semibold text-amber-900 dark:text-amber-100">Please try one of these options:</p>
|
||||
<ul className="text-sm text-amber-800 dark:text-amber-200 space-y-1.5 ml-4 list-disc">
|
||||
<li>Use a supported browser (Chrome, Edge, or Opera)</li>
|
||||
<li>
|
||||
Download the Desktop app from{' '}
|
||||
<a
|
||||
href="https://github.com/jhbruhn/respira/releases/latest"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-semibold underline hover:text-amber-900 dark:hover:text-amber-50 transition-colors"
|
||||
>
|
||||
GitHub Releases
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={connect}
|
||||
className="w-full flex items-center justify-center gap-2 px-3 py-2.5 sm:py-2 bg-blue-600 dark:bg-blue-700 text-white rounded font-semibold text-sm hover:bg-blue-700 dark:hover:bg-blue-600 active:bg-blue-800 dark:active:bg-blue-500 transition-colors cursor-pointer"
|
||||
>
|
||||
Connect to Machine
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Pattern File - Show during upload stage (before pattern is uploaded) */}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { useUIStore } from '../stores/useUIStore';
|
|||
import { convertPesToPen, type PesPatternData } from '../formats/import/pesImporter';
|
||||
import { canUploadPattern, getMachineStateCategory } from '../utils/machineStateHelpers';
|
||||
import { PatternInfoSkeleton } from './SkeletonLoader';
|
||||
import { PatternInfo } from './PatternInfo';
|
||||
import { ArrowUpTrayIcon, CheckCircleIcon, DocumentTextIcon, FolderOpenIcon } from '@heroicons/react/24/solid';
|
||||
import { createFileService } from '../platform';
|
||||
import type { IFileService } from '../platform/interfaces/IFileService';
|
||||
|
|
@ -200,66 +201,7 @@ export function FileUpload() {
|
|||
|
||||
{!isLoading && pesData && (
|
||||
<div className="mb-3">
|
||||
<div className="grid grid-cols-3 gap-2 text-xs mb-2">
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded">
|
||||
<span className="text-gray-600 dark:text-gray-400 block">Size</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{((pesData.bounds.maxX - pesData.bounds.minX) / 10).toFixed(1)} x{' '}
|
||||
{((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(1)} mm
|
||||
</span>
|
||||
</div>
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded">
|
||||
<span className="text-gray-600 dark:text-gray-400 block">Stitches</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{pesData.stitchCount.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded">
|
||||
<span className="text-gray-600 dark:text-gray-400 block">Colors / Blocks</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{pesData.uniqueColors.length} / {pesData.threads.length}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">Colors:</span>
|
||||
<div className="flex gap-1">
|
||||
{pesData.uniqueColors.slice(0, 8).map((color, idx) => {
|
||||
// Primary metadata: brand and catalog number
|
||||
const primaryMetadata = [
|
||||
color.brand,
|
||||
color.catalogNumber ? `#${color.catalogNumber}` : null
|
||||
].filter(Boolean).join(" ");
|
||||
|
||||
// Secondary metadata: chart and description
|
||||
const secondaryMetadata = [
|
||||
color.chart,
|
||||
color.description
|
||||
].filter(Boolean).join(" ");
|
||||
|
||||
const metadata = [primaryMetadata, secondaryMetadata].filter(Boolean).join(" • ");
|
||||
|
||||
const tooltipText = metadata
|
||||
? `Color ${idx + 1}: ${color.hex} - ${metadata}`
|
||||
: `Color ${idx + 1}: ${color.hex}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className="w-3 h-3 rounded-full border border-gray-300 dark:border-gray-600"
|
||||
style={{ backgroundColor: color.hex }}
|
||||
title={tooltipText}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{pesData.uniqueColors.length > 8 && (
|
||||
<div className="w-3 h-3 rounded-full bg-gray-300 dark:bg-gray-600 border border-gray-400 dark:border-gray-500 flex items-center justify-center text-xs font-bold text-gray-600 dark:text-gray-300 leading-none">
|
||||
+{pesData.uniqueColors.length - 8}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<PatternInfo pesData={pesData} showThreadBlocks />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
92
src/components/PatternInfo.tsx
Normal file
92
src/components/PatternInfo.tsx
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import type { PesPatternData } from '../formats/import/pesImporter';
|
||||
|
||||
interface PatternInfoProps {
|
||||
pesData: PesPatternData;
|
||||
showThreadBlocks?: boolean;
|
||||
}
|
||||
|
||||
export function PatternInfo({ pesData, showThreadBlocks = false }: PatternInfoProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-3 gap-2 text-xs mb-2">
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded">
|
||||
<span className="text-gray-600 dark:text-gray-400 block">Size</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{((pesData.bounds.maxX - pesData.bounds.minX) / 10).toFixed(1)} x{' '}
|
||||
{((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(1)} mm
|
||||
</span>
|
||||
</div>
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded">
|
||||
<span className="text-gray-600 dark:text-gray-400 block">Stitches</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{pesData.penStitches?.stitches.length.toLocaleString() || pesData.stitchCount.toLocaleString()}
|
||||
{pesData.penStitches && pesData.penStitches.stitches.length !== pesData.stitchCount && (
|
||||
<span
|
||||
className="text-gray-500 dark:text-gray-500 font-normal ml-1"
|
||||
title="Input stitch count from PES file (lock stitches were added for machine compatibility)"
|
||||
>
|
||||
({pesData.stitchCount.toLocaleString()})
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded">
|
||||
<span className="text-gray-600 dark:text-gray-400 block">
|
||||
{showThreadBlocks ? 'Colors / Blocks' : 'Colors'}
|
||||
</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{showThreadBlocks
|
||||
? `${pesData.uniqueColors.length} / ${pesData.threads.length}`
|
||||
: pesData.uniqueColors.length
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">Colors:</span>
|
||||
<div className="flex gap-1">
|
||||
{pesData.uniqueColors.slice(0, 8).map((color, idx) => {
|
||||
// Primary metadata: brand and catalog number
|
||||
const primaryMetadata = [
|
||||
color.brand,
|
||||
color.catalogNumber ? `#${color.catalogNumber}` : null
|
||||
].filter(Boolean).join(" ");
|
||||
|
||||
// Secondary metadata: chart and description
|
||||
const secondaryMetadata = [
|
||||
color.chart,
|
||||
color.description
|
||||
].filter(Boolean).join(" ");
|
||||
|
||||
const metadata = [primaryMetadata, secondaryMetadata].filter(Boolean).join(" • ");
|
||||
|
||||
// Show which thread blocks use this color in PatternSummaryCard
|
||||
const threadNumbers = color.threadIndices.map(i => i + 1).join(", ");
|
||||
const tooltipText = showThreadBlocks
|
||||
? (metadata
|
||||
? `Color ${idx + 1}: ${color.hex} - ${metadata}`
|
||||
: `Color ${idx + 1}: ${color.hex}`)
|
||||
: (metadata
|
||||
? `Color ${idx + 1}: ${color.hex}\n${metadata}\nUsed in thread blocks: ${threadNumbers}`
|
||||
: `Color ${idx + 1}: ${color.hex}\nUsed in thread blocks: ${threadNumbers}`);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className="w-3 h-3 rounded-full border border-gray-300 dark:border-gray-600"
|
||||
style={{ backgroundColor: color.hex }}
|
||||
title={tooltipText}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{pesData.uniqueColors.length > 8 && (
|
||||
<div className="w-3 h-3 rounded-full bg-gray-300 dark:bg-gray-600 border border-gray-400 dark:border-gray-500 flex items-center justify-center text-xs font-bold text-gray-600 dark:text-gray-300 leading-none">
|
||||
+{pesData.uniqueColors.length - 8}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import { useShallow } from 'zustand/react/shallow';
|
|||
import { useMachineStore } from '../stores/useMachineStore';
|
||||
import { usePatternStore } from '../stores/usePatternStore';
|
||||
import { canDeletePattern } from '../utils/machineStateHelpers';
|
||||
import { PatternInfo } from './PatternInfo';
|
||||
import { DocumentTextIcon, TrashIcon } from '@heroicons/react/24/solid';
|
||||
|
||||
export function PatternSummaryCard() {
|
||||
|
|
@ -44,68 +45,7 @@ export function PatternSummaryCard() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-2 text-xs mb-3">
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded">
|
||||
<span className="text-gray-600 dark:text-gray-400 block">Size</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{((pesData.bounds.maxX - pesData.bounds.minX) / 10).toFixed(1)} x{' '}
|
||||
{((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(1)} mm
|
||||
</span>
|
||||
</div>
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded">
|
||||
<span className="text-gray-600 dark:text-gray-400 block">Stitches</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{pesData.stitchCount.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded">
|
||||
<span className="text-gray-600 dark:text-gray-400 block">Colors</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{pesData.uniqueColors.length}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">Colors:</span>
|
||||
<div className="flex gap-1">
|
||||
{pesData.uniqueColors.slice(0, 8).map((color, idx) => {
|
||||
// Primary metadata: brand and catalog number
|
||||
const primaryMetadata = [
|
||||
color.brand,
|
||||
color.catalogNumber ? `#${color.catalogNumber}` : null
|
||||
].filter(Boolean).join(" ");
|
||||
|
||||
// Secondary metadata: chart and description
|
||||
const secondaryMetadata = [
|
||||
color.chart,
|
||||
color.description
|
||||
].filter(Boolean).join(" ");
|
||||
|
||||
const metadata = [primaryMetadata, secondaryMetadata].filter(Boolean).join(" • ");
|
||||
|
||||
// Show which thread blocks use this color
|
||||
const threadNumbers = color.threadIndices.map(i => i + 1).join(", ");
|
||||
const tooltipText = metadata
|
||||
? `Color ${idx + 1}: ${color.hex}\n${metadata}\nUsed in thread blocks: ${threadNumbers}`
|
||||
: `Color ${idx + 1}: ${color.hex}\nUsed in thread blocks: ${threadNumbers}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className="w-3 h-3 rounded-full border border-gray-300 dark:border-gray-600"
|
||||
style={{ backgroundColor: color.hex }}
|
||||
title={tooltipText}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{pesData.uniqueColors.length > 8 && (
|
||||
<div className="w-3 h-3 rounded-full bg-gray-300 dark:bg-gray-600 border border-gray-400 dark:border-gray-500 flex items-center justify-center text-xs font-bold text-gray-600 dark:text-gray-300 leading-none">
|
||||
+{pesData.uniqueColors.length - 8}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<PatternInfo pesData={pesData} />
|
||||
|
||||
{canDelete && (
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -60,9 +60,9 @@ export function ProgressMonitor() {
|
|||
? ((sewingProgress?.currentStitch || 0) / patternInfo.totalStitches) * 100
|
||||
: 0;
|
||||
|
||||
// Calculate color block information from pesData
|
||||
// Calculate color block information from decoded penStitches
|
||||
const colorBlocks = useMemo(() => {
|
||||
if (!pesData) return [];
|
||||
if (!pesData || !pesData.penStitches) return [];
|
||||
|
||||
const blocks: Array<{
|
||||
colorIndex: number;
|
||||
|
|
@ -76,34 +76,20 @@ export function ProgressMonitor() {
|
|||
threadChart: string | null;
|
||||
}> = [];
|
||||
|
||||
let currentColorIndex = pesData.stitches[0]?.[3] ?? 0;
|
||||
let blockStartStitch = 0;
|
||||
|
||||
for (let i = 0; i < pesData.stitches.length; i++) {
|
||||
const stitchColorIndex = pesData.stitches[i][3];
|
||||
|
||||
// When color changes, save the previous block
|
||||
if (
|
||||
stitchColorIndex !== currentColorIndex ||
|
||||
i === pesData.stitches.length - 1
|
||||
) {
|
||||
const endStitch = i === pesData.stitches.length - 1 ? i + 1 : i;
|
||||
const thread = pesData.threads[currentColorIndex];
|
||||
blocks.push({
|
||||
colorIndex: currentColorIndex,
|
||||
threadHex: thread?.hex || "#000000",
|
||||
threadCatalogNumber: thread?.catalogNumber ?? null,
|
||||
threadBrand: thread?.brand ?? null,
|
||||
threadDescription: thread?.description ?? null,
|
||||
threadChart: thread?.chart ?? null,
|
||||
startStitch: blockStartStitch,
|
||||
endStitch: endStitch,
|
||||
stitchCount: endStitch - blockStartStitch,
|
||||
});
|
||||
|
||||
currentColorIndex = stitchColorIndex;
|
||||
blockStartStitch = i;
|
||||
}
|
||||
// Use the pre-computed color blocks from decoded PEN data
|
||||
for (const penBlock of pesData.penStitches.colorBlocks) {
|
||||
const thread = pesData.threads[penBlock.colorIndex];
|
||||
blocks.push({
|
||||
colorIndex: penBlock.colorIndex,
|
||||
threadHex: thread?.hex || "#000000",
|
||||
threadCatalogNumber: thread?.catalogNumber ?? null,
|
||||
threadBrand: thread?.brand ?? null,
|
||||
threadDescription: thread?.description ?? null,
|
||||
threadChart: thread?.chart ?? null,
|
||||
startStitch: penBlock.startStitchIndex,
|
||||
endStitch: penBlock.endStitchIndex,
|
||||
stitchCount: penBlock.endStitchIndex - penBlock.startStitchIndex,
|
||||
});
|
||||
}
|
||||
|
||||
return blocks;
|
||||
|
|
|
|||
17
src/utils/bluetoothSupport.ts
Normal file
17
src/utils/bluetoothSupport.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* Check if the current browser/environment supports Web Bluetooth API
|
||||
* @returns true if Web Bluetooth is supported (or running in Electron), false otherwise
|
||||
*/
|
||||
export function isBluetoothSupported(): boolean {
|
||||
// Always supported in Electron app
|
||||
if (typeof window !== "undefined" && "electronAPI" in window) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for Web Bluetooth API support in browser
|
||||
if (typeof navigator !== "undefined" && "bluetooth" in navigator) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
Loading…
Reference in a new issue