S-Dreamer commited on
Commit
807119f
·
verified ·
1 Parent(s): 2d73609

Upload 5 files

Browse files
Files changed (5) hide show
  1. README.md +170 -10
  2. index.html +232 -931
  3. package.json +32 -0
  4. pre-commit.sh +248 -0
  5. styles.css +139 -0
README.md CHANGED
@@ -1,12 +1,172 @@
1
- ---
2
- title: piflash
3
- emoji: 🐳
4
- colorFrom: blue
5
- colorTo: blue
6
- sdk: static
7
- pinned: false
8
- tags:
9
- - deepsite
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ # PiFlash - Raspberry Pi Image Flasher Web Application
2
+
3
+ A modern, web-based tool for flashing Raspberry Pi OS images to SD cards. Built with vanilla JavaScript, HTML5, and Tailwind CSS for maximum compatibility and ease of use.
4
+
5
+ ## Features
6
+
7
+ - **Device Detection**: Automatically detects available storage devices
8
+ - **OS Image Library**: Browse recommended and community OS images
9
+ - **Custom Image Support**: Upload and flash your own image files
10
+ - **Progress Tracking**: Real-time progress monitoring with detailed status
11
+ - **Configuration Options**: Pre-configure Wi-Fi, SSH, and hostname settings
12
+ - **Responsive Design**: Works on desktop and mobile devices
13
+ - **No Installation Required**: Runs directly in your web browser
14
+
15
+ ## Quick Start
16
+
17
+ 1. **Clone or download** this repository
18
+ 2. **Open index.html** in your web browser
19
+ 3. **Select a storage device** (SD card or USB drive)
20
+ 4. **Choose an OS image** from the library or upload your own
21
+ 5. **Configure settings** (optional): Wi-Fi, SSH, hostname
22
+ 6. **Click Flash** to start the process
23
+
24
+ ## Development Setup
25
+
26
+ ```bash
27
+ # Install dependencies
28
+ npm install
29
+
30
+ # Start development server
31
+ npm run dev
32
+
33
+ # Run tests
34
+ npm test
35
+ ```
36
+
37
+ ## Browser Compatibility
38
+
39
+ - Chrome 80+
40
+ - Firefox 75+
41
+ - Safari 13+
42
+ - Edge 80+
43
+
44
+ ## Supported File Formats
45
+
46
+ - `.img` - Raw disk images
47
+ - `.zip` - Compressed disk images
48
+ - `.gz` - Gzip compressed images
49
+ - `.xz` - XZ compressed images
50
+
51
+ ## Security Considerations
52
+
53
+ ⚠️ **Important**: This application requires access to storage devices, which may require elevated permissions or browser API access. Always verify the source and integrity of image files before flashing.
54
+
55
+ ### Web API Requirements
56
+
57
+ This application uses the following web APIs:
58
+ - **File System Access API** (Chrome 86+) - For device access
59
+ - **Web USB API** (Experimental) - For USB device communication
60
+ - **File API** - For reading uploaded files
61
+
62
+ ## Configuration Options
63
+
64
+ ### Wi-Fi Setup
65
+ - **SSID**: Network name (max 32 characters)
66
+ - **Password**: Network password (WPA/WPA2)
67
+
68
+ ### SSH Configuration
69
+ - **Disabled**: SSH access turned off (default)
70
+ - **Password**: Enable SSH with password authentication
71
+ - **Key**: Enable SSH with public key authentication only
72
+
73
+ ### System Settings
74
+ - **Hostname**: Custom device name (default: raspberrypi)
75
+ - **Validation**: Verify write after flashing
76
+ - **Compression**: Auto-detect and handle compressed images
77
+
78
+ ## Flashing Process
79
+
80
+ 1. **Device Selection**: Choose target storage device
81
+ 2. **Image Preparation**: Download or prepare the OS image
82
+ 3. **Unmounting**: Safely unmount the device
83
+ 4. **Writing**: Write the image data to the device
84
+ 5. **Verification**: Verify the written data (if enabled)
85
+ 6. **Ejection**: Safely eject the device when complete
86
+
87
+ ## File Structure
88
+
89
+ ```
90
+ piflash-web-app/
91
+ ├── index.html # Main application interface
92
+ ├── styles.css # Custom CSS styles and animations
93
+ ├── js/
94
+ │ └── app.js # Main application logic
95
+ ├── package.json # Project configuration
96
+ ├── README.md # This file
97
+ ├── .gitignore # Git ignore rules
98
+ └── pre-commit.sh # Pre-commit hooks
99
+ ```
100
+
101
+ ## Contributing
102
+
103
+ 1. **Fork** the repository
104
+ 2. **Create** a feature branch (`git checkout -b feature/amazing-feature`)
105
+ 3. **Commit** your changes (`git commit -m 'Add amazing feature'`)
106
+ 4. **Push** to the branch (`git push origin feature/amazing-feature`)
107
+ 5. **Open** a Pull Request
108
+
109
+ ### Development Guidelines
110
+
111
+ - Use **ES6+** JavaScript features
112
+ - Follow **responsive design** principles
113
+ - Add **comprehensive comments** for complex logic
114
+ - Include **unit tests** for new functionality
115
+ - Maintain **cross-browser compatibility**
116
+
117
+ ## Testing
118
+
119
+ The application includes unit tests for core functionality:
120
+
121
+ ```bash
122
+ # Run all tests
123
+ npm test
124
+
125
+ # Test specific components
126
+ npm test -- --testNamePattern="device"
127
+ ```
128
+
129
+ ## Troubleshooting
130
+
131
+ ### Common Issues
132
+
133
+ **Device not detected**
134
+ - Ensure the SD card is properly inserted
135
+ - Try refreshing the device list
136
+ - Check browser permissions for device access
137
+
138
+ **Flash process fails**
139
+ - Verify the image file is not corrupted
140
+ - Ensure sufficient space on the target device
141
+ - Try using a different SD card
142
+
143
+ **Browser compatibility**
144
+ - Use a modern browser with File System Access API support
145
+ - Enable experimental web platform features if required
146
+ - Clear browser cache and cookies
147
+
148
+ ### Browser Permissions
149
+
150
+ Some browsers may require explicit permission to access storage devices:
151
+ 1. Navigate to browser settings
152
+ 2. Find "Site permissions" or "Privacy and security"
153
+ 3. Allow file system access for this application
154
+
155
+ ## License
156
+
157
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
158
+
159
+ ## Acknowledgments
160
+
161
+ - **Raspberry Pi Foundation** for the excellent hardware and documentation
162
+ - **Tailwind CSS** for the utility-first CSS framework
163
+ - **Font Awesome** for the comprehensive icon library
164
+ - **Community contributors** who help improve this tool
165
+
166
+ ## Disclaimer
167
+
168
+ This tool is provided as-is without warranty. Always backup important data before flashing storage devices. The authors are not responsible for data loss or hardware damage.
169
+
170
  ---
171
 
