TUTORIAL: BUILDING AN OFFLINE APP (LOCAL APP) IN FLOWORK OS

Published on

TUTORIAL: BUILDING AN OFFLINE APP (LOCAL APP) IN FLOWORK OS

🚀 TUTORIAL: BUILDING AN OFFLINE APP (LOCAL APP) IN FLOWORK OS

PART 1: BASIC CONCEPTS, HOW IT WORKS, & FILE ANATOMY

1. How Does It Work? (Dual-Engine Architecture)

Offline Applications in Flowork OS distinguish themselves from standard web apps by utilizing a Dual-Engine Architecture. This application does not overload your browser's memory (RAM); instead, it uses your own computer as a "Private Server".

Here is the end-to-end workflow:

1. The Client (Web UI): The user clicks the "START" button on the Web interface (`index.html` or `mobile.html`). This button does not execute heavy computation; it triggers a command in `app.js`. 2. P2P Tunnel: `app.js` calls `systemBridge.js`. This script wraps the command into a JSON payload and securely sends it through a direct P2P WebSocket tunnel to the Golang Engine running in the background of the user's PC (`localhost:5000`). 3. The Overseer (Golang): The Golang Engine receives the message, locates the target application folder (e.g., `apps/screen-recorder/`), automatically downloads any required libraries if they are missing, and executes the Python file (`script.py`). 4. Native Executor (Python): Python reads the command via the "STDIN Pipe", processes its heavy tasks (such as file system access, video rendering, or browser-less scraping), and then prints the result back as a JSON string. 5. Cycle Complete: Golang captures Python's printed output and throws it back via P2P to the Web UI. The UI receives the data and displays it to the user. All of this happens in a matter of milliseconds.

2. Folder Anatomy & Absolute File Rules

Every Local Application must be placed inside the `apps/app-name/` directory on your PC Engine.

Flowork is extremely strict about file structure. If a single core file is missing or violates the rules, the OS will refuse to render the application.

Here is the standard template skeleton (using `apps/screen-recorder/` as an example):

```plaintext apps/screen-recorder/ ├── manifest.json # [REQUIRED] App identity. Must include the "is_local": true flag so the OS knows it runs on PC. ├── schema.json # [REQUIRED] Standard I/O (Input/Output) contract between Javascript UI and Python Backend. ├── requirements.txt # [IMPORTANT] Python library list. The Golang Engine will auto-install these into the app's local folder (Portable Mode). ├── script.py # [REQUIRED] The backend computational brain. Where native logic (Python/C++/Node) is executed by Golang. ├── index.html # [REQUIRED] Desktop UI. Must have an Enterprise style. Strictly NO

```

3. Mobile Interface: `mobile.html`

This UI is rendered when users open the app via mobile. Users can turn their phones into a Remote Control to command the Engine on their PC.

Mobile UI Absolute Rules:

1. No Cards Allowed: Must be Edge-to-Edge (full screen without box margins on the sides). 2. Dock Safe Area: Must add `padding-bottom: 85px;` so the bottom area is not obstructed by OS navigation buttons. 3. No Blurs: Forbidden to use `backdrop-filter: blur()`. Use solid RGBA colors to ensure 60fps GPU performance on mobile. 4. Anti-Bounce: Add `overscroll-behavior-y: none;` to the body tag so the screen doesn't pull when scrolled (giving it a Native App feel).

Create `apps/screen-recorder/mobile.html`:

```html Screen Recorder Pro - Mobile

00:00:00

```

---

PART 4: P2P TUNNEL & JAVASCRIPT STATE LOGIC

1. P2P Tunnel: `systemBridge.js`

This file is the "exclusive pathway" for the app to communicate with the main Flowork system.

Absolute Rule: You are strictly forbidden from writing your own `fetch()` functions to target localhost. You MUST call `executeEngineTask` from this file so the OS can legally bypass CORS via `window.postMessage`.

Create `apps/screen-recorder/systemBridge.js`:

```javascript // Do not modify function names or the taskId mechanism export const detectEnvironment = () => { return 'web'; };

// The magic function to send commands to Python export const executeEngineTask = async (taskName, payload = {}) => { const env = detectEnvironment(); return new Promise((resolve, reject) => { // Create a unique ID to prevent data collisions between apps const taskId = `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;

const messageHandler = (event) => { if (event.data && event.data.type === 'FLOWORK_ENGINE_RESULT' && event.data.taskId === taskId) { window.removeEventListener('message', messageHandler); if (event.data.error) reject(new Error(event.data.error)); else resolve(event.data.response); } };

// [IMPORTANT] Because video rendering/heavy processes can take a long time, // we extend the timeout from 15 seconds to 4 Hours (14400000 ms) const timeout = setTimeout(() => { window.removeEventListener('message', messageHandler); reject(new Error(`P2P Engine Timeout.`)); }, 14400000);

window.addEventListener('message', messageHandler);

// Sending packet to the main OS (Parent Iframe) if (window.parent && window.parent !== window) { window.parent.postMessage({ type: 'FLOWORK_ENGINE_TASK', taskId: taskId, taskName: taskName, // MUST EXACTLY MATCH APP FOLDER NAME (e.g., "screen-recorder") payload: payload, environment: env }, '*'); } else { clearTimeout(timeout); window.removeEventListener('message', messageHandler); reject(new Error("Flowork OS Sandbox not detected.")); } }); };

```

2. Interaction Brain & Auto-Save: `app.js`

This is where all the UI logic occurs.

Absolute Flowork Logic Rules:

1. Auto-Save State: If a process (like recording) is running and the user accidentally refreshes the Iframe, the status (like the timer) must not reset to zero. You must save the state in `localStorage`. 2. Zero Default Alerts: Forbidden to use the default browser `alert("Hello")` as it looks bad and breaks the native OS feel. Build custom UI or modify element text to provide status updates. 3. Dynamic Dictionary Calls: When `app.js` boots up, it MUST grab the active language from the OS and read the `i18n.json` file.

Create `apps/screen-recorder/app.js`:

```javascript import { executeEngineTask } from './systemBridge.js';

