add file browser component and widget
This commit is contained in:
BIN
widgets/file_browser_widget/.DS_Store
vendored
Normal file
BIN
widgets/file_browser_widget/.DS_Store
vendored
Normal file
Binary file not shown.
2905
widgets/file_browser_widget/Cargo.lock
generated
Normal file
2905
widgets/file_browser_widget/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
widgets/file_browser_widget/Cargo.toml
Normal file
45
widgets/file_browser_widget/Cargo.toml
Normal file
@@ -0,0 +1,45 @@
|
||||
[package]
|
||||
name = "file_browser_widget"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "WASM widget for embedding the FileBrowser component in web applications"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/herocode/framework"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
|
||||
|
||||
[dependencies]
|
||||
components = { path = "../../components" }
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
js-sys = "0.3"
|
||||
yew = { version = "0.21", features = ["csr"] }
|
||||
gloo = "0.11"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde-wasm-bindgen = "0.6"
|
||||
console_error_panic_hook = "0.1"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"console",
|
||||
"Document",
|
||||
"Element",
|
||||
"HtmlElement",
|
||||
"Window",
|
||||
"Location",
|
||||
"UrlSearchParams"
|
||||
]
|
||||
|
||||
[[example]]
|
||||
name = "server"
|
||||
path = "examples/main.rs"
|
||||
|
||||
[profile.release]
|
||||
# Tell `rustc` to optimize for small code size.
|
||||
opt-level = "s"
|
||||
# Enable link time optimization
|
||||
lto = true
|
331
widgets/file_browser_widget/README.md
Normal file
331
widgets/file_browser_widget/README.md
Normal file
@@ -0,0 +1,331 @@
|
||||
# FileBrowser Widget
|
||||
|
||||
A powerful WebAssembly-based file browser widget that can be embedded in any web application. Built with Rust and Yew, compiled to WASM for maximum performance.
|
||||
|
||||
## Features
|
||||
|
||||
- 📁 **File & Directory Management**: Browse, create, delete files and directories
|
||||
- 📤 **Resumable Uploads**: Upload files with progress tracking using TUS protocol
|
||||
- 📥 **File Downloads**: Download files with proper MIME type handling
|
||||
- ✏️ **File Editing**: Built-in editors for text files and markdown with live preview
|
||||
- 🎨 **Modern UI**: Responsive Bootstrap-based interface with light/dark themes
|
||||
- 🔧 **Highly Configurable**: Extensive JavaScript API for customization
|
||||
- 🚀 **High Performance**: Compiled to WebAssembly for native-like performance
|
||||
- 🌐 **Cross-Browser**: Works in all modern browsers
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Installation
|
||||
|
||||
#### Via npm (recommended)
|
||||
```bash
|
||||
npm install @herocode/file-browser-widget
|
||||
```
|
||||
|
||||
#### Manual Download
|
||||
Download the latest release files:
|
||||
- `file_browser_widget.js`
|
||||
- `file_browser_widget_bg.wasm`
|
||||
|
||||
### 2. Include Dependencies
|
||||
|
||||
Add Bootstrap CSS and Icons to your HTML:
|
||||
```html
|
||||
<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">
|
||||
```
|
||||
|
||||
### 3. Create Container
|
||||
|
||||
Add a container element to your HTML:
|
||||
```html
|
||||
<div id="file-browser-container"></div>
|
||||
```
|
||||
|
||||
### 4. Initialize Widget
|
||||
|
||||
```javascript
|
||||
import init, {
|
||||
create_file_browser_widget,
|
||||
create_default_config
|
||||
} from '@herocode/file-browser-widget';
|
||||
|
||||
async function initFileBrowser() {
|
||||
// Initialize the WASM module
|
||||
await init();
|
||||
|
||||
// Create configuration
|
||||
const config = create_default_config('http://localhost:3001/files');
|
||||
config.set_theme('light');
|
||||
config.set_show_upload(true);
|
||||
config.set_show_download(true);
|
||||
config.set_show_delete(true);
|
||||
config.set_max_file_size(100 * 1024 * 1024); // 100MB
|
||||
|
||||
// Create and mount the widget
|
||||
const widget = create_file_browser_widget('file-browser-container', config);
|
||||
|
||||
console.log('FileBrowser widget initialized successfully!');
|
||||
}
|
||||
|
||||
// Initialize when page loads
|
||||
initFileBrowser().catch(console.error);
|
||||
```
|
||||
|
||||
## Configuration API
|
||||
|
||||
### WidgetConfig
|
||||
|
||||
The main configuration object for customizing the widget behavior:
|
||||
|
||||
```javascript
|
||||
const config = create_default_config('http://localhost:3001/files');
|
||||
|
||||
// File size limit (in bytes)
|
||||
config.set_max_file_size(50 * 1024 * 1024); // 50MB
|
||||
|
||||
// Feature toggles
|
||||
config.set_show_upload(true);
|
||||
config.set_show_download(true);
|
||||
config.set_show_delete(true);
|
||||
|
||||
// UI customization
|
||||
config.set_theme('dark'); // 'light' or 'dark'
|
||||
config.set_css_classes('my-custom-class');
|
||||
config.set_initial_path('documents/');
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `base_endpoint` | string | required | Backend API endpoint for file operations |
|
||||
| `max_file_size` | number | 104857600 | Maximum file size for uploads (bytes) |
|
||||
| `show_upload` | boolean | true | Enable/disable upload functionality |
|
||||
| `show_download` | boolean | true | Enable/disable download functionality |
|
||||
| `show_delete` | boolean | true | Enable/disable delete functionality |
|
||||
| `theme` | string | 'light' | UI theme ('light' or 'dark') |
|
||||
| `css_classes` | string | '' | Additional CSS classes for root element |
|
||||
| `initial_path` | string | '' | Initial directory path to display |
|
||||
|
||||
## JavaScript API
|
||||
|
||||
### Functions
|
||||
|
||||
#### `init()`
|
||||
Initialize the WASM module. Must be called before using other functions.
|
||||
|
||||
```javascript
|
||||
await init();
|
||||
```
|
||||
|
||||
#### `create_default_config(base_endpoint)`
|
||||
Create a default configuration object.
|
||||
|
||||
```javascript
|
||||
const config = create_default_config('http://localhost:3001/files');
|
||||
```
|
||||
|
||||
#### `create_file_browser_widget(container_id, config)`
|
||||
Create and mount a widget to a DOM element by ID.
|
||||
|
||||
```javascript
|
||||
const widget = create_file_browser_widget('my-container', config);
|
||||
```
|
||||
|
||||
#### `create_file_browser_widget_on_element(element, config)`
|
||||
Create and mount a widget to a specific DOM element.
|
||||
|
||||
```javascript
|
||||
const element = document.getElementById('my-container');
|
||||
const widget = create_file_browser_widget_on_element(element, config);
|
||||
```
|
||||
|
||||
#### `check_browser_compatibility()`
|
||||
Check if the current browser supports the widget.
|
||||
|
||||
```javascript
|
||||
if (!check_browser_compatibility()) {
|
||||
console.error('Browser not supported');
|
||||
}
|
||||
```
|
||||
|
||||
#### `get_version()`
|
||||
Get the widget version.
|
||||
|
||||
```javascript
|
||||
console.log('Widget version:', get_version());
|
||||
```
|
||||
|
||||
### Widget Handle
|
||||
|
||||
The widget functions return a handle that can be used to manage the widget:
|
||||
|
||||
```javascript
|
||||
const widget = create_file_browser_widget('container', config);
|
||||
|
||||
// Destroy the widget
|
||||
widget.destroy();
|
||||
```
|
||||
|
||||
## Backend Requirements
|
||||
|
||||
The widget expects a REST API compatible with the following endpoints:
|
||||
|
||||
### File Listing
|
||||
```
|
||||
GET /files/list/<path>
|
||||
Response: JSON array of file/directory objects
|
||||
```
|
||||
|
||||
### Directory Creation
|
||||
```
|
||||
POST /files/dirs/<path>
|
||||
Creates a new directory at the specified path
|
||||
```
|
||||
|
||||
### File/Directory Deletion
|
||||
```
|
||||
DELETE /files/delete/<path>
|
||||
Deletes the file or directory at the specified path
|
||||
```
|
||||
|
||||
### File Upload (TUS Protocol)
|
||||
```
|
||||
POST /files/upload
|
||||
Handles resumable file uploads using TUS protocol
|
||||
```
|
||||
|
||||
### File Download
|
||||
```
|
||||
GET /files/download/<path>
|
||||
Returns the file content with appropriate headers
|
||||
```
|
||||
|
||||
### Example Backend Response Format
|
||||
|
||||
File listing response:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "document.pdf",
|
||||
"path": "documents/document.pdf",
|
||||
"size": 1024000,
|
||||
"is_directory": false,
|
||||
"modified": "2023-12-01T10:30:00Z"
|
||||
},
|
||||
{
|
||||
"name": "images",
|
||||
"path": "documents/images",
|
||||
"size": 0,
|
||||
"is_directory": true,
|
||||
"modified": "2023-12-01T09:15:00Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Custom Styling
|
||||
|
||||
Add custom CSS to style the widget:
|
||||
|
||||
```css
|
||||
.file-browser-widget {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.file-browser-widget .card {
|
||||
border: none;
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple Widgets
|
||||
|
||||
You can create multiple widget instances on the same page:
|
||||
|
||||
```javascript
|
||||
// Widget 1
|
||||
const config1 = create_default_config('http://api1.example.com/files');
|
||||
const widget1 = create_file_browser_widget('container1', config1);
|
||||
|
||||
// Widget 2
|
||||
const config2 = create_default_config('http://api2.example.com/files');
|
||||
config2.set_theme('dark');
|
||||
const widget2 = create_file_browser_widget('container2', config2);
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```javascript
|
||||
try {
|
||||
const widget = create_file_browser_widget('container', config);
|
||||
} catch (error) {
|
||||
console.error('Failed to create widget:', error);
|
||||
// Show fallback UI
|
||||
}
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Building from Source
|
||||
|
||||
1. Install Rust and wasm-pack:
|
||||
```bash
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
cargo install wasm-pack
|
||||
```
|
||||
|
||||
2. Build the widget:
|
||||
```bash
|
||||
./build.sh
|
||||
```
|
||||
|
||||
3. Test locally:
|
||||
```bash
|
||||
npm run dev
|
||||
# Open http://localhost:8000/example.html
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
widgets/file_browser_widget/
|
||||
├── src/
|
||||
│ └── lib.rs # Main widget implementation
|
||||
├── dist/ # Built distribution files
|
||||
├── pkg/ # wasm-pack output
|
||||
├── Cargo.toml # Rust dependencies
|
||||
├── package.json # npm package config
|
||||
├── build.sh # Build script
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Browser Support
|
||||
|
||||
- Chrome 57+
|
||||
- Firefox 52+
|
||||
- Safari 11+
|
||||
- Edge 16+
|
||||
|
||||
The widget requires WebAssembly support and modern JavaScript features.
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see LICENSE file for details.
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Add tests if applicable
|
||||
5. Submit a pull request
|
||||
|
||||
## Support
|
||||
|
||||
- 📖 [Documentation](https://github.com/herocode/framework)
|
||||
- 🐛 [Issue Tracker](https://github.com/herocode/framework/issues)
|
||||
- 💬 [Discussions](https://github.com/herocode/framework/discussions)
|
49
widgets/file_browser_widget/build.sh
Executable file
49
widgets/file_browser_widget/build.sh
Executable file
@@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Build script for FileBrowser Widget
|
||||
# This script creates a unified distribution in dist/ that works for both:
|
||||
# 1. NPM distribution (includes package.json, TypeScript definitions, README)
|
||||
# 2. Direct embedding (includes bundled Uppy.js dependencies)
|
||||
|
||||
set -e
|
||||
|
||||
echo "🔧 Building FileBrowser Widget..."
|
||||
|
||||
# Clean previous builds
|
||||
echo "🧹 Cleaning previous builds..."
|
||||
rm -rf dist/
|
||||
|
||||
# Build with wasm-pack directly to dist directory
|
||||
echo "📦 Building WASM package directly to dist/..."
|
||||
wasm-pack build --target web --out-dir dist --release
|
||||
|
||||
# Download and bundle Uppy.js dependencies for self-contained use
|
||||
echo "📦 Downloading Uppy.js dependencies..."
|
||||
curl -s "https://releases.transloadit.com/uppy/v3.25.4/uppy.min.js" -o dist/uppy.min.js
|
||||
curl -s "https://releases.transloadit.com/uppy/v3.25.4/uppy.min.css" -o dist/uppy.min.css
|
||||
|
||||
# Verify files were downloaded correctly
|
||||
if [ -s dist/uppy.min.js ] && [ -s dist/uppy.min.css ]; then
|
||||
echo "✅ Uppy.js bundled successfully ($(wc -c < dist/uppy.min.js) bytes JS, $(wc -c < dist/uppy.min.css) bytes CSS)"
|
||||
else
|
||||
echo "❌ Failed to download Uppy.js files"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Build complete!"
|
||||
echo ""
|
||||
echo "📦 Unified distribution created in dist/ with:"
|
||||
echo " 🔹 NPM support: package.json, TypeScript definitions, README"
|
||||
echo " 🔹 Direct embedding: bundled Uppy.js dependencies"
|
||||
echo " 🔹 Self-contained: no external dependencies required"
|
||||
echo ""
|
||||
echo "🌐 Use 'cargo run --example server' to test the widget"
|
||||
echo "📦 Use 'npm publish dist/' to publish to npm"
|
||||
echo ""
|
||||
|
||||
echo "🎯 All widget files ready in dist/ directory:"
|
||||
echo " - file_browser_widget.js ($(wc -c < dist/file_browser_widget.js 2>/dev/null || echo '0') bytes)"
|
||||
echo " - file_browser_widget_bg.wasm ($(wc -c < dist/file_browser_widget_bg.wasm 2>/dev/null || echo '0') bytes)"
|
||||
echo " - uppy.min.js ($(wc -c < dist/uppy.min.js 2>/dev/null || echo '0') bytes)"
|
||||
echo " - uppy.min.css ($(wc -c < dist/uppy.min.css 2>/dev/null || echo '0') bytes)"
|
||||
echo " - package.json, *.d.ts, README.md (npm metadata)"
|
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",
|
||||
}
|
||||
}
|
48
widgets/file_browser_widget/package.json
Normal file
48
widgets/file_browser_widget/package.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "@herocode/file-browser-widget",
|
||||
"version": "0.1.0",
|
||||
"description": "WebAssembly-based file browser widget for web applications",
|
||||
"main": "dist/file_browser_widget.js",
|
||||
"types": "dist/file_browser_widget.d.ts",
|
||||
"files": [
|
||||
"dist/file_browser_widget.js",
|
||||
"dist/file_browser_widget_bg.wasm",
|
||||
"dist/file_browser_widget.d.ts",
|
||||
"dist/README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "./build.sh",
|
||||
"clean": "rm -rf pkg/ dist/",
|
||||
"dev": "python3 -m http.server 8000 --directory dist",
|
||||
"test": "echo \"No tests specified\" && exit 0"
|
||||
},
|
||||
"keywords": [
|
||||
"file-browser",
|
||||
"wasm",
|
||||
"webassembly",
|
||||
"rust",
|
||||
"yew",
|
||||
"widget",
|
||||
"file-upload",
|
||||
"file-manager"
|
||||
],
|
||||
"author": "HeroCode Team",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/herocode/framework.git",
|
||||
"directory": "widgets/file_browser_widget"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/herocode/framework/issues"
|
||||
},
|
||||
"homepage": "https://github.com/herocode/framework#readme",
|
||||
"devDependencies": {},
|
||||
"peerDependencies": {},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
331
widgets/file_browser_widget/pkg/README.md
Normal file
331
widgets/file_browser_widget/pkg/README.md
Normal file
@@ -0,0 +1,331 @@
|
||||
# FileBrowser Widget
|
||||
|
||||
A powerful WebAssembly-based file browser widget that can be embedded in any web application. Built with Rust and Yew, compiled to WASM for maximum performance.
|
||||
|
||||
## Features
|
||||
|
||||
- 📁 **File & Directory Management**: Browse, create, delete files and directories
|
||||
- 📤 **Resumable Uploads**: Upload files with progress tracking using TUS protocol
|
||||
- 📥 **File Downloads**: Download files with proper MIME type handling
|
||||
- ✏️ **File Editing**: Built-in editors for text files and markdown with live preview
|
||||
- 🎨 **Modern UI**: Responsive Bootstrap-based interface with light/dark themes
|
||||
- 🔧 **Highly Configurable**: Extensive JavaScript API for customization
|
||||
- 🚀 **High Performance**: Compiled to WebAssembly for native-like performance
|
||||
- 🌐 **Cross-Browser**: Works in all modern browsers
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Installation
|
||||
|
||||
#### Via npm (recommended)
|
||||
```bash
|
||||
npm install @herocode/file-browser-widget
|
||||
```
|
||||
|
||||
#### Manual Download
|
||||
Download the latest release files:
|
||||
- `file_browser_widget.js`
|
||||
- `file_browser_widget_bg.wasm`
|
||||
|
||||
### 2. Include Dependencies
|
||||
|
||||
Add Bootstrap CSS and Icons to your HTML:
|
||||
```html
|
||||
<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">
|
||||
```
|
||||
|
||||
### 3. Create Container
|
||||
|
||||
Add a container element to your HTML:
|
||||
```html
|
||||
<div id="file-browser-container"></div>
|
||||
```
|
||||
|
||||
### 4. Initialize Widget
|
||||
|
||||
```javascript
|
||||
import init, {
|
||||
create_file_browser_widget,
|
||||
create_default_config
|
||||
} from '@herocode/file-browser-widget';
|
||||
|
||||
async function initFileBrowser() {
|
||||
// Initialize the WASM module
|
||||
await init();
|
||||
|
||||
// Create configuration
|
||||
const config = create_default_config('http://localhost:3001/files');
|
||||
config.set_theme('light');
|
||||
config.set_show_upload(true);
|
||||
config.set_show_download(true);
|
||||
config.set_show_delete(true);
|
||||
config.set_max_file_size(100 * 1024 * 1024); // 100MB
|
||||
|
||||
// Create and mount the widget
|
||||
const widget = create_file_browser_widget('file-browser-container', config);
|
||||
|
||||
console.log('FileBrowser widget initialized successfully!');
|
||||
}
|
||||
|
||||
// Initialize when page loads
|
||||
initFileBrowser().catch(console.error);
|
||||
```
|
||||
|
||||
## Configuration API
|
||||
|
||||
### WidgetConfig
|
||||
|
||||
The main configuration object for customizing the widget behavior:
|
||||
|
||||
```javascript
|
||||
const config = create_default_config('http://localhost:3001/files');
|
||||
|
||||
// File size limit (in bytes)
|
||||
config.set_max_file_size(50 * 1024 * 1024); // 50MB
|
||||
|
||||
// Feature toggles
|
||||
config.set_show_upload(true);
|
||||
config.set_show_download(true);
|
||||
config.set_show_delete(true);
|
||||
|
||||
// UI customization
|
||||
config.set_theme('dark'); // 'light' or 'dark'
|
||||
config.set_css_classes('my-custom-class');
|
||||
config.set_initial_path('documents/');
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `base_endpoint` | string | required | Backend API endpoint for file operations |
|
||||
| `max_file_size` | number | 104857600 | Maximum file size for uploads (bytes) |
|
||||
| `show_upload` | boolean | true | Enable/disable upload functionality |
|
||||
| `show_download` | boolean | true | Enable/disable download functionality |
|
||||
| `show_delete` | boolean | true | Enable/disable delete functionality |
|
||||
| `theme` | string | 'light' | UI theme ('light' or 'dark') |
|
||||
| `css_classes` | string | '' | Additional CSS classes for root element |
|
||||
| `initial_path` | string | '' | Initial directory path to display |
|
||||
|
||||
## JavaScript API
|
||||
|
||||
### Functions
|
||||
|
||||
#### `init()`
|
||||
Initialize the WASM module. Must be called before using other functions.
|
||||
|
||||
```javascript
|
||||
await init();
|
||||
```
|
||||
|
||||
#### `create_default_config(base_endpoint)`
|
||||
Create a default configuration object.
|
||||
|
||||
```javascript
|
||||
const config = create_default_config('http://localhost:3001/files');
|
||||
```
|
||||
|
||||
#### `create_file_browser_widget(container_id, config)`
|
||||
Create and mount a widget to a DOM element by ID.
|
||||
|
||||
```javascript
|
||||
const widget = create_file_browser_widget('my-container', config);
|
||||
```
|
||||
|
||||
#### `create_file_browser_widget_on_element(element, config)`
|
||||
Create and mount a widget to a specific DOM element.
|
||||
|
||||
```javascript
|
||||
const element = document.getElementById('my-container');
|
||||
const widget = create_file_browser_widget_on_element(element, config);
|
||||
```
|
||||
|
||||
#### `check_browser_compatibility()`
|
||||
Check if the current browser supports the widget.
|
||||
|
||||
```javascript
|
||||
if (!check_browser_compatibility()) {
|
||||
console.error('Browser not supported');
|
||||
}
|
||||
```
|
||||
|
||||
#### `get_version()`
|
||||
Get the widget version.
|
||||
|
||||
```javascript
|
||||
console.log('Widget version:', get_version());
|
||||
```
|
||||
|
||||
### Widget Handle
|
||||
|
||||
The widget functions return a handle that can be used to manage the widget:
|
||||
|
||||
```javascript
|
||||
const widget = create_file_browser_widget('container', config);
|
||||
|
||||
// Destroy the widget
|
||||
widget.destroy();
|
||||
```
|
||||
|
||||
## Backend Requirements
|
||||
|
||||
The widget expects a REST API compatible with the following endpoints:
|
||||
|
||||
### File Listing
|
||||
```
|
||||
GET /files/list/<path>
|
||||
Response: JSON array of file/directory objects
|
||||
```
|
||||
|
||||
### Directory Creation
|
||||
```
|
||||
POST /files/dirs/<path>
|
||||
Creates a new directory at the specified path
|
||||
```
|
||||
|
||||
### File/Directory Deletion
|
||||
```
|
||||
DELETE /files/delete/<path>
|
||||
Deletes the file or directory at the specified path
|
||||
```
|
||||
|
||||
### File Upload (TUS Protocol)
|
||||
```
|
||||
POST /files/upload
|
||||
Handles resumable file uploads using TUS protocol
|
||||
```
|
||||
|
||||
### File Download
|
||||
```
|
||||
GET /files/download/<path>
|
||||
Returns the file content with appropriate headers
|
||||
```
|
||||
|
||||
### Example Backend Response Format
|
||||
|
||||
File listing response:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "document.pdf",
|
||||
"path": "documents/document.pdf",
|
||||
"size": 1024000,
|
||||
"is_directory": false,
|
||||
"modified": "2023-12-01T10:30:00Z"
|
||||
},
|
||||
{
|
||||
"name": "images",
|
||||
"path": "documents/images",
|
||||
"size": 0,
|
||||
"is_directory": true,
|
||||
"modified": "2023-12-01T09:15:00Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Custom Styling
|
||||
|
||||
Add custom CSS to style the widget:
|
||||
|
||||
```css
|
||||
.file-browser-widget {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.file-browser-widget .card {
|
||||
border: none;
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple Widgets
|
||||
|
||||
You can create multiple widget instances on the same page:
|
||||
|
||||
```javascript
|
||||
// Widget 1
|
||||
const config1 = create_default_config('http://api1.example.com/files');
|
||||
const widget1 = create_file_browser_widget('container1', config1);
|
||||
|
||||
// Widget 2
|
||||
const config2 = create_default_config('http://api2.example.com/files');
|
||||
config2.set_theme('dark');
|
||||
const widget2 = create_file_browser_widget('container2', config2);
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```javascript
|
||||
try {
|
||||
const widget = create_file_browser_widget('container', config);
|
||||
} catch (error) {
|
||||
console.error('Failed to create widget:', error);
|
||||
// Show fallback UI
|
||||
}
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Building from Source
|
||||
|
||||
1. Install Rust and wasm-pack:
|
||||
```bash
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
cargo install wasm-pack
|
||||
```
|
||||
|
||||
2. Build the widget:
|
||||
```bash
|
||||
./build.sh
|
||||
```
|
||||
|
||||
3. Test locally:
|
||||
```bash
|
||||
npm run dev
|
||||
# Open http://localhost:8000/example.html
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
widgets/file_browser_widget/
|
||||
├── src/
|
||||
│ └── lib.rs # Main widget implementation
|
||||
├── dist/ # Built distribution files
|
||||
├── pkg/ # wasm-pack output
|
||||
├── Cargo.toml # Rust dependencies
|
||||
├── package.json # npm package config
|
||||
├── build.sh # Build script
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Browser Support
|
||||
|
||||
- Chrome 57+
|
||||
- Firefox 52+
|
||||
- Safari 11+
|
||||
- Edge 16+
|
||||
|
||||
The widget requires WebAssembly support and modern JavaScript features.
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see LICENSE file for details.
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Add tests if applicable
|
||||
5. Submit a pull request
|
||||
|
||||
## Support
|
||||
|
||||
- 📖 [Documentation](https://github.com/herocode/framework)
|
||||
- 🐛 [Issue Tracker](https://github.com/herocode/framework/issues)
|
||||
- 💬 [Discussions](https://github.com/herocode/framework/discussions)
|
111
widgets/file_browser_widget/pkg/file_browser_widget.d.ts
vendored
Normal file
111
widgets/file_browser_widget/pkg/file_browser_widget.d.ts
vendored
Normal file
@@ -0,0 +1,111 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export function main(): void;
|
||||
/**
|
||||
* Create and mount a FileBrowser widget to the specified DOM element
|
||||
*/
|
||||
export function create_file_browser_widget(container_id: string, config: JSWidgetConfig): FileBrowserWidgetHandle;
|
||||
/**
|
||||
* Create and mount a FileBrowser widget to a specific DOM element
|
||||
*/
|
||||
export function create_file_browser_widget_on_element(element: Element, config: JSWidgetConfig): FileBrowserWidgetHandle;
|
||||
/**
|
||||
* Utility function to create a default configuration
|
||||
*/
|
||||
export function create_default_config(base_endpoint: string): JSWidgetConfig;
|
||||
/**
|
||||
* Get version information
|
||||
*/
|
||||
export function get_version(): string;
|
||||
/**
|
||||
* Check if the widget is compatible with the current browser
|
||||
*/
|
||||
export function check_browser_compatibility(): boolean;
|
||||
/**
|
||||
* Handle for managing the widget instance
|
||||
*/
|
||||
export class FileBrowserWidgetHandle {
|
||||
private constructor();
|
||||
free(): void;
|
||||
/**
|
||||
* Destroy the widget instance
|
||||
*/
|
||||
destroy(): void;
|
||||
/**
|
||||
* Update the widget configuration
|
||||
*/
|
||||
update_config(_config: JSWidgetConfig): void;
|
||||
}
|
||||
/**
|
||||
* JavaScript-compatible configuration wrapper
|
||||
*/
|
||||
export class JSWidgetConfig {
|
||||
free(): void;
|
||||
constructor(base_endpoint: string);
|
||||
setMaxFileSize(size: number): void;
|
||||
setShowUpload(show: boolean): void;
|
||||
setShowDownload(show: boolean): void;
|
||||
setShowDelete(show: boolean): void;
|
||||
setTheme(theme: string): void;
|
||||
setCssClasses(classes: string): void;
|
||||
setInitialPath(path: string): void;
|
||||
}
|
||||
|
||||
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||
|
||||
export interface InitOutput {
|
||||
readonly memory: WebAssembly.Memory;
|
||||
readonly main: () => void;
|
||||
readonly __wbg_jswidgetconfig_free: (a: number, b: number) => void;
|
||||
readonly jswidgetconfig_new: (a: number, b: number) => number;
|
||||
readonly jswidgetconfig_setMaxFileSize: (a: number, b: number) => void;
|
||||
readonly jswidgetconfig_setShowUpload: (a: number, b: number) => void;
|
||||
readonly jswidgetconfig_setShowDownload: (a: number, b: number) => void;
|
||||
readonly jswidgetconfig_setShowDelete: (a: number, b: number) => void;
|
||||
readonly jswidgetconfig_setTheme: (a: number, b: number, c: number) => void;
|
||||
readonly jswidgetconfig_setCssClasses: (a: number, b: number, c: number) => void;
|
||||
readonly jswidgetconfig_setInitialPath: (a: number, b: number, c: number) => void;
|
||||
readonly __wbg_filebrowserwidgethandle_free: (a: number, b: number) => void;
|
||||
readonly filebrowserwidgethandle_destroy: (a: number) => void;
|
||||
readonly filebrowserwidgethandle_update_config: (a: number, b: number) => void;
|
||||
readonly create_file_browser_widget: (a: number, b: number, c: number) => [number, number, number];
|
||||
readonly create_file_browser_widget_on_element: (a: any, b: number) => [number, number, number];
|
||||
readonly create_default_config: (a: number, b: number) => number;
|
||||
readonly get_version: () => [number, number];
|
||||
readonly check_browser_compatibility: () => number;
|
||||
readonly __wbindgen_exn_store: (a: number) => void;
|
||||
readonly __externref_table_alloc: () => number;
|
||||
readonly __wbindgen_export_2: WebAssembly.Table;
|
||||
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
||||
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
readonly __externref_drop_slice: (a: number, b: number) => void;
|
||||
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||
readonly __wbindgen_export_7: WebAssembly.Table;
|
||||
readonly __externref_table_dealloc: (a: number) => void;
|
||||
readonly closure26_externref_shim: (a: number, b: number, c: any, d: any) => void;
|
||||
readonly closure23_externref_shim: (a: number, b: number, c: any, d: any, e: any) => void;
|
||||
readonly closure48_externref_shim: (a: number, b: number, c: any) => void;
|
||||
readonly closure58_externref_shim: (a: number, b: number, c: any) => void;
|
||||
readonly __wbindgen_start: () => void;
|
||||
}
|
||||
|
||||
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
||||
/**
|
||||
* Instantiates the given `module`, which can either be bytes or
|
||||
* a precompiled `WebAssembly.Module`.
|
||||
*
|
||||
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
|
||||
*
|
||||
* @returns {InitOutput}
|
||||
*/
|
||||
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
|
||||
|
||||
/**
|
||||
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
|
||||
* for everything else, calls `WebAssembly.instantiate` directly.
|
||||
*
|
||||
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
|
||||
*
|
||||
* @returns {Promise<InitOutput>}
|
||||
*/
|
||||
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
|
1074
widgets/file_browser_widget/pkg/file_browser_widget.js
Normal file
1074
widgets/file_browser_widget/pkg/file_browser_widget.js
Normal file
File diff suppressed because it is too large
Load Diff
BIN
widgets/file_browser_widget/pkg/file_browser_widget_bg.wasm
Normal file
BIN
widgets/file_browser_widget/pkg/file_browser_widget_bg.wasm
Normal file
Binary file not shown.
35
widgets/file_browser_widget/pkg/file_browser_widget_bg.wasm.d.ts
vendored
Normal file
35
widgets/file_browser_widget/pkg/file_browser_widget_bg.wasm.d.ts
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export const memory: WebAssembly.Memory;
|
||||
export const main: () => void;
|
||||
export const __wbg_jswidgetconfig_free: (a: number, b: number) => void;
|
||||
export const jswidgetconfig_new: (a: number, b: number) => number;
|
||||
export const jswidgetconfig_setMaxFileSize: (a: number, b: number) => void;
|
||||
export const jswidgetconfig_setShowUpload: (a: number, b: number) => void;
|
||||
export const jswidgetconfig_setShowDownload: (a: number, b: number) => void;
|
||||
export const jswidgetconfig_setShowDelete: (a: number, b: number) => void;
|
||||
export const jswidgetconfig_setTheme: (a: number, b: number, c: number) => void;
|
||||
export const jswidgetconfig_setCssClasses: (a: number, b: number, c: number) => void;
|
||||
export const jswidgetconfig_setInitialPath: (a: number, b: number, c: number) => void;
|
||||
export const __wbg_filebrowserwidgethandle_free: (a: number, b: number) => void;
|
||||
export const filebrowserwidgethandle_destroy: (a: number) => void;
|
||||
export const filebrowserwidgethandle_update_config: (a: number, b: number) => void;
|
||||
export const create_file_browser_widget: (a: number, b: number, c: number) => [number, number, number];
|
||||
export const create_file_browser_widget_on_element: (a: any, b: number) => [number, number, number];
|
||||
export const create_default_config: (a: number, b: number) => number;
|
||||
export const get_version: () => [number, number];
|
||||
export const check_browser_compatibility: () => number;
|
||||
export const __wbindgen_exn_store: (a: number) => void;
|
||||
export const __externref_table_alloc: () => number;
|
||||
export const __wbindgen_export_2: WebAssembly.Table;
|
||||
export const __wbindgen_malloc: (a: number, b: number) => number;
|
||||
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
export const __externref_drop_slice: (a: number, b: number) => void;
|
||||
export const __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||
export const __wbindgen_export_7: WebAssembly.Table;
|
||||
export const __externref_table_dealloc: (a: number) => void;
|
||||
export const closure26_externref_shim: (a: number, b: number, c: any, d: any) => void;
|
||||
export const closure23_externref_shim: (a: number, b: number, c: any, d: any, e: any) => void;
|
||||
export const closure48_externref_shim: (a: number, b: number, c: any) => void;
|
||||
export const closure58_externref_shim: (a: number, b: number, c: any) => void;
|
||||
export const __wbindgen_start: () => void;
|
21
widgets/file_browser_widget/pkg/package.json
Normal file
21
widgets/file_browser_widget/pkg/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "file_browser_widget",
|
||||
"type": "module",
|
||||
"description": "WASM widget for embedding the FileBrowser component in web applications",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/herocode/framework"
|
||||
},
|
||||
"files": [
|
||||
"file_browser_widget_bg.wasm",
|
||||
"file_browser_widget.js",
|
||||
"file_browser_widget.d.ts"
|
||||
],
|
||||
"main": "file_browser_widget.js",
|
||||
"types": "file_browser_widget.d.ts",
|
||||
"sideEffects": [
|
||||
"./snippets/*"
|
||||
]
|
||||
}
|
210
widgets/file_browser_widget/src/lib.rs
Normal file
210
widgets/file_browser_widget/src/lib.rs
Normal file
@@ -0,0 +1,210 @@
|
||||
use wasm_bindgen::prelude::*;
|
||||
use yew::prelude::*;
|
||||
use components::{FileBrowser, FileBrowserConfig};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use web_sys::{Element, HtmlElement};
|
||||
|
||||
// Initialize panic hook for better error messages in development
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
}
|
||||
|
||||
/// Configuration object that can be passed from JavaScript
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Properties)]
|
||||
pub struct WidgetConfig {
|
||||
/// Base endpoint for file operations (e.g., "/files" or "https://api.example.com/files")
|
||||
pub base_endpoint: String,
|
||||
/// Maximum file size for uploads in bytes (default: 100MB)
|
||||
pub max_file_size: Option<u64>,
|
||||
/// Whether to show upload functionality (default: true)
|
||||
pub show_upload: Option<bool>,
|
||||
/// Whether to show download functionality (default: true)
|
||||
pub show_download: Option<bool>,
|
||||
/// Whether to show delete functionality (default: true)
|
||||
pub show_delete: Option<bool>,
|
||||
/// Theme: "light" or "dark" (default: "light")
|
||||
pub theme: Option<String>,
|
||||
/// Custom CSS classes to apply to the root element
|
||||
pub css_classes: Option<String>,
|
||||
/// Initial path to display (default: "")
|
||||
pub initial_path: Option<String>,
|
||||
}
|
||||
|
||||
/// JavaScript-compatible configuration wrapper
|
||||
#[wasm_bindgen]
|
||||
pub struct JSWidgetConfig {
|
||||
inner: WidgetConfig,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl JSWidgetConfig {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(base_endpoint: String) -> JSWidgetConfig {
|
||||
JSWidgetConfig {
|
||||
inner: WidgetConfig {
|
||||
base_endpoint,
|
||||
max_file_size: None,
|
||||
show_upload: None,
|
||||
show_download: None,
|
||||
show_delete: None,
|
||||
theme: None,
|
||||
css_classes: None,
|
||||
initial_path: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = setMaxFileSize)]
|
||||
pub fn set_max_file_size(&mut self, size: f64) {
|
||||
self.inner.max_file_size = Some(size as u64);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = setShowUpload)]
|
||||
pub fn set_show_upload(&mut self, show: bool) {
|
||||
self.inner.show_upload = Some(show);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = setShowDownload)]
|
||||
pub fn set_show_download(&mut self, show: bool) {
|
||||
self.inner.show_download = Some(show);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = setShowDelete)]
|
||||
pub fn set_show_delete(&mut self, show: bool) {
|
||||
self.inner.show_delete = Some(show);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = setTheme)]
|
||||
pub fn set_theme(&mut self, theme: String) {
|
||||
self.inner.theme = Some(theme);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = setCssClasses)]
|
||||
pub fn set_css_classes(&mut self, classes: String) {
|
||||
self.inner.css_classes = Some(classes);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = setInitialPath)]
|
||||
pub fn set_initial_path(&mut self, path: String) {
|
||||
self.inner.initial_path = Some(path);
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WidgetConfig> for FileBrowserConfig {
|
||||
fn from(widget_config: WidgetConfig) -> Self {
|
||||
FileBrowserConfig {
|
||||
base_endpoint: widget_config.base_endpoint,
|
||||
max_file_size: widget_config.max_file_size.unwrap_or(100 * 1024 * 1024), // 100MB default
|
||||
show_upload: widget_config.show_upload.unwrap_or(true),
|
||||
show_download: widget_config.show_download.unwrap_or(true),
|
||||
show_delete: widget_config.show_delete.unwrap_or(true),
|
||||
show_create_dir: true, // Always enable directory creation
|
||||
theme: widget_config.theme.unwrap_or_else(|| "light".to_string()),
|
||||
css_classes: widget_config.css_classes.unwrap_or_default(),
|
||||
initial_path: widget_config.initial_path.unwrap_or_default(),
|
||||
chunk_size: 1024 * 1024, // 1MB chunks for uploads
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget wrapper component
|
||||
#[function_component(FileBrowserWidget)]
|
||||
fn file_browser_widget(props: &WidgetConfig) -> Html {
|
||||
let config: FileBrowserConfig = props.clone().into();
|
||||
|
||||
html! {
|
||||
<div class="file-browser-widget">
|
||||
<FileBrowser ..config />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle for managing the widget instance
|
||||
#[wasm_bindgen]
|
||||
pub struct FileBrowserWidgetHandle {
|
||||
app_handle: yew::AppHandle<FileBrowserWidget>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl FileBrowserWidgetHandle {
|
||||
/// Destroy the widget instance
|
||||
pub fn destroy(self) {
|
||||
// The app handle will be dropped, cleaning up the widget
|
||||
}
|
||||
|
||||
/// Update the widget configuration
|
||||
pub fn update_config(&self, _config: JSWidgetConfig) {
|
||||
// For now, we'll need to recreate the widget to update config
|
||||
// In a more advanced implementation, we could send messages to update config
|
||||
web_sys::console::warn_1(&"Config updates require recreating the widget instance".into());
|
||||
}
|
||||
}
|
||||
|
||||
/// Create and mount a FileBrowser widget to the specified DOM element
|
||||
#[wasm_bindgen]
|
||||
pub fn create_file_browser_widget(
|
||||
container_id: &str,
|
||||
config: JSWidgetConfig,
|
||||
) -> Result<FileBrowserWidgetHandle, JsValue> {
|
||||
let document = web_sys::window()
|
||||
.ok_or("No global window object")?
|
||||
.document()
|
||||
.ok_or("No document object")?;
|
||||
|
||||
let container = document
|
||||
.get_element_by_id(container_id)
|
||||
.ok_or_else(|| format!("Element with id '{}' not found", container_id))?;
|
||||
|
||||
let app_handle = yew::Renderer::<FileBrowserWidget>::with_root_and_props(
|
||||
container,
|
||||
config.inner,
|
||||
).render();
|
||||
|
||||
Ok(FileBrowserWidgetHandle { app_handle })
|
||||
}
|
||||
|
||||
/// Create and mount a FileBrowser widget to a specific DOM element
|
||||
#[wasm_bindgen]
|
||||
pub fn create_file_browser_widget_on_element(
|
||||
element: &Element,
|
||||
config: JSWidgetConfig,
|
||||
) -> Result<FileBrowserWidgetHandle, JsValue> {
|
||||
let app_handle = yew::Renderer::<FileBrowserWidget>::with_root_and_props(
|
||||
element.clone(),
|
||||
config.inner,
|
||||
).render();
|
||||
|
||||
Ok(FileBrowserWidgetHandle { app_handle })
|
||||
}
|
||||
|
||||
/// Utility function to create a default configuration
|
||||
#[wasm_bindgen]
|
||||
pub fn create_default_config(base_endpoint: &str) -> JSWidgetConfig {
|
||||
JSWidgetConfig::new(base_endpoint.to_string())
|
||||
}
|
||||
|
||||
/// Get version information
|
||||
#[wasm_bindgen]
|
||||
pub fn get_version() -> String {
|
||||
env!("CARGO_PKG_VERSION").to_string()
|
||||
}
|
||||
|
||||
/// Check if the widget is compatible with the current browser
|
||||
#[wasm_bindgen]
|
||||
pub fn check_browser_compatibility() -> bool {
|
||||
// Basic compatibility checks
|
||||
let window = web_sys::window();
|
||||
if window.is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let window = window.unwrap();
|
||||
|
||||
// Check for required APIs
|
||||
let has_fetch = js_sys::Reflect::has(&window, &"fetch".into()).unwrap_or(false);
|
||||
let has_blob = js_sys::Reflect::has(&window, &"Blob".into()).unwrap_or(false);
|
||||
let has_form_data = js_sys::Reflect::has(&window, &"FormData".into()).unwrap_or(false);
|
||||
|
||||
has_fetch && has_blob && has_form_data
|
||||
}
|
Reference in New Issue
Block a user