172
+ **Happy Flashing!** 🥧✨
index.html CHANGED
@@ -1,933 +1,234 @@
1
- /**
2
- * PiFlash - Raspberry Pi Image Flasher
3
- * Main application logic and UI interactions
4
- */
5
-
6
- class PiFlashApp {
7
- constructor() {
8
- this.selectedDevice = null;
9
- this.selectedOS = null;
10
- this.flashingProgress = null;
11
- this.flashingInterval = null;
12
- this.currentTab = 'recommended';
13
-
14
- // Mock data for demonstration
15
- this.mockDevices = [
16
- {
17
- id: 'sdb',
18
- name: 'SanDisk Ultra 32GB',
19
- path: '/dev/sdb',
20
- size: '29.8GB',
21
- sizeBytes: 32000000000,
22
- type: 'sd'
23
- },
24
- {
25
- id: 'sdc',
26
- name: 'Samsung EVO 64GB',
27
- path: '/dev/sdc',
28
- size: '59.5GB',
29
- sizeBytes: 64000000000,
30
- type: 'sd'
31
- },
32
- {
33
- id: 'sdd',
34
- name: 'Kingston Canvas 16GB',
35
- path: '/dev/sdd',
36
- size: '14.9GB',
37
- sizeBytes: 16000000000,
38
- type: 'usb'
39
- }
40
- ];
41
-
42
- this.osImages = {
43
- recommended: [
44
- {
45
- id: 'rpi-os-64',
46
- name: 'Raspberry Pi OS (64-bit)',
47
- description: 'Recommended for most users',
48
- version: 'v2023-05-03',
49
- size: '1.2GB',
50
- sizeBytes: 1200000000,
51
- category: 'official',
52
- image: 'https://www.raspberrypi.com/app/uploads/2022/02/COLOUR-Raspberry-Pi-Symbol-Registered.png'
53
- },
54
- {
55
- id: 'rpi-os-lite-32',
56
- name: 'Raspberry Pi OS Lite (32-bit)',
57
- description: 'Minimal image for headless setups',
58
- version: 'v2023-05-03',
59
- size: '450MB',
60
- sizeBytes: 450000000,
61
- category: 'official',
62
- image: 'https://www.raspberrypi.com/app/uploads/2022/02/COLOUR-Raspberry-Pi-Symbol-Registered.png'
63
- },
64
- {
65
- id: 'ubuntu-server',
66
- name: 'Ubuntu Server 22.04 LTS',
67
- description: 'Official Ubuntu for Raspberry Pi',
68
- version: 'v22.04.2',
69
- size: '1.8GB',
70
- sizeBytes: 1800000000,
71
- category: 'ubuntu',
72
- image: 'https://assets.ubuntu.com/v1/29985a98-ubuntu-logo32.png'
73
- }
74
- ],
75
- all: [
76
- {
77
- id: 'rpi-os-64',
78
- name: 'Raspberry Pi OS (64-bit)',
79
- description: 'Recommended for most users',
80
- version: 'v2023-05-03',
81
- size: '1.2GB',
82
- sizeBytes: 1200000000,
83
- category: 'official',
84
- image: 'https://www.raspberrypi.com/app/uploads/2022/02/COLOUR-Raspberry-Pi-Symbol-Registered.png'
85
- },
86
- {
87
- id: 'rpi-os-lite-32',
88
- name: 'Raspberry Pi OS Lite (32-bit)',
89
- description: 'Minimal image for headless setups',
90
- version: 'v2023-05-03',
91
- size: '450MB',
92
- sizeBytes: 450000000,
93
- category: 'official',
94
- image: 'https://www.raspberrypi.com/app/uploads/2022/02/COLOUR-Raspberry-Pi-Symbol-Registered.png'
95
- },
96
- {
97
- id: 'ubuntu-server',
98
- name: 'Ubuntu Server 22.04 LTS',
99
- description: 'Official Ubuntu for Raspberry Pi',
100
- version: 'v22.04.2',
101
- size: '1.8GB',
102
- sizeBytes: 1800000000,
103
- category: 'ubuntu',
104
- image: 'https://assets.ubuntu.com/v1/29985a98-ubuntu-logo32.png'
105
- },
106
- {
107
- id: 'retropie',
108
- name: 'RetroPie 4.8',
109
- description: 'Turn your Pi into a retro gaming machine',
110
- version: 'v4.8',
111
- size: '2.5GB',
112
- sizeBytes: 2500000000,
113
- category: 'gaming',
114
- image: 'https://retropie.org.uk/wp-content/uploads/2017/07/cropped-RetroPieLogo-32x32.png'
115
- },
116
- {
117
- id: 'libreelec',
118
- name: 'LibreELEC 11.0',
119
- description: 'Kodi media center OS',
120
- version: 'v11.0.3',
121
- size: '350MB',
122
- sizeBytes: 350000000,
123
- category: 'media',
124
- image: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjQwIiBoZWlnaHQ9IjQwIiByeD0iOCIgZmlsbD0iIzAwN0ZGRiIvPgo8cGF0aCBkPSJNMTAgMTBoMjB2MjBIMTBWMTB6IiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K'
125
- },
126
- {
127
- id: 'kali-linux',
128
- name: 'Kali Linux 2023.2',
129
- description: 'Security testing and penetration testing',
130
- version: 'v2023.2',
131
- size: '3.1GB',
132
- sizeBytes: 3100000000,
133
- category: 'security',
134
- image: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjQwIiBoZWlnaHQ9IjQwIiByeD0iOCIgZmlsbD0iIzAwMDAwMCIvPgo8cGF0aCBkPSJNMTAgMTBoMjB2MjBIMTBWMTB6IiBmaWxsPSIjRkYwMDAwIi8+Cjwvc3ZnPgo='
135
- }
136
- ],
137
- other: []
138
- };
139
-
140
- this.init();
141
- }
142
-
143
- /**
144
- * Initialize the application
145
- */
146
- init() {
147
- console.log('Initializing PiFlash application...');
148
- this.setupEventListeners();
149
- this.loadDevices();
150
- this.loadOSImages();
151
- this.updateFlashButton();
152
- }
153
-
154
- /**
155
- * Set up all event listeners
156
- */
157
- setupEventListeners() {
158
- // Device refresh button
159
- document.getElementById('refreshDevices').addEventListener('click', () => {
160
- this.refreshDevices();
161
- });
162
-
163
- // OS image tabs
164
- document.getElementById('tabRecommended').addEventListener('click', () => {
165
- this.switchTab('recommended');
166
- });
167
- document.getElementById('tabAll').addEventListener('click', () => {
168
- this.switchTab('all');
169
- });
170
- document.getElementById('tabOther').addEventListener('click', () => {
171
- this.switchTab('other');
172
- });
173
-
174
- // Search functionality
175
- document.getElementById('searchOS').addEventListener('input', (e) => {
176
- this.filterOSImages(e.target.value);
177
- });
178
-
179
- // Custom image upload
180
- document.getElementById('customImageUpload').addEventListener('click', () => {
181
- document.getElementById('customImageFile').click();
182
- });
183
-
184
- document.getElementById('customImageFile').addEventListener('change', (e) => {
185
- this.handleCustomImageUpload(e.target.files[0]);
186
- });
187
-
188
- // Flash button
189
- document.getElementById('flashButton').addEventListener('click', () => {
190
- this.startFlashing();
191
- });
192
-
193
- // Cancel flash button
194
- document.getElementById('cancelFlash').addEventListener('click', () => {
195
- this.cancelFlashing();
196
- });
197
-
198
- // Flash another button
199
- document.getElementById('flashAnother').addEventListener('click', () => {
200
- this.resetApplication();
201
- });
202
-
203
- // Form validation
204
- this.setupFormValidation();
205
- }
206
-
207
- /**
208
- * Set up form validation
209
- */
210
- setupFormValidation() {
211
- const inputs = ['wifiSSID', 'wifiPassword', 'hostname'];
212
- inputs.forEach(id => {
213
- const input = document.getElementById(id);
214
- input.addEventListener('input', () => {
215
- this.validateInput(input);
216
- });
217
- });
218
- }
219
-
220
- /**
221
- * Validate input fields
222
- */
223
- validateInput(input) {
224
- const value = input.value.trim();
225
-
226
- if (input.id === 'hostname') {
227
- // Validate hostname format
228
- const hostnameRegex = /^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*$/;
229
- if (value && !hostnameRegex.test(value)) {
230
- input.classList.add('border-red-500');
231
- this.showTooltip(input, 'Invalid hostname format');
232
- } else {
233
- input.classList.remove('border-red-500');
234
- this.hideTooltip(input);
235
- }
236
- }
237
-
238
- if (input.id === 'wifiSSID') {
239
- // Basic SSID validation
240
- if (value && value.length > 32) {
241
- input.classList.add('border-red-500');
242
- this.showTooltip(input, 'SSID too long (max 32 characters)');
243
- } else {
244
- input.classList.remove('border-red-500');
245
- this.hideTooltip(input);
246
- }
247
- }
248
- }
249
-
250
- /**
251
- * Show tooltip for validation errors
252
- */
253
- showTooltip(element, message) {
254
- // Remove existing tooltip
255
- this.hideTooltip(element);
256
-
257
- const tooltip = document.createElement('div');
258
- tooltip.className = 'absolute z-10 px-2 py-1 text-xs text-white bg-red-600 rounded shadow-lg tooltip';
259
- tooltip.textContent = message;
260
- tooltip.style.top = '-30px';
261
- tooltip.style.left = '0';
262
-
263
- element.parentElement.style.position = 'relative';
264
- element.parentElement.appendChild(tooltip);
265
- }
266
-
267
- /**
268
- * Hide tooltip
269
- */
270
- hideTooltip(element) {
271
- const tooltip = element.parentElement.querySelector('.tooltip');
272
- if (tooltip) {
273
- tooltip.remove();
274
- }
275
- }
276
-
277
- /**
278
- * Load and display available devices
279
- */
280
- loadDevices() {
281
- console.log('Loading devices...');
282
- const deviceList = document.getElementById('deviceList');
283
-
284
- // Show loading state
285
- deviceList.innerHTML = '<div class="skeleton h-16 rounded-lg"></div>';
286
-
287
- // Simulate loading delay
288
- setTimeout(() => {
289
- deviceList.innerHTML = '';
290
-
291
- if (this.mockDevices.length === 0) {
292
- deviceList.innerHTML = `
293
- <div class="text-center py-8 text-gray-500">
294
- <i class="fas fa-exclamation-circle text-2xl mb-2"></i>
295
- <p class="text-sm">No storage devices found</p>
296
- <p class="text-xs mt-1">Insert an SD card or USB drive</p>
297
- </div>
298
- `;
299
- return;
300
- }
301
-
302
- this.mockDevices.forEach(device => {
303
- const deviceElement = this.createDeviceElement(device);
304
- deviceList.appendChild(deviceElement);
305
- });
306
- }, 1000);
307
- }
308
-
309
- /**
310
- * Create device element
311
- */
312
- createDeviceElement(device) {
313
- const deviceDiv = document.createElement('div');
314
- deviceDiv.className = 'device-card p-3 border border-gray-200 rounded-lg cursor-pointer';
315
- deviceDiv.setAttribute('data-device-id', device.id);
316
-
317
- const icon = device.type === 'sd' ? 'fas fa-sd-card' : 'fas fa-usb';
318
- const iconColor = device.type === 'sd' ? 'text-blue-500' : 'text-green-500';
319
-
320
- deviceDiv.innerHTML = `
321
- <div class="flex items-center justify-between">
322
- <div class="flex items-center">
323
- <i class="${icon} ${iconColor} mr-3"></i>
324
- <div>
325
- <h3 class="font-medium text-gray-800">${device.name}</h3>
326
- <p class="text-sm text-gray-600">${device.path} • ${device.size}</p>
327
- </div>
328
- </div>
329
- <div class="flex items-center">
330
- <i class="fas fa-check text-green-500 hidden device-selected-icon"></i>
331
- </div>
332
- </div>
333
- `;
334
-
335
- deviceDiv.addEventListener('click', () => {
336
- this.selectDevice(device);
337
- });
338
-
339
- return deviceDiv;
340
- }
341
-
342
- /**
343
- * Select a device
344
- */
345
- selectDevice(device) {
346
- console.log('Selecting device:', device.name);
347
-
348
- // Remove previous selection
349
- document.querySelectorAll('.device-card').forEach(card => {
350
- card.classList.remove('selected');
351
- card.querySelector('.device-selected-icon').classList.add('hidden');
352
- });
353
-
354
- // Add selection to current device
355
- const deviceElement = document.querySelector(`[data-device-id="${device.id}"]`);
356
- deviceElement.classList.add('selected');
357
- deviceElement.querySelector('.device-selected-icon').classList.remove('hidden');
358
-
359
- this.selectedDevice = device;
360
- this.updateFlashSummary();
361
- this.updateFlashButton();
362
- }
363
-
364
- /**
365
- * Refresh devices list
366
- */
367
- refreshDevices() {
368
- console.log('Refreshing devices...');
369
- const refreshButton = document.getElementById('refreshDevices');
370
- const icon = refreshButton.querySelector('i');
371
-
372
- icon.classList.add('rotate-animation');
373
-
374
- setTimeout(() => {
375
- this.loadDevices();
376
- icon.classList.remove('rotate-animation');
377
- }, 1000);
378
- }
379
-
380
- /**
381
- * Switch between OS image tabs
382
- */
383
- switchTab(tab) {
384
- console.log('Switching to tab:', tab);
385
-
386
- // Update tab buttons
387
- document.querySelectorAll('.tab-button').forEach(button => {
388
- button.classList.remove('active');
389
- });
390
- document.getElementById(`tab${tab.charAt(0).toUpperCase() + tab.slice(1)}`).classList.add('active');
391
-
392
- this.currentTab = tab;
393
- this.loadOSImages();
394
- }
395
-
396
- /**
397
- * Load and display OS images
398
- */
399
- loadOSImages() {
400
- console.log('Loading OS images for tab:', this.currentTab);
401
- const osImageList = document.getElementById('osImageList');
402
-
403
- // Show loading state
404
- osImageList.innerHTML = '<div class="skeleton h-20 rounded-lg mb-3"></div>'.repeat(3);
405
-
406
- setTimeout(() => {
407
- osImageList.innerHTML = '';
408
- const images = this.osImages[this.currentTab] || [];
409
-
410
- if (images.length === 0) {
411
- osImageList.innerHTML = `
412
- <div class="text-center py-8 text-gray-500">
413
- <i class="fas fa-image text-2xl mb-2"></i>
414
- <p class="text-sm">No images available</p>
415
- </div>
416
- `;
417
- return;
418
- }
419
-
420
- images.forEach(osImage => {
421
- const imageElement = this.createOSImageElement(osImage);
422
- osImageList.appendChild(imageElement);
423
- });
424
- }, 500);
425
- }
426
-
427
- /**
428
- * Create OS image element
429
- */
430
- createOSImageElement(osImage) {
431
- const imageDiv = document.createElement('div');
432
- imageDiv.className = 'os-image-card p-3 border border-gray-200 rounded-lg cursor-pointer fade-in';
433
- imageDiv.setAttribute('data-os-id', osImage.id);
434
-
435
- imageDiv.innerHTML = `
436
- <div class="flex items-start">
437
- <img src="${osImage.image}" alt="${osImage.name}" class="w-10 h-10 rounded mr-3 flex-shrink-0" onerror="this.src='data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjQwIiBoZWlnaHQ9IjQwIiByeD0iOCIgZmlsbD0iIzZCNzI4MCIvPgo8cGF0aCBkPSJNMTIgMTVoMTZ2MkgxMnYtMnptMCA0aDE2djJIMTJ2LTJ6bTAgNGgxMnYySDE2di0yeiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg=='">
438
- <div class="flex-1 min-w-0">
439
- <h3 class="font-medium text-gray-800 truncate">${osImage.name}</h3>
440
- <p class="text-sm text-gray-600 mb-1">${osImage.description}</p>
441
- <div class="flex items-center justify-between text-xs text-gray-500">
442
- <span>${osImage.version}</span>
443
- <span>${osImage.size}</span>
444
- </div>
445
- </div>
446
- <div class="ml-2 flex items-center">
447
- <i class="fas fa-check text-green-500 hidden os-selected-icon"></i>
448
- </div>
449
- </div>
450
- `;
451
-
452
- imageDiv.addEventListener('click', () => {
453
- this.selectOSImage(osImage);
454
- });
455
-
456
- return imageDiv;
457
- }
458
-
459
- /**
460
- * Select an OS image
461
- */
462
- selectOSImage(osImage) {
463
- console.log('Selecting OS image:', osImage.name);
464
-
465
- // Remove previous selection
466
- document.querySelectorAll('.os-image-card').forEach(card => {
467
- card.classList.remove('selected');
468
- const icon = card.querySelector('.os-selected-icon');
469
- if (icon) icon.classList.add('hidden');
470
- });
471
-
472
- // Add selection to current image
473
- const imageElement = document.querySelector(`[data-os-id="${osImage.id}"]`);
474
- if (imageElement) {
475
- imageElement.classList.add('selected');
476
- const icon = imageElement.querySelector('.os-selected-icon');
477
- if (icon) icon.classList.remove('hidden');
478
- }
479
-
480
- this.selectedOS = osImage;
481
- this.updateFlashSummary();
482
- this.updateFlashButton();
483
- }
484
-
485
- /**
486
- * Filter OS images based on search
487
- */
488
- filterOSImages(searchTerm) {
489
- const searchLower = searchTerm.toLowerCase();
490
- const images = this.osImages[this.currentTab] || [];
491
- const filteredImages = images.filter(image =>
492
- image.name.toLowerCase().includes(searchLower) ||
493
- image.description.toLowerCase().includes(searchLower)
494
- );
495
-
496
- const osImageList = document.getElementById('osImageList');
497
- osImageList.innerHTML = '';
498
-
499
- if (filteredImages.length === 0) {
500
- osImageList.innerHTML = `
501
- <div class="text-center py-8 text-gray-500">
502
- <i class="fas fa-search text-2xl mb-2"></i>
503
- <p class="text-sm">No images found for "${searchTerm}"</p>
504
- </div>
505
- `;
506
- return;
507
- }
508
-
509
- filteredImages.forEach(osImage => {
510
- const imageElement = this.createOSImageElement(osImage);
511
- osImageList.appendChild(imageElement);
512
- });
513
- }
514
-
515
- /**
516
- * Handle custom image upload
517
- */
518
- handleCustomImageUpload(file) {
519
- if (!file) return;
520
-
521
- console.log('Handling custom image upload:', file.name);
522
-
523
- const customOS = {
524
- id: 'custom-' + Date.now(),
525
- name: file.name,
526
- description: 'Custom image file',
527
- version: 'Custom',
528
- size: this.formatFileSize(file.size),
529
- sizeBytes: file.size,
530
- category: 'custom',
531
- file: file,
532
- image: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjQwIiBoZWlnaHQ9IjQwIiByeD0iOCIgZmlsbD0iIzEwQjk4MSIvPgo8cGF0aCBkPSJNMTYgMTJsOCA4LTggOHYtNEgxMnYtOGg0di00eiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg=='
533
- };
534
-
535
- this.selectOSImage(customOS);
536
-
537
- // Update the upload area to show selected file
538
- const uploadArea = document.getElementById('customImageUpload');
539
- uploadArea.innerHTML = `
540
- <div class="flex items-center justify-center">
541
- <i class="fas fa-file-check text-2xl text-green-600 mr-2"></i>
542
- <div class="text-left">
543
- <h3 class="font-medium text-gray-700">${file.name}</h3>
544
- <p class="text-sm text-gray-500">${this.formatFileSize(file.size)}</p>
545
- </div>
546
- </div>
547
- `;
548
- uploadArea.classList.add('border-green-400', 'bg-green-50');
549
- }
550
-
551
- /**
552
- * Format file size
553
- */
554
- formatFileSize(bytes) {
555
- const units = ['B', 'KB', 'MB', 'GB', 'TB'];
556
- let size = bytes;
557
- let unitIndex = 0;
558
-
559
- while (size >= 1024 && unitIndex < units.length - 1) {
560
- size /= 1024;
561
- unitIndex++;
562
- }
563
-
564
- return `${size.toFixed(1)}${units[unitIndex]}`;
565
- }
566
-
567
- /**
568
- * Update flash summary
569
- */
570
- updateFlashSummary() {
571
- const summaryDiv = document.getElementById('flashSummary');
572
-
573
- if (!this.selectedDevice && !this.selectedOS) {
574
- summaryDiv.innerHTML = `
575
- <div class="flex items-center mb-2">
576
- <i class="fas fa-info-circle text-gray-500 mr-2"></i>
577
- <h3 class="font-medium text-gray-700">Select Image and Device</h3>
578
- </div>
579
- <p class="text-sm text-gray-600">
580
- Choose an OS image and storage device to get started.
581
- </p>
582
- `;
583
- return;
584
- }
585
-
586
- if (this.selectedOS && !this.selectedDevice) {
587
- summaryDiv.innerHTML = `
588
- <div class="flex items-center mb-2">
589
- <i class="fas fa-exclamation-triangle text-yellow-500 mr-2"></i>
590
- <h3 class="font-medium text-gray-700">Select Storage Device</h3>
591
- </div>
592
- <p class="text-sm text-gray-600">
593
- <strong>Image:</strong> ${this.selectedOS.name}<br>
594
- Choose a storage device to continue.
595
- </p>
596
- `;
597
- return;
598
- }
599
-
600
- if (!this.selectedOS && this.selectedDevice) {
601
- summaryDiv.innerHTML = `
602
- <div class="flex items-center mb-2">
603
- <i class="fas fa-exclamation-triangle text-yellow-500 mr-2"></i>
604
- <h3 class="font-medium text-gray-700">Select OS Image</h3>
605
  </div>
606
- <p class="text-sm text-gray-600">
607
- <strong>Device:</strong> ${this.selectedDevice.name}<br>
608
- Choose an OS image to continue.
609
- </p>
610
- `;
611
- return;
612
- }
613
-
614
- // Both selected
615
- const isDeviceCompatible = this.selectedDevice.sizeBytes >= this.selectedOS.sizeBytes;
616
- const statusIcon = isDeviceCompatible ? 'fas fa-check-circle text-green-500' : 'fas fa-exclamation-triangle text-red-500';
617
- const statusText = isDeviceCompatible ? 'Ready to Flash' : 'Size Mismatch';
618
-
619
- summaryDiv.innerHTML = `
620
- <div class="flex items-center mb-2">
621
- <i class="${statusIcon} mr-2"></i>
622
- <h3 class="font-medium text-gray-700">${statusText}</h3>
623
- </div>
624
- <div class="text-sm text-gray-600 space-y-1">
625
- <p><strong>Image:</strong> ${this.selectedOS.name} (${this.selectedOS.size})</p>
626
- <p><strong>Device:</strong> ${this.selectedDevice.name} (${this.selectedDevice.size})</p>
627
- ${!isDeviceCompatible ? '<p class="text-red-600 mt-2"><strong>Warning:</strong> Device is too small for this image.</p>' : ''}
628
- </div>
629
- `;
630
- }
631
-
632
- /**
633
- * Update flash button state
634
- */
635
- updateFlashButton() {
636
- const flashButton = document.getElementById('flashButton');
637
- const canFlash = this.selectedDevice && this.selectedOS &&
638
- this.selectedDevice.sizeBytes >= this.selectedOS.sizeBytes;
639
-
640
- if (canFlash) {
641
- flashButton.disabled = false;
642
- flashButton.className = 'w-full mt-6 py-3 px-4 bg-red-600 hover:bg-red-700 text-white font-semibold rounded-lg shadow-md transition duration-300 flex items-center justify-center';
643
- flashButton.innerHTML = '<i class="fas fa-bolt mr-2"></i> Flash!';
644
- } else {
645
- flashButton.disabled = true;
646
- flashButton.className = 'w-full mt-6 py-3 px-4 bg-gray-400 text-white font-semibold rounded-lg shadow-md transition duration-300 flex items-center justify-center cursor-not-allowed';
647
-
648
- if (!this.selectedDevice && !this.selectedOS) {
649
- flashButton.innerHTML = '<i class="fas fa-bolt mr-2"></i> Select Image and Device';
650
- } else if (!this.selectedDevice) {
651
- flashButton.innerHTML = '<i class="fas fa-bolt mr-2"></i> Select Storage Device';
652
- } else if (!this.selectedOS) {
653
- flashButton.innerHTML = '<i class="fas fa-bolt mr-2"></i> Select OS Image';
654
- } else {
655
- flashButton.innerHTML = '<i class="fas fa-exclamation-triangle mr-2"></i> Device Too Small';
656
- }
657
- }
658
- }
659
-
660
- /**
661
- * Start the flashing process
662
- */
663
- startFlashing() {
664
- if (!this.selectedDevice || !this.selectedOS) return;
665
-
666
- console.log('Starting flash process...');
667
-
668
- // Show confirmation dialog
669
- const confirmed = confirm(
670
- `Are you sure you want to flash "${this.selectedOS.name}" to "${this.selectedDevice.name}"?\n\n` +
671
- `This will PERMANENTLY erase all data on the device!\n\n` +
672
- `Device: ${this.selectedDevice.path} (${this.selectedDevice.size})\n` +
673
- `Image: ${this.selectedOS.name} (${this.selectedOS.size})`
674
- );
675
-
676
- if (!confirmed) return;
677
-
678
- // Hide flash summary and show progress
679
- document.getElementById('flashSummary').parentElement.classList.add('hidden');
680
- document.getElementById('progressSection').classList.remove('hidden');
681
- document.getElementById('progressSection').classList.add('slide-up');
682
-
683
- // Initialize progress
684
- this.flashingProgress = {
685
- stage: 'preparing',
686
- percent: 0,
687
- speed: 0,
688
- eta: 0,
689
- startTime: Date.now()
690
- };
691
-
692
- this.updateProgress();
693
- this.simulateFlashingProcess();
694
- }
695
-
696
- /**
697
- * Simulate the flashing process
698
- */
699
- simulateFlashingProcess() {
700
- const stages = [
701
- { name: 'preparing', duration: 2000, message: 'Preparing device...' },
702
- { name: 'unmounting', duration: 1000, message: 'Unmounting device...' },
703
- { name: 'writing', duration: 15000, message: 'Writing image...' },
704
- { name: 'verifying', duration: 8000, message: 'Verifying write...' },
705
- { name: 'ejecting', duration: 1000, message: 'Ejecting device...' }
706
- ];
707
-
708
- let currentStageIndex = 0;
709
- let stageStartTime = Date.now();
710
- let totalDuration = stages.reduce((sum, stage) => sum + stage.duration, 0);
711
- let elapsedTotal = 0;
712
-
713
- this.flashingInterval = setInterval(() => {
714
- const now = Date.now();
715
- const currentStage = stages[currentStageIndex];
716
- const stageElapsed = now - stageStartTime;
717
- const stageProgress = Math.min(stageElapsed / currentStage.duration, 1);
718
-
719
- // Update progress
720
- const overallProgress = (elapsedTotal + (stageProgress * currentStage.duration)) / totalDuration;
721
- this.flashingProgress.percent = Math.round(overallProgress * 100);
722
- this.flashingProgress.stage = currentStage.name;
723
- this.flashingProgress.message = currentStage.message;
724
-
725
- // Calculate speed and ETA (simulation)
726
- if (currentStage.name === 'writing') {
727
- const bytesWritten = stageProgress * this.selectedOS.sizeBytes;
728
- const timeElapsed = stageElapsed / 1000; // seconds
729
- this.flashingProgress.speed = bytesWritten / timeElapsed; // bytes per second
730
- this.flashingProgress.eta = (this.selectedOS.sizeBytes - bytesWritten) / this.flashingProgress.speed * 1000; // ms
731
- }
732
-
733
- this.updateProgress();
734
-
735
- // Move to next stage
736
- if (stageProgress >= 1) {
737
- elapsedTotal += currentStage.duration;
738
- currentStageIndex++;
739
- stageStartTime = now;
740
-
741
- if (currentStageIndex >= stages.length) {
742
- clearInterval(this.flashingInterval);
743
- this.completeFlashing();
744
- }
745
- }
746
- }, 100);
747
- }
748
-
749
- /**
750
- * Update progress display
751
- */
752
- updateProgress() {
753
- const progress = this.flashingProgress;
754
-
755
- // Update progress bar
756
- document.getElementById('progressBar').style.width = `${progress.percent}%`;
757
- document.getElementById('progressPercent').textContent = `${progress.percent}%`;
758
- document.getElementById('progressStatus').textContent = progress.message || 'Processing...';
759
-
760
- // Update time remaining
761
- const timeRemaining = document.getElementById('timeRemaining');
762
- if (progress.eta && progress.eta > 0) {
763
- const minutes = Math.floor(progress.eta / 60000);
764
- const seconds = Math.floor((progress.eta % 60000) / 1000);
765
- timeRemaining.textContent = `${minutes}m ${seconds}s`;
766
- } else {
767
- timeRemaining.textContent = 'Calculating...';
768
- }
769
-
770
- // Update progress steps
771
- const stepsDiv = document.getElementById('progressSteps');
772
- const steps = [
773
- { id: 'preparing', name: 'Preparing device', icon: 'fas fa-cog' },
774
- { id: 'unmounting', name: 'Unmounting device', icon: 'fas fa-eject' },
775
- { id: 'writing', name: 'Writing image', icon: 'fas fa-pen' },
776
- { id: 'verifying', name: 'Verifying write', icon: 'fas fa-check-double' },
777
- { id: 'ejecting', name: 'Ejecting device', icon: 'fas fa-sign-out-alt' }
778
- ];
779
-
780
- stepsDiv.innerHTML = steps.map(step => {
781
- let statusClass = 'text-gray-400';
782
- let statusIcon = 'far fa-circle';
783
-
784
- if (step.id === progress.stage) {
785
- statusClass = 'text-blue-600';
786
- statusIcon = 'fas fa-spinner fa-spin';
787
- } else if (steps.findIndex(s => s.id === step.id) < steps.findIndex(s => s.id === progress.stage)) {
788
- statusClass = 'text-green-600';
789
- statusIcon = 'fas fa-check-circle';
790
- }
791
-
792
- return `
793
- <div class="flex items-center ${statusClass}">
794
- <i class="${statusIcon} mr-2"></i>
795
- <span>${step.name}</span>
796
- </div>
797
- `;
798
- }).join('');
799
- }
800
-
801
- /**
802
- * Cancel flashing process
803
- */
804
- cancelFlashing() {
805
- const confirmed = confirm('Are you sure you want to cancel the flashing process?\n\nThis may leave your device in an unusable state.');
806
-
807
- if (!confirmed) return;
808
-
809
- console.log('Cancelling flash process...');
810
-
811
- if (this.flashingInterval) {
812
- clearInterval(this.flashingInterval);
813
- this.flashingInterval = null;
814
- }
815
-
816
- // Show cancellation message
817
- document.getElementById('progressStatus').textContent = 'Cancelled by user';
818
- document.getElementById('progressBar').classList.add('bg-red-600');
819
-
820
- setTimeout(() => {
821
- this.resetApplication();
822
- }, 2000);
823
- }
824
-
825
- /**
826
- * Complete flashing process
827
- */
828
- completeFlashing() {
829
- console.log('Flash process completed successfully!');
830
-
831
- // Hide progress section and show completion
832
- document.getElementById('progressSection').classList.add('hidden');
833
- document.getElementById('completionSection').classList.remove('hidden');
834
- document.getElementById('completionSection').classList.add('slide-up');
835
-
836
- // Update completion message
837
- const completionMessage = document.getElementById('completionMessage');
838
- completionMessage.textContent = `Successfully flashed ${this.selectedOS.name} to ${this.selectedDevice.name}. Your SD card is ready to use!`;
839
-
840
- // Play success sound (if available)
841
- this.playSuccessSound();
842
- }
843
-
844
- /**
845
- * Play success sound
846
- */
847
- playSuccessSound() {
848
- try {
849
- // Create a simple success tone
850
- const audioContext = new (window.AudioContext || window.webkitAudioContext)();
851
- const oscillator = audioContext.createOscillator();
852
- const gainNode = audioContext.createGain();
853
-
854
- oscillator.connect(gainNode);
855
- gainNode.connect(audioContext.destination);
856
-
857
- oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
858
- oscillator.frequency.setValueAtTime(1000, audioContext.currentTime + 0.1);
859
- gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
860
- gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
861
-
862
- oscillator.start(audioContext.currentTime);
863
- oscillator.stop(audioContext.currentTime + 0.3);
864
- } catch (error) {
865
- console.log('Could not play success sound:', error);
866
- }
867
- }
868
-
869
- /**
870
- * Reset application to initial state
871
- */
872
- resetApplication() {
873
- console.log('Resetting application...');
874
-
875
- // Reset selections
876
- this.selectedDevice = null;
877
- this.selectedOS = null;
878
- this.flashingProgress = null;
879
-
880
- if (this.flashingInterval) {
881
- clearInterval(this.flashingInterval);
882
- this.flashingInterval = null;
883
- }
884
-
885
- // Reset UI
886
- document.querySelectorAll('.device-card').forEach(card => {
887
- card.classList.remove('selected');
888
- card.querySelector('.device-selected-icon').classList.add('hidden');
889
- });
890
-
891
- document.querySelectorAll('.os-image-card').forEach(card => {
892
- card.classList.remove('selected');
893
- const icon = card.querySelector('.os-selected-icon');
894
- if (icon) icon.classList.add('hidden');
895
- });
896
-
897
- // Reset custom upload
898
- const uploadArea = document.getElementById('customImageUpload');
899
- uploadArea.innerHTML = `
900
- <i class="fas fa-file-upload text-3xl text-gray-400 mb-2"></i>
901
- <h3 class="font-medium text-gray-700">Use Custom Image</h3>
902
- <p class="text-sm text-gray-500">Upload your own .img or .zip file</p>
903
- `;
904
- uploadArea.classList.remove('border-green-400', 'bg-green-50');
905
-
906
- // Reset form fields
907
- document.getElementById('wifiSSID').value = '';
908
- document.getElementById('wifiPassword').value = '';
909
- document.getElementById('hostname').value = '';
910
- document.getElementById('sshOption').value = 'disabled';
911
- document.getElementById('customImageFile').value = '';
912
-
913
- // Hide sections
914
- document.getElementById('progressSection').classList.add('hidden');
915
- document.getElementById('completionSection').classList.add('hidden');
916
- document.getElementById('flashSummary').parentElement.classList.remove('hidden');
917
-
918
- // Update UI
919
- this.updateFlashSummary();
920
- this.updateFlashButton();
921
- }
922
- }
923
-
924
- // Initialize the application when DOM is loaded
925
- document.addEventListener('DOMContentLoaded', () => {
926
- console.log('DOM loaded, initializing PiFlash...');
927
- new PiFlashApp();
928
- });
929
 
