New Project Flow - Developer Documentation

Overview

This document explains how Oscilla's new project creation flow works, from the splash screen through project initialization. It covers the interaction between client-side UI, server API, and the project loading system.


Table of Contents

  1. Architecture Overview
  2. Splash Screen System
  3. New Project Creation Flow
  4. Project Browser Flow
  5. Preferences System
  6. Pin State Management
  7. File Locations
  8. API Endpoints

Architecture Overview

┌─────────────────────────────────────────────────────────────────┐
│                        SPLASH SCREEN                             │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐                       │
│  │   New    │  │  Browse  │  │   Help   │                       │
│  │ Project  │  │ Projects │  │ Tutorial │                       │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘                       │
└───────┼─────────────┼─────────────┼────────────────────────────┘
        │             │             │
        ▼             ▼             ▼
   ┌─────────┐  ┌─────────┐  ┌──────────────┐
   │ Prompt  │  │ Fetch   │  │ Load helper- │
   │ for     │  │ Projects│  │ score project│
   │ Name    │  │ List    │  └──────────────┘
   └────┬────┘  └────┬────┘
        │            │
        ▼            ▼
   ┌─────────────────────────┐  ┌──────────────────┐
   │ POST /api/project/new   │  │  Project Modal   │
   │ {name: "my-project"}    │  │  (sorted by date)│
   └────┬────────────────────┘  └────┬─────────────┘
        │                            │
        ▼                            ▼
   ┌─────────────────────────────────────────┐
   │ Server: Copy template → new project     │
   │ Returns: {ok: true}                     │
   └────┬────────────────────────────────────┘
        │
        ▼
   ┌─────────────────────────────────────────┐
   │ Redirect: /?project=my-project          │
   └────┬────────────────────────────────────┘
        │
        ▼
   ┌─────────────────────────────────────────┐
   │ projectLoader.js: loadProject()         │
   │ • Load preferences.json                 │
   │ • Apply preferences (pins, colors, etc) │
   │ • Load score.svg                        │
   │ • Initialize UI                         │
   │ • Show Inkscape hint (if enabled)       │
   └─────────────────────────────────────────┘

Splash Screen System

Location

Visual Design

Modal Behavior:

Three Action Buttons:

<!-- Order: New → Browse → Help -->
<button id="new-project-btn">
  <svg><!-- Plus icon --></svg>
  <span>New</span>
</button>

<button id="browse-projects-btn">
  <svg><!-- Folder icon --></svg>
  <span>Browse</span>
</button>

<button id="open-tutorial-btn">
  <svg><!-- Help icon --></svg>
  <span>Help</span>
</button>

When Splash Appears

The splash screen appears when:

  1. User visits Oscilla without a ?project= URL parameter
  2. No project has been loaded yet in the session

Code location: projectLoader.js (bottom of file)

const projectFromURL = urlParams.get("project");
if (projectFromURL) {
  loadProject(projectFromURL);
} else {
  window.showSplashScreen();
}

Hiding the Splash

The splash is hidden when a project loads:

// In loadProject() function
showLoader(projectName); // This hides splash via z-index layering

// Later, after load completes:
hideLoader();
setSplashVisibility(false);

New Project Creation Flow

Step-by-Step Process

1. User Clicks "New" Button

File: projectLoader.jswireSplashActions()

newProjectBtn.onclick = async () => {
  console.log("[Splash] Creating new project");
  
  // Step 1: Prompt for name
  const name = prompt("New project name:");
  if (!name) return;

  try {
    // Step 2: Call server API
    const res = await fetch("/api/project/new", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ name })
    });

    const data = await res.json();
    if (!data.ok) {
      alert(data.error);
      return;
    }

    // Step 3: Set hint for post-load guidance
    sessionStorage.setItem("oscilla.showInkscapeHint", name);
    
    // Step 4: Navigate to new project
    window.location.href = `/?project=${encodeURIComponent(name)}`;
    
  } catch (err) {
    console.error("[Splash] Failed to create project:", err);
    alert("Failed to create project. Please try again.");
  }
};