let currentLang = 'en'; let dictionary = {}; let isRecording = false; let timerInterval; let recordingStartTime;

// Custom function to update status without using alert() const updateStatus = (textKey, directText = null, isError = false) => { const statusEl = document.getElementById('status-text'); if (directText) { statusEl.innerText = directText; } else { statusEl.innerText = dictionary[currentLang][textKey] || textKey; } statusEl.style.color = isError ? "#FF006E" : "#00BBF9"; };

// UI Management (Button colors, blinking indicators, disabling dropdowns) const setUIState = (recording) => { const btn = document.getElementById('btn-action'); const btnText = document.getElementById('btn-text'); const dot = document.getElementById('rec-dot'); const fpsSelect = document.getElementById('select-fps');

if (recording) { btn.className = 'btn-stop'; btnText.innerText = dictionary[currentLang]['stop_btn']; dot.style.display = 'inline-block'; fpsSelect.disabled = true; updateStatus('status_recording'); } else { btn.className = 'btn-start'; btnText.innerText = dictionary[currentLang]['start_btn']; dot.style.display = 'none'; fpsSelect.disabled = false; clearInterval(timerInterval); document.getElementById('timer').innerText = "00:00:00"; } };

// Timer Logic and Auto-Save to localStorage const startTimerUI = () => { recordingStartTime = Date.now(); localStorage.setItem('flowork_rec_start', recordingStartTime); // AUTO-SAVE

timerInterval = setInterval(() => { const diff = Date.now() - recordingStartTime; const hours = Math.floor(diff / 3600000).toString().padStart(2, '0'); const minutes = Math.floor((diff % 3600000) / 60000).toString().padStart(2, '0'); const seconds = Math.floor((diff % 60000) / 1000).toString().padStart(2, '0'); document.getElementById('timer').innerText = `${hours}:${minutes}:${seconds}`; }, 1000); };

// Execute P2P (Linked with data-flowork-action) const toggleRecording = async () => { const btn = document.getElementById('btn-action'); btn.disabled = true; // Prevent double clicks

if (!isRecording) { isRecording = true; const fpsVal = document.getElementById('select-fps').value; setUIState(true); startTimerUI(); btn.disabled = false;

try { // Calling Python (Scenario B: Start) // 'screen-recorder' MUST match the folder name! const response = await executeEngineTask('screen-recorder', { action: 'start', fps: parseInt(fpsVal) });

// This line will only execute when Python finishes rendering the video (Lock File deleted) isRecording = false; setUIState(false); localStorage.removeItem('flowork_rec_start'); updateStatus('save_success', dictionary[currentLang]['save_success'] + response.filepath);

} catch (error) { isRecording = false; setUIState(false); updateStatus('error_p2p', `P2P ERROR: ${error.message}`, true); }

} else { try { // Calling Python (Scenario A: Stop) await executeEngineTask('screen-recorder', { action: 'stop' }); btn.disabled = false; } catch (error) { updateStatus('error_p2p', `STOP ERROR: ${error.message}`, true); btn.disabled = false; } } };

// Absolute Initialization when the app boots const initApp = async () => { try { // 1. Load Dictionary const response = await fetch('./i18n.json'); dictionary = await response.json();

// 2. Check Language from Main OS (URL Params) const urlParams = new URLSearchParams(window.location.search); currentLang = urlParams.get('lang') || 'en';

// 3. Apply Dictionary Text to HTML without mutating DOM Structure document.querySelectorAll('[data-i18n]').forEach(el => { const key = el.getAttribute('data-i18n'); if(dictionary[currentLang] && dictionary[currentLang][key]) { el.innerText = dictionary[currentLang][key]; } });

// 4. Event Delegation (Zero Logic in HTML) document.body.addEventListener('click', (e) => { const actionTarget = e.target.closest('[data-flowork-action]'); if (actionTarget) { const action = actionTarget.getAttribute('data-flowork-action'); if (action === 'toggleRecording') toggleRecording(); } });

// 5. Restore Auto-Save if Iframe gets accidentally refreshed const savedTime = localStorage.getItem('flowork_rec_start'); if (savedTime) { isRecording = true; recordingStartTime = parseInt(savedTime); setUIState(true); timerInterval = setInterval(() => { const diff = Date.now() - recordingStartTime; const hours = Math.floor(diff / 3600000).toString().padStart(2, '0'); const minutes = Math.floor((diff % 3600000) / 60000).toString().padStart(2, '0'); const seconds = Math.floor((diff % 60000) / 1000).toString().padStart(2, '0'); document.getElementById('timer').innerText = `${hours}:${minutes}:${seconds}`; }, 1000); }

} catch (err) { console.error("Failed to boot app:", err); updateStatus('error_p2p', "Failed to load i18n dictionary", true); } };

// Start the Engine initApp();

```

---

🎉 CONGRATULATIONS! YOUR LOCAL APP IS READY!

For complementary files like `readme_en.md`, `readme_id.md`, `icon.svg`, and `cover.webp`, just insert brief descriptions and images according to your creativity.

How to Test (Workflow):

1. Ensure the Golang Engine is running (`go run main.go`). 2. Open Flowork OS on your PC / Mobile. 3. Navigate to the Store page. Click the "My PC Apps" tab. 4. The "Screen Recorder Pro" app will appear there. 5. Click the app, hit START from Web/Mobile, and boom! Python on your PC will instantly record your screen silently (God Mode).

---