930
- // Unit tests for key functions
931
- if (typeof module !== 'undefined' && module.exports) {
932
- module.exports = PiFlashApp;
933
- }
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>PiFlash - Raspberry Pi Image Flasher</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <link rel="stylesheet" href="styles.css">
10
+ </head>
11
+ <body class="bg-gray-50 min-h-screen">
12
+ <div class="container mx-auto px-4 py-8">
13
+ <!-- Header -->
14
+ <header class="mb-10 text-center">
15
+ <div class="flex justify-center items-center mb-4">
16
+ <i class="fas fa-raspberry-pi text-4xl text-red-600 mr-3"></i>
17
+ <h1 class="text-4xl font-bold text-gray-800">PiFlash</h1>
18
+ </div>
19
+ <p class="text-lg text-gray-600 max-w-2xl mx-auto">
20
+ A simple web-based tool to flash Raspberry Pi OS images to your SD cards. No additional software required!
21
+ </p>
22
+ </header>
23
+
24
+ <!-- Main Content -->
25
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
26
+ <!-- Left Column - Device Selection -->
27
+ <div class="lg:col-span-1 space-y-6">
28
+ <div class="bg-white rounded-xl shadow-md p-6 card-hover">
29
+ <h2 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
30
+ <i class="fas fa-sd-card mr-2 text-blue-500"></i> Select Storage Device
31
+ </h2>
32
+ <div id="deviceList" class="space-y-3">
33
+ <!-- Devices will be populated by JavaScript -->
34
+ </div>
35
+ <button id="refreshDevices" class="w-full mt-2 flex items-center justify-center text-blue-500 hover:text-blue-700">
36
+ <i class="fas fa-sync-alt mr-2"></i> Refresh Devices
37
+ </button>
38
+ </div>
39
+
40
+ <div class="bg-white rounded-xl shadow-md p-6 card-hover">
41
+ <h2 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
42
+ <i class="fas fa-cog mr-2 text-purple-500"></i> Flash Options
43
+ </h2>
44
+ <div class="space-y-4">
45
+ <div>
46
+ <label class="block text-sm font-medium text-gray-700 mb-1">Validation</label>
47
+ <select id="validationOption" class="w-full p-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500">
48
+ <option value="verify">Verify after writing</option>
49
+ <option value="skip">Skip verification</option>
50
+ </select>
51
+ </div>
52
+ <div>
53
+ <label class="block text-sm font-medium text-gray-700 mb-1">Compression</label>
54
+ <select id="compressionOption" class="w-full p-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500">
55
+ <option value="auto">Auto (recommended)</option>
56
+ <option value="always">Always decompress</option>
57
+ <option value="never">Never decompress</option>
58
+ </select>
59
+ </div>
60
+ <div class="flex items-center">
61
+ <input type="checkbox" id="unmount" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" checked>
62
+ <label for="unmount" class="ml-2 block text-sm text-gray-700">Unmount before writing</label>
63
+ </div>
64
+ <div class="flex items-center">
65
+ <input type="checkbox" id="eject" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" checked>
66
+ <label for="eject" class="ml-2 block text-sm text-gray-700">Eject when complete</label>
67
+ </div>
68
+ </div>
69
+ </div>
70
+ </div>
71
+
72
+ <!-- Middle Column - OS Selection -->
73
+ <div class="lg:col-span-1 space-y-6">
74
+ <div class="bg-white rounded-xl shadow-md p-6 card-hover h-full">
75
+ <h2 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
76
+ <i class="fas fa-download mr-2 text-green-500"></i> Select OS Image
77
+ </h2>
78
+
79
+ <div class="mb-4">
80
+ <div class="flex space-x-2 mb-3">
81
+ <button id="tabRecommended" class="tab-button px-3 py-1 bg-blue-100 text-blue-700 rounded-full text-sm font-medium">Recommended</button>
82
+ <button id="tabAll" class="tab-button px-3 py-1 bg-gray-100 text-gray-700 rounded-full text-sm font-medium">All</button>
83
+ <button id="tabOther" class="tab-button px-3 py-1 bg-gray-100 text-gray-700 rounded-full text-sm font-medium">Other</button>
84
+ </div>
85
+ <div class="relative">
86
+ <input type="text" id="searchOS" placeholder="Search OS images..." class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500">
87
+ <i class="fas fa-search absolute left-3 top-3 text-gray-400"></i>
88
+ </div>
89
+ </div>
90
+
91
+ <div id="osImageList" class="space-y-3 overflow-y-auto max-h-96">
92
+ <!-- OS images will be populated by JavaScript -->
93
+ </div>
94
+
95
+ <!-- Custom Image Upload -->
96
+ <div class="mt-4">
97
+ <input type="file" id="customImageFile" accept=".img,.zip,.gz,.xz" class="hidden">
98
+ <div id="customImageUpload" class="border-2 border-dashed border-gray-300 rounded-lg p-4 text-center hover:border-blue-400 cursor-pointer">
99
+ <i class="fas fa-file-upload text-3xl text-gray-400 mb-2"></i>
100
+ <h3 class="font-medium text-gray-700">Use Custom Image</h3>
101
+ <p class="text-sm text-gray-500">Upload your own .img or .zip file</p>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ </div>
106
+
107
+ <!-- Right Column - Flash Progress -->
108
+ <div class="lg:col-span-1 space-y-6">
109
+ <div class="bg-white rounded-xl shadow-md p-6 card-hover">
110
+ <h2 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
111
+ <i class="fas fa-bolt mr-2 text-yellow-500"></i> Flash Summary
112
+ </h2>
113
+
114
+ <div id="flashSummary" class="bg-gray-50 border border-gray-200 rounded-lg p-4 mb-4">
115
+ <div class="flex items-center mb-2">
116
+ <i class="fas fa-info-circle text-gray-500 mr-2"></i>
117
+ <h3 class="font-medium text-gray-700">Select Image and Device</h3>
118
+ </div>
119
+ <p class="text-sm text-gray-600">
120
+ Choose an OS image and storage device to get started.
121
+ </p>
122
+ </div>
123
+
124
+ <div class="space-y-4">
125
+ <div>
126
+ <label class="block text-sm font-medium text-gray-700 mb-1">Wi-Fi Configuration (Optional)</label>
127
+ <input type="text" id="wifiSSID" placeholder="SSID" class="w-full p-2 border border-gray-300 rounded-md mb-2 focus:ring-blue-500 focus:border-blue-500">
128
+ <input type="password" id="wifiPassword" placeholder="Password" class="w-full p-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500">
129
+ </div>
130
+
131
+ <div>
132
+ <label class="block text-sm font-medium text-gray-700 mb-1">Hostname (Optional)</label>
133
+ <input type="text" id="hostname" placeholder="raspberrypi" class="w-full p-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500">
134
+ </div>
135
+
136
+ <div>
137
+ <label class="block text-sm font-medium text-gray-700 mb-1">Enable SSH</label>
138
+ <select id="sshOption" class="w-full p-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500">
139
+ <option value="disabled">Disabled</option>
140
+ <option value="password">Enabled with password authentication</option>
141
+ <option value="key">Enabled with public key only</option>
142
+ </select>
143
+ </div>
144
+ </div>
145
+
146
+ <button id="flashButton" class="w-full mt-6 py-3 px-4 bg-gray-400 text-white font-semibold rounded-lg shadow-md transition duration-300 flex items-center justify-center cursor-not-allowed" disabled>
147
+ <i class="fas fa-bolt mr-2"></i> Select Image and Device
148
+ </button>
149
+ </div>
150
+
151
+ <!-- Progress Section (Initially Hidden) -->
152
+ <div id="progressSection" class="bg-white rounded-xl shadow-md p-6 card-hover hidden">
153
+ <h2 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
154
+ <i class="fas fa-spinner fa-spin mr-2 text-blue-500"></i> Flashing in Progress
155
+ </h2>
156
+
157
+ <div class="space-y-4">
158
+ <div>
159
+ <div class="flex justify-between text-sm text-gray-600 mb-1">
160
+ <span id="progressStatus">Preparing...</span>
161
+ <span id="progressPercent">0%</span>
162
+ </div>
163
+ <div class="w-full bg-gray-200 rounded-full h-2.5">
164
+ <div id="progressBar" class="progress-bar bg-blue-600 h-2.5 rounded-full" style="width: 0%"></div>
165
+ </div>
166
+ </div>
167
+
168
+ <div class="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
169
+ <div class="flex items-center">
170
+ <i class="fas fa-exclamation-triangle text-yellow-500 mr-2"></i>
171
+ <h3 class="font-medium text-yellow-800">Important</h3>
172
+ </div>
173
+ <p class="text-sm text-yellow-700 mt-1">
174
+ Do not remove the SD card or close this browser tab during the flashing process.
175
+ </p>
176
+ </div>
177
+
178
+ <div id="progressSteps" class="space-y-2 text-sm">
179
+ <!-- Progress steps will be populated by JavaScript -->
180
+ </div>
181
+
182
+ <div class="pt-4 border-t border-gray-200">
183
+ <p class="text-sm text-gray-500">
184
+ <i class="fas fa-clock mr-1"></i> Estimated time remaining: <span id="timeRemaining">Calculating...</span>
185
+ </p>
186
+ </div>
187
+
188
+ <button id="cancelFlash" class="w-full py-2 px-4 bg-red-600 hover:bg-red-700 text-white font-semibold rounded-lg">
189
+ Cancel Flashing
190
+ </button>
191
+ </div>
192
+ </div>
193
+
194
+ <!-- Completion Section (Initially Hidden) -->
195
+ <div id="completionSection" class="bg-white rounded-xl shadow-md p-6 card-hover hidden">
196
+ <div class="text-center">
197
+ <div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 mb-4">
198
+ <i class="fas fa-check text-green-600 text-xl"></i>
199
+ </div>
200
+ <h3 class="text-lg font-medium text-gray-900 mb-2">Flash Complete!</h3>
201
+ <p class="text-sm text-gray-500 mb-4" id="completionMessage">
202
+ Your SD card is ready to use.
203
+ </p>
204
+ <div class="bg-gray-50 p-3 rounded-lg text-left mb-4">
205
+ <p class="text-sm font-medium text-gray-700 mb-1">Next Steps:</p>
206
+ <ul class="text-sm text-gray-600 list-disc list-inside space-y-1">
207
+ <li>Insert the SD card into your Raspberry Pi</li>
208
+ <li>Connect power to boot the device</li>
209
+ <li>Follow the setup instructions</li>
210
+ </ul>
211
+ </div>
212
+ <button id="flashAnother" class="w-full py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none">
213
+ Flash Another Card
214
+ </button>
215
+ </div>
216
+ </div>
217
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
 