2. Server Creates Project

File: server.js/api/project/new endpoint

The server:

  1. Receives project name
  2. Validates name (no slashes, dots, etc.)
  3. Checks if project already exists
  4. Copies template directory to new project
  5. Returns success/error response

Template Location:

public/scores/template/
├── score.svg           # Blank SVG template
├── preferences.json    # Default preferences
├── audio/              # Empty audio directory
├── pages/              # Empty pages directory
├── text/               # Empty text directory
└── video/              # Empty video directory

3. Browser Redirects

After successful creation, the browser navigates to:

/?project=my-new-project

This triggers the project loading flow.

4. Project Loads

File: projectLoader.jsloadProject(projectName)

async function loadProject(projectName, options = {}) {
  // 1. Show loader (hides splash)
  showLoader(projectName);
  
  // 2. Set paths
  window.projectBase = `scores/${projectName}/`;
  window.currentProject = projectName;
  
  // 3. Load preferences
  const prefs = await loadPreferences(window.projectBase);
  applyPreferences(prefs);
  
  // 4. Load score.svg
  const svgUrl = `${window.projectBase}score.svg`;
  // ... load and initialize SVG
  
  // 5. Hide loader
  hideLoader();
  
  // 6. Show Inkscape hint if new project
  const hintProject = sessionStorage.getItem("oscilla.showInkscapeHint");
  if (hintProject === projectName) {
    sessionStorage.removeItem("oscilla.showInkscapeHint");
    showInkscapeHint(projectName);
  }
}

5. Inkscape Hint Appears

File: projectScoreSetup.jsshowInkscapeHint()

A modal dialog appears explaining:

The user can dismiss this dialog, and there's a checkbox to "Don't show again" which stores the preference in localStorage.


Project Browser Flow

When User Clicks "Browse"

1. Fetch Projects from Server

Client: projectLoader.jsbrowseBtn.onclick

browseBtn.onclick = async () => {
  const projects = await fetchProjects();
  openProjectModal(projects);
};

API Call:

async function fetchProjects() {
  const res = await fetch("/api/projects");
  if (!res.ok) throw new Error("Failed to fetch projects");
  return res.json();
}

2. Server Returns Project List with Metadata

Server: server.js/api/projects endpoint

Returns JSON array:

[
  {
    "name": "my-project",
    "modified": "2025-02-02T10:30:00.000Z"
  },
  {
    "name": "another-project",
    "modified": "2025-02-01T15:20:00.000Z"
  }
]

Server Implementation:

app.get("/api/projects", (req, res) => {
  const scoresDir = path.join(WRITE_DIR, "public", "scores");
  
  fs.readdir(scoresDir, { withFileTypes: true }, (err, entries) => {
    if (err) return res.status(500).json([]);
    
    const projects = entries
      .filter(e => e.isDirectory())
      .filter(e => !e.name.startsWith("."))
      .map(e => {
        const projectPath = path.join(scoresDir, e.name);
        const stats = fs.statSync(projectPath);
        return {
          name: e.name,
          modified: stats.mtime.toISOString()
        };
      });
    
    res.json(projects);
  });
});

3. Display Modal with Sorted List

Client: projectLoader.jsopenProjectModal()

async function openProjectModal(projects) {
  const modal = document.getElementById("project-modal");
  const list = document.getElementById("project-list");
  
  // Normalize data
  projects = projects.map(p => 
    typeof p === 'string' ? { name: p } : p
  );
  
  // Sort by date (newest first)
  projects.sort((a, b) => {
    if (a.modified && b.modified) {
      return new Date(b.modified) - new Date(a.modified);
    }
    return (a.name || '').localeCompare(b.name || '');
  });
  
  // Create list items
  projects.forEach(project => {
    list.appendChild(makeProjectListItem(project));
  });
  
  modal.classList.remove("hidden");
}

4. Format Timestamps

Client: projectLoader.jsmakeProjectListItem()

Timestamps are formatted as:

