Skip to content

Software specification

The desktop application that reads, browses and (later) edits FilmOpen projects.

Version 1.2
Date 10 September 2026
Status Describes the code at the end of milestone 3
Companion documents FilmOpen Project Specification 1.5 (docs/FilmOpen-Project-Specification v1.0.md) defines the file format this software reads and writes. FilmOpen — Milestone 1, 2 and 3 (docs/FilmOpen-Milestone *.md) record what was built, why, and what comes next.
Repository filmopen-c, branch round-03-c (milestone 3), on top of round-02-c (milestone 2) and c (milestone 1)

  1. Purpose
  2. Design principles
  3. Technology
  4. Architecture
  5. The model layer
  6. Services: where files come from
  7. State: providers
  8. The tree
  9. The user interface
  10. Localisation
  11. The sample project
  12. Testing
  13. Conventions for contributors
  14. Known limitations
  15. Third-party code Appendix A — File map Appendix B — Glossary of code names

A FilmOpen project is a folder of plain JSON files describing a film completely: characters, outfits, locations, props, styles, documents; the script as seasons, episodes, sequences and scenes with their blocks; shots and cues; models, platforms and plugins; render batches; commentary; and official pointers. Every entity file is versioned per author (ch_main-hero_30yo_john123_v2.json), forking copies a file into a new author’s name, and a small _official.json pointer names the version the project has chosen. The intent is that a film is developed the way software is developed: many people, many versions, comparison, and a deliberate choice of what is official.

FilmOpen the application is the reader and editor for such folders. Milestone 1 delivered the reader: open a folder, see the whole project as a tree, inspect any object, see every version of it side by side, see the media rendered for it and what people said about it, and be warned about everything the format’s rules say a reader should warn about. Milestone 2 made it a desktop application that lives on a machine: it knows where a project’s media is (the manifest, §4.4 of the format), where the shared library of models, platforms and plugins is, where its own preferences and log are, which projects the user has opened, and it can create a project folder. Milestone 3 made it a writer: a new project comes with its creator’s first project file, the project file is edited in a form that saves as you type, any version can be forked, a director sets official with a star, anyone picks their preferred version with a check, and a version can be labelled.

The application is not the format. The Project Specification is authoritative about what files mean; this document is authoritative about how the application is built. Where the application simplifies the format’s rules, this document says so.

A developer, or an AI development session, who has read the Project Specification and needs to continue the code without re-deriving its structure. It explains what exists, the reasons behind the shape of it, and the rules that must stay true when adding to it.


These follow directly from the format’s own principles (Project Specification §2) and from decisions made during milestone 1.

  1. Warn, don’t enforce. No file is rejected for being imperfect. A header that disagrees with its filename, a field of the wrong type, a segment that is too long, an unknown file: each is a warning the user can see, never a crash and never a silent fix. The loader tolerates any JSON that parses to an object.
  2. Never guess. When the format says a reader must report rather than choose, the application reports. An ambiguous reference (several authors, nothing official, no viewer) is unresolved, not first-match. A folder with several project authors and no official pointer asks which to open. An implicit epoch falls back to default only, never to “whatever exists”.
  3. The files are the only state. Every write goes to a file through the project’s ProjectSource, and the project is re-read afterwards; nothing is edited in memory and saved later. The app writes only under the viewer’s own handle (§17, §18.3 of the format): a form is offered on the viewer’s own version and Fork on everyone else’s; the official pointer is a director’s to write and to remove; the pick lives in the viewer’s own commentary file. Reads and writes are confined to the project folder; the media root and the library are sandboxes of their own, and the library is never written. Creating a project refuses a folder that already holds files.
  4. The model knows the format, not the screen. Everything under lib/models/ and lib/services/ is pure Dart, free of Flutter and free of user-facing text. Diagnostics are codes with arguments. This keeps the core testable without a widget tree and ready to become a separate package.
  5. Language-independent by construction. Every string a person reads comes from a localisation file, including the display labels for the format’s own field names, entity types and counts. The format’s JSON keys are never translated; the screen is.
  6. One navigation path. Whatever is clicked, in the tree or in the detail pane, goes through one controller that sets what is shown, highlights the matching tree row and reveals it.
  7. Small, honest surface. The application says what it does not do (placeholders show a message, the README says “nothing writes yet”). Product text is not aspirational.
  8. Model after API Dash. The window layout follows the API Dash desktop client: a narrow icon rail, a resizable sidebar, a main pane. One component is adapted from it under its licence; the rest is FilmOpen code.

Concern Choice Notes
Language and toolkit Dart 3.13, Flutter 3.47 (stable) Windows desktop is the first target; the same code runs in Chrome for previews and in widget tests.
State flutter_riverpod 3 Notifier / AsyncNotifier / Provider only; no code generation.
Split view multi_split_view 3.6 Wrapped in widgets/split_view.dart, adapted from API Dash.
Folder picker file_selector Desktop only; hidden on the web.
Preferences shared_preferences (SharedPreferencesWithCache) Theme, language, viewer handle, sidebar width, verbose logs, recent projects, library folder. See §6.4 for where the file lives.
Application folders path_provider The app-support folder (preferences, shared library) and, on mobile, the log folder. Desktop log folders are computed from the environment (§6.4).
File browser and URL schemes url_launcher Only for the mobile “show in folder” branch; desktop uses the OS command directly.
Localisation flutter_localizations, intl 0.20, Flutter gen-l10n ARB files in lib/l10n/; generated code committed.
Lints flutter_lints 6 flutter analyze must report no issues.
Tests flutter_test Unit, fixture and widget tests; 40 at the end of milestone 1, 83 at milestone 2, 104 at milestone 3.

Platforms enabled: windows, web. Android, iOS, macOS and Linux are not configured; the code has branches for them (paths, file reveal) that compile but have not run.

flutter pub get # also regenerates localisations (generate: true)
flutter gen-l10n # regenerate localisations by hand after editing ARB files
flutter analyze
flutter test
flutter run -d windows
flutter run -d chrome
flutter run -d web-server --web-port 8642 --web-hostname localhost # headless preview
flutter build windows --release # build\windows\x64\runner\Release\filmopen.exe

The Windows toolchain needs Visual Studio Build Tools with the C++ desktop workload; flutter doctor must show Visual Studio – develop Windows apps as OK.


