EmberlyEmberly Docs

Flicker Development Guide

Complete guide to building, developing, and contributing to Flicker desktop application.

Flicker is built with Tauri 2.0, React 19, and Rust. This guide covers project setup, development workflow, architecture, and contribution guidelines.


Project Overview

AspectDetails
Repositoryhttps://github.com/EmberlyOSS/Flicker
LicenseAGPL-3.0 (Open Source)
Current Versionv0.2.0-beta
FrameworkTauri 2.0 (Cross-platform desktop)
FrontendReact 19 + TypeScript + Tailwind CSS
BackendRust with async Tokio runtime
Package Managerbun (recommended) / npm
PlatformsWindows, macOS, Linux

Architecture Overview

High-Level Structure

Flicker (Tauri App)
├── Frontend (React + TypeScript)
│   ├── UI Components (Pages, Settings, Upload)
│   ├── State Management (Context API)
│   ├── Hooks (useHotkeys, useTheme, useNotifications)
│   └── Styling (Tailwind CSS + custom themes)

├── Tauri Bridge (IPC Communication)
│   └── Serialized JSON commands/events

└── Backend (Rust)
    ├── Tauri Command Handlers
    │   └── Screenshot, Upload, Config
    ├── Common Modules
    │   ├── Emberly API Client
    │   ├── File Operations
    │   ├── Authentication
    │   └── History Management
    └── Platform-Specific Code
        ├── Desktop (Windows, macOS, Linux)
        └── Mobile (Future)

Data Flow

User Hotkey (System)

Tauri Global Shortcut Plugin

Frontend JS Handler (useHotkeys)

Invoke Rust Command: capture_screenshot()

Rust Backend (Desktop Module)

Native Screenshot Capture (OS-specific)

Save to temp file

Invoke: upload_file()

Multipart HTTP POST to Emberly API

Receive UploadResponse with URL

Add to history (localStorage)

Copy URL to clipboard

Show success notification

Prerequisites & Setup

System Requirements

All Platforms:

  • Node.js 18+ (node --version)
  • Rust 1.70+ (rustc --version)
  • Git

Windows:

  • Visual Studio Build Tools or MSVC compiler
  • Windows 10 or later

macOS:

  • Xcode or Command Line Tools (xcode-select --install)
  • macOS 11 or later

Linux:

  • Build essentials: sudo apt install build-essential
  • GTK 3.0+ development files: sudo apt install libgtk-3-dev
  • libssl-dev: sudo apt install libssl-dev

Installation

1. Clone Repository

git clone https://github.com/EmberlyOSS/Flicker.git
cd Flicker

2. Install Node Dependencies

# Using bun (recommended, faster):
curl -fsSL https://bun.sh/install | bash
bun install
 
# Or using npm:
npm install
 
# Or yarn:
yarn install

3. Install Rust (if not already installed)

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env

4. Verify Installation

node --version        # Should be v18+
bun --version        # If using bun
npm --version        # If using npm
rustc --version      # Should be 1.70+
cargo --version      # Should be 1.70+

Development Workflow

Starting Development Server

# Main development command
bun run tauri:dev
# or
npm run tauri:dev
 
# This:
# 1. Starts Vite dev server on localhost:5173 (via beforeDevCommand)
# 2. Compiles Rust backend
# 3. Launches Tauri development window
# 4. Enables hot reload for React components
# 5. Watches Rust files for changes

Plain bun run dev only starts the Vite frontend server on its own — it won't launch the desktop window.

First startup may take 2-3 minutes as Rust builds. Subsequent runs are faster using incremental compilation.

Development Window

When bun run tauri:dev succeeds:

  • Tauri window opens with Flicker app
  • Layout: 900×800 pixels (resizable)
  • DevTools available: F12 or Ctrl+Shift+I
  • Hot reload on save (React components only)
  • Rust changes require app restart

File Structure During Development

Flicker/
├── src/                    ← React frontend (edit & hot reload)
│   ├── components/         ← All React components
│   ├── hooks/             ← Custom React hooks
│   ├── context/           ← Context API state
│   ├── App.tsx            ← Root component
│   └── main.tsx           ← DOM render entry

├── src-tauri/             ← Rust backend
│   ├── src/
│   │   ├── main.rs        ← Entry point
│   │   ├── lib.rs         ← Command handlers
│   │   ├── common/        ← Shared modules
│   │   ├── desktop/       ← Platform code
│   │   └── mobile/        ← Mobile stubs
│   ├── Cargo.toml         ← Rust dependencies
│   └── tauri.conf.json    ← Tauri configuration