220
+ <!-- Footer -->
221
+ <footer class="mt-16 text-center text-sm text-gray-500">
222
+ <p>PiFlash is an open-source tool. Use at your own risk.</p>
223
+ <p class="mt-1">Not affiliated with the Raspberry Pi Foundation.</p>
224
+ <div class="flex justify-center space-x-4 mt-3">
225
+ <a href="https://github.com/your-org/piflash" class="text-blue-500 hover:text-blue-700"><i class="fab fa-github"></i> GitHub</a>
226
+ <a href="#" class="text-blue-500 hover:text-blue-700"><i class="fas fa-question-circle"></i> Help</a>
227
+ <a href="#" class="text-blue-500 hover:text-blue-700"><i class="fas fa-bug"></i> Report Issue</a>
228
+ </div>
229
+ </footer>
230
+ </div>
231
+
232
+ <script src="js/app.js"></script>
233
+ </body>
234
+ </html>
package.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "piflash-web-app",
3
+ "version": "1.0.0",
4
+ "description": "A web-based Raspberry Pi image flasher application",
5
+ "main": "index.html",
6
+ "scripts": {
7
+ "start": "serve -s . -p 3000",
8
+ "build": "echo 'Static files - no build required'",
9
+ "test": "jest",
10
+ "dev": "live-server --port=3000 --open=index.html"
11
+ },
12
+ "keywords": [
13
+ "raspberry-pi",
14
+ "flasher",
15
+ "web-app",
16
+ "sd-card",
17
+ "imaging"
18
+ ],
19
+ "author": "PiFlash Development Team",
20
+ "license": "MIT",
21
+ "devDependencies": {
22
+ "jest": "^29.5.0",
23
+ "live-server": "^1.2.2",
24
+ "serve": "^14.2.0"
25
+ },
26
+ "dependencies": {},
27
+ "browserslist": [
28
+ "> 1%",
29
+ "last 2 versions",
30
+ "not dead"
31
+ ]
32
+ }
pre-commit.sh ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+
3
+ # PiFlash Pre-commit Hook
4
+ # Ensures code quality and consistency before commits
5
+
6
+ set -euo pipefail
7
+
8
+ echo "🔍 Running pre-commit checks for PiFlash..."
9
+
10
+ # Color codes for output
11
+ RED='\033[0;31m'
12
+ GREEN='\033[0;32m'
13
+ YELLOW='\033[1;33m'
14
+ BLUE='\033[0;34m'
15
+ NC='\033[0m' # No Color
16
+
17
+ # Helper function to print colored output
18
+ print_status() {
19
+ echo -e "${BLUE}[PiFlash]${NC} $1"
20
+ }
21
+
22
+ print_success() {
23
+ echo -e "${GREEN}✅${NC} $1"
24
+ }
25
+
26
+ print_warning() {
27
+ echo -e "${YELLOW}⚠️${NC} $1"
28
+ }
29
+
30
+ print_error() {
31
+ echo -e "${RED}❌${NC} $1"
32
+ }
33
+
34
+ # Check if we're in a git repository
35
+ if ! git rev-parse --git-dir > /dev/null 2>&1; then
36
+ print_error "Not in a git repository"
37
+ exit 1
38
+ fi
39
+
40
+ # Get list of staged files
41
+ STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
42
+
43
+ if [ -z "$STAGED_FILES" ]; then
44
+ print_warning "No staged files found"
45
+ exit 0
46
+ fi
47
+
48
+ print_status "Staged files: $(echo $STAGED_FILES | wc -w)"
49
+
50
+ # Check 1: Validate HTML files
51
+ print_status "Validating HTML files..."
52
+ HTML_FILES=$(echo "$STAGED_FILES" | grep -E '\.(html|htm)$' || true)
53
+ if [ -n "$HTML_FILES" ]; then
54
+ for file in $HTML_FILES; do
55
+ if [ -f "$file" ]; then
56
+ # Basic HTML validation
57
+ if ! grep -q "<!DOCTYPE html>" "$file"; then
58
+ print_error "Missing DOCTYPE in $file"
59
+ exit 1
60
+ fi
61
+
62
+ # Check for basic structure
63
+ if ! grep -q "<html" "$file" || ! grep -q "</html>" "$file"; then
64
+ print_error "Invalid HTML structure in $file"
65
+ exit 1
66
+ fi
67
+
68
+ print_success "HTML validation passed for $file"
69
+ fi
70
+ done
71
+ else
72
+ print_status "No HTML files to validate"
73
+ fi
74
+
75
+ # Check 2: Validate CSS files
76
+ print_status "Validating CSS files..."
77
+ CSS_FILES=$(echo "$STAGED_FILES" | grep -E '\.(css|scss|sass)$' || true)
78
+ if [ -n "$CSS_FILES" ]; then
79
+ for file in $CSS_FILES; do
80
+ if [ -f "$file" ]; then
81
+ # Basic CSS syntax check
82
+ if ! node -e "
83
+ const fs = require('fs');
84
+ const css = fs.readFileSync('$file', 'utf8');
85
+ const openBraces = (css.match(/\{/g) || []).length;
86
+ const closeBraces = (css.match(/\}/g) || []).length;
87
+ if (openBraces !== closeBraces) {
88
+ console.error('Mismatched braces in CSS');
89
+ process.exit(1);
90
+ }
91
+ " 2>/dev/null; then
92
+ print_error "CSS syntax error in $file"
93
+ exit 1
94
+ fi
95
+
96
+ print_success "CSS validation passed for $file"
97
+ fi
98
+ done
99
+ else
100
+ print_status "No CSS files to validate"
101
+ fi
102
+
103
+ # Check 3: Validate JavaScript files
104
+ print_status "Validating JavaScript files..."
105
+ JS_FILES=$(echo "$STAGED_FILES" | grep -E '\.(js|jsx|ts|tsx)$' || true)
106
+ if [ -n "$JS_FILES" ]; then
107
+ for file in $JS_FILES; do
108
+ if [ -f "$file" ]; then
109
+ # Check for basic syntax using Node.js
110
+ if ! node -c "$file" 2>/dev/null; then
111
+ print_error "JavaScript syntax error in $file"
112
+ exit 1
113
+ fi
114
+
115
+ # Check for console.log statements (optional warning)
116
+ if grep -q "console\.log" "$file"; then
117
+ print_warning "console.log found in $file - consider removing for production"
118
+ fi
119
+
120
+ # Check for TODO comments
121
+ if grep -qi "TODO\|FIXME\|HACK" "$file"; then
122
+ print_warning "TODO/FIXME/HACK comments found in $file"
123
+ fi
124
+
125
+ print_success "JavaScript validation passed for $file"
126
+ fi
127
+ done
128
+ else
129
+ print_status "No JavaScript files to validate"
130
+ fi
131
+
132
+ # Check 4: Validate JSON files
133
+ print_status "Validating JSON files..."
134
+ JSON_FILES=$(echo "$STAGED_FILES" | grep -E '\.(json)$' || true)
135
+ if [ -n "$JSON_FILES" ]; then
136
+ for file in $JSON_FILES; do
137
+ if [ -f "$file" ]; then
138
+ if ! node -e "JSON.parse(require('fs').readFileSync('$file', 'utf8'))" 2>/dev/null; then
139
+ print_error "Invalid JSON in $file"
140
+ exit 1
141
+ fi
142
+
143
+ print_success "JSON validation passed for $file"
144
+ fi
145
+ done
146
+ else
147
+ print_status "No JSON files to validate"
148
+ fi
149
+
150
+ # Check 5: File size limits
151
+ print_status "Checking file sizes..."
152
+ for file in $STAGED_FILES; do
153
+ if [ -f "$file" ]; then
154
+ file_size=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file" 2>/dev/null || echo 0)
155
+ max_size=1048576 # 1MB in bytes
156
+
157
+ if [ "$file_size" -gt "$max_size" ]; then
158
+ print_error "File $file is too large ($(($file_size / 1024))KB > 1MB)"
159
+ exit 1
160
+ fi
161
+ fi
162
+ done
163
+ print_success "File size check passed"
164
+
165
+ # Check 6: Line ending consistency
166
+ print_status "Checking line endings..."
167
+ for file in $STAGED_FILES; do
168
+ if [ -f "$file" ]; then
169
+ # Check for mixed line endings
170
+ if file "$file" | grep -q "CRLF"; then
171
+ print_warning "CRLF line endings found in $file - consider using LF"
172
+ fi
173
+ fi
174
+ done
175
+ print_success "Line ending check completed"
176
+
177
+ # Check 7: Trailing whitespace
178
+ print_status "Checking for trailing whitespace..."
179
+ WHITESPACE_FILES=""
180
+ for file in $STAGED_FILES; do
181
+ if [ -f "$file" ]; then
182
+ if grep -q '[[:space:]]$' "$file"; then
183
+ WHITESPACE_FILES="$WHITESPACE_FILES $file"
184
+ fi
185
+ fi
186
+ done
187
+
188
+ if [ -n "$WHITESPACE_FILES" ]; then
189
+ print_warning "Trailing whitespace found in:$WHITESPACE_FILES"
190
+ print_status "Attempting to fix trailing whitespace..."
191
+
192
+ for file in $WHITESPACE_FILES; do
193
+ # Remove trailing whitespace
194
+ sed -i 's/[[:space:]]*$//' "$file"
195
+ git add "$file"
196
+ done
197
+
198
+ print_success "Trailing whitespace fixed and re-staged"
199
+ fi
200
+
201
+ # Check 8: Commit message format (if available)
202
+ if [ -n "${1:-}" ]; then
203
+ COMMIT_MSG_FILE="$1"
204
+ if [ -f "$COMMIT_MSG_FILE" ]; then
205
+ commit_msg=$(head -n1 "$COMMIT_MSG_FILE")
206
+
207
+ # Check commit message length
208
+ if [ ${#commit_msg} -gt 72 ]; then
209
+ print_warning "Commit message is longer than 72 characters"
210
+ fi
211
+
212
+ # Check for conventional commit format (optional)
213
+ if echo "$commit_msg" | grep -qE '^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .+'; then
214
+ print_success "Conventional commit format detected"
215
+ else
216
+ print_warning "Consider using conventional commit format (feat:, fix:, docs:, etc.)"
217
+ fi
218
+ fi
219
+ fi
220
+
221
+ # Check 9: Security scan for sensitive data
222
+ print_status "Scanning for sensitive data..."
223
+ SENSITIVE_PATTERNS=(
224
+ "password\s*=\s*['\"][^'\"]*['\"]"
225
+ "api[_-]?key\s*=\s*['\"][^'\"]*['\"]"
226
+ "secret\s*=\s*['\"][^'\"]*['\"]"
227
+ "token\s*=\s*['\"][^'\"]*['\"]"
228
+ "private[_-]?key"
229
+ )
230
+
231
+ for file in $STAGED_FILES; do
232
+ if [ -f "$file" ]; then
233
+ for pattern in "${SENSITIVE_PATTERNS[@]}"; do
234
+ if grep -iE "$pattern" "$file" >/dev/null 2>&1; then
235
+ print_error "Potential sensitive data found in $file: $pattern"
236
+ print_error "Please review and remove sensitive information before committing"
237
+ exit 1
238
+ fi
239
+ done
240
+ fi
241
+ done
242
+ print_success "Security scan completed"
243
+
244
+ # Final success message
245
+ print_success "All pre-commit checks passed!"
246
+ print_status "Ready to commit $(echo $STAGED_FILES | wc -w) files"
247
+
248
+ exit 0
styles.css ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .progress-bar {
2
+ transition: width 0.3s ease;
3
+ }
4
+
5
+ .card-hover:hover {
6
+ transform: translateY(-5px);
7
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
8
+ }
9
+
10
+ .card-hover {
11
+ transition: all 0.3s ease;
12
+ }
13
+
14
+ .flashing-animation {
15
+ animation: pulse 2s infinite;
16
+ }
17
+
18
+ @keyframes pulse {
19
+ 0% { opacity: 1; }
20
+ 50% { opacity: 0.6; }
21
+ 100% { opacity: 1; }
22
+ }
23
+
24
+ .tab-button.active {
25
+ background-color: rgb(219 234 254);
26
+ color: rgb(29 78 216);
27
+ }
28
+
29
+ .tab-button:not(.active) {
30
+ background-color: rgb(243 244 246);
31
+ color: rgb(55 65 81);
32
+ }
33
+
34
+ .tab-button:hover {
35
+ background-color: rgb(229 231 235);
36
+ }
37
+
38
+ .os-image-card {
39
+ transition: all 0.2s ease;
40
+ }
41
+
42
+ .os-image-card:hover {
43
+ border-color: rgb(147 197 253);
44
+ background-color: rgb(248 250 252);
45
+ }
46
+
47
+ .os-image-card.selected {
48
+ border-color: rgb(59 130 246);
49
+ background-color: rgb(239 246 255);
50
+ }
51
+
52
+ .device-card {
53
+ transition: all 0.2s ease;
54
+ }
55
+
56
+ .device-card:hover {
57
+ background-color: rgb(243 244 246);
58
+ }
59
+
60
+ .device-card.selected {
61
+ background-color: rgb(219 234 254);
62
+ border-color: rgb(59 130 246);
63
+ }
64
+
65
+ .rotate-animation {
66
+ animation: rotate 1s linear infinite;
67
+ }
68
+
69
+ @keyframes rotate {
70
+ from { transform: rotate(0deg); }
71
+ to { transform: rotate(360deg); }
72
+ }
73
+
74
+ .bounce-animation {
75
+ animation: bounce 1s ease-in-out infinite;
76
+ }
77
+
78
+ @keyframes bounce {
79
+ 0%, 20%, 50%, 80%, 100% {
80
+ transform: translateY(0);
81
+ }
82
+ 40% {
83
+ transform: translateY(-10px);
84
+ }
85
+ 60% {
86
+ transform: translateY(-5px);
87
+ }
88
+ }
89
+
90
+ /* Custom scrollbar for OS list */
91
+ .overflow-y-auto::-webkit-scrollbar {
92
+ width: 6px;
93
+ }
94
+
95
+ .overflow-y-auto::-webkit-scrollbar-track {
96
+ background: #f1f1f1;
97
+ border-radius: 3px;
98
+ }
99
+
100
+ .overflow-y-auto::-webkit-scrollbar-thumb {
101
+ background: #c1c1c1;
102
+ border-radius: 3px;
103
+ }
104
+
105
+ .overflow-y-auto::-webkit-scrollbar-thumb:hover {
106
+ background: #a8a8a8;
107
+ }
108
+
109
+ /* Loading skeleton animations */
110
+ .skeleton {
111
+ animation: skeleton-loading 1s linear infinite alternate;
112
+ }
113
+
114
+ @keyframes skeleton-loading {
115
+ 0% {
116
+ background-color: hsl(200, 20%, 80%);
117
+ }
118
+ 100% {
119
+ background-color: hsl(200, 20%, 95%);
120
+ }
121
+ }
122
+
123
+ .fade-in {
124
+ animation: fadeIn 0.5s ease-in;
125
+ }
126
+
127
+ @keyframes fadeIn {
128
+ from { opacity: 0; transform: translateY(10px); }
129
+ to { opacity: 1; transform: translateY(0); }
130
+ }
131
+
132
+ .slide-up {
133
+ animation: slideUp 0.3s ease-out;
134
+ }
135
+
136
+ @keyframes slideUp {
137
+ from { transform: translateY(20px); opacity: 0; }
138
+ to { transform: translateY(0); opacity: 1; }
139
+ }