log

November

November 16th

I asked deepseek for help:

Let’s compare all possible ways of watching a pc folder for new files from a browser tab, comparing the size and complexity of any necessary intermediaries. These must all be local-only solutions, no cloud server. Ideally the user should not have to run a separate app on their pc but if they do it should be as small as possible. New tech is ok!

https://chat.deepseek.com/share/dqq85dbt3lrib4qmp0

We ended up with this demo. It doesn’t work in Firefox, deepseek was a little confused. Works great in chrome!

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Firefox Folder Watcher</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            background: #f5f5f5;
        }
        .container {
            background: white;
            padding: 30px;
            border-radius: 10px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        button {
            background: #0066cc;
            color: white;
            border: none;
            padding: 12px 24px;
            border-radius: 6px;
            cursor: pointer;
            font-size: 16px;
            margin: 10px 0;
        }
        button:hover {
            background: #0052a3;
        }
        button:disabled {
            background: #ccc;
            cursor: not-allowed;
        }
        .status {
            padding: 15px;
            border-radius: 6px;
            margin: 15px 0;
            background: #e8f4fd;
            border-left: 4px solid #0066cc;
        }
        .file-list {
            border: 1px solid #ddd;
            border-radius: 6px;
            padding: 15px;
            max-height: 300px;
            overflow-y: auto;
            background: #fafafa;
        }
        .file-item {
            padding: 8px;
            border-bottom: 1px solid #eee;
            display: flex;
            justify-content: space-between;
        }
        .file-item:last-child {
            border-bottom: none;
        }
        .new-file {
            background: #e8ffe8;
            animation: highlight 2s ease;
        }
        @keyframes highlight {
            from { background: #a8ffa8; }
            to { background: #e8ffe8; }
        }
        .error {
            background: #ffe8e8;
            border-left: 4px solid #cc0000;
            color: #cc0000;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>πŸ“ Firefox Folder Watcher</h1>
        <p>Experimental proof of concept using FileSystemObserver API</p>
        
        <div class="status" id="status">
            Click "Select Folder" to begin watching for new files...
        </div>

        <button id="selectFolder">Select Folder</button>
        <button id="stopWatching" disabled>Stop Watching</button>

        <h3>Files in selected folder:</h3>
        <div class="file-list" id="fileList">
            <div class="file-item">No folder selected</div>
        </div>

        <h3>File Events:</h3>
        <div class="file-list" id="eventLog">
            <div class="file-item">Events will appear here...</div>
        </div>
    </div>

    <script>
        class FolderWatcher {
            constructor() {
                this.directoryHandle = null;
                this.observer = null;
                this.isWatching = false;
                
                this.elements = {
                    status: document.getElementById('status'),
                    fileList: document.getElementById('fileList'),
                    eventLog: document.getElementById('eventLog'),
                    selectFolder: document.getElementById('selectFolder'),
                    stopWatching: document.getElementById('stopWatching')
                };

                this.bindEvents();
                this.checkBrowserSupport();
            }

            bindEvents() {
                this.elements.selectFolder.addEventListener('click', () => this.selectFolder());
                this.elements.stopWatching.addEventListener('click', () => this.stopWatching());
            }

            async checkBrowserSupport() {
                if (!('showDirectoryPicker' in window)) {
                    this.showError('File System Access API not supported in this browser');
                    this.elements.selectFolder.disabled = true;
                    return;
                }

                if (!('FileSystemObserver' in window)) {
                    this.showError('FileSystemObserver API not available. This proof of concept requires Firefox with experimental APIs enabled.');
                    this.elements.selectFolder.disabled = true;
                    return;
                }

                this.updateStatus('Browser support detected! Ready to select folder.', 'success');
            }

            async selectFolder() {
                try {
                    this.directoryHandle = await window.showDirectoryPicker();
                    this.updateStatus(`Watching folder: ${this.directoryHandle.name}`, 'success');
                    
                    await this.loadExistingFiles();
                    await this.startWatching();
                    
                    this.elements.selectFolder.disabled = true;
                    this.elements.stopWatching.disabled = false;
                    
                } catch (error) {
                    if (error.name === 'AbortError') {
                        this.updateStatus('Folder selection cancelled');
                    } else {
                        this.showError(`Error selecting folder: ${error.message}`);
                    }
                }
            }

            async loadExistingFiles() {
                const files = [];
                for await (const entry of this.directoryHandle.values()) {
                    if (entry.kind === 'file') {
                        const file = await entry.getFile();
                        files.push({
                            name: entry.name,
                            handle: entry,
                            size: file.size,
                            lastModified: file.lastModified
                        });
                    }
                }

                this.displayFiles(files);
            }

            displayFiles(files) {
                this.elements.fileList.innerHTML = '';
                
                if (files.length === 0) {
                    this.elements.fileList.innerHTML = '<div class="file-item">No files found</div>';
                    return;
                }

                files.forEach(file => {
                    const fileElement = document.createElement('div');
                    fileElement.className = 'file-item';
                    fileElement.innerHTML = `
                        <span><strong>${file.name}</strong></span>
                        <span>${this.formatFileSize(file.size)} β€’ ${new Date(file.lastModified).toLocaleTimeString()}</span>
                    `;
                    this.elements.fileList.appendChild(fileElement);
                });
            }

            async startWatching() {
                if (!this.directoryHandle) {
                    throw new Error('No directory selected');
                }

                // Create the observer
                this.observer = new FileSystemObserver((records, observer) => {
                    this.handleFileChanges(records);
                });

                // Start observing the directory
                await this.observer.observe(this.directoryHandle);
                this.isWatching = true;
                this.updateStatus(`Watching for file changes in: ${this.directoryHandle.name}`, 'success');
            }

            handleFileChanges(records) {
                records.forEach(record => {
                    const timestamp = new Date().toLocaleTimeString();
                    let message = '';
                    
                    switch (record.type) {
                        case 'created':
                            message = `πŸ“„ NEW FILE: ${record.changedHandle.name}`;
                            this.addFileToList(record.changedHandle);
                            break;
                        case 'modified':
                            message = `✏️ MODIFIED: ${record.changedHandle.name}`;
                            break;
                        case 'moved':
                            message = `πŸ“ MOVED: ${record.relativePathComponents?.join('/') || record.changedHandle.name}`;
                            break;
                        case 'deleted':
                            message = `πŸ—‘οΈ DELETED: ${record.changedHandle.name}`;
                            break;
                        default:
                            message = `πŸ” CHANGE: ${record.changedHandle.name} (${record.type})`;
                    }
                    
                    this.logEvent(`${timestamp} - ${message}`);
                });
            }

            async addFileToList(fileHandle) {
                try {
                    const file = await fileHandle.getFile();
                    const fileElement = document.createElement('div');
                    fileElement.className = 'file-item new-file';
                    fileElement.innerHTML = `
                        <span><strong>${file.name}</strong> πŸ†•</span>
                        <span>${this.formatFileSize(file.size)} β€’ ${new Date(file.lastModified).toLocaleTimeString()}</span>
                    `;
                    
                    // Add to top of list
                    this.elements.fileList.insertBefore(fileElement, this.elements.fileList.firstChild);
                    
                    // Remove highlight animation after 2 seconds
                    setTimeout(() => {
                        fileElement.classList.remove('new-file');
                    }, 2000);
                    
                } catch (error) {
                    console.error('Error reading new file:', error);
                }
            }

            logEvent(message) {
                const eventElement = document.createElement('div');
                eventElement.className = 'file-item';
                eventElement.textContent = message;
                
                this.elements.eventLog.insertBefore(eventElement, this.elements.eventLog.firstChild);
                
                // Keep only last 50 events
                if (this.elements.eventLog.children.length > 50) {
                    this.elements.eventLog.removeChild(this.elements.eventLog.lastChild);
                }
            }

            async stopWatching() {
                if (this.observer) {
                    this.observer.disconnect();
                    this.observer = null;
                }
                
                this.isWatching = false;
                this.directoryHandle = null;
                
                this.updateStatus('Stopped watching folder');
                this.elements.selectFolder.disabled = false;
                this.elements.stopWatching.disabled = true;
                this.elements.fileList.innerHTML = '<div class="file-item">No folder selected</div>';
            }

            updateStatus(message, type = 'info') {
                this.elements.status.textContent = message;
                this.elements.status.className = 'status';
                if (type === 'error') {
                    this.elements.status.classList.add('error');
                } else if (type === 'success') {
                    this.elements.status.style.borderLeftColor = '#00cc00';
                }
            }

            showError(message) {
                this.updateStatus(message, 'error');
                console.error(message);
            }

            formatFileSize(bytes) {
                if (bytes === 0) return '0 Bytes';
                const k = 1024;
                const sizes = ['Bytes', 'KB', 'MB', 'GB'];
                const i = Math.floor(Math.log(bytes) / Math.log(k));
                return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
            }
        }

        // Initialize the folder watcher when page loads
        document.addEventListener('DOMContentLoaded', () => {
            new FolderWatcher();
        });
    </script>
</body>
</html>