├── index.html             ← HTML entry
├── package.json           ← Frontend dependencies
├── tsconfig.json          ← TypeScript config
├── vite.config.ts         ← Vite build config
└── tailwind.config.js     ← Tailwind theme

Building & Packaging

Production Build

# Full build (React + Rust), produces installers
bun run tauri:build
 
# Outputs to: src-tauri/target/release/bundle/

bun run build on its own only runs tsc && vite build — the frontend type-check and bundle, with no Rust compilation or installer output. Use bun run tauri:build to actually produce a runnable app.

Platform-Specific Builds

Cross-compiling for a different OS than the one you're building on isn't supported (Tauri needs each platform's native webview toolchain) — each of these must run on that actual platform, which is why the project's release workflow uses a matrix of Windows/macOS/Ubuntu runners:

# Windows (must run on Windows)
bun run tauri:build
 
# macOS Intel (must run on macOS)
bun run tauri:build -- --target x86_64-apple-darwin
 
# macOS Apple Silicon (must run on macOS)
bun run tauri:build -- --target aarch64-apple-darwin
 
# Linux (must run on Linux)
bun run tauri:build

Build Artifacts

After bun run tauri:build, installers appear in:

Windows:
  src-tauri/target/release/bundle/msi/        (.msi installer)
  src-tauri/target/release/bundle/nsis/       (.exe installer)

macOS:
  src-tauri/target/release/bundle/dmg/        (.dmg disk image)
  src-tauri/target/release/bundle/macos/      (.app bundle)

Linux:
  src-tauri/target/release/bundle/appimage/   (.AppImage)
  src-tauri/target/release/bundle/deb/        (.deb package)

Code Standards

TypeScript / React

File Structure:

// src/components/MyComponent.tsx
import React, { useState, useEffect } from 'react';
import type { ComponentProps } from '@/types';  // Import types
 
interface MyComponentProps {
  title: string;
  onUpload?: (file: File) => void;
}
 
/**
 * MyComponent - Brief description
 * 
 * @param props Component props
 * @returns Rendered component
 */
export const MyComponent: React.FC<MyComponentProps> = ({
  title,
  onUpload
}) => {
  const [state, setState] = useState<string>('');
 
  useEffect(() => {
    // Side effects
  }, []);
 
  return (
    <div className="space-y-4">
      <h1>{title}</h1>
      {/* JSX */}
    </div>
  );
};

Guidelines:

  • Use functional components with hooks
  • TypeScript strict mode (no any types)
  • Use interfaces for props
  • Document with JSDoc comments
  • Use Tailwind CSS classes
  • Prefer composition over inheritance
  • Keep components focused (single responsibility)

Rust Code

Command Handler Example:

// src-tauri/src/lib.rs
#[tauri::command]
async fn capture_screenshot(
    state: tauri::State<'_, AppState>,
    monitor_index: Option<usize>,
) -> Result<ScreenshotResult, String> {
    match desktop::screenshot::capture_screenshot(monitor_index).await {
        Ok(result) => Ok(result),
        Err(e) => Err(format!("Screenshot failed: {}", e)),
    }
}

Guidelines:

  • Use Result<T, E> for error handling
  • Document with /// doc comments
  • Use async/await with Tokio
  • Proper error messages for frontend
  • Validate inputs (file size, formats)
  • Use modules for organization

Styling with Tailwind

Theme System:

// src/colors.ts - Define theme colors
export const themes = {
  'stranger-things': {
    primary: 'hsl(260 100% 50%)',  // Purple
    success: 'hsl(120 100% 50%)',  // Green
    // ...
  }
};
 
// In component:
<div className="bg-primary text-primary-foreground">
  {/* Tailwind applies CSS variables */}
</div>
 
// CSS variables set by theme hook:
// :root {
//   --primary: hsl(260 100% 50%);
//   --primary-foreground: hsl(0 0% 100%);
// }

Testing

Unit Tests

There is no test runner configured yet — package.json has no test script, and no test suite currently exists in the frontend. The snippet below is illustrative of the intended setup (Vitest + Testing Library) for when one is added, not a command you can run today.

# Not yet available:
npm run test
npm run test -- --watch
npm run test -- --coverage

Intended test structure:

// src/components/__tests__/MyComponent.test.tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import { MyComponent } from '../MyComponent';
 
describe('MyComponent', () => {
  it('should render with title', () => {
    render(<MyComponent title="Test" />);
    expect(screen.getByText('Test')).toBeInTheDocument();
  });
 
  it('should call onUpload when clicked', () => {
    const onUpload = vi.fn();
    render(<MyComponent onUpload={onUpload} />);
    screen.getByRole('button', { name: /upload/i }).click();
    expect(onUpload).toHaveBeenCalled();
  });
});

Manual Testing Checklist

Before submitting PR, test:

  • App launches without crashes
  • Screenshots capture correctly
  • Uploads succeed with valid auth
  • Hotkeys work system-wide
  • Settings persist after restart
  • All themes apply correctly
  • UI is responsive
  • No console errors (F12)
  • Notifications display
  • History updates

Architecture Deep Dive

Frontend State Management

AppContext:

interface AppContextType {
  // Authentication
  isLoggedIn: boolean;
  user: UserProfile | null;
  
  // Configuration
  config: AppConfig;
  updateConfig: (updates: Partial<AppConfig>) => void;
  
  // Upload History
  history: UploadHistory[];
  addToHistory: (upload: UploadHistory) => void;
  
  // UI State
  currentPage: 'upload' | 'history' | 'settings' | 'analytics';
  setCurrentPage: (page: string) => void;
  
  // Notifications
  notifications: Notification[];
  addNotification: (notification: Notification) => void;
}

Usage:

import { useAppContext } from '@/context/AppContext';
 
export function MyComponent() {
  const { isLoggedIn, user, addNotification } = useAppContext();
  
  if (!isLoggedIn) return <LoginOverlay />;
  
  return <div>Welcome, {user?.name}!</div>;
}

Tauri Commands (IPC Bridge)

From Frontend (TypeScript):

import { invoke } from '@tauri-apps/api/core';
 
async function uploadFile(filePath: string) {
  try {
    const response = await invoke<UploadResponse>('upload_file', {
      filePath,
      visibility: 'public'
    });
    console.log('Uploaded:', response.url);
  } catch (error) {
    console.error('Upload failed:', error);
  }
}

From Backend (Rust):

#[tauri::command]
async fn upload_file(
    file_path: String,
    visibility: String,
) -> Result<UploadResponse, String> {
    // Implementation
}

Emberly API Integration

Client Location: src-tauri/src/common/emberly_api.rs

Key Endpoints:

impl EmberlyCient {  // Note: typo in actual code
    pub async fn login(email: &str, password: &str) -> Result<LoginResponse>;
    pub async fn upload_file(
        token: &str,
        file: File,
        metadata: FileMetadata,
    ) -> Result<UploadResponse>;
    pub async fn get_profile(token: &str) -> Result<UserProfile>;
}

Usage in upload flow:

let response = EmberlyCient::upload_file(
    token,
    file,
    FileMetadata {
        filename: "screenshot.png".to_string(),
        visibility: "public".to_string(),
    }
).await?;
 
// Response contains shareable URL
println!("URL: {}", response.url);

Key Components

UploadArea

File: src/components/upload/UploadArea.tsx

Purpose: Main UI for file uploads

Features:

  • Drag-and-drop zone
  • File picker button
  • Progress display
  • Error messages
  • Upload preview

Props:

interface UploadAreaProps {
  onUpload: (filePath: string, response: UploadResponse) => void;
  uploadToken: string;
  visibility: 'PUBLIC' | 'PRIVATE';
  password?: string;
  behavior?: BehaviorConfig; // post-upload action, clipboard format
}

ScreenshotToast

File: src/components/overlays/ScreenshotToast.tsx

Purpose: Show upload progress and status

Displays:

  • Upload progress percentage
  • File name being uploaded
  • Success/error state
  • Time elapsed
  • URL for copying

SettingsPage

File: src/components/settings/SettingsPage.tsx

Features:

  • Tabbed interface: Account, Capture, Hotkeys, Preferences (appearance/theme + sounds), About
  • Real-time config updates
  • Settings validation

Common Development Tasks

Adding a New Setting

1. Define Type in src/types.ts:

export interface AppConfig {
  // ... existing fields
  myNewSetting: string;  // Add here
}

2. Add to Default Config (src/config.ts):

export const DEFAULT_CONFIG: AppConfig = {
  // ... existing
  myNewSetting: 'default_value',
};

3. Create Setting Component (src/components/settings):

// NewSettingComponent.tsx
export function NewSetting() {
  const { config, updateConfig } = useAppContext();
  
  return (
    <input 
      value={config.myNewSetting}
      onChange={(e) => updateConfig({ myNewSetting: e.target.value })}
    />
  );
}

4. Add to Settings Page:

// SettingsPage.tsx - import and add to correct tab
import { NewSetting } from './NewSettingComponent';
 
export function SettingsPage() {
  return (
    <div>
      {/* ... other sections */}
      <section>
        <h3>My Section</h3>
        <NewSetting />
      </section>
    </div>
  );
}

Adding a New Tauri Command

1. Implement in Rust (src-tauri/src/lib.rs):

#[tauri::command]
async fn my_command(param: String) -> Result<String, String> {
    match do_something(&param) {
        Ok(result) => Ok(result),
        Err(e) => Err(format!("Error: {}", e)),
    }
}

2. Use in React (src/components/):

import { invoke } from '@tauri-apps/api/core';
 
const result = await invoke<string>('my_command', { param: 'value' });

Adding a New Theme

1. Add to src/colors.ts:

export const themes = {
  // ... existing themes
  'my-theme': {
    // 15+ color variables in HSL format
    primary: 'hsl(200 100% 50%)',
    secondary: 'hsl(160 100% 50%)',
    // ... others
  }
};

**2. Theme will automatic appear in Settings → Appearance


Git Workflow

Branch Naming

feature/* - New features      (feature/screenshot-filters)
fix/*     - Bug fixes         (fix/upload-timeout)
docs/*    - Documentation    (docs/api-guide)
refactor/*- Code cleanup     (refactor/component-structure)
perf/*    - Performance      (perf/image-compression)

Commit Messages

feat(upload): add retry logic for failed uploads
fix(hotkeys): prevent duplicate hotkey registration
docs(setup): clarify prerequisites
refactor(screenshots): simplify capture code
perf(ui): optimize component re-renders

Pull Request Process

1. Create feature branch:

git checkout -b feature/my-feature

2. Make changes and test:

bun run tauri:dev
# Test thoroughly

3. Commit with clear messages:

git add .
git commit -m "feat(upload): add password protection"

4. Push and create PR:

git push origin feature/my-feature
# Go to GitHub and create Pull Request

5. PR Description:

## Description
What this PR does
 
## Why
Why it was needed
 
## Changes
- Change 1
- Change 2
 
## Testing
How to test it
 
## Screenshots
Before/after (if applicable)

Debugging

Chrome DevTools

Press F12 or Ctrl+Shift+I to open DevTools:

  • Console — View logs and errors
  • Elements — Inspect React components
  • Network — Monitor API calls to Emberly
  • Sources — Debug JavaScript
  • Performance — Profile app performance

Rust Debugging

See Rust logs in console:

RUST_LOG=debug bun run tauri:dev

Add debug prints:

println!("Debug: {:?}", my_value);
eprintln!("Error: {}", error);

Common Issues

Hot reload not working:

# Restart dev server
Ctrl+C
bun run tauri:dev

Rust compilation error:

# Clean and rebuild
cd src-tauri && cargo clean && cd ..
bun run tauri:dev

Module not found errors:

# Reinstall dependencies
rm -rf node_modules
bun install

Performance Optimization

React Optimization

// Use React.memo for expensive components
export const MyComponent = React.memo(
  ({ prop }: Props) => {
    return <div>{prop}</div>;
  },
  (prev, next) => prev.prop === next.prop
);
 
// Use useMemo for expensive calculations
const memoizedResult = useMemo(
  () => computeExpensiveValue(data),
  [data]
);
 
// Use useCallback for stable function references
const handleClick = useCallback(() => {
  doSomething();
}, []);

Image Optimization

// Compress before upload
async function compressImage(file: File): Promise<Blob> {
  const canvas = await getCanvasContext(file);
  return new Promise(resolve => {
    canvas.toBlob(resolve, 'image/png', 0.8);  // 80% quality
  });
}

Lazy Loading

// Lazy load heavy components
const SettingsPage = lazy(() => import('./pages/Settings'));
 
export function Router() {
  return (
    <Suspense fallback={<Loading />}>
      <SettingsPage />
    </Suspense>
  );
}

Security Considerations

API Keys & Tokens

  • Never commit tokens to git
  • Use .env files (add to .gitignore)
  • Environment variables: process.env.MY_TOKEN

User Data

  • Passwords transmitted over HTTPS only
  • Tokens stored in secure storage (Tauri plugin)
  • Don't log sensitive data

File Uploads

  • Validate file type before upload
  • Check file size limits
  • Scan for malware (Emberly does this)
  • Throttle upload requests

Resources


Getting Help


Contributing

See the Contributing Guide in the Flicker repository (separate from the main Emberly website's contributing docs) for full guidelines on:

  • Code standards and style
  • Testing requirements
  • PR process
  • Commit message conventions