function makeProjectListItem(project) {
  const item = document.createElement("div");
  item.className = "project-item";
  
  // Format timestamp
  if (project.modified) {
    const date = new Date(project.modified);
    const now = new Date();
    const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));
    
    if (diffDays === 0) meta.textContent = "today";
    else if (diffDays === 1) meta.textContent = "yesterday";
    else if (diffDays < 7) meta.textContent = `${diffDays}d ago`;
    else meta.textContent = date.toLocaleDateString(/* ... */);
  }
  
  // Click handler
  item.onclick = () => {
    window.loadProject(project.name, { resetOnLoad: true });
    closeProjectModal();
  };
  
  return item;
}

Preferences System

File Structure

Each project has a preferences.json file:

public/scores/my-project/
└── preferences.json

Default Preferences

Location: projectLoader.jsloadPreferences()

{
  "darkMode": false,
  "defaultPlaybackSpeed": 1.0,
  "defaultViewMode": "scroll",
  "pinControls": true,
  "pinTopbar": true
}

Loading Preferences

When a project loads:

async function loadPreferences(basePath) {
  const prefsPath = `${basePath}preferences.json`;
  try {
    const res = await fetch(prefsPath);
    if (!res.ok) throw new Error("No preferences.json found");
    return await res.json();
  } catch (err) {
    // Return defaults
    return {
      darkMode: false,
      defaultPlaybackSpeed: 1.0,
      defaultViewMode: "scroll",
      pinControls: true,
      pinTopbar: true,
    };
  }
}

Applying Preferences

Location: projectLoader.jsapplyPreferences(prefs)

window.applyPreferences = function applyPreferences(prefs) {
  // 1. Dark mode
  applyDarkMode?.(!!prefs.darkMode);

  // 2. Duration
  if (prefs.duration_minutes > 0) {
    window.duration = prefs.duration_minutes * 60 * 1000;
  }

  // 3. Pin states (NEW)
  if (prefs.pinControls !== undefined) {
    window.oscillaControlsPinned = prefs.pinControls;
  } else {
    window.oscillaControlsPinned = true; // Default pinned
  }
  
  if (prefs.pinTopbar !== undefined) {
    window.oscillaTopbarPinned = prefs.pinTopbar;
  } else {
    window.oscillaTopbarPinned = true; // Default pinned
  }

  // 4. Visual preferences
  const playhead = document.getElementById("playhead");
  if (playhead && prefs.playheadColor) {
    playhead.style.backgroundColor = prefs.playheadColor;
  }
  // ... etc
};

User-Facing Preferences Dialog

Location: oscillaPreferences.jsopenPreferencesDialog()

Users can access via: Menu → Preferences

The dialog includes:

When saved, preferences are POST'd to:

/save-preferences/:projectName

Server writes to preferences.json in the project directory.


Pin State Management

How Pinning Works

Pin State Variables:

// Set from preferences (default: true)
window.oscillaControlsPinned = true;
window.oscillaTopbarPinned = true;

// Used by transport system
window.controlsPinned = window.oscillaControlsPinned ?? true;
window.topbarPinned = window.oscillaTopbarPinned ?? true;

Initialization Flow

1. Preferences Load First

File: projectLoader.jsloadProject()

// Load preferences early in project load
const prefs = await loadPreferences(window.projectBase);
applyPreferences(prefs); // Sets window.oscillaControlsPinned/Topbar

2. Transport Reads Preference

File: oscillaTransport.js (top-level)

// Read from preference, default to true
window.controlsPinned = window.oscillaControlsPinned ?? true;
window.topbarPinned = window.oscillaTopbarPinned ?? true;

3. UI Initializes

File: oscillaTransport.jsinitializeControlsPin()

export function initializeControlsPin() {
  const pinButton = document.getElementById("pin-controls");
  
  // Set initial visual state based on preference
  pinButton.classList.toggle("active", window.controlsPinned);
  
  // Wire toggle handler
  pinButton.addEventListener("click", () => {
    window.controlsPinned = !window.controlsPinned;
    pinButton.classList.toggle("active", window.controlsPinned);
    
    if (window.controlsPinned) {
      controls?.classList.remove('dismissed');
    } else {
      window.hideControlsLater();
    }
  });
}

