mirror of
https://github.com/jhbruhn/respira.git
synced 2026-03-14 02:38: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
|
run: npm run test:run
|
||||||
|
|
||||||
- name: Build application
|
- 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:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
# pull_request event is required only for autolabeler
|
|
||||||
pull_request:
|
|
||||||
types: [opened, reopened, synchronize]
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
@ -24,6 +21,8 @@ jobs:
|
||||||
- name: Draft release
|
- name: Draft release
|
||||||
id: drafter
|
id: drafter
|
||||||
uses: release-drafter/release-drafter@v6
|
uses: release-drafter/release-drafter@v6
|
||||||
|
with:
|
||||||
|
disable-autolabeler: true
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
|
@ -51,7 +50,7 @@ jobs:
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Build web app
|
- name: Build web app
|
||||||
run: npm run build -- --base=/respira/
|
run: npm run web:build -- --base=/respira/
|
||||||
|
|
||||||
- name: Create web artifact zip
|
- name: Create web artifact zip
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -92,7 +91,7 @@ jobs:
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Package and make
|
- name: Package and make
|
||||||
run: npm run make
|
run: npm run electron:make
|
||||||
|
|
||||||
- name: Upload Windows artifacts
|
- name: Upload Windows artifacts
|
||||||
if: matrix.os == 'windows-latest'
|
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",
|
"license": "Apache-2.0",
|
||||||
"main": ".vite/build/main.js",
|
"main": ".vite/build/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host 0.0.0.0",
|
|
||||||
"dev:electron": "vite --mode electron",
|
|
||||||
"build": "tsc -b && vite build",
|
|
||||||
"lint": "eslint .",
|
"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": "vitest",
|
||||||
"test:ui": "vitest --ui",
|
"test:ui": "vitest --ui",
|
||||||
"test:run": "vitest run",
|
"test:run": "vitest run",
|
||||||
"start": "electron-forge start",
|
"electron:dev": "vite --mode electron",
|
||||||
"package": "electron-forge package",
|
"electron:start": "electron-forge start",
|
||||||
"make": "electron-forge make",
|
"electron:package": "electron-forge package",
|
||||||
"icons": "electron-icon-builder --input=./public/icons/icon.png --output=./public -f"
|
"electron:make": "electron-forge make",
|
||||||
|
"electron:icons": "electron-icon-builder --input=./public/icons/icon.png --output=./public -f"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
"electron-store": "^10.0.0",
|
"electron-store": "^10.0.0",
|
||||||
"konva": "^10.0.12",
|
"konva": "^10.0.12",
|
||||||
"pyodide": "^0.27.4",
|
"pyodide": "^0.29.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-konva": "^19.2.1",
|
"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 { getErrorDetails } from './utils/errorCodeHelpers';
|
||||||
import { getStateVisualInfo } from './utils/machineStateHelpers';
|
import { getStateVisualInfo } from './utils/machineStateHelpers';
|
||||||
import { CheckCircleIcon, BoltIcon, PauseCircleIcon, ExclamationTriangleIcon, ArrowPathIcon, XMarkIcon, InformationCircleIcon } from '@heroicons/react/24/solid';
|
import { CheckCircleIcon, BoltIcon, PauseCircleIcon, ExclamationTriangleIcon, ArrowPathIcon, XMarkIcon, InformationCircleIcon } from '@heroicons/react/24/solid';
|
||||||
|
import { isBluetoothSupported } from './utils/bluetoothSupport';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
function App() {
|
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">
|
<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 */}
|
{/* Left Column - Controls */}
|
||||||
<div className="flex flex-col gap-4 md:gap-5 lg:gap-6 lg:overflow-hidden">
|
<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 && (
|
{!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">
|
{isBluetoothSupported() ? (
|
||||||
<div className="w-6 h-6 text-gray-600 dark:text-gray-400 flex-shrink-0 mt-0.5">
|
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md border-l-4 border-gray-400 dark:border-gray-600">
|
||||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div className="flex items-start gap-3 mb-3">
|
||||||
<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" />
|
<div className="w-6 h-6 text-gray-600 dark:text-gray-400 flex-shrink-0 mt-0.5">
|
||||||
</svg>
|
<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>
|
||||||
<div className="flex-1 min-w-0">
|
) : (
|
||||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-1">Get Started</h3>
|
<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">
|
||||||
<p className="text-xs text-gray-600 dark:text-gray-400">Connect to your embroidery machine</p>
|
<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>
|
||||||
</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) */}
|
{/* 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 { convertPesToPen, type PesPatternData } from '../formats/import/pesImporter';
|
||||||
import { canUploadPattern, getMachineStateCategory } from '../utils/machineStateHelpers';
|
import { canUploadPattern, getMachineStateCategory } from '../utils/machineStateHelpers';
|
||||||
import { PatternInfoSkeleton } from './SkeletonLoader';
|
import { PatternInfoSkeleton } from './SkeletonLoader';
|
||||||
|
import { PatternInfo } from './PatternInfo';
|
||||||
import { ArrowUpTrayIcon, CheckCircleIcon, DocumentTextIcon, FolderOpenIcon } from '@heroicons/react/24/solid';
|
import { ArrowUpTrayIcon, CheckCircleIcon, DocumentTextIcon, FolderOpenIcon } from '@heroicons/react/24/solid';
|
||||||
import { createFileService } from '../platform';
|
import { createFileService } from '../platform';
|
||||||
import type { IFileService } from '../platform/interfaces/IFileService';
|
import type { IFileService } from '../platform/interfaces/IFileService';
|
||||||
|
|
@ -200,66 +201,7 @@ export function FileUpload() {
|
||||||
|
|
||||||
{!isLoading && pesData && (
|
{!isLoading && pesData && (
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<div className="grid grid-cols-3 gap-2 text-xs mb-2">
|
<PatternInfo pesData={pesData} showThreadBlocks />
|
||||||
<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>
|
|
||||||
</div>
|
</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 { useMachineStore } from '../stores/useMachineStore';
|
||||||
import { usePatternStore } from '../stores/usePatternStore';
|
import { usePatternStore } from '../stores/usePatternStore';
|
||||||
import { canDeletePattern } from '../utils/machineStateHelpers';
|
import { canDeletePattern } from '../utils/machineStateHelpers';
|
||||||
|
import { PatternInfo } from './PatternInfo';
|
||||||
import { DocumentTextIcon, TrashIcon } from '@heroicons/react/24/solid';
|
import { DocumentTextIcon, TrashIcon } from '@heroicons/react/24/solid';
|
||||||
|
|
||||||
export function PatternSummaryCard() {
|
export function PatternSummaryCard() {
|
||||||
|
|
@ -44,68 +45,7 @@ export function PatternSummaryCard() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-2 text-xs mb-3">
|
<PatternInfo pesData={pesData} />
|
||||||
<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>
|
|
||||||
|
|
||||||
{canDelete && (
|
{canDelete && (
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -60,9 +60,9 @@ export function ProgressMonitor() {
|
||||||
? ((sewingProgress?.currentStitch || 0) / patternInfo.totalStitches) * 100
|
? ((sewingProgress?.currentStitch || 0) / patternInfo.totalStitches) * 100
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
// Calculate color block information from pesData
|
// Calculate color block information from decoded penStitches
|
||||||
const colorBlocks = useMemo(() => {
|
const colorBlocks = useMemo(() => {
|
||||||
if (!pesData) return [];
|
if (!pesData || !pesData.penStitches) return [];
|
||||||
|
|
||||||
const blocks: Array<{
|
const blocks: Array<{
|
||||||
colorIndex: number;
|
colorIndex: number;
|
||||||
|
|
@ -76,34 +76,20 @@ export function ProgressMonitor() {
|
||||||
threadChart: string | null;
|
threadChart: string | null;
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
let currentColorIndex = pesData.stitches[0]?.[3] ?? 0;
|
// Use the pre-computed color blocks from decoded PEN data
|
||||||
let blockStartStitch = 0;
|
for (const penBlock of pesData.penStitches.colorBlocks) {
|
||||||
|
const thread = pesData.threads[penBlock.colorIndex];
|
||||||
for (let i = 0; i < pesData.stitches.length; i++) {
|
blocks.push({
|
||||||
const stitchColorIndex = pesData.stitches[i][3];
|
colorIndex: penBlock.colorIndex,
|
||||||
|
threadHex: thread?.hex || "#000000",
|
||||||
// When color changes, save the previous block
|
threadCatalogNumber: thread?.catalogNumber ?? null,
|
||||||
if (
|
threadBrand: thread?.brand ?? null,
|
||||||
stitchColorIndex !== currentColorIndex ||
|
threadDescription: thread?.description ?? null,
|
||||||
i === pesData.stitches.length - 1
|
threadChart: thread?.chart ?? null,
|
||||||
) {
|
startStitch: penBlock.startStitchIndex,
|
||||||
const endStitch = i === pesData.stitches.length - 1 ? i + 1 : i;
|
endStitch: penBlock.endStitchIndex,
|
||||||
const thread = pesData.threads[currentColorIndex];
|
stitchCount: penBlock.endStitchIndex - penBlock.startStitchIndex,
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return blocks;
|
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