lib/
models/ the format: filename grammar, entities, the project index, resolution, diagnostics (pure Dart)
services/ where files come from and go to (disk, bundled assets, memory), the loader, the manifest's
media root, the shared library, the log, application folders, the file-browser reveal,
what a new project contains (project_template), the writer (save, fork, official, pick)
(pure Dart except asset_source and the *_io / *_stub pairs, which are conditional imports)
providers/ Riverpod state: the open project, browser navigation, settings, localisation
tree/ the tree model and the builder that lays a project out as a tree
screens/ pages: dashboard, browser (tree pane, detail pane and its tabs), settings
widgets/ reusable pieces: tree view, split view, attribute view, screenplay view, chips
l10n/ ARB files, generated localisations, label helpers, field-label table
utils/ natural (numeric-aware) string ordering
theme.dart, app.dart, main.dart, consts.dart

Dependency rule. Arrows point downward only:

screens → widgets → providers → tree → l10n
models ← services (project_source only)
  • models/ imports nothing from Flutter and nothing from the layers above. It imports services/project_source.dart (an abstract interface plus an in-memory implementation), which is also pure Dart. This is a known small inversion, accepted for now; a later package split would move ProjectSource into the model package.
  • services/asset_source.dart is the one Flutter-dependent service (it reads the asset bundle) and is imported only by providers/. services/app_log.dart and project_creator’s model inputs are pure Dart. Every service that needs dart:io comes as a triple — x.dart (a conditional export), x_io.dart, x_stub.dart — so the same call compiles on the web and answers “not available” there: directory_source, asset_disk, app_paths, log_file, reveal, project_creator, library_install.
  • tree/ depends on Flutter only for IconData; icons are chosen in widgets/entity_icons.dart.
  • l10n/labels.dart is the single bridge from model codes to human text.