Visual Feedback

CSS:

/* Pin button shows "active" state when pinned */
.gui-button.active {
  background: rgba(255, 255, 255, 0.2);
  border-color: rgba(255, 255, 255, 0.4);
}

When pinned:

When unpinned:


File Locations

Client-Side Files

public/
├── index.html                    # Main HTML (splash screen structure)
├── css/
│   └── oscillaSplash.css        # Splash screen styles
└── js/
    ├── projectLoader.js         # Project loading, splash wiring
    ├── oscillaTransport.js      # Transport controls, pin logic
    ├── oscillaPreferences.js    # Preferences dialog
    └── projectScoreSetup.js     # Score setup, Inkscape hint

Server-Side Files

server.js                        # Main server (API endpoints)
serverUtils.js                   # Project creation utilities

Project Template

public/scores/template/
├── score.svg                    # Blank SVG template
├── preferences.json             # Default preferences with pins=true
├── audio/
├── pages/
├── text/
└── video/

User Projects

public/scores/my-project/
├── score.svg                    # User's score
├── preferences.json             # User's preferences
├── audio/                       # User's audio files
├── pages/                       # User's page overlays
├── text/                        # User's text files
└── video/                       # User's video files

API Endpoints

GET /api/projects

Returns: List of projects with metadata

Response:

[
  {
    "name": "my-project",
    "modified": "2025-02-02T10:30:00.000Z"
  }
]

Server Implementation:


POST /api/project/new

Purpose: Create new project from template

Request Body:

{
  "name": "my-new-project"
}

Response:

{
  "ok": true
}

Or on error:

{
  "ok": false,
  "error": "Project already exists"
}

Server Logic:

  1. Validate project name
  2. Check if project exists
  3. Copy template directory to scores/my-new-project/
  4. Return success/failure

Implementation: serverUtils.jscreateNewProject(name)


POST /save-preferences/:project

Purpose: Save project preferences

Request Body:

{
  "darkMode": true,
  "pinControls": true,
  "pinTopbar": false,
  "playheadColor": "#ff0000"
}

Response:

{
  "ok": true
}

Server Logic:

  1. Validate project exists
  2. Write JSON to scores/:project/preferences.json
  3. Return success/failure

Development Tips

Testing New Project Flow

  1. Clear localStorage/sessionStorage to test first-run experience:

    localStorage.clear();
    sessionStorage.clear();
    
  2. Test without URL params to see splash screen:

    http://localhost:8001/
    
  3. Test with URL params to bypass splash:

    http://localhost:8001/?project=test-project
    

Debugging Pin State

Check console for these messages:

[Prefs] applyPreferences() called: {...}
[UI] Controls pin initialized.
[UI] Topbar pin initialized.
[UI] Controls pinned — will stay visible.

Check window variables in console:

console.log('oscillaControlsPinned:', window.oscillaControlsPinned);
console.log('controlsPinned:', window.controlsPinned);
console.log('topbarPinned:', window.topbarPinned);

Modifying Default Preferences

To change defaults for all new projects:

  1. Edit template/preferences.json
  2. Edit projectLoader.jsloadPreferences() defaults object
  3. Edit oscillaPreferences.js → field definitions defaults

Common Issues

Issue: Splash blocks clicks after hiding

Issue: Pins don't persist after reload

Issue: Project modal shows wrong dates


Summary

The new project flow creates a seamless experience:

  1. Splash screen appears as modal blocker (must choose action)
  2. "New" button prompts for name → creates project → redirects
  3. Server copies template → returns success
  4. Client loads project → applies preferences (pins defaulted to true)
  5. Inkscape hint shows for new projects (dismissible, rememberable)
  6. User starts working immediately with pinned controls

The system is designed to be:


Last Updated: 2025-02-02
Oscilla Version: 0.4.2

Tip: use ← → or ↑ ↓ to navigate the docs