add file browser component and widget
This commit is contained in:
		
							
								
								
									
										208
									
								
								widgets/file_browser_widget/examples/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										208
									
								
								widgets/file_browser_widget/examples/README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,208 @@
 | 
			
		||||
# FileBrowser Widget
 | 
			
		||||
 | 
			
		||||
A WebAssembly-based file browser widget that can be embedded in any web application.
 | 
			
		||||
 | 
			
		||||
## Features
 | 
			
		||||
 | 
			
		||||
- File and directory browsing
 | 
			
		||||
- File upload with progress tracking (using TUS protocol)
 | 
			
		||||
- File download
 | 
			
		||||
- Directory creation and deletion
 | 
			
		||||
- File editing (markdown with live preview, text files)
 | 
			
		||||
 | 
			
		||||
## Running the Example
 | 
			
		||||
 | 
			
		||||
1. **Start a local server** (required for WASM):
 | 
			
		||||
   ```bash
 | 
			
		||||
   python3 -m http.server 8081
 | 
			
		||||
   # or
 | 
			
		||||
   npx serve .
 | 
			
		||||
   ```
 | 
			
		||||
 | 
			
		||||
2. **Start the mock backend** (in another terminal):
 | 
			
		||||
   ```bash
 | 
			
		||||
   cd ../file_browser_demo
 | 
			
		||||
   cargo run --bin mock_server
 | 
			
		||||
   ```
 | 
			
		||||
 | 
			
		||||
3. **Open the example**:
 | 
			
		||||
   - Navigate to `http://localhost:8081`
 | 
			
		||||
   - The widget will load with a configuration panel
 | 
			
		||||
   - Try different settings and see them applied in real-time
 | 
			
		||||
 | 
			
		||||
## Key Features Demonstrated
 | 
			
		||||
 | 
			
		||||
### Runtime Configuration
 | 
			
		||||
The example shows how to configure the widget at runtime without rebuilding:
 | 
			
		||||
 | 
			
		||||
```javascript
 | 
			
		||||
// Create base configuration
 | 
			
		||||
const config = create_default_config('http://localhost:3001/files');
 | 
			
		||||
 | 
			
		||||
// Apply runtime settings using corrected method names
 | 
			
		||||
config.setTheme('light');                    // Theme selection
 | 
			
		||||
config.setMaxFileSize(100 * 1024 * 1024);   // 100MB limit
 | 
			
		||||
config.setShowUpload(true);                  // Enable upload
 | 
			
		||||
config.setShowDownload(true);                // Enable download
 | 
			
		||||
config.setShowDelete(false);                 // Disable delete
 | 
			
		||||
config.setInitialPath('documents/');         // Start in documents folder
 | 
			
		||||
 | 
			
		||||
// Create widget with configuration
 | 
			
		||||
const widget = create_file_browser_widget('container-id', config);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Dynamic Reconfiguration
 | 
			
		||||
The widget can be recreated with new settings:
 | 
			
		||||
 | 
			
		||||
```javascript
 | 
			
		||||