project folder ──┐
media root ─────┤ three ProjectSources (folder / assets / memory), each its own sandbox
shared library ──┘ listPaths(), readText(), locate(), diskPath(), related()
ProjectWriter ◄── (save / fork / official / pick: writeText, deleteFile on the project's source, then a re-read)
ProjectLoader ──► FilmProject (manifest, media root, library; index: entities by stem (with origin),
│ groups by type+tag, epochs, versions, official pointers, commentary, batches,
│ takes (with media location), other files, warnings, project candidates,
│ selected project)
projectProvider (AsyncNotifier) ◄── libraryProvider (FutureProvider: the library folder, seeded on first run)
├─► treeModelProvider ──► TreeBuilder(project, l10n, viewer) ──► TreeModel(roots, index by selection key)
│ │
▼ ▼
browserProvider (BrowserState: selection, highlighted node, toggled rows) ◄── TreePane clicks / DetailPane links
DetailPane: switch on Selection → ProjectOverview | ProjectChooser | CategoryView | EntityDetail | BatchDetail | OtherFilesView

Everything is derived: change the project, the viewer handle or the language, and the tree and pane rebuild from the index. Nothing is cached beyond Riverpod’s own memoisation. A write is a file write followed by refresh(): the new index replaces the old, and because the browser resets its navigation only when the source changes (projectSourceKeyProvider), the page being edited stays where it is.


5.1 Filename grammar — models/stems.dart

Section titled “5.1 Filename grammar — models/stems.dart”

FileNameParser.parse(path) implements Project Specification §5.4 and returns one of a sealed set:

Class Filename shape Holds
VersionStem <type>_<tag>[_<epoch>]_<author>_v<n> type, tag, epoch?, Author, v; stem, versionsKey (ch_main-hero_30yo), groupKey (ch_main-hero)
OfficialStem <type>_<tag>[_<epoch>]_official type, tag, epoch?; versionsKey
CommentaryStem cm_<type>_<tag>_<author> target type, tag, author
BatchStem rd_<id>_<author> id, author
TakeName <deliverable>_<source stem>_r<id>_<renderer>_<n>.<ext> deliverable, source VersionStem, batch id, renderer, n, extension; batchStem
UnknownName anything else the path

Rules enforced by the parser: exact token counts per type (library types carry an epoch, spec types do not); v<n> with n ≥ 1; take number n is a positive integer without leading zeros; the reserved owner official is never accepted as an author; a take’s batch token is r followed by at least one character. parseStem(stem) parses a stem without extension, as found in forkedFrom and batch source fields.

namingWarnings(parsed) returns warnings (as codes) for any segment outside [a-z0-9][a-z0-9-]{0,19} or longer than ten characters (§5.2). It is applied to every parsed name, including takes and commentary.

Author (models/author.dart) splits owner[.workspace]; equality is by full handle.

EntityType (models/entity_type.dart) is the enum of the nineteen prefixes with category (library / spec / commentary / renderBatch), hasEpoch, isVersioned, and the dotted-tag containment rules parentType / containedType (outfits nest under characters; areas under locations). Deliverable is the enum of take prefixes with isImage. Neither enum carries display text.

  • Entity: one versioned file. Holds the VersionStem, the project-relative path and the complete json map (unknown fields preserved). Typed accessors (name, label — the version label of §7, forkedFrom, notes, kind, stringList(key), blocks) never throw on a wrong JSON type. headerMismatches and fieldTypeWarnings return warning codes.
  • OfficialPointer: from an _official.json; fromJson returns null when the official field is missing, which the loader reports. Keeps its whole json so the writer can rewrite it without losing fields.
  • Commentary / CommentaryEntry: one cm_ file with prefer — the author’s pick — and entries (on, at, rating ±1, text, block, field); keeps its whole json for the same reason.
  • RenderBatch / RenderItem: one rd_ file with its items; totals cost.
  • Take: a media file discovered by name; never listed inside an entity (§12.1).

Two helpers, stringField(json, key) and numField(json, key), are the only way JSON scalars are read.

FilmProject is immutable after loading and holds:

Field Content
entitiesByStem every versioned entity by full stem
groupsByKey EntityGroup per type+tag; each group holds EntityVersions per epoch (spec types use the single epoch null); versions are sorted by author handle then number
officialsByKey pointers by versions key
commentaries, batches, takes as discovered
companionFiles named by the grammar but not JSON (a plugin’s .js beside its manifest)
otherFiles not named by the grammar; reported, never touched
warnings every ProjectWarning raised while loading
projectCandidates every pj version in the folder, stable order
selectedProject the pj version in use, or null when the user must choose (needsProjectChoice)

Queries: group(type, tag), groupsOf(type) in natural tag order, childGroups(type, parentTag) for dotted tags one level down, officialPointer / officialEntity / isOfficial, takesFor(group), commentaryFor(group), batch(stem), groupCounts. withSelectedProject(entity) answers the chooser without reloading.

5.3a Project-level knowledge — models/project_kind.dart, models/epochs.dart

Section titled “5.3a Project-level knowledge — models/project_kind.dart, models/epochs.dart”

ProjectKind is the enum of §8.1 (short, feature, miniseries, series) with the 1.4 words read as their nearest value (fromName), isEpisodic (seasons and episodes rather than installments and acts, §10.1) and defaultRootKey (the story-root list a blank project of that kind starts with, §8.5). recommendedGenres is the vocabulary of Appendix A.9. EpochDeclaration / declaredEpochs(json) read the project’s epochs object in the order §8.2 defines (unnumbered first in file order, then by order); encodeEpochs(list, previous:) writes it back renumbered 1..n, keeping fields the app does not know. FilmProject exposes kind, declaredEpochList, defaultEpoch (the first declared, else default), epochLabel, sortedEpochs(...) (declared order, then natural), isDirector(author) (§8.3), commentaryBy, pickOf(group, viewer) and isPickOf.

Implements §6.1 in a simplified but faithful form. Given a type, a reference (tag or full stem), an optional epoch, whether that epoch was explicit, and an optional viewer:

  1. A full stem names its file exactly.
  2. No such tag → unresolved (noEntity).
  3. Epoch: for library types the wanted epoch is the given one or the project’s default epoch (the first it declares, §8.2; default when it declares none). If there is no file for it: an explicit epoch, or the default itself, is unresolved (noEpoch); an implicit epoch falls back to the default and marks fellBack; no file at the default either → unresolved (noEpochOrDefault).
  4. Author: the viewer’s own highest version, then the viewer’s owner’s highest; then the official version; then, if exactly one owner has versions, that owner’s highest.
  5. Otherwise unresolved (ambiguous, listing the owners).

Not implemented from §6.1: the rule that a file named by an official pointer resolves through official only (the “official sees the official world” half). The application resolves from the viewer’s point of view for every file. This is recorded as a milestone 2 item.

5.5 Project selection — ProjectLoader._selectProject

Section titled “5.5 Project selection — ProjectLoader._selectProject”

Implements §4.1: a valid official pj pointer wins; else a sole author’s highest version; else no selection and the UI asks. Several project tags in one folder also ask. An official pointer to a missing file is a warning and falls through as if absent.

5.6 Diagnostics — models/diagnostics.dart

Section titled “5.6 Diagnostics — models/diagnostics.dart”

ProjectWarning(kind, args, path) with WarningKind (twenty-five kinds: naming, duplicate stem, header missing or mismatched, field type, official pointer problems, unreadable JSON, project selection, since milestone 2 the manifest and library kinds: noManifest, manifestTagMismatch, dataLocatorMalformed, dataLocatorUnsupported, dataRootUnavailable, dataRootAbsolute, libraryStemShadowed, libraryUnavailable, and since milestone 3 noEpochsDeclared, §8.2). ResolutionProblem(kind, type, reference, epoch, owners) with ProblemKind (five kinds). Both have a toString() for logs only; the UI renders them through the localisation layer.

filmopen-project.json (Project Specification §4.4) is the one file in a project that is not somebody’s versioned work. ProjectManifest.fromJson reads tag, data and note tolerantly (a missing or mistyped field is null and the loader warns). DataLocator is the typed “where”: LocatorType (local, dropbox, google-drive, onedrive, icloud, url) or an unknown name kept as text; isLocal, isAbsolutePath (POSIX, drive-letter and UNC forms). ProjectManifest.template is what the creator writes. projectManifestFileName is the one constant that names the file.

5.8 Short names — models/short_name.dart

Section titled “5.8 Short names — models/short_name.dart”

The format’s segment (§5.2) as a person types it: ShortName.pattern ([a-z0-9][a-z0-9-]{0,19}), validate (a ShortNameProblem: empty, invalid characters, bad start, too long), isLongerThanRecommended (over ten: warn, never refuse), normalise (free text to the nearest segment, never inventing one). The filename grammar in stems.dart uses the same pattern, so there is exactly one place the rule lives; widgets/short_name_field.dart is the input box built on it.

EntityOrigin { project, library } says which root a versioned file came from; EntityGroup.isFromLibrary is true when every version of an entity came from the library. Take.media is the MediaLocation resolved at load time, because a take may sit in the media root rather than the project folder.


ProjectSource (services/project_source.dart) is the abstraction the loader reads through. One source is one root; a project’s media root (§4.4) and the shared library (§14.5) are sources of their own:

String get label; // folder name, or a neutral label
String? get rootPath; // absolute path when it is a folder
Future<List<String>> listPaths(); // root-relative, '/'-separated, recursive
Future<String> readText(String path);
MediaLocation? locate(String path); // AssetMedia(key) | FileMedia(path) | null
String? diskPath(String path); // absolute path on this machine, for the file browser; null if not on disk
Future<ProjectSource?> related(String relativeFolder); // the folder a manifest names: '../x-data', 'media'; null if absent
bool get canWrite; // false for bundled assets
Future<void> writeText(String relativePath, String text); // create or replace, through the same sandbox check
Future<void> deleteFile(String relativePath); // absent is not an error

MediaLocation keeps the model Flutter-free; widgets/media_image.dart turns it into an ImageProvider.

Implementations:

  • DirectoryProjectSource (directory_source_io.dart, desktop): recursive scan that skips hidden entries and the folders build, conform, node_modules. Every read goes through file(relativePath), which rejects schemes, absolute paths, backslashes, empty, . and .. segments, then checks that the absolute path, and the symlink-resolved path when the file exists, lies inside the folder. Violations throw ProjectPathException. related() resolves .. against the folder’s URI (or takes an absolute path as given) and returns a new DirectoryProjectSource — a sandbox of its own, never a hole in this one — or null when the folder does not exist. A conditional export (directory_source.dart) swaps in a stub on the web, where canOpenDirectories is false.
  • AssetProjectSource (asset_source.dart): a folder bundled as assets — the sample project, its media root the-cartographer-data, the seed copy of the library — enumerated from the asset manifest. related() resolves a sibling asset folder; diskPath() finds the file beside the executable (asset_disk_io.dart: <exe dir>/data/flutter_assets/… on Windows and Linux, the App.framework bundle on macOS) so that even the sample’s files can be shown in the file browser. It is read-only (canWrite false); snapshot(label:) copies its text files into a writable MemoryProjectSource whose media root is still the bundled folder — how the web preview opens the sample, so editing can be tried where there is no disk.
  • MemoryProjectSource: a Map<String, String> for tests and fixtures, and for session-only projects (the web), with a relatedSources map for the folders a manifest may name. Writable.

ProjectLoader.load(source, {library}) (services/project_loader.dart) works from up to three roots. It reads the manifest first (a missing one is noManifest; a manifest without filmopen or tag is headerMissing; the folder without a manifest is its own media root) and opens the media root it names when the locator is local and the folder exists (else dataLocatorUnsupported / dataRootUnavailable; an absolute path is dataRootAbsolute). It indexes the library’s files first with EntityOrigin.library (a library folder that cannot be listed is libraryUnavailable and the project goes on without it), then the project’s with EntityOrigin.project — the project’s copy of a stem replaces the library’s with libraryStemShadowed and none of the replaced file’s own warnings survive (per-entity warnings are emitted at the end); a project’s official pointer replaces a library’s silently (§14.5: accepting a library entry is pointing official at it); the library’s unknown files are not the project’s “other files”. Then it walks the media root for takes only (anything else there is reference material reached by path, §6.3). Finally it builds the index as in milestone 1, checks the manifest’s tag against the pj files (manifestTagMismatch) and selects the project. It logs one project.loaded debug entry with counts and timing.

6.1 Creating a project — services/project_template.dart, project_creator*.dart

Section titled “6.1 Creating a project — services/project_template.dart, project_creator*.dart”

writeNewProject(source, {tag, author, name, kind, epochLabel}) (pure Dart) writes into any writable ProjectSource: filmopen-project.json from ProjectManifest.template, and pj_<tag>_<author>_v1.json with the header, the kind, an empty genre, the current year, one epoch (default, labelled in the user’s language, with the current year, §8.2), the creator in contributors as director (§8.3), an empty story-root list for the kind (§8.5) and timestamps. createProject(...) (_io) makes <parent>/<tag>/ first, refusing a folder that already holds files (ProjectFolderNotEmptyException) and a tag that is not a segment; where folders cannot be made (the web) the provider writes the same documents into a MemoryProjectSource that lives for the session. encodeProjectJson is the one JSON encoder for written files: two-space indentation and a final newline (§18.1); rfc3339Now() the one timestamp format (§7).

6.1a Writing — services/project_writer.dart

Section titled “6.1a Writing — services/project_writer.dart”

ProjectWriter(project) is pure Dart and does every write into an open project, through project.source.writeText / deleteFile, logging each (entity.saved, entity.forked, official.set, official.cleared, pick.set):

  • saveEntity(entity, json, viewer:) — replaces the author’s own file with json plus a fresh updated. Refuses a read-only source (ProjectReadOnlyException), a library file, no viewer (NoViewerException) and another author’s file (NotYourFileException, §17 rule 2). Whether to warn that the version is official (§13.2) is the caller’s decision; the editor shows a banner and still allows it.
  • fork(entity, viewer:) — §13.1: the viewer’s next number at that epoch (v1 when they have none), a complete copy with author, v, forkedFrom and fresh timestamps, minus the version label; written beside the original. Returns the new VersionStem.
  • setOfficial(entity, viewer:) / clearOfficial(entity, viewer:) — writes <versions key>_official.json naming the entity with setBy and setAt (rewriting an existing pointer’s other fields, dropping its note), or deletes the pointer file. The UI offers this to directors only (FilmProject.isDirector, §8.3).
  • setPick(group, entity?, viewer:)prefer in the viewer’s own cm_ file for the entity (§13.4), created with empty entries when they have none; null removes prefer and keeps the file.

Nothing renames or deletes another author’s file (§18.3).

6.2 The log — services/app_log.dart, log_file*.dart

Section titled “6.2 The log — services/app_log.dart, log_file*.dart”

Structured logging, pure Dart. LogLevel is verbosity from error to trace; error and warning are always written, info is the default threshold, debug is what the Verbose logs setting adds, trace is per-file detail that only FILMOPEN_LOG_LEVEL=trace in the environment turns on (the environment pins the level; the setting then cannot lower it). AppLog.instance fans each LogEntry (UTC ts, level, dotted event such as project.opened, a data object, optional error and stack) to its sinks: FileLogSink (one JSON object per line, appended and flushed synchronously so a crash loses nothing, rotated to <name>.1.jsonl past 5 MB), LineLogSink (the console in debug builds), MemoryLogSink (tests). A sink that throws is skipped, never fatal. AppLog.timed wraps a future and logs its duration or its failure. main.dart opens the file sink before anything else, routes FlutterError.onError and PlatformDispatcher.onError into it, and writes app.started. Events are identifiers, not sentences, so the log stays language-independent and groupable.

6.3 Showing files — services/reveal*.dart

Section titled “6.3 Showing files — services/reveal*.dart”

revealInFileManager(path) opens the platform’s file browser with the file selected (or the folder opened): Windows explorer.exe /select, <path> (the switch and the path as two arguments — Dart quotes an argument containing spaces, and Explorer ignores a quoted /select,C:\a b\f), macOS open -R, Linux the freedesktop org.freedesktop.FileManager1.ShowItems D-Bus call with xdg-open of the folder as fallback. iOS (shareddocuments://) and Android (a content:// Documents URI for shared storage) go through url_launcher and are best-effort: those platforms are not built here and the branches have not run. It returns false rather than throwing, and the caller says so in a snackbar. On the web canRevealFiles is false and the buttons are not shown.

6.4 Application files — services/app_paths*.dart

Section titled “6.4 Application files — services/app_paths*.dart”

Where the application keeps its own files, in the place each operating system expects. Project data never lives here.

Preferences (shared_preferences) Shared library Log file
Windows %APPDATA%\ai.filmopen\filmopen\shared_preferences.json %APPDATA%\ai.filmopen\filmopen\library\ %LOCALAPPDATA%\FilmOpen\logs\filmopen.jsonl
macOS NSUserDefaults (~/Library/Preferences/<bundle id>.plist) ~/Library/Application Support/<bundle id>/library/ ~/Library/Logs/FilmOpen/filmopen.jsonl
Linux $XDG_DATA_HOME/filmopen/shared_preferences.json (~/.local/share/…) $XDG_DATA_HOME/filmopen/library/ $XDG_STATE_HOME/filmopen/logs/ (~/.local/state/…)
Android the system SharedPreferences store the app’s support folder, library/ the app’s support folder, logs/
iOS NSUserDefaults the app’s support folder, library/ the app’s support folder, logs/
Web browser storage the bundled copy, read in place none: console only

The app-support folder is the one path_provider derives from the executable’s company and product names (windows/runner/Runner.rc: ai.filmopen, filmopen). On Windows and Linux shared_preferences keeps a JSON file there, and Settings → About shows that file with a show-in-folder button; on macOS and mobile the plugin uses the system’s own preference store, which is not a file to show, so the line is absent. The library sits in the same folder so everything the app owns on a machine is in one place; a library folder that cannot be read is a libraryUnavailable warning on the project, shown on the Settings library card, never a failure to open. Preferences are plain JSON: theme, language, handle, widths, verbose flag, recent project paths, library path — nothing secret. API keys and other credentials, when they arrive, go to the platform’s credential store (Windows Credential Manager / DPAPI, the macOS and iOS Keychain, the Android Keystore, libsecret on Linux), not to this file; the code says so at sharedPreferencesProvider.

Note for development: two builds of the app on one machine (for instance filmopen-a and filmopen-c) share these folders and the log file, because they share the product identity.

6.5 Installing the library — services/library_install*.dart

Section titled “6.5 Installing the library — services/library_install*.dart”

installLibraryIfEmpty(folder, seed) copies the bundled library (assets/sample/library) into the app’s library folder when that folder holds no files (empty subfolders do not count) and never touches it afterwards. Updating the library — a git pull, a download — is the user’s or a future updater’s job, independent of the app’s own builds (§14.5). On the web the bundled copy is read in place.


Provider Type Responsibility
projectProvider AsyncNotifier<FilmProject> Loads the sample at start — read in place on desktop, as a writable session copy on the web — with the library from libraryProvider (which it watches: choosing another library folder re-reads the open project). openDirectory(path), openSample() replace the page (loading spinner) and, on success, put the folder at the top of the recent list. createProject(...) writes the folder (or a session-only project where there is no disk) and opens it, returning the new pj stem. refresh() re-reads without entering a loading state, so the current project stays on screen. selectProject(stem) answers the chooser. saveEntity, fork, setOfficial, setPick call the ProjectWriter with the viewer from Settings and then refresh(). Every open, write, success and failure is logged.
projectSourceKeyProvider Provider<String?> Names which source is open (type and root path or label), unchanged by a re-read. The browser resets its navigation on this, not on every new index, so a page survives the re-read that follows a save.
settingsProvider Notifier<Settings> themeMode, viewerHandle (empty = no viewer), sidebarWidth, locale (null = platform), verboseLogs (applied to AppLog.instance on load and on change), recentProjects (most recent first, at most twelve, each a {path, name} JSON string; unreadable entries are dropped), libraryPath (null = the app’s own folder). Persisted through sharedPreferencesProvider, which main overrides with a real store and tests leave null.
libraryProvider FutureProvider<ProjectSource?> The shared library: the folder chosen in Settings; else the app’s library folder, seeded from the bundled copy on first run; else (web) the bundled copy read in place.
preferencesLocationProvider FutureProvider<String?> The preferences folder, for Settings → About; null on the web.
localizationsProvider Provider<AppLocalizations> The strings in effect for code without a BuildContext: the chosen locale, else the platform locale, resolved to a supported one by language code. Widgets use context.l10n; both resolve the same way.
treeModelProvider Provider<TreeModel> Rebuilds the tree from the project, the viewer and the language.
browserProvider Notifier<BrowserState> selection (what the detail pane shows), nodeId (highlighted row), toggled (rows flipped from their default expansion). navigate(selection, {nodeId}) is the only way to change what is shown; without a node id it looks the row up via TreeModel.nodeIdFor and expands its ancestors. toggle(node), setAllExpanded(bool). Resets to the overview when another source is opened (projectSourceKeyProvider), not on a re-read.
treeFilterProvider Notifier<String> The sidebar filter text.
navIndexProvider Notifier<int> Which rail page is shown.

Expansion state stores toggles away from the default rather than the expanded set, so a freshly loaded tree keeps sensible defaults (Script expanded, Characters expanded, scenes collapsed) without a reset step.


8.1 Model — tree/tree_node.dart, tree/selection.dart

Section titled “8.1 Model — tree/tree_node.dart, tree/selection.dart”

TreeNode: id (stable, path-like), label, detail (muted secondary text), tooltip, icon, selection (what clicking shows; null for pure groupings), children, initiallyExpanded, isOfficial (star), isPick (the viewer’s pick, a green check), hasProblem (warning icon, error-coloured).

Selection is sealed: ProjectOverviewSelection, CategorySelection(type), EntitySelection(type, tag, {epoch, stem}), BatchSelection(stem), OtherFilesSelection. Each exposes matchKeys, most specific first (entity:<stem>, entity:<type>:<tag>@<epoch>, entity:<type>:<tag>), which TreeModel indexes so a link from the detail pane can find and highlight the right row even when it names a version the tree does not show.

Top-level rows, in order:

  1. Project — name, tag, its pj versions as children with the official star; flagged when a choice is pending.
  2. Script — the selected project’s story roots (seasons / episodes / sequences / scenes, whichever the file has), each unit’s own child list beneath it, in the file’s order. A scene gets Shots and Cues wrapper rows; any unit but a season gets Cues. A wrapper lists the unit’s explicit shots / cues order or, absent that, every <unit>.<n> group in natural order (the tooltip says which). Empty wrappers are omitted; units and warning rows never are. Unresolved references are warning rows with the reason; a unit that lists one of its own ancestors is a circular story reference row and is not recursed into. A unit resolved by falling back to default says so in its tooltip.
  3. Characters, Locations, Props, Styles, Misc, Documents — each entity by tag, then its epochs in the project’s declared order (§8.2), then one version row per author showing that author’s latest version (author vN, with the version label or from <stem> as detail; official star; the viewer’s pick as a check). The tree does not list every iteration: beneath an author’s row sit only the older versions of theirs that still matter — the official one and the viewer’s pick. Dotted tags nest under their parent (outfits under a character as Outfits, areas under a location as Areas); one whose parent file is missing is listed at the top level with a warning. The same version rule applies to the project’s own versions and to models, platforms and plugins.
  4. Models, Platforms, Plugins — entity, then versions.
  5. Render batchesr<id> with author, kind and item count.
  6. Other files — present when the folder holds companion or unknown files.

Every visible string comes from the AppLocalizations the builder is given.

A flat, virtualised ListView over the visible rows. A row shows indent, chevron (toggles), icon, label plus muted detail as one rich text, then the star or warning icon. Tap selects when the row has a selection, otherwise toggles. There is deliberately no double-tap handler: it would delay every single tap by the double-tap timeout. With a filter, only rows that match or have a matching descendant are shown, fully expanded.


Dashboard: a 64-pixel rail (Project, Settings, About) beside an IndexedStack of pages. The browser page is a SidebarSplitView (sidebar 220–560 px, seeded from settings, saved on drag end) with TreePane left and DetailPane right. Material 3, ColorScheme.fromSeed, compact density, light and dark themes, and an AppColors theme extension for the two semantic colours the scheme lacks (the official star, a positive rating).

Header with project name and subtitle (folder path, or “bundled sample”), a chip showing the viewer handle when one is set, expand-all / collapse-all, and the project menu (⋮): New project… and Open project… (desktop only), Re-read files, then a Projects section listing the bundled sample and every folder opened before (name, path), the open one ticked. Choosing a remembered folder that no longer exists removes it from the list with a snackbar, and the project on screen stays. Below the header, the filter field and the tree.

New project dialog (screens/browser/new_project_dialog.dart): parent folder (desktop only; typed or browsed; pre-filled with the folder of the last opened project), the short name in a ShortNameField, an optional title, the username (a ShortNameField accepting owner[.workspace], pre-filled from Settings, mandatory: “Lowercase letters, digits and dashes. All your files will contain this username”), and the kind (a pull-down of §8.1). It says which project file it will write and that the creator is its director, refuses a non-empty folder with a message, saves the username to Settings when it differs from the handle there, creates and opens the project, and the caller takes the user to the new project file’s page, whose Edit tab is first. On the web the dialog says the project lives in the browser session only.

ShortNameField (widgets/short_name_field.dart) is the one input box for anything that must be a segment (§5.2): it filters as the user types (lower case, spaces and underscores to dashes, other characters dropped, twenty at most), explains the remaining problem under the box, and warns above ten characters without refusing. With allowWorkspace it accepts one dot and validates both sides, for an author handle (§5.6). Any place that asks for a tag, an epoch token or a handle reuses it.

Switches on the selection. When the project needs a choice, the Project chooser replaces everything: a card per pj candidate (name, stem, author, version, language, forked-from) that selects it.

  • Project overview — name, logline, stem chip (click opens the pj entity), official chip, source chip, a filmopen-project.json chip (or a No manifest chip) and a Media: … chip naming the media root (or Media in the project folder); synopsis; cards for Details (kind and genres in words), Contributors, Epochs (one chip per epoch in order — token, label, year — the first marked as default), Tracks; Contents of the folder as count chips that open the category (entities the library contributed are not counted here); a Shared library card with where it was indexed from and its own counts; Warnings rendered in the current language.
  • Category view — a list of every entity of one type with tag, epoch and version counts and an “official set” chip.
  • Entity detail — header (name; chips for the type — named for the project’s kind for story levels, §10.1 —, stem, Shared library when the file came from the library, forked-from as a link, updated, kind); an Epoch row when the entity has several, in the project’s order; and the Version row: an Author pull-down (the authors with a version at this epoch; choosing one shows their official version if they have it, else their latest), a Version pull-down of that author’s versions (v2, or v2 · goth when labelled; star or check as trailing icon), the official star (a toggle: grey, yellow when set; enabled for directors of a writable project; setting writes the pointer, clearing deletes it), the pick check (a toggle: grey, green when set; enabled for any viewer of a writable project), a label button (the author only; a small dialog writes label), and Fork. Tooltips say why a control is disabled (no username, read-only, directors only). Then tabs:
    • Edit (project files only, first when present) — the project form (tabs/project_edit_tab.dart): title, kind (pull-down; changing it moves an empty story-root list to the new kind’s key), year, rating, genre (widgets/genre_picker.dart: a pull-down of check boxes over the recommended list plus whatever the file names, shown as chips), logline, synopsis, and the epochs (tabs/epochs_editor.dart: a reorderable list with a drag handle, token chip with a default badge on the first, label and year boxes, a remove button disabled for an epoch that files use or the last one, and Add epoch with a small dialog — token, label, year). Every change re-encodes the draft and saves it 700 ms after the last keystroke, with a status at the top right (Saved, Unsaved changes, Saving…, the error); a pending change is written when the page is left. An official version shows a banner offering Fork. On somebody else’s file, or a read-only project, the tab says so and offers Fork.
    • Overview — every attribute of the chosen file except the header, as a nested key/value view; labels are translated with the raw key on hover; lists of scalars become chips, lists of objects become boxed sub-views. A separate File card shows path and header dates; a red card lists header/filename disagreements.
    • Script (scenes only) — the blocks as a screenplay page: block ids in the gutter, action, centred character cue with (O.S.) / (V.O.) / ^, parenthetical direction, transitions right-aligned, titles, notes boxed.
    • Compare — the versions at this epoch as columns, ordered as §13.3: the version being viewed, the viewer’s own, the viewer’s pick, official, then the rest in author/version order. Rows are dotted leaf paths (appearance.hair.color) so each attribute diffs on its own; scene blocks align by id; differing rows are tinted. Copy-left (§13.3) is not implemented.
    • Takes — every take discovered for any version of the entity, as cards (image preview for image deliverables, placeholder icon otherwise), with deliverable, take number, source version, batch chip (opens the batch), renderer, path; “picked” marks takes the chosen version’s picks name.
    • Commentary — all authors’ commentary consolidated: a Preferences card, then entries newest first with rating icon, author, date, on chip, block/field chips, text.
    • JSON — the file pretty-printed with two-space indentation; top right, a folder button that shows the file in the platform’s file browser (disabled with an explanation when the file is not on disk; absent on the web) and a copy button.
  • Batch detail — header chips (stem, author, kind, tier, status, created, item count, total cost) and a table of items; the source column links to the entity.
  • Other files — two lists: named by the grammar but not JSON; not named by the grammar.

Appearance (System / Light / Dark), Language (System / English / Español; language names in their own language), Identity (the author handle, with helper text saying whether a handle is in effect), Shared library (what it is; the folder in use with a show-in-file-browser button; Choose folder…; Use default when a folder was chosen), Logs (what is logged; the Verbose logs checkbox; the log file’s path and a View log button that shows it in the file browser; on the web, a line saying entries go to the console), About (version, specification version, URL, the preferences folder with a show button, Open-source notices → Flutter’s licence page, which includes the vendored API Dash licence).

  • The viewer handle is empty by default, so a first-run user sees the format’s rule that official wins. Setting a handle is visible in the tree header.
  • Refresh never blanks the screen.
  • Every empty state says what would fill it (no takes yet, only one version, no blocks).
  • Every write goes through ProjectWriter and is followed by a re-read; the UI never edits the index. Writes happen only under the viewer’s handle; the star is for directors; the library is never written.
  • On the web nothing reaches a disk: the sample and new projects are session copies, and the tree header says so.
  • Every transient message goes through widgets/messages.dart (showMessage), so they all look alike and can carry one action (for instance Settings when a username is missing).

Three layers, all in lib/l10n/:

  1. Messagesapp_en.arb (template) and app_es.arb, compiled by gen-l10n (l10n.yaml: nullable-getter: false) into app_localizations.dart and one file per language. Widgets use context.l10n; providers use localizationsProvider. Counts use ICU plurals (countScene(n) → “1 scene” / “3 scenes”).
  2. The format’s vocabulary — the JSON keys are normative and never translated. fieldLabel(l10n, key) in field_labels.dart maps 310 keys from Appendix A to messages (fieldName, fieldForkedFrom, …) and returns null for unknown keys, in which case the raw key is shown. Entity types (typeScene, typeScenePlural, countScene) and deliverables (deliverableClip) have their own messages, reached through the Labels extension in labels.dart (typeLabel, typePlural, typeCount, deliverableLabel).
  3. DiagnosticswarningText(ProjectWarning) and problemText(ResolutionProblem) in labels.dart turn model codes into sentences.

The generated files are committed so editors and the analyzer see them; every build regenerates them. To add a language: copy app_en.arb, translate, add the language name to settings_page.dart. To add a field label: add field<Key> to every ARB and one case to field_labels.dart. British spelling is used in English messages (colour, licence).


assets/sample/ holds The Cartographer from Appendix B of the Project Specification, extended so the tree has depth, laid out the way a real project is meant to be (§4.4, §14.5):

  • the-cartographer/ — the project: filmopen-project.json (tag cartographer, data../the-cartographer-data) and 40 JSON files, flat (folders carry no meaning in the format): two project versions (John’s, and Suda’s Thai remake forked from it) with an official pointer; the character main-hero at two epochs with three versions at 30yo and an official pointer to Maria’s fork; a second character; two outfits; two locations, one with an area; a prop, a style with an official pointer, a misc entry, a document; a season with two episodes, one using a sequence; two scenes, one of which has two versions and an official pointer; shots and cues including a J-cut and an episode-level music cue; two commentary files; two render batches; and one unknown text file so the Other files row appears.
  • the-cartographer-data/ — the media root: the nine placeholder takes (four small PNGs, empty .mp4 and .wav files named as takes).
  • library/ — the seed of the shared library: a model, a platform and a plugin manifest.

Being under assets/ and in Git is a development convenience (the build copies them to build/flutter_assets/assets/sample/…); a real project’s media root is a synced folder, and the real library is the folder the app installs beside its preferences.

The sample is what the app opens at start and what most tests read. In-memory fixtures (test/fixtures.dart) cover what the sample cannot: ambiguity, missing epochs, multi-author project folders, bad field types, cycles.


File Covers
test/stems_test.dart the filename grammar: every name kind, takes back to their source and batch, rejections, naming warnings, the reserved owner
test/project_test.dart loading the sample: counts, warnings, resolution rules, the tree’s shape
test/fixture_test.dart spec behaviours on tiny in-memory projects: ambiguous resolve, default-only fallback, project selection (ask, invalid pointer, sole author), tolerance of wrong types, leaf scenes and unresolved rows in the tree, cycle detection
test/directory_source_test.dart the path sandbox on a real temp folder, and skipped folders
test/localization_test.dart Spanish messages, field labels, diagnostics and tree text
test/widget_test.dart the app: overview and tree, every entity tab lays out without overflow, tree highlight when choosing a version, navigation from the detail pane reveals the row, the project chooser
test/manifest_test.dart the manifest and media root (§4.4): locators, takes found in the media root and beside the JSON, no manifest, unsupported type, missing folder, absolute path, malformed data, tag mismatch, a manifest missing filmopen or tag
test/library_test.dart the shared library (§14.5): entries marked by origin, separate counts, project stem shadows the library’s with a warning and none of the replaced file’s warnings survive, a fork beside library versions, a project official pointer overriding silently, an unreadable library folder as a warning rather than a failure
test/short_name_test.dart the segment rule in one place: validation, the ten-character advice, normalisation, and that the filename grammar warns by the same rule
test/app_log_test.dart JSON-line shape, thresholds, verbose and the environment pin, a failing sink, unencodable data, timed, the file sink and its rotation
test/settings_test.dart recent projects (tolerant parsing, order, no duplicates, cap), library path and verbose defaults
test/project_creator_test.dart creating a project on a real temp folder: manifest and first version (director, one epoch, root list for the kind), the kind’s root list, refusing a non-empty folder and a bad tag
test/project_writer_test.dart the writer on an in-memory project (§13, §17): save keeps unknown fields and stamps updated, refuses other authors, no viewer and read-only sources; fork numbering for own and others’ versions, labels not copied; official set (rewriting the pointer, dropping its note), new pointer, clear; pick set, file created, cleared; epoch order, re-encoding, default-epoch fallback, the no-epochs warning; kind vocabulary
test/editing_test.dart editing through the screen on an in-memory project: the project form saves after the delay and the page survives the re-read, somebody else’s file offers Fork, Fork makes and shows the next version, the tree shows one row per author with official and pick beneath and the label as detail, the star and the check write their files and the star is disabled for a non-director
test/project_menu_test.dart the ⋮ menu’s items, the New project dialog’s filtering, the mandatory username and the file it announces, the JSON tab’s folder button, the library chip

Widget tests load projects in setUpAll because file IO does not complete inside a widget test’s fake-async zone. Tree rows draw label and detail as one rich text; match them with find.text('label detail', findRichText: true).

flutter analyze and flutter test must both be clean before a commit. Tests that need dart:io import the _io file directly (log_file_io.dart), because the analyzer resolves a conditional export to its stub.


  • No user-facing text outside lib/l10n/. Models and services emit codes. Widgets take strings from context.l10n; providers from localizationsProvider.
  • Every write goes through ProjectWriter (or writeNewProject for a new project), through the source’s writeText / deleteFile, followed by refresh(). No widget writes a file, and nothing edits the index in memory.
  • Write only as the viewer. A write without a handle throws NoViewerException, another author’s file NotYourFileException; the UI turns these into messages and offers Fork or Settings.
  • No Flutter imports in lib/models/, lib/services/project_source.dart or lib/services/app_log.dart.
  • Platform code comes in threes: x.dart (conditional export), x_io.dart, x_stub.dart; the stub answers “not available” (canX = false, null, or UnsupportedError) and the UI hides the affordance. Never import dart:io from a file the web build reaches.
  • Log events, not sentences: AppLog.instance.info('project.opened', data: {...}); dotted identifiers, structured data, no user-facing text. Log every open, write, failure and setting change; log timings at debug.
  • One rule for short names: anything that must be a segment goes through ShortName and, in the UI, ShortNameField.
  • Never throw on a bad file. Add a WarningKind and report.
  • Never choose for the user where the Project Specification says report. Return an unresolved Resolution or a null selection.
  • One way to navigate: browserProvider.navigate(...).
  • Comments explain why, and cite the Project Specification section (§6.1) when a rule comes from it.
  • Filenames and identifiers follow the format’s vocabulary: stem, tag, epoch, author, official, take, batch, cue, block.
  • Tests for every spec rule on an in-memory fixture, not only on the sample.
  • Keep kAppVersion in lib/consts.dart equal to version: in pubspec.yaml.
  • Commit pubspec.lock (it is an application) and the generated localisation files.

  • Only the project file has a form. Other entity types are read here; they are forked, starred, picked and labelled, but their content is edited by hand or by a later milestone (a JSON editor, then per-type forms).
  • Character ages from epochs (§9.1 birthYear) are specified but not yet proposed by the app: the character form does not exist yet.
  • No copy-left in Compare, and no per-attribute compare of two epochs of one entity (§13.3) yet.
  • A save re-reads the whole folder. Fine for hundreds of files; a project of thousands would want an incremental index.
  • Concurrent edits are not detected. The last write wins; an outside change is picked up by the next re-read (or Re-read files) when the form is clean.
  • Locators other than local (Dropbox, Google Drive, OneDrive, iCloud, URL) are recognised and reported, not resolved.
  • Mobile “show in folder” (iOS shareddocuments://, Android content://) has not run: Android and iOS are not configured.
  • The shared library is not updated by the app: it is seeded once and then left alone.
  • Warnings are not grouped; the manifest warnings join the same list.
  • Resolution through official for official files (§6.1 second paragraph) is not implemented; every file resolves from the viewer’s point of view.
  • Fountain, SRT, OTIO, timeline export and every §15–16 concern are out of scope so far.
  • Media: images are shown; audio and video takes show an icon only.
  • Narrow layouts: the window is designed for a desktop width; below about 690 px the split view cannot honour its minimums. The web build is a preview, not a target.
  • Spanish is a first translation, not yet reviewed by a native speaker.
  • Windows path length limits are not checked before writing a new project folder.
  • Warnings are per load and are not persisted or grouped.

lib/widgets/split_view.dart is adapted from API Dash (lib/widgets/splitview_dashboard.dart, Apache License 2.0, revision 8044b218…). The licence is vendored at third_party/apidash/LICENSE, registered with Flutter’s LicenseRegistry in main.dart, and listed in THIRD_PARTY_NOTICES.md. The rail-beside-sidebar arrangement follows API Dash’s dashboard; the theme, model, tree, views and state are FilmOpen code. FilmOpen’s own licence is not yet decided.


filmopen-c/
README.md how to run, layout, languages
THIRD_PARTY_NOTICES.md
l10n.yaml gen-l10n configuration
pubspec.yaml, pubspec.lock
analysis_options.yaml flutter_lints
assets/sample/the-cartographer/ the sample project (manifest + 40 JSON files)
assets/sample/the-cartographer-data/ its media root (9 placeholder takes)
assets/sample/library/ the seed of the shared library (model, platform, plugin)
third_party/apidash/LICENSE
docs/
FilmOpen-Project-Specification v1.0.md the format (v1.3 draft inside)
FilmOpen-Software-Specification v1.0.md this document
FilmOpen-Milestone 1.md milestone 1: the reader
FilmOpen-Milestone 2.md milestone 2: manifest, media root, library, log, projects
FilmOpen-Development-Plan-*.md earlier planning notes
Pre-commit-improvements.md the review that compared filmopen-c with filmopen-a
lib/
main.dart log start-up, licence registration, preferences, ProviderScope
app.dart MaterialApp: themes, locale, delegates
theme.dart buildTheme, AppColors, monospace stack
consts.dart app name/version, sample asset path, spec URL
models/ author, entity_type, stems, entity, project, diagnostics, manifest, short_name, project_kind, epochs
services/ project_source (+memory), asset_source, asset_disk(_io/_stub), directory_source(_io/_stub),
project_loader, project_template, project_writer, app_log, log_file(_io/_stub),
app_paths(_io/_stub), reveal(_io/_stub), project_creator(_io/_stub), library_install(_io/_stub)
providers/ project (+library), settings, localization, browser, app_paths
tree/ tree_node (TreeNode, TreeModel), selection, tree_builder
screens/ dashboard, settings_page, browser/{browser_page, tree_pane, new_project_dialog, detail_pane,
detail/{project_overview, project_chooser, category_view, entity_detail,
batch_detail, other_files_view, tabs/{project_edit, epochs_editor, overview, script,
compare, takes, commentary, json}}}
widgets/ tree_view, split_view, attribute_view, screenplay_view, info_chip, genre_picker,
section_card, empty_state, entity_icons, media_image, short_name_field, messages
l10n/ app_en.arb, app_es.arb, generated app_localizations*.dart, labels.dart, field_labels.dart
utils/ natural_compare
test/ stems, project, fixtures (+fixture_test), directory_source, localization, widget,
manifest, library, short_name, app_log, settings, project_creator, project_menu,
project_writer, editing
windows/, web/ platform runners (window title "FilmOpen")

Size at the end of milestone 1: about 5,500 lines of Dart outside generated code, 600 lines of tests, 549 localisation messages per language. At the end of milestone 2: about 7,300 lines, 1,100 lines of tests, 608 messages per language. At the end of milestone 3: about 8,900 lines, 1,600 lines of tests, 693 messages per language.

Name Meaning
stem a filename without extension, e.g. sc_5_john123_v1
versions key the stem minus author and version, e.g. ch_main-hero_30yo; what an official pointer names
group key type and tag only, e.g. ch_main-hero; every epoch and version of one entity
EntityGroup one entity: all epochs
EntityVersions one entity at one epoch: all authors’ versions
viewer the author handle the user browses as; empty means none
resolution the outcome of looking up a reference: an entity, or a ResolutionProblem
selection what the detail pane shows; the tree indexes rows by selection keys
wrapper row the Shots / Cues grouping under a story unit
companion file a file the grammar names that is not JSON (plugin code)
other file a file the grammar does not name
manifest filmopen-project.json: the fixed-name file that identifies a project folder and names its media root
media root the folder takes and referenced media are read from: the manifest’s data folder, else the project folder
locator the manifest’s typed “where” ({type, path}) for the media root
library, shared library the models, platforms and plugins installed beside projects, indexed with every project
origin whether an entity file came from the project folder or the library
short name a segment (§5.2) as a person types it: a folder name, a tag, an epoch
pick the version the viewer prefers: prefer in their own commentary file; a green check in the UI
version label the header’s label: a short word shown beside the version number in lists
session copy a project held in memory for one run of the web preview; nothing reaches a disk
default epoch the first epoch the project declares; where implicit references fall back to
reveal showing a file in the platform’s file browser with the file selected