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
- Architecture Overview
- Splash Screen System
- New Project Creation Flow
- Project Browser Flow
- Preferences System
- Pin State Management
- File Locations
- 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
- HTML:
public/index.html(#splash.launcher) - CSS:
public/css/oscillaSplash.css - JavaScript:
public/js/system/launcher.js, started bywireSplashActions()inpublic/js/system/projectLoader.js
Layout
The start page takes the whole window; the score interface (top bar, transport, score) is hidden while it shows. It is set on a grid, flush left, Swiss fashion: a label column and a content column, the rows divided by rules, one type family (Plex Sans; Plex Mono for numbers and dates), flat colour with the logo the one coloured element besides the primary button. From the top:
- the logo in the label column, the name large beside it, the tagline under it; the whole composition, foot included, centred in the window;
- Start: New project, Projects ▾, Demos ▾, Import…,
Documentation. (Live coding has its own start page, live.html. The
API still takes
{ name, live: true }on/api/project/new:createNewProject(name, { live })setsliveConsole: "perform",darkMode: true,pinTopbarandpinControlsfalse,playheadVisibility: "none", and writes an empty stage forscore.svg(LIVE_STAGE_SVG) -- no button on the start page asks for it now.) Projects and Demos drop down a list with its own search (Enter opens the first match, the arrow keys move through it), under the button or over it when there is more room there. Projects are listed recently opened first (localStorageoscillaRecentProjects), then by date; demos bydemoCategory; - Recent: the last three projects opened, a numbered list: title, description, when;
- the version, in the label column level with the tagline (
/api/version); at the foot the copyright (Rob Canning, AGPL-3.0) and the projects folder.
The lists come from GET /api/projects (name, modified, category,
title, description, live, the last four from each project's
preferences.json: projectTitle, projectDescription, liveConsole). A project still carrying the template's title shows
its name instead.
Light and dark follow the system setting (prefers-color-scheme). Below
760 px wide the buttons go two to a row and a drop-down fills the screen.
When Splash Appears
The splash screen appears when:
- User visits Oscilla without a
?project=URL parameter - 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 project"
File: launcher.js → askName(), submitName()
A form opens under the buttons, asking for the name (no prompt() box).
On Create:
- The name is checked against
/^[A-Za-z0-9_-][A-Za-z0-9 _-]*$/and against the projects already listed; an error shows beside the field. POST /api/project/newwith{ name }; a server error shows there too.- On success the page goes to
/?project=<name>.
Import… picks a .oscilla or .zip file, then opens the same form as
"Import file as", filled in with the file's name, and posts the file and
the name to /api/project/import.
2. Server Creates Project
File: server.js → /api/project/new endpoint
The server:
- Receives project name
- Validates name (no slashes, dots, etc.)
- Checks if project already exists
- Copies
templatedirectory to new project - 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.js → loadProject(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.js → showInkscapeHint()
A modal dialog appears explaining:
- How to edit the score in Inkscape
- Where the score.svg file is located
- Next steps for the user
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.js → browseBtn.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.js → openProjectModal()
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.js → makeProjectListItem()
Timestamps are formatted as:
- "today" - modified today
- "yesterday" - modified yesterday
- "3d ago" - modified 3 days ago
- "Jan 15" - modified earlier (with year if different)
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.js → loadPreferences()
{
"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.js → applyPreferences(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.js → openPreferencesDialog()
Users can access via: Menu → Preferences
The dialog includes:
- Project metadata (title, author, description)
- Visual preferences (colors, dark mode)
- Pin Controls checkbox (default: true)
- Pin Top Bar checkbox (default: true)
- Playback settings
- Touch/interaction settings
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.js → loadProject()
// 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.js → initializeControlsPin()
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:
- Button has "active" class
- Controls/topbar stay visible
- Auto-hide is disabled
When unpinned:
- Button loses "active" class
- Controls/topbar auto-hide after inactivity
- User must move mouse to reveal
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:
- Reads
public/scores/directory - Filters out hidden directories (starting with
.) - Uses
fs.statSync()to get modification time - Returns sorted array of project objects
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:
- Validate project name
- Check if project exists
- Copy
templatedirectory toscores/my-new-project/ - Return success/failure
Implementation: serverUtils.js → createNewProject(name)
POST /save-preferences/:project
Purpose: Save project preferences
Request Body:
{
"darkMode": true,
"pinControls": true,
"pinTopbar": false,
"playheadColor": "#ff0000"
}
Response:
{
"ok": true
}
Server Logic:
- Validate project exists
- Write JSON to
scores/:project/preferences.json - Return success/failure
Development Tips
Testing New Project Flow
-
Clear localStorage/sessionStorage to test first-run experience:
localStorage.clear(); sessionStorage.clear(); -
Test without URL params to see splash screen:
http://localhost:8001/ -
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:
- Edit
template/preferences.json - Edit
projectLoader.js→loadPreferences()defaults object - Edit
oscillaPreferences.js→ field definitions defaults
Common Issues
Issue: Splash blocks clicks after hiding
- Cause:
pointer-eventsCSS still blocking - Fix: Ensure splash has
.hiddenclass ordisplay: none
Issue: Pins don't persist after reload
- Cause: Preferences not being saved
- Fix: Check
/save-preferencesendpoint is working
Issue: Project modal shows wrong dates
- Cause: Server not returning
modifiedfield - Fix: Check
/api/projectsendpoint implementation
Summary
The new project flow creates a seamless experience:
- Splash screen appears as modal blocker (must choose action)
- "New" button prompts for name → creates project → redirects
- Server copies template → returns success
- Client loads project → applies preferences (pins defaulted to true)
- Inkscape hint shows for new projects (dismissible, rememberable)
- User starts working immediately with pinned controls
The system is designed to be:
- Modal: Forces deliberate choice (no accidental clicks)
- Fast: Minimal steps from idea to working
- Configurable: Preferences system allows customization
- Developer-friendly: Clear separation of concerns, documented APIs
Last Updated: 2025-02-02
Oscilla Version: 0.4.2
Tip: use ← → or ↑ ↓ to navigate the docs