function updateWidget() {
 | 
			
		||||
    // Destroy existing widget
 | 
			
		||||
    if (currentWidget) {
 | 
			
		||||
        currentWidget.destroy();
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Create new widget with updated config
 | 
			
		||||
    const newConfig = create_default_config(newEndpoint);
 | 
			
		||||
    newConfig.setTheme(selectedTheme);
 | 
			
		||||
    currentWidget = create_file_browser_widget('container', newConfig);
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Error Handling
 | 
			
		||||
The example includes comprehensive error handling:
 | 
			
		||||
 | 
			
		||||
- WASM initialization errors
 | 
			
		||||
- Browser compatibility checks
 | 
			
		||||
- Widget creation failures
 | 
			
		||||
- Network connectivity issues
 | 
			
		||||
 | 
			
		||||
## Widget API Reference
 | 
			
		||||
 | 
			
		||||
### Core Functions
 | 
			
		||||
 | 
			
		||||
```javascript
 | 
			
		||||
// Initialize WASM module (call once)
 | 
			
		||||
await init();
 | 
			
		||||
 | 
			
		||||
// Create default configuration
 | 
			
		||||
const config = create_default_config(baseEndpoint);
 | 
			
		||||
 | 
			
		||||
// Create widget instance
 | 
			
		||||
const widget = create_file_browser_widget(containerId, config);
 | 
			
		||||
 | 
			
		||||
// Utility functions
 | 
			
		||||
const version = get_version();
 | 
			
		||||
const isCompatible = check_browser_compatibility();
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Configuration Methods
 | 
			
		||||
 | 
			
		||||
```javascript
 | 
			
		||||
config.setTheme(theme);              // 'light' | 'dark'
 | 
			
		||||
config.setMaxFileSize(bytes);        // Number in bytes
 | 
			
		||||
config.setShowUpload(enabled);       // Boolean
 | 
			
		||||
config.setShowDownload(enabled);     // Boolean
 | 
			
		||||
config.setShowDelete(enabled);       // Boolean
 | 
			
		||||
config.setCssClasses(classes);       // String of CSS classes
 | 
			
		||||
config.setInitialPath(path);         // String path
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Widget Handle Methods
 | 
			
		||||
 | 
			
		||||
```javascript
 | 
			
		||||
widget.destroy();                    // Clean up widget
 | 
			
		||||
// Note: Currently no update method - recreate widget for config changes
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Advanced Usage
 | 
			
		||||
 | 
			
		||||
### Custom Styling
 | 
			
		||||
```javascript
 | 
			
		||||
config.setCssClasses('my-custom-theme dark-mode');
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Multiple Widgets
 | 
			
		||||
```javascript
 | 
			
		||||
const widget1 = create_file_browser_widget('container1', config1);
 | 
			
		||||
const widget2 = create_file_browser_widget('container2', config2);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Integration with Frameworks
 | 
			
		||||
 | 
			
		||||
**React:**
 | 
			
		||||
```jsx
 | 
			
		||||
function FileBrowserComponent({ endpoint }) {
 | 
			
		||||
    const containerRef = useRef();
 | 
			
		||||
    const widgetRef = useRef();
 | 
			
		||||
    
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        async function initWidget() {
 | 
			
		||||
            await init();
 | 
			
		||||
            const config = create_default_config(endpoint);
 | 
			
		||||
            widgetRef.current = create_file_browser_widget(
 | 
			
		||||
                containerRef.current, 
 | 
			
		||||
                config
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        initWidget();
 | 
			
		||||
        
 | 
			
		||||
        return () => widgetRef.current?.destroy();
 | 
			
		||||
    }, [endpoint]);
 | 
			
		||||
    
 | 
			
		||||
    return <div ref={containerRef} />;
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
**Vue:**
 | 
			
		||||
```vue
 | 
			
		||||
<template>
 | 
			
		||||
  <div ref="container"></div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  async mounted() {
 | 
			
		||||
    await init();
 | 
			
		||||
    const config = create_default_config(this.endpoint);
 | 
			
		||||
    this.widget = create_file_browser_widget(this.$refs.container, config);
 | 
			
		||||
  },
 | 
			
		||||
  beforeUnmount() {
 | 
			
		||||
    this.widget?.destroy();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Troubleshooting
 | 
			
		||||
 | 
			
		||||
### Common Issues
 | 
			
		||||
 | 
			
		||||
1. **"config.setTheme is not a function"**
 | 
			
		||||
   - Ensure you're using the latest widget build
 | 
			
		||||
   - Check that WASM module is properly initialized
 | 
			
		||||
 | 
			
		||||
2. **Widget not appearing**
 | 
			
		||||
   - Verify container element exists
 | 
			
		||||
   - Check browser console for errors
 | 
			
		||||
   - Ensure WASM files are served correctly
 | 
			
		||||
 | 
			
		||||
3. **Backend connection errors**
 | 
			
		||||
   - Verify backend is running on specified endpoint
 | 
			
		||||
   - Check CORS configuration
 | 
			
		||||
   - Ensure all required API endpoints are implemented
 | 
			
		||||
 | 
			
		||||
### Debug Mode
 | 
			
		||||
```javascript
 | 
			
		||||
// Enable debug logging
 | 
			
		||||
console.log('Widget version:', get_version());
 | 
			
		||||
console.log('Browser compatible:', check_browser_compatibility());
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Performance Notes
 | 
			
		||||
 | 
			
		||||
- **Initial Load**: ~368KB total (WASM + JS)
 | 
			
		||||
- **Runtime Memory**: ~2-5MB depending on file list size
 | 
			
		||||
- **Startup Time**: ~100-300ms on modern browsers
 | 
			
		||||
- **File Operations**: Near-native performance via WASM
 | 
			
		||||
 | 
			
		||||
The widget is optimized for production use with minimal overhead.
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								widgets/file_browser_widget/examples/compressed/uppy.min.css.gz
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								widgets/file_browser_widget/examples/compressed/uppy.min.css.gz
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								widgets/file_browser_widget/examples/compressed/uppy.min.js.gz
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								widgets/file_browser_widget/examples/compressed/uppy.min.js.gz
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										412
									
								
								widgets/file_browser_widget/examples/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										412
									
								
								widgets/file_browser_widget/examples/index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,412 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
    <title>FileBrowser Widget Example</title>
 | 
			
		||||
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
 | 
			
		||||
    <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
 | 
			
		||||
    <script src="/uppy.min.js"></script>
 | 
			
		||||
    <link href="/uppy.min.css" rel="stylesheet">
 | 
			
		||||
    <style>
 | 
			
		||||
        body { 
 | 
			
		||||
            padding: 20px; 
 | 
			
		||||
            background-color: #f8f9fa;
 | 
			
		||||
        }
 | 
			
		||||
        .widget-container { 
 | 
			
		||||
            border: 2px dashed #dee2e6; 
 | 
			
		||||
            border-radius: 8px; 
 | 
			
		||||
            padding: 15px; 
 | 
			
		||||
            background: white;
 | 
			
		||||
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
 | 
			
		||||
            min-height: 400px;
 | 
			
		||||
        }
 | 
			
		||||
        .widget-container:empty::after {
 | 
			
		||||
            content: "Widget will render here...";
 | 
			
		||||
            color: #6c757d;
 | 
			
		||||
            font-style: italic;
 | 
			
		||||
            display: flex;
 | 
			
		||||
            align-items: center;
 | 
			
		||||
            justify-content: center;
 | 
			
		||||
            height: 200px;
 | 
			
		||||
        }
 | 
			
		||||
        .config-panel {
 | 
			
		||||
            background: white;
 | 
			
		||||
            border-radius: 8px;
 | 
			
		||||
            padding: 20px;
 | 
			
		||||
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
 | 
			
		||||
            margin-bottom: 20px;
 | 
			
		||||
        }
 | 
			
		||||
        .status-indicator {
 | 
			
		||||
            display: inline-block;
 | 
			
		||||
            width: 12px;
 | 
			
		||||
            height: 12px;
 | 
			
		||||
            border-radius: 50%;
 | 
			
		||||
            margin-right: 8px;
 | 
			
		||||
        }
 | 
			
		||||
        .status-success { background-color: #28a745; }
 | 
			
		||||
        .status-error { background-color: #dc3545; }
 | 
			
		||||
        .status-loading { background-color: #ffc107; }
 | 
			
		||||
    </style>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
    <div class="container-fluid">
 | 
			
		||||
        <div class="row">
 | 
			
		||||
            <div class="col-12">
 | 
			
		||||
                <!-- Widget Header -->
 | 
			
		||||
                <div class="config-panel mb-4">
 | 
			
		||||
                    <div class="row align-items-center">
 | 
			
		||||
                        <div class="col-md-6">
 | 
			
		||||
                            <h3 class="mb-0">
 | 
			
		||||
                                <i class="bi bi-hdd-stack text-primary"></i>
 | 
			
		||||
                                File Browser Widget
 | 
			
		||||
                                <span class="badge bg-secondary ms-2" id="widget-version">v0.1.0</span>
 | 
			
		||||
                            </h3>
 | 
			
		||||
                            <p class="text-muted mb-0 mt-1">Self-contained WASM widget for file management</p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col-md-6 text-end">
 | 
			
		||||
                            <div class="btn-group" role="group">
 | 
			
		||||
                                <button type="button" class="btn btn-outline-primary btn-sm" data-bs-toggle="modal" data-bs-target="#assetsModal">
 | 
			
		||||
                                    <i class="bi bi-file-earmark-zip"></i>
 | 
			
		||||
                                    Assets
 | 
			
		||||
                                </button>
 | 
			
		||||
                                <button type="button" class="btn btn-outline-primary btn-sm" onclick="window.open('https://github.com/herocode/framework/tree/main/widgets/file_browser_widget', '_blank')">
 | 
			
		||||
                                    <i class="bi bi-code-slash"></i>
 | 
			
		||||
                                    Code
 | 
			
		||||
                                </button>
 | 
			
		||||
                                <button type="button" class="btn btn-outline-primary btn-sm" onclick="window.open('#documentation', '_blank')">
 | 
			
		||||
                                    <i class="bi bi-book"></i>
 | 
			
		||||
                                    Documentation
 | 
			
		||||
                                </button>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="row">
 | 
			
		||||
            <div class="col-md-4">
 | 
			
		||||
                <div class="config-panel">
 | 
			
		||||
                    <h4>
 | 
			
		||||
                        <i class="bi bi-gear"></i>
 | 
			
		||||
                        Configuration
 | 
			
		||||
                    </h4>
 | 
			
		||||
                    <div class="mb-3">
 | 
			
		||||
                        <label for="endpoint" class="form-label">Base Endpoint:</label>
 | 
			
		||||
                        <input type="text" id="endpoint" class="form-control" value="http://localhost:3001/files">
 | 
			
		||||
                        <div class="form-text">Backend API endpoint for file operations</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    
 | 
			
		||||
                    <div class="mb-3">
 | 
			
		||||
                        <label for="theme" class="form-label">Theme:</label>
 | 
			
		||||
                        <select id="theme" class="form-select">
 | 
			
		||||
                            <option value="light">Light</option>
 | 
			
		||||
                            <option value="dark">Dark</option>
 | 
			
		||||
                        </select>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    
 | 
			
		||||
                    <div class="mb-3">
 | 
			
		||||
                        <label for="max-file-size" class="form-label">Max File Size (MB):</label>
 | 
			
		||||
                        <input type="number" id="max-file-size" class="form-control" value="100" min="1" max="1000">
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div class="mb-3">
 | 
			
		||||
                        <label class="form-label">Features:</label>
 | 
			
		||||
                        <div class="form-check">
 | 
			
		||||
                            <input class="form-check-input" type="checkbox" id="show-upload" checked>
 | 
			
		||||
                            <label class="form-check-label" for="show-upload">Show Upload</label>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="form-check">
 | 
			
		||||
                            <input class="form-check-input" type="checkbox" id="show-download" checked>
 | 
			
		||||
                            <label class="form-check-label" for="show-download">Show Download</label>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="form-check">
 | 
			
		||||
                            <input class="form-check-input" type="checkbox" id="show-delete" checked>
 | 
			
		||||
                            <label class="form-check-label" for="show-delete">Show Delete</label>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div class="mb-3">
 | 
			
		||||
                        <label for="initial-path" class="form-label">Initial Path:</label>
 | 
			
		||||
                        <input type="text" id="initial-path" class="form-control" placeholder="e.g., documents/">
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <button id="recreate-widget" class="btn btn-primary w-100">
 | 
			
		||||
                        <i class="bi bi-arrow-clockwise"></i>
 | 
			
		||||
                        Apply Configuration
 | 
			
		||||
                    </button>
 | 
			
		||||
                    
 | 
			
		||||
                    <div class="mt-3">
 | 
			
		||||
                        <div class="d-flex align-items-center justify-content-between">
 | 
			
		||||
                            <div id="status" class="small">
 | 
			
		||||
                                <span class="status-indicator status-loading"></span>
 | 
			
		||||
                                <span id="status-text">Initializing...</span>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="small">
 | 
			
		||||
                                <span class="badge bg-success" id="browser-compat">Compatible</span>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div class="col-md-8">
 | 
			
		||||
                <!-- Widget Rendering Area -->
 | 
			
		||||
                <div class="widget-container">
 | 
			
		||||
                    <div id="file-browser-widget"></div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <script type="module">
 | 
			
		||||
        import init, { 
 | 
			
		||||
            create_file_browser_widget, 
 | 
			
		||||
            create_default_config,
 | 
			
		||||
            check_browser_compatibility,
 | 
			
		||||
            get_version
 | 
			
		||||
        } from '/file_browser_widget.js';
 | 
			
		||||
 | 
			
		||||
        let currentWidget = null;
 | 
			
		||||
        let isInitialized = false;
 | 
			
		||||
 | 
			
		||||
        function updateStatus(text, type = 'loading') {
 | 
			
		||||
            const statusElement = document.getElementById('status-text');
 | 
			
		||||
            const indicatorElement = document.querySelector('.status-indicator');
 | 
			
		||||
            
 | 
			
		||||
            statusElement.textContent = text;
 | 
			
		||||
            indicatorElement.className = `status-indicator status-${type}`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        async function initWidget() {
 | 
			
		||||
            try {
 | 
			
		||||
                updateStatus('Loading WASM module...', 'loading');
 | 
			
		||||
                await init();
 | 
			
		||||
                
 | 
			
		||||
                updateStatus('Checking compatibility...', 'loading');
 | 
			
		||||
                const version = get_version();
 | 
			
		||||
                const isCompatible = check_browser_compatibility();
 | 
			
		||||
                
 | 
			
		||||
                document.getElementById('widget-version').textContent = version;
 | 
			
		||||
                document.getElementById('browser-compat').textContent = isCompatible ? 'Yes ✓' : 'No ✗';
 | 
			
		||||
                
 | 
			
		||||
                if (!isCompatible) {
 | 
			
		||||
                    updateStatus('Browser not compatible', 'error');
 | 
			
		||||
                    document.getElementById('file-browser-widget').innerHTML = 
 | 
			
		||||
                        '<div class="alert alert-danger">Your browser is not compatible with this widget</div>';
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                isInitialized = true;
 | 
			
		||||
                updateStatus('Ready', 'success');
 | 
			
		||||
                createWidget();
 | 
			
		||||
                
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                console.error('Failed to initialize widget:', error);
 | 
			
		||||
                updateStatus(`Initialization failed: ${error.message}`, 'error');
 | 
			
		||||
                document.getElementById('file-browser-widget').innerHTML = 
 | 
			
		||||
                    `<div class="alert alert-danger">Failed to initialize: ${error.message}</div>`;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        function createWidget() {
 | 
			
		||||
            if (!isInitialized) {
 | 
			
		||||
                updateStatus('Widget not initialized', 'error');
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                updateStatus('Creating widget...', 'loading');
 | 
			
		||||
                
 | 
			
		||||
                // Destroy existing widget
 | 
			
		||||
                if (currentWidget) {
 | 
			
		||||
                    currentWidget.destroy();
 | 
			
		||||
                    currentWidget = null;
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                // Clear container
 | 
			
		||||
                const container = document.getElementById('file-browser-widget');
 | 
			
		||||
                container.innerHTML = '';
 | 
			
		||||
                
 | 
			
		||||
                // Get configuration from form
 | 
			
		||||
                const config = create_default_config(document.getElementById('endpoint').value);
 | 
			
		||||
                
 | 
			
		||||
                // Apply configuration using the corrected method names
 | 
			
		||||
                config.setTheme(document.getElementById('theme').value);
 | 
			
		||||
                config.setMaxFileSize(parseInt(document.getElementById('max-file-size').value) * 1024 * 1024);
 | 
			
		||||
                config.setShowUpload(document.getElementById('show-upload').checked);
 | 
			
		||||
                config.setShowDownload(document.getElementById('show-download').checked);
 | 
			
		||||
                config.setShowDelete(document.getElementById('show-delete').checked);
 | 
			
		||||
                
 | 
			
		||||
                const initialPath = document.getElementById('initial-path').value.trim();
 | 
			
		||||
                if (initialPath) {
 | 
			
		||||
                    config.setInitialPath(initialPath);
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                // Create widget
 | 
			
		||||
                currentWidget = create_file_browser_widget('file-browser-widget', config);
 | 
			
		||||
                updateStatus('Widget ready', 'success');
 | 
			
		||||
                
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                console.error('Failed to create widget:', error);
 | 
			
		||||
                updateStatus(`Widget creation failed: ${error.message}`, 'error');
 | 
			
		||||
                document.getElementById('file-browser-widget').innerHTML = 
 | 
			
		||||
                    `<div class="alert alert-danger">Failed to create widget: ${error.message}</div>`;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Event listeners
 | 
			
		||||
        document.getElementById('recreate-widget').addEventListener('click', createWidget);
 | 
			
		||||
 | 
			
		||||
        // Auto-recreate on configuration changes
 | 
			
		||||
        ['endpoint', 'theme', 'max-file-size', 'show-upload', 'show-download', 'show-delete', 'initial-path'].forEach(id => {
 | 
			
		||||
            const element = document.getElementById(id);
 | 
			
		||||
            if (element.type === 'checkbox') {
 | 
			
		||||
                element.addEventListener('change', () => {
 | 
			
		||||
                    if (isInitialized) createWidget();
 | 
			
		||||
                });
 | 
			
		||||
            } else {
 | 
			
		||||
                element.addEventListener('input', () => {
 | 
			
		||||
                    if (isInitialized) {
 | 
			
		||||
                        clearTimeout(element.debounceTimer);
 | 
			
		||||
                        element.debounceTimer = setTimeout(createWidget, 500);
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Initialize when page loads
 | 
			
		||||
        initWidget();
 | 
			
		||||
    </script>
 | 
			
		||||
 | 
			
		||||
    <!-- Assets Modal -->
 | 
			
		||||
    <div class="modal fade" id="assetsModal" tabindex="-1" aria-labelledby="assetsModalLabel" aria-hidden="true">
 | 
			
		||||
        <div class="modal-dialog modal-lg">
 | 
			
		||||
            <div class="modal-content">
 | 
			
		||||
                <div class="modal-header">
 | 
			
		||||
                    <h5 class="modal-title" id="assetsModalLabel">
 | 
			
		||||
                        <i class="bi bi-file-earmark-zip text-primary"></i>
 | 
			
		||||
                        Widget Assets & Size Optimization
 | 
			
		||||
                    </h5>
 | 
			
		||||
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="modal-body">
 | 
			
		||||
                    <div class="row mb-4">
 | 
			
		||||
                        <div class="col-md-6">
 | 
			
		||||
                            <h6 class="text-success">
 | 
			
		||||
                                <i class="bi bi-check-circle"></i>
 | 
			
		||||
                                Distribution Files
 | 
			
		||||
                            </h6>
 | 
			
		||||
                            <p class="small text-muted mb-3">Self-contained widget distribution with no external dependencies.</p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col-md-6 text-end">
 | 
			
		||||
                            <div class="small">
 | 
			
		||||
                                <div class="badge bg-success mb-2">67.9% compression ratio</div>
 | 
			
		||||
                                <div class="text-muted">Optimized for web delivery</div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    
 | 
			
		||||
                    <div class="table-responsive">
 | 
			
		||||
                        <table class="table table-striped">
 | 
			
		||||
                            <thead class="table-dark">
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <th>Asset</th>
 | 
			
		||||
                                    <th>Description</th>
 | 
			
		||||
                                    <th class="text-end">Original</th>
 | 
			
		||||
                                    <th class="text-end">Gzipped</th>
 | 
			
		||||
                                    <th class="text-end">Savings</th>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                            </thead>
 | 
			
		||||
                            <tbody>
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <td><code>file_browser_widget_bg.wasm</code></td>
 | 
			
		||||
                                    <td class="small text-muted">WebAssembly binary</td>
 | 
			
		||||
                                    <td class="text-end">331KB</td>
 | 
			
		||||
                                    <td class="text-end text-info">136KB</td>
 | 
			
		||||
                                    <td class="text-end text-success">59%</td>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <td><code>file_browser_widget.js</code></td>
 | 
			
		||||
                                    <td class="small text-muted">JavaScript bindings</td>
 | 
			
		||||
                                    <td class="text-end">39KB</td>
 | 
			
		||||
                                    <td class="text-end text-info">7KB</td>
 | 
			
		||||
                                    <td class="text-end text-success">82%</td>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <td><code>uppy.min.js</code></td>
 | 
			
		||||
                                    <td class="small text-muted">File upload library</td>
 | 
			
		||||
                                    <td class="text-end">564KB</td>
 | 
			
		||||
                                    <td class="text-end text-info">172KB</td>
 | 
			
		||||
                                    <td class="text-end text-success">70%</td>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <td><code>uppy.min.css</code></td>
 | 
			
		||||
                                    <td class="small text-muted">Upload UI styling</td>
 | 
			
		||||
                                    <td class="text-end">90KB</td>
 | 
			
		||||
                                    <td class="text-end text-info">14KB</td>
 | 
			
		||||
                                    <td class="text-end text-success">84%</td>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <td><code>file_browser_widget.d.ts</code></td>
 | 
			
		||||
                                    <td class="small text-muted">TypeScript definitions</td>
 | 
			
		||||
                                    <td class="text-end">5KB</td>
 | 
			
		||||
                                    <td class="text-end text-muted">-</td>
 | 
			
		||||
                                    <td class="text-end text-muted">-</td>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                                <tr class="table-active fw-bold">
 | 
			
		||||
                                    <td>Total Distribution</td>
 | 
			
		||||
                                    <td class="small text-muted">Complete widget package</td>
 | 
			
		||||
                                    <td class="text-end">1.03MB</td>
 | 
			
		||||
                                    <td class="text-end text-success">329KB</td>
 | 
			
		||||
                                    <td class="text-end text-success">68%</td>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                            </tbody>
 | 
			
		||||
                        </table>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    
 | 
			
		||||
                    <div class="row mt-4">
 | 
			
		||||
                        <div class="col-md-6">
 | 
			
		||||
                            <h6 class="text-info">
 | 
			
		||||
                                <i class="bi bi-speedometer2"></i>
 | 
			
		||||
                                Performance Benefits
 | 
			
		||||
                            </h6>
 | 
			
		||||
                            <ul class="small">
 | 
			
		||||
                                <li>Faster initial load times</li>
 | 
			
		||||
                                <li>Reduced bandwidth usage</li>
 | 
			
		||||
                                <li>Better mobile experience</li>
 | 
			
		||||
                                <li>CDN-friendly distribution</li>
 | 
			
		||||
                            </ul>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col-md-6">
 | 
			
		||||
                            <h6 class="text-warning">
 | 
			
		||||
                                <i class="bi bi-tools"></i>
 | 
			
		||||
                                Optimization Techniques
 | 
			
		||||
                            </h6>
 | 
			
		||||
                            <ul class="small">
 | 
			
		||||
                                <li>wasm-opt binary optimization</li>
 | 
			
		||||
                                <li>Gzip compression (level 9)</li>
 | 
			
		||||
                                <li>Dead code elimination</li>
 | 
			
		||||
                                <li>Release build optimizations</li>
 | 
			
		||||
                            </ul>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="modal-footer">
 | 
			
		||||
                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
 | 
			
		||||
                    <button type="button" class="btn btn-primary" onclick="window.open('https://github.com/herocode/framework/tree/main/widgets/file_browser_widget', '_blank')">
 | 
			
		||||
                        <i class="bi bi-download"></i>
 | 
			
		||||
                        Download Widget
 | 
			
		||||
                    </button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Bootstrap JS -->
 | 
			
		||||
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										237
									
								
								widgets/file_browser_widget/examples/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										237
									
								
								widgets/file_browser_widget/examples/main.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,237 @@
 | 
			
		||||
use std::fs;
 | 
			
		||||
use std::io::prelude::*;
 | 
			
		||||
use std::net::{TcpListener, TcpStream};
 | 
			
		||||
use std::path::Path;
 | 
			
		||||
use std::process::Command;
 | 
			
		||||
 | 
			
		||||
fn main() {
 | 
			
		||||
    println!("🚀 Starting FileBrowser Widget Example Server...");
 | 
			
		||||
    println!();
 | 
			
		||||
    
 | 
			
		||||
    // Check if we have the built widget files in dist/ directory
 | 
			
		||||
    let dist_dir = Path::new("dist");
 | 
			
		||||
    let widget_files = [
 | 
			
		||||
        "file_browser_widget.js", 
 | 
			
		||||
        "file_browser_widget_bg.wasm",
 | 
			
		||||
        "uppy.min.js",
 | 
			
		||||
        "uppy.min.css"
 | 
			
		||||
    ];
 | 
			
		||||
    
 | 
			
		||||
    let mut missing_files = Vec::new();
 | 
			
		||||
    for file in &widget_files {
 | 
			
		||||
        let file_path = dist_dir.join(file);
 | 
			
		||||
        if !file_path.exists() {
 | 
			
		||||
            missing_files.push(*file);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Check if we have the HTML file in examples/ directory
 | 
			
		||||
    let examples_dir = Path::new("examples");
 | 
			
		||||
    let html_file = examples_dir.join("index.html");
 | 
			
		||||
    if !html_file.exists() {
 | 
			
		||||
        missing_files.push("examples/index.html");
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    if !missing_files.is_empty() {
 | 
			
		||||
        println!("❌ Error: Missing required files:");
 | 
			
		||||
        for file in &missing_files {
 | 
			
		||||
            println!("   - {}", file);
 | 
			
		||||
        }
 | 
			
		||||
        println!();
 | 
			
		||||
        println!("💡 Run the build script first: ./build.sh");
 | 
			
		||||
        println!("   This will generate the required widget files in dist/.");
 | 
			
		||||
        std::process::exit(1);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    println!("✅ All required files found");
 | 
			
		||||
    println!();
 | 
			
		||||
    
 | 
			
		||||
    // Create compressed versions for optimized serving
 | 
			
		||||
    create_compressed_assets();
 | 
			
		||||
    
 | 
			
		||||
    let listener = TcpListener::bind("127.0.0.1:8081").unwrap();
 | 
			
		||||
    println!("🌐 FileBrowser Widget Example Server");
 | 
			
		||||
    println!("📡 Serving on http://localhost:8081");
 | 
			
		||||
    println!("🔗 Open http://localhost:8081 in your browser to test the widget");
 | 
			
		||||
    println!("⏹️  Press Ctrl+C to stop the server");
 | 
			
		||||
    println!();
 | 
			
		||||
 | 
			
		||||
    for stream in listener.incoming() {
 | 
			
		||||
        let stream = stream.unwrap();
 | 
			
		||||
        handle_connection(stream);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn handle_connection(mut stream: TcpStream) {
 | 
			
		||||
    let mut buffer = [0; 1024];
 | 
			
		||||
    stream.read(&mut buffer).unwrap();
 | 
			
		||||
 | 
			
		||||
    let request = String::from_utf8_lossy(&buffer[..]);
 | 
			
		||||
    let request_line = request.lines().next().unwrap_or("");
 | 
			
		||||
    
 | 
			
		||||
    if let Some(path) = request_line.split_whitespace().nth(1) {
 | 
			
		||||
        let file_path = match path {
 | 
			
		||||
            "/" => "index.html",  // Serve the HTML file from examples/
 | 
			
		||||
            path if path.starts_with('/') => {
 | 
			
		||||
                let clean_path = &path[1..]; // Remove leading slash
 | 
			
		||||
                
 | 
			
		||||
                // Check if this is a request for a static asset
 | 
			
		||||
                if is_static_asset(clean_path) {
 | 
			
		||||
                    clean_path
 | 
			
		||||
                } else {
 | 
			
		||||
                    // For all non-asset routes, serve index.html to support client-side routing
 | 
			
		||||
                    "index.html"
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            _ => "index.html",
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        serve_file(&mut stream, file_path);
 | 
			
		||||
    } else {
 | 
			
		||||
        serve_404(&mut stream);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn is_static_asset(path: &str) -> bool {
 | 
			
		||||
    // Check if the path is for a static asset (widget files)
 | 
			
		||||
    matches!(path, 
 | 
			
		||||
        "file_browser_widget.js" | 
 | 
			
		||||
        "file_browser_widget_bg.wasm" | 
 | 
			
		||||
        "file_browser_widget.d.ts" |
 | 
			
		||||
        "uppy.min.js" | 
 | 
			
		||||
        "uppy.min.css" |
 | 
			
		||||
        "favicon.ico"
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn create_compressed_assets() {
 | 
			
		||||
    println!("🗜 Creating compressed assets for optimized serving...");
 | 
			
		||||
    
 | 
			
		||||
    // Create examples/compressed directory
 | 
			
		||||
    let compressed_dir = Path::new("examples/compressed");
 | 
			
		||||
    if !compressed_dir.exists() {
 | 
			
		||||
        fs::create_dir_all(compressed_dir).expect("Failed to create compressed directory");
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // List of files to compress from dist/
 | 
			
		||||
    let files_to_compress = [
 | 
			
		||||
        "file_browser_widget.js",
 | 
			
		||||
        "file_browser_widget_bg.wasm",
 | 
			
		||||
        "uppy.min.js",
 | 
			
		||||
        "uppy.min.css",
 | 
			
		||||
    ];
 | 
			
		||||
    
 | 
			
		||||
    for file in &files_to_compress {
 | 
			
		||||
        let source_path = format!("dist/{}", file);
 | 
			
		||||
        let compressed_path = format!("examples/compressed/{}.gz", file);
 | 
			
		||||
        
 | 
			
		||||
        // Check if source exists and compressed version needs updating
 | 
			
		||||
        if Path::new(&source_path).exists() {
 | 
			
		||||
            let needs_compression = !Path::new(&compressed_path).exists() ||
 | 
			
		||||
                fs::metadata(&source_path).unwrap().modified().unwrap() >
 | 
			
		||||
                fs::metadata(&compressed_path).unwrap_or_else(|_| fs::metadata(&source_path).unwrap()).modified().unwrap();
 | 
			
		||||
            
 | 
			
		||||
            if needs_compression {
 | 
			
		||||
                let output = Command::new("gzip")
 | 
			
		||||
                    .args(&["-9", "-c", &source_path])
 | 
			
		||||
                    .output()
 | 
			
		||||
                    .expect("Failed to execute gzip");
 | 
			
		||||
                
 | 
			
		||||
                if output.status.success() {
 | 
			
		||||
                    fs::write(&compressed_path, output.stdout)
 | 
			
		||||
                        .expect("Failed to write compressed file");
 | 
			
		||||
                    
 | 
			
		||||
                    let original_size = fs::metadata(&source_path).unwrap().len();
 | 
			
		||||
                    let compressed_size = fs::metadata(&compressed_path).unwrap().len();
 | 
			
		||||
                    let ratio = (compressed_size as f64 / original_size as f64 * 100.0) as u32;
 | 
			
		||||
                    
 | 
			
		||||
                    println!("  ✅ {} compressed: {} → {} bytes ({}%)", file, original_size, compressed_size, ratio);
 | 
			
		||||
                } else {
 | 
			
		||||
                    println!("  ⚠️  Failed to compress {}", file);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    println!("🎯 Compressed assets ready in examples/compressed/");
 | 
			
		||||
    println!();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn serve_file(stream: &mut TcpStream, file_path: &str) {
 | 
			
		||||
    let current_dir = std::env::current_dir().unwrap();
 | 
			
		||||
    
 | 
			
		||||
    // Determine which directory to serve from based on file type
 | 
			
		||||
    let (full_path, use_gzip) = match file_path {
 | 
			
		||||
        "index.html" => (current_dir.join("examples").join(file_path), false),
 | 
			
		||||
        _ => {
 | 
			
		||||
            let base_path = current_dir.join("dist").join(file_path);
 | 
			
		||||
            let gzip_path = current_dir.join("examples/compressed").join(format!("{}.gz", file_path));
 | 
			
		||||
            
 | 
			
		||||
            // Prefer gzipped version from examples/compressed if it exists
 | 
			
		||||
            if gzip_path.exists() {
 | 
			
		||||
                (gzip_path, true)
 | 
			
		||||
            } else {
 | 
			
		||||
                (base_path, false)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    if full_path.exists() && full_path.is_file() {
 | 
			
		||||
        match fs::read(&full_path) {
 | 
			
		||||
            Ok(contents) => {
 | 
			
		||||
                let content_type = get_content_type(file_path);
 | 
			
		||||
                let mut response = format!(
 | 
			
		||||
                    "HTTP/1.1 200 OK\r\nContent-Type: {}\r\nContent-Length: {}",
 | 
			
		||||
                    content_type,
 | 
			
		||||
                    contents.len()
 | 
			
		||||
                );
 | 
			
		||||
                
 | 
			
		||||
                // Add gzip encoding header if serving compressed content
 | 
			
		||||
                if use_gzip {
 | 
			
		||||
                    response.push_str("\r\nContent-Encoding: gzip");
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                response.push_str("\r\n\r\n");
 | 
			
		||||
                
 | 
			
		||||
                let _ = stream.write_all(response.as_bytes());
 | 
			
		||||
                let _ = stream.write_all(&contents);
 | 
			
		||||
                
 | 
			
		||||
                let compression_info = if use_gzip { " (gzipped)" } else { "" };
 | 
			
		||||
                println!("📄 Served: {}{} ({} bytes)", file_path, compression_info, contents.len());
 | 
			
		||||
            }
 | 
			
		||||
            Err(_) => serve_404(stream),
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        serve_404(stream);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn serve_404(stream: &mut TcpStream) {
 | 
			
		||||
    let response = "HTTP/1.1 404 NOT FOUND\r\nContent-Type: text/html\r\n\r\n<h1>404 Not Found</h1>";
 | 
			
		||||
    stream.write_all(response.as_bytes()).unwrap();
 | 
			
		||||
    stream.flush().unwrap();
 | 
			
		||||
    println!("❌ 404 Not Found");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn get_content_type(file_path: &str) -> &'static str {
 | 
			
		||||
    let extension = Path::new(file_path)
 | 
			
		||||
        .extension()
 | 
			
		||||
        .and_then(|ext| ext.to_str())
 | 
			
		||||
        .unwrap_or("");
 | 
			
		||||
    
 | 
			
		||||
    match extension {
 | 
			
		||||
        "html" => "text/html; charset=utf-8",
 | 
			
		||||
        "js" => "application/javascript",
 | 
			
		||||
        "css" => "text/css",
 | 
			
		||||
        "wasm" => "application/wasm",
 | 
			
		||||
        "json" => "application/json",
 | 
			
		||||
        "png" => "image/png",
 | 
			
		||||
        "jpg" | "jpeg" => "image/jpeg",
 | 
			
		||||
        "gif" => "image/gif",
 | 
			
		||||
        "svg" => "image/svg+xml",
 | 
			
		||||
        "ico" => "image/x-icon",
 | 
			
		||||
        "ts" => "application/typescript",
 | 
			
		||||
        "md" => "text/markdown",
 | 
			
		||||
        _ => "text/plain",
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user