App-thema’s, meerschermen, annotaties en grafiekslides #1
36 changed files with 1857 additions and 125 deletions
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Report a problem so we can fix it
|
||||||
|
title: "[Bug] "
|
||||||
|
labels: bug
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what went wrong.
|
||||||
|
|
||||||
|
**To reproduce**
|
||||||
|
Steps to reproduce the behaviour:
|
||||||
|
1. Go to '…'
|
||||||
|
2. Click on '…'
|
||||||
|
3. See error
|
||||||
|
|
||||||
|
**Expected behaviour**
|
||||||
|
What you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots / sample deck**
|
||||||
|
If applicable, add screenshots or attach a minimal `.md` / `.ocideck` that
|
||||||
|
triggers the issue.
|
||||||
|
|
||||||
|
**Environment**
|
||||||
|
- OciDeck version:
|
||||||
|
- OS and version:
|
||||||
|
- Flutter version (`flutter --version`):
|
||||||
|
- Single or dual screen (if presenter-related):
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Anything else that might help.
|
||||||
|
|
||||||
|
> For **security vulnerabilities**, do not open a public issue — see
|
||||||
|
> [SECURITY.md](../../SECURITY.md).
|
||||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Security vulnerability
|
||||||
|
url: https://github.com/security/advisories
|
||||||
|
about: Please report security issues privately — see SECURITY.md, do not open a public issue.
|
||||||
23
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
23
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea or improvement
|
||||||
|
title: "[Feature] "
|
||||||
|
labels: enhancement
|
||||||
|
---
|
||||||
|
|
||||||
|
**Problem / motivation**
|
||||||
|
What are you trying to do, and what's missing or awkward today?
|
||||||
|
|
||||||
|
**Proposed solution**
|
||||||
|
A clear description of what you'd like to happen.
|
||||||
|
|
||||||
|
**Marp / file-format impact**
|
||||||
|
Does this affect how decks are stored? OciDeck keeps the Marp Markdown the single
|
||||||
|
source of truth and puts non-Marp data in sidecars — describe how your idea fits
|
||||||
|
that model (see [docs/FILE_FORMAT.md](../../docs/FILE_FORMAT.md)).
|
||||||
|
|
||||||
|
**Alternatives considered**
|
||||||
|
Any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Mockups, examples, or links.
|
||||||
24
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
24
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
<!-- What does this change do, and why? Link any related issue (e.g. "Closes #123"). -->
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
<!-- Bullet the notable changes. -->
|
||||||
|
|
||||||
|
-
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] `make check` passes (format-check, analyze, full test suite).
|
||||||
|
- [ ] Added/updated tests for the behaviour I changed.
|
||||||
|
- [ ] New UI strings go through `context.l10n.d('…')` **and** are translated in
|
||||||
|
every supported language (nl/en/it/de/fr/es/fy/pap).
|
||||||
|
- [ ] If I changed how anything is stored, I updated
|
||||||
|
[`docs/FILE_FORMAT.md`](../docs/FILE_FORMAT.md).
|
||||||
|
- [ ] Docs updated where relevant (README / docs/).
|
||||||
|
|
||||||
|
## Notes for reviewers
|
||||||
|
|
||||||
|
<!-- Anything that needs extra attention, screenshots, or manual test steps
|
||||||
|
(e.g. dual-screen presenting or drawing, which need real hardware). -->
|
||||||
36
.github/workflows/ci.yml
vendored
Normal file
36
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["**"]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
name: Format · Analyze · Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: stable
|
||||||
|
flutter-version: 3.44.1
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Flutter version
|
||||||
|
run: flutter --version
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: flutter pub get
|
||||||
|
|
||||||
|
# The same quality gate developers run locally:
|
||||||
|
# format-check + flutter analyze + the full test suite.
|
||||||
|
- name: Quality gate (make check)
|
||||||
|
run: make check
|
||||||
|
|
||||||
|
# Fail the build if any dependency is not open source.
|
||||||
|
- name: Licence compliance (make licenses)
|
||||||
|
run: make licenses
|
||||||
18
AUTHORS.md
Normal file
18
AUTHORS.md
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Authors
|
||||||
|
|
||||||
|
OciDeck is created and maintained by:
|
||||||
|
|
||||||
|
- **Brenno de Winter**
|
||||||
|
|
||||||
|
The name is a wink: *Oci* comes from the **Ocicats** (Brenno's cats) and *Deck*
|
||||||
|
is short for a presentation deck.
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
Thanks to everyone who has contributed code, translations, documentation, bug
|
||||||
|
reports, and ideas. (Add yourself here in your first pull request.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
OciDeck also stands on the shoulders of open-source software; see
|
||||||
|
[`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md) for the components it builds on.
|
||||||
49
CHANGELOG.md
Normal file
49
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to OciDeck are documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
|
and the project aims to follow [Semantic Versioning](https://semver.org/).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Source-code slides** — a dark "code sheet" with per-language syntax
|
||||||
|
highlighting, stored as a fenced code block.
|
||||||
|
- **Charts** — bar, line, and pie chart slides. Data is entered in an in-app grid
|
||||||
|
or imported from CSV; the spec is stored as JSON in a ```chart block. Data can
|
||||||
|
stay inline or be linked to a CSV in a separate `data/` directory. Rendered
|
||||||
|
natively in-app (preview, presenter, PDF, PPTX) and as self-contained SVG in
|
||||||
|
the HTML export.
|
||||||
|
- **Per-slide TLP classification** — each slide can carry its own Traffic Light
|
||||||
|
Protocol level; slides classified stricter than the level the deck is shown at
|
||||||
|
are withheld when presenting and exporting.
|
||||||
|
- **Dual-screen presenter** — on a second display the beamer shows the slide
|
||||||
|
while the laptop shows the presenter view (current/next slide, notes, timer),
|
||||||
|
kept in sync over method channels.
|
||||||
|
- **Annotation layer** — draw on slides while presenting (pen, highlighter,
|
||||||
|
eraser, laser pointer). Kept fully separate from the Marp Markdown, mirrored
|
||||||
|
live to the beamer, and persisted in a `<name>.ink.json` sidecar.
|
||||||
|
- **App theming** — customizable app appearance profiles, including a dark
|
||||||
|
interface.
|
||||||
|
- Project documentation: contributing guide, security policy, architecture and
|
||||||
|
build notes, user guide, keyboard-shortcut reference, third-party notices, and
|
||||||
|
the EUPL-1.2 licence text.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Slide transitions in the presenter no longer flash a black frame (neighbour
|
||||||
|
images are precached and `gaplessPlayback` is enabled) — important for
|
||||||
|
recording.
|
||||||
|
|
||||||
|
## [1.0.0]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial release: structured, slide-by-slide editor for Marp presentations with
|
||||||
|
typed slide templates, live preview, fullscreen presenter, deck-wide TLP
|
||||||
|
marking, media handling, import, and export to Marp Markdown, PDF, PPTX, and
|
||||||
|
self-contained HTML. Decks save as a self-contained project/package with copied
|
||||||
|
assets. Localized in Dutch, English, Italian, German, French, Spanish, Frisian,
|
||||||
|
and Papiamento.
|
||||||
|
|
||||||
|
[Unreleased]: https://example.com/ocideck/compare/v1.0.0...HEAD
|
||||||
|
[1.0.0]: https://example.com/ocideck/releases/tag/v1.0.0
|
||||||
65
CODE_OF_CONDUCT.md
Normal file
65
CODE_OF_CONDUCT.md
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity and
|
||||||
|
orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment:
|
||||||
|
|
||||||
|
- Demonstrating empathy and kindness toward other people.
|
||||||
|
- Being respectful of differing opinions, viewpoints, and experiences.
|
||||||
|
- Giving and gracefully accepting constructive feedback.
|
||||||
|
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience.
|
||||||
|
- Focusing on what is best not just for us as individuals, but for the overall
|
||||||
|
community.
|
||||||
|
|
||||||
|
Examples of unacceptable behavior:
|
||||||
|
|
||||||
|
- The use of sexualized language or imagery, and sexual attention or advances of
|
||||||
|
any kind.
|
||||||
|
- Trolling, insulting or derogatory comments, and personal or political attacks.
|
||||||
|
- Public or private harassment.
|
||||||
|
- Publishing others' private information, such as a physical or email address,
|
||||||
|
without their explicit permission.
|
||||||
|
- Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting.
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement via the
|
||||||
|
repository's private contact channels (for example, a GitHub security advisory or
|
||||||
|
direct message to the maintainer). All complaints will be reviewed and
|
||||||
|
investigated promptly and fairly. Community leaders are obligated to respect the
|
||||||
|
privacy and security of the reporter of any incident.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.1, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
84
CONTRIBUTING.md
Normal file
84
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
# Contributing to OciDeck
|
||||||
|
|
||||||
|
Thanks for your interest in improving OciDeck! This document explains how to set
|
||||||
|
up the project, the quality bar, and how to propose changes.
|
||||||
|
|
||||||
|
By contributing you agree that your contributions are licensed under the project
|
||||||
|
licence, the **European Union Public Licence v. 1.2 (EUPL-1.2)** — see
|
||||||
|
[`LICENSE.md`](LICENSE.md).
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **Flutter** 3.44+ (stable channel) with **Dart 3.12+**.
|
||||||
|
- A desktop target enabled: **macOS**, **Windows**, or **Linux**.
|
||||||
|
- `make` (the `Makefile` is the entry point for all quality checks).
|
||||||
|
|
||||||
|
See [`docs/BUILD.md`](docs/BUILD.md) for platform-specific build notes (including
|
||||||
|
the macOS CocoaPods locale caveat and the vendored plugin forks).
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make setup # flutter pub get
|
||||||
|
flutter run -d macos # or -d windows / -d linux
|
||||||
|
```
|
||||||
|
|
||||||
|
## The quality gate
|
||||||
|
|
||||||
|
Run this before every push — it is exactly what CI runs:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make check # format-check + analyze + full test suite
|
||||||
|
```
|
||||||
|
|
||||||
|
Individual steps:
|
||||||
|
|
||||||
|
| Command | What it does |
|
||||||
|
| --- | --- |
|
||||||
|
| `make format` | Rewrites Dart files with `dart format`. |
|
||||||
|
| `make format-check` | Fails if any file needs formatting. |
|
||||||
|
| `make analyze` | `flutter analyze` (analyzer + lints + type checks). |
|
||||||
|
| `make test` | The full test suite. |
|
||||||
|
| `make licenses` | Verify every dependency uses an open-source licence. |
|
||||||
|
| `make check-full` | `check` plus licence compliance and a dependency-freshness report. |
|
||||||
|
|
||||||
|
Targeted test groups for focused work:
|
||||||
|
|
||||||
|
| Target | Covers |
|
||||||
|
| --- | --- |
|
||||||
|
| `make test-contracts` | Markdown generation/parsing, save-load round-trips, migration |
|
||||||
|
| `make test-preview` | Slide rendering, footers, TLP, inline Markdown, charts |
|
||||||
|
| `make test-export` | PDF/PPTX export and project file-save behaviour |
|
||||||
|
| `make test-state` | Providers, undo/redo, search/replace, settings, recovery |
|
||||||
|
| `make test-services` | Image, caption, description sidecar services |
|
||||||
|
| `make test-presenter` | Fullscreen presenter navigation and shortcuts |
|
||||||
|
|
||||||
|
## Coding guidelines
|
||||||
|
|
||||||
|
- **Formatting & analysis must pass clean** (`make check`). No analyzer warnings.
|
||||||
|
- **Architecture**: skim [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) before
|
||||||
|
larger changes. Keep the Marp Markdown the single source of truth; anything
|
||||||
|
that isn't plain Marp belongs in a sidecar (see the file format).
|
||||||
|
- **Localization is enforced.** UI strings go through `context.l10n.d('Nederlandse
|
||||||
|
brontekst')`. The test `test/app_localizations_test.dart` fails if a literal
|
||||||
|
`.d('…')` string lacks a translation in **every** supported language
|
||||||
|
(Dutch is the source; en/it/de/fr/es/fy/pap need an entry). Add your strings to
|
||||||
|
`lib/l10n/app_localizations.dart` for all languages, or the suite goes red.
|
||||||
|
- **Tests**: add or update tests for behaviour you change — especially the
|
||||||
|
Markdown round-trip and any file-format change.
|
||||||
|
- **File format**: if you change how anything is stored, update
|
||||||
|
[`docs/FILE_FORMAT.md`](docs/FILE_FORMAT.md) in the same change.
|
||||||
|
|
||||||
|
## Proposing changes
|
||||||
|
|
||||||
|
1. Branch from the default branch; keep each branch/PR focused on one topic.
|
||||||
|
2. Write clear commit messages (imperative subject, a short body explaining the
|
||||||
|
*why*).
|
||||||
|
3. Make sure `make check` is green.
|
||||||
|
4. Open a pull request describing the change and linking any related issue. Fill
|
||||||
|
in the PR template checklist.
|
||||||
|
|
||||||
|
## Reporting bugs and requesting features
|
||||||
|
|
||||||
|
Use the GitHub issue templates. For **security issues, do not open a public
|
||||||
|
issue** — follow [`SECURITY.md`](SECURITY.md).
|
||||||
290
LICENSE.md
Normal file
290
LICENSE.md
Normal file
|
|
@ -0,0 +1,290 @@
|
||||||
|
<!--
|
||||||
|
SPDX-License-Identifier: EUPL-1.2
|
||||||
|
Copyright © Brenno de Winter
|
||||||
|
|
||||||
|
OciDeck is licensed under the European Union Public Licence (EUPL) v. 1.2.
|
||||||
|
The full, authoritative licence text follows. The EUPL is published by the
|
||||||
|
European Union in 23 official languages, each equally authentic; the English
|
||||||
|
version is reproduced below. The other language versions are available at
|
||||||
|
https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
|
||||||
|
-->
|
||||||
|
|
||||||
|
# European Union Public Licence v. 1.2
|
||||||
|
|
||||||
|
EUPL © the European Union 2007, 2016
|
||||||
|
|
||||||
|
This European Union Public Licence (the 'EUPL') applies to the Work (as defined
|
||||||
|
below) which is provided under the terms of this Licence. Any use of the Work,
|
||||||
|
other than as authorised under this Licence is prohibited (to the extent such
|
||||||
|
use is covered by a right of the copyright holder of the Work).
|
||||||
|
|
||||||
|
The Work is provided under the terms of this Licence when the Licensor (as
|
||||||
|
defined below) has placed the following notice immediately following the
|
||||||
|
copyright notice for the Work:
|
||||||
|
|
||||||
|
> Licensed under the EUPL
|
||||||
|
|
||||||
|
or has expressed by any other means his willingness to license under the EUPL.
|
||||||
|
|
||||||
|
## 1. Definitions
|
||||||
|
|
||||||
|
In this Licence, the following terms have the following meaning:
|
||||||
|
|
||||||
|
- **The Licence**: this Licence.
|
||||||
|
- **The Original Work**: the work or software distributed or communicated by the
|
||||||
|
Licensor under this Licence, available as Source Code and also as Executable
|
||||||
|
Code as the case may be.
|
||||||
|
- **Derivative Works**: the works or software that could be created by the
|
||||||
|
Licensee, based upon the Original Work or modifications thereof. This Licence
|
||||||
|
does not define the extent of modification or dependence on the Original Work
|
||||||
|
required in order to classify a work as a Derivative Work; this extent is
|
||||||
|
determined by copyright law applicable in the country mentioned in Article 15.
|
||||||
|
- **The Work**: the Original Work or its Derivative Works.
|
||||||
|
- **The Source Code**: the human-readable form of the Work which is the most
|
||||||
|
convenient for people to study and modify.
|
||||||
|
- **The Executable Code**: any code which has generally been compiled and which
|
||||||
|
is meant to be interpreted by a computer as a program.
|
||||||
|
- **The Licensor**: the natural or legal person that distributes or communicates
|
||||||
|
the Work under the Licence.
|
||||||
|
- **Contributor(s)**: any natural or legal person who modifies the Work under
|
||||||
|
the Licence, or otherwise contributes to the creation of a Derivative Work.
|
||||||
|
- **The Licensee** or **'You'**: any natural or legal person who makes any usage
|
||||||
|
of the Work under the terms of the Licence.
|
||||||
|
- **Distribution** or **Communication**: any act of selling, giving, lending,
|
||||||
|
renting, distributing, communicating, transmitting, or otherwise making
|
||||||
|
available, online or offline, copies of the Work or providing access to its
|
||||||
|
essential functionalities at the disposal of any other natural or legal
|
||||||
|
person.
|
||||||
|
|
||||||
|
## 2. Scope of the rights granted by the Licence
|
||||||
|
|
||||||
|
The Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
|
||||||
|
sublicensable licence to do the following, for the duration of copyright vested
|
||||||
|
in the Original Work:
|
||||||
|
|
||||||
|
- use the Work in any circumstance and for all usage,
|
||||||
|
- reproduce the Work,
|
||||||
|
- modify the Work, and make Derivative Works based upon the Work,
|
||||||
|
- communicate to the public, including the right to make available or display
|
||||||
|
the Work or copies thereof to the public and perform publicly, as the case may
|
||||||
|
be, the Work,
|
||||||
|
- distribute the Work or copies thereof,
|
||||||
|
- lend and rent the Work or copies thereof,
|
||||||
|
- sublicense rights in the Work or copies thereof.
|
||||||
|
|
||||||
|
Those rights can be exercised on any media, supports and formats, whether now
|
||||||
|
known or later invented, as far as the applicable law permits so.
|
||||||
|
|
||||||
|
In the countries where moral rights apply, the Licensor waives his right to
|
||||||
|
exercise his moral right to the extent allowed by law in order to make effective
|
||||||
|
the licence of the economic rights here above listed.
|
||||||
|
|
||||||
|
The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to
|
||||||
|
any patents held by the Licensor, to the extent necessary to make use of the
|
||||||
|
rights granted on the Work under this Licence.
|
||||||
|
|
||||||
|
## 3. Communication of the Source Code
|
||||||
|
|
||||||
|
The Licensor may provide the Work either in its Source Code form, or as
|
||||||
|
Executable Code. If the Work is provided as Executable Code, the Licensor
|
||||||
|
provides in addition a machine-readable copy of the Source Code of the Work along
|
||||||
|
with each copy of the Work that the Licensor distributes or indicates, in a
|
||||||
|
notice following the copyright notice attached to the Work, a repository where
|
||||||
|
the Source Code is easily and freely accessible for as long as the Licensor
|
||||||
|
continues to distribute or communicate the Work.
|
||||||
|
|
||||||
|
## 4. Limitations on copyright
|
||||||
|
|
||||||
|
Nothing in this Licence is intended to deprive the Licensee of the benefits from
|
||||||
|
any exception or limitation to the exclusive rights of the rights owners in the
|
||||||
|
Work, of the exhaustion of those rights or of other applicable limitations
|
||||||
|
thereto.
|
||||||
|
|
||||||
|
## 5. Obligations of the Licensee
|
||||||
|
|
||||||
|
The grant of the rights mentioned above is subject to some restrictions and
|
||||||
|
obligations imposed on the Licensee. Those obligations are the following:
|
||||||
|
|
||||||
|
**Attribution right**: The Licensee shall keep intact all copyright, patent or
|
||||||
|
trademarks notices and all notices that refer to the Licence and to the
|
||||||
|
disclaimer of warranties. The Licensee must include a copy of such notices and a
|
||||||
|
copy of the Licence with every copy of the Work he/she distributes or
|
||||||
|
communicates. The Licensee must cause any Derivative Work to carry prominent
|
||||||
|
notices stating that the Work has been modified and the date of modification.
|
||||||
|
|
||||||
|
**Copyleft clause**: If the Licensee distributes or communicates copies of the
|
||||||
|
Original Works or Derivative Works, this Distribution or Communication will be
|
||||||
|
done under the terms of this Licence or of a later version of this Licence unless
|
||||||
|
the Original Work is expressly distributed only under this version of the Licence
|
||||||
|
— for example by communicating 'EUPL v. 1.2 only'. The Licensee (becoming
|
||||||
|
Licensor) cannot offer or impose any additional terms or conditions on the Work
|
||||||
|
or Derivative Work that alter or restrict the terms of the Licence.
|
||||||
|
|
||||||
|
**Compatibility clause**: If the Licensee Distributes or Communicates Derivative
|
||||||
|
Works or copies thereof based upon both the Work and another work licensed under
|
||||||
|
a Compatible Licence, this Distribution or Communication can be done under the
|
||||||
|
terms of this Compatible Licence. For the sake of this clause, 'Compatible
|
||||||
|
Licence' refers to the licences listed in the Appendix attached to this Licence.
|
||||||
|
Should the Licensee's obligations under the Compatible Licence conflict with
|
||||||
|
his/her obligations under this Licence, the obligations of the Compatible Licence
|
||||||
|
shall prevail.
|
||||||
|
|
||||||
|
**Provision of Source Code**: When distributing or communicating copies of the
|
||||||
|
Work, the Licensee will provide a machine-readable copy of the Source Code or
|
||||||
|
indicate a repository where this Source will be easily and freely available for
|
||||||
|
as long as the Licensee continues to distribute or communicate the Work.
|
||||||
|
|
||||||
|
**Legal Protection**: This Licence does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or names of the Licensor, except as required
|
||||||
|
for reasonable and customary use in describing the origin of the Work and
|
||||||
|
reproducing the content of the copyright notice.
|
||||||
|
|
||||||
|
## 6. Chain of Authorship
|
||||||
|
|
||||||
|
The original Licensor warrants that the copyright in the Original Work granted
|
||||||
|
hereunder is owned by him/her or licensed to him/her and that he/she has the
|
||||||
|
power and authority to grant the Licence.
|
||||||
|
|
||||||
|
Each Contributor warrants that the copyright in the modifications he/she brings
|
||||||
|
to the Work are owned by him/her or licensed to him/her and that he/she has the
|
||||||
|
power and authority to grant the Licence.
|
||||||
|
|
||||||
|
Each time You accept the Licence, the original Licensor and subsequent
|
||||||
|
Contributors grant You a licence to their contributions to the Work, under the
|
||||||
|
terms of this Licence.
|
||||||
|
|
||||||
|
## 7. Disclaimer of Warranty
|
||||||
|
|
||||||
|
The Work is a work in progress, which is continuously improved by numerous
|
||||||
|
Contributors. It is not a finished work and may therefore contain defects or
|
||||||
|
'bugs' inherent to this type of development.
|
||||||
|
|
||||||
|
For the above reason, the Work is provided under the Licence on an 'as is' basis
|
||||||
|
and without warranties of any kind concerning the Work, including without
|
||||||
|
limitation merchantability, fitness for a particular purpose, absence of defects
|
||||||
|
or errors, accuracy, non-infringement of intellectual property rights other than
|
||||||
|
copyright as stated in Article 6 of this Licence.
|
||||||
|
|
||||||
|
This disclaimer of warranty is an essential part of the Licence and a condition
|
||||||
|
for the grant of any rights to the Work.
|
||||||
|
|
||||||
|
## 8. Disclaimer of Liability
|
||||||
|
|
||||||
|
Except in the cases of wilful misconduct or damages directly caused to natural
|
||||||
|
persons, the Licensor will in no event be liable for any direct or indirect,
|
||||||
|
material or moral, damages of any kind, arising out of the Licence or of the use
|
||||||
|
of the Work, including without limitation, damages for loss of goodwill, work
|
||||||
|
stoppage, computer failure or malfunction, loss of data or any commercial
|
||||||
|
damage, even if the Licensor has been advised of the possibility of such damage.
|
||||||
|
However, the Licensor will be liable under statutory product liability laws as
|
||||||
|
far such laws apply to the Work.
|
||||||
|
|
||||||
|
## 9. Additional agreements
|
||||||
|
|
||||||
|
While distributing the Work, You may choose to conclude an additional agreement,
|
||||||
|
defining obligations or services consistent with this Licence. However, if
|
||||||
|
accepting obligations, You may act only on your own behalf and on your sole
|
||||||
|
responsibility, not on behalf of the original Licensor or any other Contributor,
|
||||||
|
and only if You agree to indemnify, defend, and hold each Contributor harmless
|
||||||
|
for any liability incurred by, or claims asserted against such Contributor by the
|
||||||
|
fact You have accepted any warranty or additional liability.
|
||||||
|
|
||||||
|
## 10. Acceptance of the Licence
|
||||||
|
|
||||||
|
The provisions of this Licence can be accepted by clicking on an icon 'I agree'
|
||||||
|
placed under the bottom of a window displaying the text of this Licence or by
|
||||||
|
affirming consent in any other similar way, in accordance with the rules of
|
||||||
|
applicable law. Clicking on that icon indicates your clear and irrevocable
|
||||||
|
acceptance of this Licence and all of its terms and conditions.
|
||||||
|
|
||||||
|
Similarly, you irrevocably accept this Licence and all of its terms and
|
||||||
|
conditions by exercising any rights granted to You by Article 2 of this Licence,
|
||||||
|
such as the use of the Work, the creation by You of a Derivative Work or the
|
||||||
|
Distribution or Communication by You of the Work or copies thereof.
|
||||||
|
|
||||||
|
## 11. Information to the public
|
||||||
|
|
||||||
|
In case of any Distribution or Communication of the Work by means of electronic
|
||||||
|
communication by You (for example, by offering to download the Work from a remote
|
||||||
|
location) the distribution channel or media (for example, a website) must at
|
||||||
|
least provide to the public the information requested by the applicable law
|
||||||
|
regarding the Licensor, the Licence and the way it may be accessible, concluded,
|
||||||
|
stored and reproduced by the Licensee.
|
||||||
|
|
||||||
|
## 12. Termination of the Licence
|
||||||
|
|
||||||
|
The Licence and the rights granted hereunder will terminate automatically upon
|
||||||
|
any breach by the Licensee of the terms of the Licence.
|
||||||
|
|
||||||
|
Such a termination will not terminate the licences of any person who has received
|
||||||
|
the Work from the Licensee under the Licence, provided such persons remain in
|
||||||
|
full compliance with the Licence.
|
||||||
|
|
||||||
|
## 13. Miscellaneous
|
||||||
|
|
||||||
|
Without prejudice to Article 9 above, the Licence represents the complete
|
||||||
|
agreement between the Parties as to the Work.
|
||||||
|
|
||||||
|
If any provision of the Licence is invalid or unenforceable under applicable law,
|
||||||
|
this will not affect the validity or enforceability of the Licence as a whole.
|
||||||
|
Such provision will be construed or reformed so as necessary to make it valid and
|
||||||
|
enforceable.
|
||||||
|
|
||||||
|
The European Commission may put into force translations or new versions of the
|
||||||
|
Licence, so far this is required and reasonable, without reducing the scope of
|
||||||
|
the rights granted by the Licence. New versions of the Licence will be published
|
||||||
|
with a unique version number.
|
||||||
|
|
||||||
|
All linguistic versions of this Licence, approved by the European Commission,
|
||||||
|
have identical value. Parties can take advantage of the linguistic version of
|
||||||
|
their choice.
|
||||||
|
|
||||||
|
## 14. Jurisdiction
|
||||||
|
|
||||||
|
Without prejudice to specific agreement between parties,
|
||||||
|
|
||||||
|
- any litigation resulting from the interpretation of this License, arising
|
||||||
|
between the European Union institutions, bodies, offices or agencies, as a
|
||||||
|
Licensor, and any Licensee, will be subject to the jurisdiction of the Court of
|
||||||
|
Justice of the European Union, as laid down in article 272 of the Treaty on the
|
||||||
|
Functioning of the European Union,
|
||||||
|
- any litigation arising between other parties and resulting from the
|
||||||
|
interpretation of this License, will be subject to the exclusive jurisdiction
|
||||||
|
of the competent court where the Licensor resides or conducts its primary
|
||||||
|
business.
|
||||||
|
|
||||||
|
## 15. Applicable Law
|
||||||
|
|
||||||
|
Without prejudice to specific agreement between parties,
|
||||||
|
|
||||||
|
- this Licence shall be governed by the law of the European Union Member State
|
||||||
|
where the Licensor has his seat, resides or has his registered office,
|
||||||
|
- this licence shall be governed by Belgian law if the Licensor has no seat,
|
||||||
|
residence or registered office inside a European Union Member State.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix
|
||||||
|
|
||||||
|
**'Compatible Licences' according to Article 5 EUPL are:**
|
||||||
|
|
||||||
|
- GNU General Public License (GPL) v. 2, v. 3
|
||||||
|
- GNU Affero General Public License (AGPL) v. 3
|
||||||
|
- Open Software License (OSL) v. 2.1, v. 3.0
|
||||||
|
- Eclipse Public License (EPL) v. 1.0
|
||||||
|
- CeCILL v. 2.0, v. 2.1
|
||||||
|
- Mozilla Public Licence (MPL) v. 2
|
||||||
|
- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
|
||||||
|
- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for
|
||||||
|
works other than software
|
||||||
|
- European Union Public Licence (EUPL) v. 1.1, v. 1.2
|
||||||
|
- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong
|
||||||
|
Reciprocity (LiLiQ-R+).
|
||||||
|
|
||||||
|
The European Commission may update this Appendix to later versions of the above
|
||||||
|
licences without producing a new version of the EUPL, as long as they provide the
|
||||||
|
rights granted in Article 2 of this Licence and protect the covered Source Code
|
||||||
|
from exclusive appropriation.
|
||||||
|
|
||||||
|
All other changes or additions to this Appendix require the production of a new
|
||||||
|
EUPL version.
|
||||||
15
Makefile
15
Makefile
|
|
@ -1,4 +1,4 @@
|
||||||
.PHONY: setup format format-check analyze test test-contracts test-preview test-export test-state test-services test-presenter deps-outdated check check-full help
|
.PHONY: setup format format-check analyze test test-contracts test-preview test-export test-state test-services test-presenter deps-outdated licenses check check-full help
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@echo "OciDeck quality targets:"
|
@echo "OciDeck quality targets:"
|
||||||
|
|
@ -11,6 +11,7 @@ help:
|
||||||
@echo " make test-services Caption/description/image service tests."
|
@echo " make test-services Caption/description/image service tests."
|
||||||
@echo " make test-presenter Fullscreen presenter interaction tests."
|
@echo " make test-presenter Fullscreen presenter interaction tests."
|
||||||
@echo " make deps-outdated Advisory dependency freshness report."
|
@echo " make deps-outdated Advisory dependency freshness report."
|
||||||
|
@echo " make licenses Verify all dependencies use open-source licences."
|
||||||
|
|
||||||
# Install Flutter/Dart dependencies.
|
# Install Flutter/Dart dependencies.
|
||||||
setup:
|
setup:
|
||||||
|
|
@ -105,12 +106,20 @@ deps-outdated:
|
||||||
@echo "Failure means: inspect network/tooling first; outdated packages are not necessarily regressions."
|
@echo "Failure means: inspect network/tooling first; outdated packages are not necessarily regressions."
|
||||||
flutter pub outdated
|
flutter pub outdated
|
||||||
|
|
||||||
|
# Open-source licence compliance check for all resolved dependencies.
|
||||||
|
licenses:
|
||||||
|
@echo "== OciDeck check: licences =="
|
||||||
|
@echo "Command: dart run tool/check_licenses.dart"
|
||||||
|
@echo "Covers: licence of every resolved Dart/Flutter package (direct + transitive)."
|
||||||
|
@echo "Failure means: a dependency uses an unrecognised or non-open-source licence — review it."
|
||||||
|
dart run tool/check_licenses.dart
|
||||||
|
|
||||||
# Full local quality gate. Intended for humans, CI logs, and LLM-assisted debugging.
|
# Full local quality gate. Intended for humans, CI logs, and LLM-assisted debugging.
|
||||||
check: format-check analyze test
|
check: format-check analyze test
|
||||||
@echo "== OciDeck check complete =="
|
@echo "== OciDeck check complete =="
|
||||||
@echo "Validated: formatting, static analysis, and the full Flutter test suite."
|
@echo "Validated: formatting, static analysis, and the full Flutter test suite."
|
||||||
|
|
||||||
# Extended local check with advisory dependency freshness after the required gate.
|
# Extended local check with advisory dependency freshness after the required gate.
|
||||||
check-full: check deps-outdated
|
check-full: check licenses deps-outdated
|
||||||
@echo "== OciDeck extended check complete =="
|
@echo "== OciDeck extended check complete =="
|
||||||
@echo "Validated: required quality gate plus dependency freshness report."
|
@echo "Validated: required quality gate, licence compliance, and dependency freshness."
|
||||||
|
|
|
||||||
59
README.md
59
README.md
|
|
@ -1,19 +1,27 @@
|
||||||
# OciDeck
|
# OciDeck
|
||||||
|
|
||||||
A desktop application for building [Marp](https://marp.app/) presentations through a structured, slide-by-slide editor — no raw Markdown wrangling required. Compose decks from typed slide templates (title, bullets, quotes, tables, images, video, audio), preview them live, present them fullscreen, and export to Marp Markdown, PDF, and PPTX.
|
A desktop application for building [Marp](https://marp.app/) presentations through a structured, slide-by-slide editor — no raw Markdown wrangling required. Compose decks from typed slide templates (title, bullets, quotes, tables, images, video, audio, source code, charts), preview them live, present them fullscreen — even across two screens — and export to Marp Markdown, PDF, PPTX, and self-contained HTML.
|
||||||
|
|
||||||
Built with Flutter for macOS, Windows, and Linux.
|
Built with Flutter for macOS, Windows, and Linux.
|
||||||
|
|
||||||
|
> **What's in a name?** *OciDeck* is a small wink: **Oci** is borrowed from the *Ocicats* — the cats of [Brenno de Winter](https://nl.wikipedia.org/wiki/Brenno_de_Winter) — and **Deck** is short for a presentation deck. So: the cats' presentation tool.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Structured slide editors** — dedicated editors per slide type: title, bullets, two-column bullets, bullets + image, single/two images, quote, table, section divider, image-only, video, audio, and free-form Markdown.
|
- **Structured slide editors** — dedicated editors per slide type: title, bullets, two-column bullets, bullets + image, single/two images, quote, table, section divider, image-only, video, audio, source code, charts, and free-form Markdown.
|
||||||
|
- **Source-code slides** — a dark "code sheet" with syntax highlighting per language, stored as a fenced code block.
|
||||||
|
- **Charts** — bar, line, and pie charts rendered natively (preview, presenter, PDF, PPTX) and as self-contained SVG in the HTML export. Data is entered in an in-app grid or imported from CSV; the spec is stored as JSON in the Markdown, with optional linking to a CSV kept in a tidy `data/` directory.
|
||||||
- **Live preview** — see each slide rendered as you edit, with inline Markdown, footers, and TLP (Traffic Light Protocol) marking. Free-Markdown slides render fenced code with syntax highlighting and `$…$` / `$$…$$` LaTeX math.
|
- **Live preview** — see each slide rendered as you edit, with inline Markdown, footers, and TLP (Traffic Light Protocol) marking. Free-Markdown slides render fenced code with syntax highlighting and `$…$` / `$$…$$` LaTeX math.
|
||||||
- **Fullscreen presenter** — keyboard-driven navigation, presenter view, and a slide-grid overview.
|
- **Traffic Light Protocol** — a deck-wide classification plus an optional **per-slide TLP level**; slides classified stricter than the level the deck is shown at are automatically withheld, both when presenting and exporting.
|
||||||
|
- **Fullscreen presenter** — keyboard-driven navigation, presenter view, blank screen, auto-advance, and a slide-grid overview.
|
||||||
|
- **Dual-screen presenter** — when a second display is connected, the beamer shows the slide while the laptop shows the presenter view (current/next slide, notes, timer), kept in sync.
|
||||||
|
- **Annotation layer** — draw on slides while presenting (pen, highlighter, eraser, laser pointer). Kept as a separate layer that never touches the Marp Markdown, mirrored live to the beamer, and saved in a `.ink.json` sidecar.
|
||||||
- **Media handling** — drag-and-drop images, an image carousel picker, captions, and descriptions stored as sidecar metadata.
|
- **Media handling** — drag-and-drop images, an image carousel picker, captions, and descriptions stored as sidecar metadata.
|
||||||
- **Import / export** — round-trips Marp Markdown, imports existing slides, and exports to PDF, PPTX (with speaker notes), and a self-contained offline HTML deck (code highlighting, math, and mermaid diagrams render in the browser). Decks are saved as a self-contained package with copied assets.
|
- **Import / export** — round-trips Marp Markdown, imports existing slides, and exports to PDF, PPTX (with speaker notes), and a self-contained offline HTML deck (code highlighting, math, charts, and mermaid diagrams render in the browser). Decks are saved as a self-contained package with copied assets.
|
||||||
- **Productivity** — find & replace, slide finder, undo/redo, skip-slide state, multi-select with bulk copy-to-another-deck / delete / skip, and tabbed multi-deck editing. `Ctrl/Cmd+O` opens, `Ctrl/Cmd+S` saves.
|
- **Productivity** — find & replace, slide finder, undo/redo, skip-slide state, multi-select with bulk copy-to-another-deck / delete / skip, and tabbed multi-deck editing. `Ctrl/Cmd+O` opens, `Ctrl/Cmd+S` saves.
|
||||||
- **Crash recovery** — automatic snapshots so work survives an unexpected exit.
|
- **Crash recovery** — automatic snapshots so work survives an unexpected exit.
|
||||||
- **Theming** — a bundled Marp CSS theme (`assets/themes/ocideck.css`) and a bundled EB Garamond font (no network fetch).
|
- **Theming** — customizable deck style profiles and app appearance (including a dark interface), a bundled Marp CSS theme (`assets/themes/ocideck.css`), and a bundled EB Garamond font (no network fetch).
|
||||||
|
- **Localized** — Dutch, English, Italian, German, French, Spanish, Frisian, and Papiamento.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
|
@ -69,11 +77,44 @@ State is managed with [Riverpod](https://riverpod.dev/).
|
||||||
## File format
|
## File format
|
||||||
|
|
||||||
Presentations are saved as standard, Marp-compatible Markdown (`.md`) with a
|
Presentations are saved as standard, Marp-compatible Markdown (`.md`) with a
|
||||||
defined project folder layout and an optional portable `.ocideck` package. The
|
defined project folder layout and an optional portable `.ocideck` package.
|
||||||
full specification — front matter, per-slide markup, style profile, captions,
|
Anything that isn't plain Marp is kept in side files so the `.md` stays pure and
|
||||||
and the package format — is documented in
|
portable: image captions, the annotation layer (`.ink.json`), and linked chart
|
||||||
|
data (`data/*.csv`). The full specification — front matter, per-slide markup,
|
||||||
|
style profile, sidecars, and the package format — is documented in
|
||||||
[`docs/FILE_FORMAT.md`](docs/FILE_FORMAT.md).
|
[`docs/FILE_FORMAT.md`](docs/FILE_FORMAT.md).
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
| Document | What it covers |
|
||||||
|
| --- | --- |
|
||||||
|
| [User Guide](docs/USER_GUIDE.md) | Using the app: slide types, charts, presenting, exporting, theming |
|
||||||
|
| [Keyboard shortcuts](docs/SHORTCUTS.md) | Editor and presenter shortcuts |
|
||||||
|
| [File format](docs/FILE_FORMAT.md) | The Marp Markdown, front matter, sidecars, and `.ocideck` package |
|
||||||
|
| [Architecture](docs/ARCHITECTURE.md) | How the code fits together (for contributors) |
|
||||||
|
| [Build & release](docs/BUILD.md) | Building from source and producing distributables |
|
||||||
|
| [Contributing](CONTRIBUTING.md) | Setup, the quality gate, and how to propose changes |
|
||||||
|
| [Security policy](SECURITY.md) | How to report a vulnerability |
|
||||||
|
| [Changelog](CHANGELOG.md) | Notable changes per version |
|
||||||
|
| [Third-party notices](THIRD_PARTY_NOTICES.md) | Bundled components and their licences |
|
||||||
|
| [Licence compliance](docs/LICENSE_COMPLIANCE.md) | Open-source policy and the `make licenses` check |
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please read [`CONTRIBUTING.md`](CONTRIBUTING.md) and our
|
||||||
|
[`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md). In short: `make check` must pass, new
|
||||||
|
UI strings must be translated in all languages, and file-format changes must be
|
||||||
|
reflected in `docs/FILE_FORMAT.md`. For security issues, see
|
||||||
|
[`SECURITY.md`](SECURITY.md).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
All rights reserved. _(Update this section if you intend to open-source the project.)_
|
Copyright © Brenno de Winter.
|
||||||
|
|
||||||
|
OciDeck is licensed under the **European Union Public Licence v. 1.2 (EUPL-1.2)**.
|
||||||
|
You may use, study, share, and modify the software under the terms of that
|
||||||
|
licence. The full text is in [`LICENSE.md`](LICENSE.md); the official versions in
|
||||||
|
all EU languages are available from the
|
||||||
|
[EUPL collection](https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12).
|
||||||
|
|
||||||
|
SPDX-License-Identifier: `EUPL-1.2`
|
||||||
|
|
|
||||||
45
SECURITY.md
Normal file
45
SECURITY.md
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Reporting a vulnerability
|
||||||
|
|
||||||
|
**Please do not report security vulnerabilities through public GitHub issues,
|
||||||
|
discussions, or pull requests.**
|
||||||
|
|
||||||
|
Instead, report them privately via GitHub's **"Report a vulnerability"** button
|
||||||
|
under the repository's **Security** tab (Security Advisories). If that is not
|
||||||
|
available to you, contact the maintainer directly and wait for a reply before
|
||||||
|
disclosing anything publicly.
|
||||||
|
|
||||||
|
When reporting, please include as much of the following as you can:
|
||||||
|
|
||||||
|
- A description of the issue and its impact.
|
||||||
|
- Steps to reproduce (a minimal deck or input file if relevant).
|
||||||
|
- The OciDeck version, operating system, and Flutter version.
|
||||||
|
- Any proof-of-concept, logs, or screenshots.
|
||||||
|
|
||||||
|
## What to expect
|
||||||
|
|
||||||
|
- **Acknowledgement** of your report as quickly as we reasonably can.
|
||||||
|
- An assessment and, where confirmed, a fix developed under coordinated
|
||||||
|
(responsible) disclosure.
|
||||||
|
- Credit for the discovery if you wish — let us know how you would like to be
|
||||||
|
named.
|
||||||
|
|
||||||
|
We ask that you give us a reasonable opportunity to address the issue before any
|
||||||
|
public disclosure, and that you avoid privacy violations, data destruction, or
|
||||||
|
service disruption while researching.
|
||||||
|
|
||||||
|
## Scope notes
|
||||||
|
|
||||||
|
OciDeck is an offline desktop application. Areas of particular interest:
|
||||||
|
|
||||||
|
- Parsing of untrusted decks (`.md`), packages (`.ocideck`), sidecars
|
||||||
|
(`.ink.json`, captions), and linked CSV data.
|
||||||
|
- Importing presentations from a URL.
|
||||||
|
- The HTML export, which inlines third-party JavaScript (marked, highlight.js,
|
||||||
|
mermaid, MathJax) to render offline.
|
||||||
|
|
||||||
|
## Supported versions
|
||||||
|
|
||||||
|
Security fixes target the latest released version and the default development
|
||||||
|
branch. Older versions may not receive fixes.
|
||||||
74
THIRD_PARTY_NOTICES.md
Normal file
74
THIRD_PARTY_NOTICES.md
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
# Third-Party Notices
|
||||||
|
|
||||||
|
OciDeck is licensed under the EUPL-1.2 (see [`LICENSE.md`](LICENSE.md)). It
|
||||||
|
builds on, and bundles, third-party components that remain under their own
|
||||||
|
licences. This file lists them; each component's full licence text is available
|
||||||
|
from its project or package page.
|
||||||
|
|
||||||
|
## Bundled runtime assets
|
||||||
|
|
||||||
|
Shipped inside the app and embedded into the **offline HTML export**
|
||||||
|
(`assets/web_export/`) and the UI:
|
||||||
|
|
||||||
|
| Component | Used for | Licence |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| [marked](https://github.com/markedjs/marked) | Markdown → HTML in the export | MIT |
|
||||||
|
| [highlight.js](https://github.com/highlightjs/highlight.js) | Code highlighting in the export | BSD-3-Clause |
|
||||||
|
| [Mermaid](https://github.com/mermaid-js/mermaid) | Diagrams in the export | MIT (bundles [DOMPurify](https://github.com/cure53/DOMPurify), Apache-2.0 / MPL-2.0) |
|
||||||
|
| [MathJax](https://github.com/mathjax/MathJax) (`tex-svg.js`) | Math rendering in the export | Apache-2.0 |
|
||||||
|
| [EB Garamond](https://github.com/octaviopardo/EBGaramond12) font | Bundled deck font | SIL Open Font License 1.1 |
|
||||||
|
|
||||||
|
## Vendored (forked) plugins
|
||||||
|
|
||||||
|
Kept in `third_party/` and wired in via `pubspec.yaml` (path dependency /
|
||||||
|
`dependency_overrides`). Both are forks of upstream plugins with local native
|
||||||
|
changes; see [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md#vendored-forks).
|
||||||
|
|
||||||
|
| Component | Origin | Licence | Local changes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `desktop_multi_window` | [MixinNetwork/flutter-plugins](https://github.com/MixinNetwork/flutter-plugins) | MIT | Added native macOS window placement/fullscreen/close methods for the dual-screen presenter |
|
||||||
|
| `screen_retriever_macos` | [leanflutter/screen_retriever](https://github.com/leanflutter/screen_retriever) | MIT | Packaging fix for recent Xcode/CocoaPods |
|
||||||
|
|
||||||
|
## Dart & Flutter packages
|
||||||
|
|
||||||
|
Direct dependencies (see `pubspec.yaml` for exact version constraints). Each is
|
||||||
|
distributed under its own OSI-approved licence as published on
|
||||||
|
[pub.dev](https://pub.dev); most are MIT, BSD-3-Clause, or Apache-2.0.
|
||||||
|
|
||||||
|
- `flutter`, `flutter_localizations` (Flutter SDK — BSD-3-Clause)
|
||||||
|
- `flutter_riverpod`
|
||||||
|
- `file_picker`
|
||||||
|
- `path_provider`, `path`
|
||||||
|
- `uuid`
|
||||||
|
- `screen_retriever`, `window_manager`
|
||||||
|
- `shared_preferences`
|
||||||
|
- `pasteboard`
|
||||||
|
- `pdf`
|
||||||
|
- `archive`
|
||||||
|
- `video_player`
|
||||||
|
- `characters`
|
||||||
|
- `url_launcher`
|
||||||
|
- `desktop_drop`
|
||||||
|
- `image`
|
||||||
|
- `flutter_highlight`, `highlight`
|
||||||
|
- `flutter_math_fork`
|
||||||
|
- `wakelock_plus`
|
||||||
|
- `fl_chart`
|
||||||
|
- `cupertino_icons`
|
||||||
|
|
||||||
|
> To regenerate an authoritative, version-pinned licence inventory you can use a
|
||||||
|
> tool such as `flutter pub deps` together with a licence-collection package.
|
||||||
|
|
||||||
|
## Licence audit
|
||||||
|
|
||||||
|
A scan of all resolved Dart/Flutter packages (direct **and** transitive) and the
|
||||||
|
bundled assets found only OSI-approved open-source licences — MIT, BSD
|
||||||
|
(2-/3-Clause), Apache-2.0, MPL-2.0, and the SIL Open Font License 1.1. No
|
||||||
|
proprietary or source-unavailable components are included. (The only MPL-2.0
|
||||||
|
dependency is `dbus`, used on Linux.) Re-run such a scan after changing
|
||||||
|
dependencies.
|
||||||
|
|
||||||
|
Run it yourself with `make licenses` (or `dart run tool/check_licenses.dart`).
|
||||||
|
The policy, method, and latest result are documented in
|
||||||
|
[`docs/LICENSE_COMPLIANCE.md`](docs/LICENSE_COMPLIANCE.md).
|
||||||
|
|
||||||
120
docs/ARCHITECTURE.md
Normal file
120
docs/ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
# OciDeck — Architecture
|
||||||
|
|
||||||
|
A high-level map of how OciDeck is put together, for contributors. For how files
|
||||||
|
are stored on disk, see [`FILE_FORMAT.md`](FILE_FORMAT.md).
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- **Flutter** desktop app (macOS, Windows, Linux), Dart 3.12+.
|
||||||
|
- **State**: [Riverpod](https://riverpod.dev/).
|
||||||
|
- **Storage**: standard Marp Markdown (`.md`) as the single source of truth, with
|
||||||
|
sidecars for anything that isn't plain Marp.
|
||||||
|
|
||||||
|
## Module layout
|
||||||
|
|
||||||
|
```
|
||||||
|
lib/
|
||||||
|
models/ # Deck, Slide, Settings/ThemeProfile, Chart, Annotation
|
||||||
|
services/ # markdown, file, export, image, caption, description,
|
||||||
|
# recovery, rasterizer, marp_html, annotation_codec
|
||||||
|
state/ # Riverpod providers: deck, editor, settings, tabs, clipboard
|
||||||
|
widgets/ # app shell, panels, dialogs, per-type editors, slides, presenter
|
||||||
|
l10n/ # AppLocalizations (8 languages)
|
||||||
|
theme/ # app theming
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data model
|
||||||
|
|
||||||
|
- **`Deck`** holds metadata, a list of **`Slide`**s, the active **`ThemeProfile`**,
|
||||||
|
the deck-wide TLP level, and an in-memory **annotation layer** (`Map<slideId,
|
||||||
|
List<InkStroke>>`) that is *never* serialized into the Markdown.
|
||||||
|
- **`Slide`** is a single immutable value with a `SlideType` and typed fields. A
|
||||||
|
few types reuse `customMarkdown` for their payload: free-Markdown (raw),
|
||||||
|
`code` (the source), and `chart` (the JSON spec).
|
||||||
|
- Slide ids are **regenerated on every parse**, so they are stable only within a
|
||||||
|
session. Anything persisted that must survive a reload (annotations) re-anchors
|
||||||
|
by slide order + a content fingerprint rather than by id.
|
||||||
|
|
||||||
|
## Markdown round-trip
|
||||||
|
|
||||||
|
`MarkdownService` is the contract:
|
||||||
|
|
||||||
|
- `generateDeck` / `generateSlide` write Marp Markdown. OciDeck extras live in
|
||||||
|
front-matter keys and `<!-- … -->` comments that Marp ignores.
|
||||||
|
- `parseDeck` / `_parseBlock` read it back. `code` and `chart` slides are detected
|
||||||
|
by their `_class` and parsed separately (their fenced block would otherwise
|
||||||
|
confuse the generic line parser).
|
||||||
|
|
||||||
|
This service is heavily covered by the round-trip tests — treat it as the
|
||||||
|
source-of-truth for the file format and keep `FILE_FORMAT.md` in sync.
|
||||||
|
|
||||||
|
## The two rendering worlds
|
||||||
|
|
||||||
|
Charts, diagrams, and slides are rendered in **two independent places**, which is
|
||||||
|
the key thing to understand before touching rendering:
|
||||||
|
|
||||||
|
1. **In-app** — `widgets/slides/slide_preview.dart` (`SlidePreviewWidget`) renders
|
||||||
|
a slide as Flutter widgets. The *same* widget is used for the editor preview,
|
||||||
|
thumbnails, the fullscreen presenter, and — via `services/slide_rasterizer.dart`
|
||||||
|
— the **PDF and PPTX** exports (rasterized to images). So anything that must
|
||||||
|
appear in PDF/PPTX must render here. Charts use `fl_chart`.
|
||||||
|
2. **HTML export** — `services/marp_html_service.dart` produces a single
|
||||||
|
self-contained `.html` that renders in a browser using inlined JavaScript
|
||||||
|
(marked, highlight.js, mermaid, MathJax). Charts are pre-rendered to inline
|
||||||
|
**SVG in Dart** here (no JS chart library). Fidelity differs from the in-app
|
||||||
|
renderer by design.
|
||||||
|
|
||||||
|
## Presenter
|
||||||
|
|
||||||
|
`widgets/presentation/fullscreen_presenter.dart` drives presenting:
|
||||||
|
|
||||||
|
- Keyboard navigation, presenter view, blank screen, grid overview, auto-advance,
|
||||||
|
and the **annotation tools** (pen/highlighter/eraser/laser).
|
||||||
|
- Neighbour slide images are **precached** and `gaplessPlayback` is on, so slide
|
||||||
|
changes never flash black (important for screen recording).
|
||||||
|
|
||||||
|
### Dual-screen mode
|
||||||
|
|
||||||
|
When a second display is present (`shouldUseDualScreen`), the presenter runs in
|
||||||
|
two OS windows:
|
||||||
|
|
||||||
|
- The **laptop** window shows the presenter view.
|
||||||
|
- A borderless **audience** window (`audience_window.dart`) fills the external
|
||||||
|
screen with the slide.
|
||||||
|
- They sync over method channels (`ocideck/audience`, `ocideck/presenter`):
|
||||||
|
current index, blank state, ink strokes, and the laser pointer. Media plays only
|
||||||
|
on the beamer to avoid double audio.
|
||||||
|
|
||||||
|
This needs a real second window, which `window_manager` (single-window) can't do,
|
||||||
|
hence the vendored multi-window fork below.
|
||||||
|
|
||||||
|
## Sidecars (separate layers)
|
||||||
|
|
||||||
|
To keep the `.md` pure Marp, three kinds of data live beside it (see
|
||||||
|
`FILE_FORMAT.md` §6):
|
||||||
|
|
||||||
|
- **Captions** — `.ocideck_captions.json` (per image, in `images/`).
|
||||||
|
- **Annotations** — `<name>.ink.json` (`services/annotation_codec.dart`).
|
||||||
|
- **Linked chart data** — `data/*.csv` (the living source for a chart).
|
||||||
|
|
||||||
|
## Vendored forks
|
||||||
|
|
||||||
|
Two upstream plugins are forked into `third_party/` and wired via `pubspec.yaml`
|
||||||
|
(path dependency / `dependency_overrides`):
|
||||||
|
|
||||||
|
- **`desktop_multi_window`** (MixinNetwork) — published 0.3.0 dropped the native
|
||||||
|
window-geometry API. The fork adds macOS `window_setFrame`,
|
||||||
|
`window_coverScreen` (borderless fill of a chosen screen), and `window_close`,
|
||||||
|
exposed on `WindowController`. This is what makes the dual-screen audience
|
||||||
|
window possible.
|
||||||
|
- **`screen_retriever_macos`** (leanflutter) — a packaging fix for recent
|
||||||
|
Xcode/CocoaPods.
|
||||||
|
|
||||||
|
If you bump either upstream, re-apply the local changes (they're small and
|
||||||
|
documented in the diff) and re-test the dual-screen presenter.
|
||||||
|
|
||||||
|
## Localization
|
||||||
|
|
||||||
|
`l10n/app_localizations.dart` holds Dutch source strings (`d('…')`) and
|
||||||
|
translation maps for en/it/de/fr/es/fy/pap. A test enforces that every literal
|
||||||
|
`.d('…')` has a translation in every language — add new strings to all maps.
|
||||||
82
docs/BUILD.md
Normal file
82
docs/BUILD.md
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
# OciDeck — Build & Release
|
||||||
|
|
||||||
|
How to build OciDeck from source and produce distributable apps.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **Flutter** 3.44+ (stable), **Dart** 3.12+. Check with `flutter --version`.
|
||||||
|
- A desktop toolchain for your target:
|
||||||
|
- **macOS**: Xcode + CocoaPods.
|
||||||
|
- **Windows**: Visual Studio with the "Desktop development with C++" workload.
|
||||||
|
- **Linux**: see Flutter's Linux desktop prerequisites (GTK, clang, ninja, etc.).
|
||||||
|
- Enable the desktop target once if needed, e.g. `flutter config --enable-macos-desktop`.
|
||||||
|
|
||||||
|
## Get dependencies
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make setup # flutter pub get
|
||||||
|
```
|
||||||
|
|
||||||
|
OciDeck uses two **vendored plugin forks** under `third_party/`, wired through
|
||||||
|
`pubspec.yaml` (a path dependency for `desktop_multi_window`) and
|
||||||
|
`dependency_overrides` (`screen_retriever_macos`, and a pin of
|
||||||
|
`video_player_avfoundation`). `flutter pub get` resolves these automatically — no
|
||||||
|
extra steps. See [`ARCHITECTURE.md`](ARCHITECTURE.md#vendored-forks).
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```sh
|
||||||
|
flutter run -d macos # or -d windows / -d linux
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quality gate
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make check # format-check + flutter analyze + full test suite
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building release apps
|
||||||
|
|
||||||
|
```sh
|
||||||
|
flutter build macos --release
|
||||||
|
flutter build windows --release
|
||||||
|
flutter build linux --release
|
||||||
|
```
|
||||||
|
|
||||||
|
Artifacts land under `build/<platform>/`.
|
||||||
|
|
||||||
|
### macOS notes
|
||||||
|
|
||||||
|
- **Swift Package Manager is disabled** for this project (`flutter:` →
|
||||||
|
`config: enable-swift-package-manager: false` in `pubspec.yaml`); CocoaPods is
|
||||||
|
used instead. The "plugin does not support Swift Package Manager" message
|
||||||
|
during a build is therefore expected and harmless.
|
||||||
|
- **`video_player_avfoundation` is pinned** (see `dependency_overrides`) because a
|
||||||
|
newer release ships a Swift module whose private Objective-C dependency isn't
|
||||||
|
packaged correctly by CocoaPods on recent Xcode.
|
||||||
|
- **CocoaPods + Ruby locale**: on some setups `pod install` (run by
|
||||||
|
`flutter build macos`) fails with `Encoding::CompatibilityError` /
|
||||||
|
"Unicode Normalization not appropriate for ASCII-8BIT". This is a Ruby/CocoaPods
|
||||||
|
locale issue, not a project problem. Fix it by forcing a UTF-8 locale:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8
|
||||||
|
flutter build macos --release
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Distribution**: code-sign and notarize the `.app` for distribution outside
|
||||||
|
the App Store (Developer ID + `notarytool`). This is environment-specific and
|
||||||
|
not automated here.
|
||||||
|
|
||||||
|
### Windows / Linux notes
|
||||||
|
|
||||||
|
- Windows: distribute the contents of `build/windows/x64/runner/Release/` (or
|
||||||
|
package with MSIX/an installer).
|
||||||
|
- Linux: distribute `build/linux/x64/release/bundle/` (or package as a
|
||||||
|
Flatpak/AppImage/Snap as you prefer).
|
||||||
|
|
||||||
|
## CI
|
||||||
|
|
||||||
|
`.github/workflows/ci.yml` runs `make check` on Ubuntu for every push and pull
|
||||||
|
request. It does not build native binaries; it validates formatting, static
|
||||||
|
analysis, and the test suite (which are platform-independent).
|
||||||
|
|
@ -25,9 +25,12 @@ opzichte van de map van het `.md`-bestand.
|
||||||
```
|
```
|
||||||
mijn_presentatie/
|
mijn_presentatie/
|
||||||
├── Mijn_presentatie.md # de presentatie (Marp Markdown)
|
├── Mijn_presentatie.md # de presentatie (Marp Markdown)
|
||||||
|
├── Mijn_presentatie.ink.json # annotatielaag-sidecar (zie §6.2)
|
||||||
├── images/ # gekopieerde afbeeldingen
|
├── images/ # gekopieerde afbeeldingen
|
||||||
│ ├── foto.png
|
│ ├── foto.png
|
||||||
│ └── .ocideck_captions.json # bijschriften-sidecar (zie §6)
|
│ └── .ocideck_captions.json # bijschriften-sidecar (zie §6.1)
|
||||||
|
├── data/ # gekoppelde grafiek-CSV's (zie §6.3)
|
||||||
|
│ └── omzet.csv
|
||||||
├── logos/ # gekopieerd logo van het stijlprofiel
|
├── logos/ # gekopieerd logo van het stijlprofiel
|
||||||
│ └── logo.png
|
│ └── logo.png
|
||||||
├── media/ # video/audio (alleen in het pakket, zie §7)
|
├── media/ # video/audio (alleen in het pakket, zie §7)
|
||||||
|
|
@ -42,6 +45,11 @@ De mappen `images/`, `logos/`, `themes/` (en `node_modules/`, `build/`, `.git/`,
|
||||||
`.dart_tool/`) worden overgeslagen wanneer OciDeck een map scant op
|
`.dart_tool/`) worden overgeslagen wanneer OciDeck een map scant op
|
||||||
presentaties.
|
presentaties.
|
||||||
|
|
||||||
|
> Naast de `.md` staan **sidecars** die bewust géén onderdeel van de Marp-
|
||||||
|
> Markdown zijn (zodat het `.md` puur en uitwisselbaar blijft): de
|
||||||
|
> annotatielaag (`<naam>.ink.json`, §6.2), bijschriften (`.ocideck_captions.json`,
|
||||||
|
> §6.1) en gekoppelde grafiekdata (`data/*.csv`, §6.3).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Markdown-structuur op hoofdlijnen
|
## 2. Markdown-structuur op hoofdlijnen
|
||||||
|
|
@ -134,6 +142,8 @@ JSON heeft deze velden (met standaardwaarden):
|
||||||
| `footerText` | `""` | Vrije footertekst; tokens: `{page}`, `{total}`, `{date}`, `{title}`. |
|
| `footerText` | `""` | Vrije footertekst; tokens: `{page}`, `{total}`, `{date}`, `{title}`. |
|
||||||
| `footerShowPageNumbers` | `false` | Toon "pagina / totaal" rechtsonder. |
|
| `footerShowPageNumbers` | `false` | Toon "pagina / totaal" rechtsonder. |
|
||||||
| `footerPosition` | `right` | `left`/`center`/`right`. |
|
| `footerPosition` | `right` | `left`/`center`/`right`. |
|
||||||
|
| `closingSlideEnabled` | `false` | Voeg automatisch een slotslide toe bij presenteren/exporteren. |
|
||||||
|
| `closingSlideMarkdown` | `"# Bedankt\n\nVragen?"` | Markdown van die slotslide. |
|
||||||
|
|
||||||
Onbekende/ontbrekende velden vallen terug op de standaardwaarden, dus oudere
|
Onbekende/ontbrekende velden vallen terug op de standaardwaarden, dus oudere
|
||||||
bestanden migreren probleemloos.
|
bestanden migreren probleemloos.
|
||||||
|
|
@ -159,11 +169,16 @@ De eerste class bepaalt (samen met de inhoud) het **slidetype**:
|
||||||
| Quote | `quote` | een `>`-regel aanwezig |
|
| Quote | `quote` | een `>`-regel aanwezig |
|
||||||
| Video | `video` | een `<video>`-tag aanwezig |
|
| Video | `video` | een `<video>`-tag aanwezig |
|
||||||
| Tabel | `table` | alleen een tabel, geen kop/bullets/tekst |
|
| Tabel | `table` | alleen een tabel, geen kop/bullets/tekst |
|
||||||
|
| Broncode | `code` | — |
|
||||||
|
| Grafiek | `chart` | — |
|
||||||
| Alleen bullets | *(geen)* | bullets aanwezig |
|
| Alleen bullets | *(geen)* | bullets aanwezig |
|
||||||
| Twee afbeeldingen | *(geen)* | twee achtergrond-afbeeldingen |
|
| Twee afbeeldingen | *(geen)* | twee achtergrond-afbeeldingen |
|
||||||
| Grote afbeelding | *(geen)* | één afbeelding, geen bullets |
|
| Grote afbeelding | *(geen)* | één afbeelding, geen bullets |
|
||||||
| Vrije Markdown | *(geen)* | geen kop/bullets/afbeelding/quote |
|
| Vrije Markdown | *(geen)* | geen kop/bullets/afbeelding/quote |
|
||||||
|
|
||||||
|
> `code`- en `chart`-slides bevatten een fenced codeblok dat de generieke
|
||||||
|
> regel-parser zou verstoren; ze worden daarom apart herkend aan hun `_class`.
|
||||||
|
|
||||||
Extra gedragsklassen:
|
Extra gedragsklassen:
|
||||||
|
|
||||||
- `logo-safe` — gereserveerde ruimte zodat het logo de inhoud niet overlapt.
|
- `logo-safe` — gereserveerde ruimte zodat het logo de inhoud niet overlapt.
|
||||||
|
|
@ -280,6 +295,34 @@ worden `|` als `\|` en regeleindes als `<br>` weggeschreven:
|
||||||
|
|
||||||
**Vrije Markdown** (geen class) — de inhoud wordt letterlijk weggeschreven.
|
**Vrije Markdown** (geen class) — de inhoud wordt letterlijk weggeschreven.
|
||||||
|
|
||||||
|
**Broncode** (`code`) — een optionele kop plus een fenced codeblok; de
|
||||||
|
info-string is de programmeertaal (highlight.js-id, leeg = platte tekst). De
|
||||||
|
code zelf staat verbatim in het blok:
|
||||||
|
````markdown
|
||||||
|
# Optionele kop
|
||||||
|
|
||||||
|
```dart
|
||||||
|
void main() => print('hi');
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
|
**Grafiek** (`chart`) — een fenced ```chart```-blok met de grafiekspecificatie
|
||||||
|
als **JSON**. Kleine grafieken bewaren hun data inline; data-gedreven grafieken
|
||||||
|
verwijzen via `source` naar een CSV in `data/` (zie §6.3). Bij opslaan wordt de
|
||||||
|
inline data weggelaten zodra er een `source` is (de CSV is dan de bron); bij
|
||||||
|
openen wordt die weer ingelezen.
|
||||||
|
````markdown
|
||||||
|
```chart
|
||||||
|
{
|
||||||
|
"type": "bar", // bar | line | pie
|
||||||
|
"title": "Omzet",
|
||||||
|
"source": "data/omzet.csv", // optioneel; anders inline x/series
|
||||||
|
"x": ["Q1", "Q2"],
|
||||||
|
"series": [ { "name": "2025", "data": [10, 14] } ]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
### Afbeeldingsgrootte (`imageSize`)
|
### Afbeeldingsgrootte (`imageSize`)
|
||||||
Eén integer-veld met typeafhankelijke betekenis: bij `image`/`title`/`quote` het
|
Eén integer-veld met typeafhankelijke betekenis: bij `image`/`title`/`quote` het
|
||||||
achtergrond-percentage (`![bg N%]`), bij `split` de paneelbreedte (geklemd
|
achtergrond-percentage (`![bg N%]`), bij `split` de paneelbreedte (geklemd
|
||||||
|
|
@ -287,7 +330,12 @@ achtergrond-percentage (`![bg N%]`), bij `split` de paneelbreedte (geklemd
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Afbeeldings-bijschriften (captions)
|
## 6. Sidecars en losse data
|
||||||
|
|
||||||
|
Drie soorten gegevens staan bewust náást het `.md` in plaats van erin, zodat de
|
||||||
|
Marp-Markdown puur en uitwisselbaar blijft.
|
||||||
|
|
||||||
|
### 6.1 Afbeeldings-bijschriften (captions)
|
||||||
|
|
||||||
Bijschriften worden op **twee** plaatsen bewaard:
|
Bijschriften worden op **twee** plaatsen bewaard:
|
||||||
|
|
||||||
|
|
@ -309,6 +357,54 @@ Bijschriften worden op **twee** plaatsen bewaard:
|
||||||
```
|
```
|
||||||
Een lege caption verwijdert de sleutel; een leeg bestand wordt verwijderd.
|
Een lege caption verwijdert de sleutel; een leeg bestand wordt verwijderd.
|
||||||
|
|
||||||
|
### 6.2 Annotatielaag (`<naam>.ink.json`)
|
||||||
|
|
||||||
|
Vrije-hand-annotaties (pen, markeerstift) die tijdens het presenteren worden
|
||||||
|
gemaakt, staan in een aparte JSON-sidecar naast de `.md` (en in het pakket, §7).
|
||||||
|
De Marp-`.md` wordt er nooit door aangeraakt.
|
||||||
|
|
||||||
|
- Coördinaten zijn **genormaliseerd** (0–1) binnen het 16:9-vlak, zodat een
|
||||||
|
streek identiek schaalt op laptop en beamer.
|
||||||
|
- Omdat slide-id's bij elke keer inlezen opnieuw worden gegenereerd, worden
|
||||||
|
strekken op schijf **per slide verankerd op volgorde + een inhoud-fingerprint**.
|
||||||
|
Bij heropenen worden ze her-gekoppeld aan de slide met dezelfde fingerprint
|
||||||
|
(bij voorkeur dezelfde index); strekken van een gewijzigde/verwijderde slide
|
||||||
|
vervallen.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"slides": [
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"fp": "a1b2c3d4",
|
||||||
|
"strokes": [
|
||||||
|
{ "tool": "pen", "color": 4294198070, "width": 0.004,
|
||||||
|
"points": [0.1, 0.2, 0.15, 0.22] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`points` is een platte lijst `[x0, y0, x1, y1, …]`; `color` is een ARGB-int;
|
||||||
|
`tool` is `pen` of `highlighter` (laser-aanwijzingen zijn vluchtig en worden niet
|
||||||
|
bewaard).
|
||||||
|
|
||||||
|
### 6.3 Grafiekdata (`data/*.csv`)
|
||||||
|
|
||||||
|
Een grafiek-slide (§5) kan zijn data inline in het `chart`-blok houden óf via
|
||||||
|
`"source": "data/<naam>.csv"` verwijzen naar een CSV in de aparte **`data/`**-map
|
||||||
|
naast het deck. Die map houdt alle gekoppelde databestanden bij elkaar,
|
||||||
|
gescheiden van `images/`/`media/`. De CSV is dan de bron van waarheid: hij wordt
|
||||||
|
los bewerkt (bijv. in een spreadsheet), bij opslaan/`Opslaan als…` meegekopieerd,
|
||||||
|
en in het pakket meegenomen (§7). Bij openen wordt de CSV ingelezen en de data
|
||||||
|
in het geheugen aan de grafiek gehangen; in de `.md` blijft alleen de
|
||||||
|
`source`-verwijzing staan.
|
||||||
|
|
||||||
|
CSV-vorm: eerste rij = reeksnamen (eerste cel = labelkolom), elke volgende rij is
|
||||||
|
`label, waarde1, waarde2, …`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Draagbaar pakket (`.ocideck`)
|
## 7. Draagbaar pakket (`.ocideck`)
|
||||||
|
|
@ -320,7 +416,9 @@ onderling met relatieve paden. Werkt ook als het deck nog niet is opgeslagen.
|
||||||
```
|
```
|
||||||
<titel>.ocideck (zip)
|
<titel>.ocideck (zip)
|
||||||
├── <titel>.md # Marp Markdown
|
├── <titel>.md # Marp Markdown
|
||||||
|
├── <titel>.ink.json # annotatielaag (indien aanwezig, §6.2)
|
||||||
├── images/… # alle gebruikte afbeeldingen
|
├── images/… # alle gebruikte afbeeldingen
|
||||||
|
├── data/… # gekoppelde grafiek-CSV's (§6.3)
|
||||||
├── media/… # gebruikte video/audio
|
├── media/… # gebruikte video/audio
|
||||||
├── logos/… # logo uit het stijlprofiel
|
├── logos/… # logo uit het stijlprofiel
|
||||||
└── themes/<theme>.css # gegenereerde thema-CSS (Marp/CLI-bruikbaar)
|
└── themes/<theme>.css # gegenereerde thema-CSS (Marp/CLI-bruikbaar)
|
||||||
|
|
@ -349,6 +447,7 @@ genegeerd, op presenter-notities na):
|
||||||
| `<!-- ocideck_two_bullets_left/right: <base64url> -->` | Canonieke opslag van de twee bulletkolommen. |
|
| `<!-- ocideck_two_bullets_left/right: <base64url> -->` | Canonieke opslag van de twee bulletkolommen. |
|
||||||
| `<!-- advance: N.N -->` | Auto-doorschakelen na N,N seconden (0 = uit). |
|
| `<!-- advance: N.N -->` | Auto-doorschakelen na N,N seconden (0 = uit). |
|
||||||
| `<!-- skip -->` | Slide overslaan bij presenteren én exporteren. |
|
| `<!-- skip -->` | Slide overslaan bij presenteren én exporteren. |
|
||||||
|
| `<!-- tlp: <key> -->` | Per-slide TLP-niveau (zie §3.1). De slide wordt achtergehouden als het presentatie-TLP lager is. Alleen geschreven als ≠ `none`. |
|
||||||
| `<!-- … (vrije tekst) … -->` | **Presenter-notities** (elk overig commentaar dat niet met `_` begint). |
|
| `<!-- … (vrije tekst) … -->` | **Presenter-notities** (elk overig commentaar dat niet met `_` begint). |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
66
docs/LICENSE_COMPLIANCE.md
Normal file
66
docs/LICENSE_COMPLIANCE.md
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
# OciDeck — Open-Source Licence Compliance
|
||||||
|
|
||||||
|
OciDeck is released under the **EUPL-1.2** (see [`../LICENSE.md`](../LICENSE.md)).
|
||||||
|
This document records the policy that the project only includes open-source
|
||||||
|
software, how that is verified, and the result of the latest check.
|
||||||
|
|
||||||
|
## Policy
|
||||||
|
|
||||||
|
Every dependency and every bundled asset must be available under an OSI-approved
|
||||||
|
open-source licence. No proprietary or source-unavailable components are shipped.
|
||||||
|
|
||||||
|
Accepted licence families: **MIT, BSD (2-/3-Clause), Apache-2.0, MPL-2.0, ISC,
|
||||||
|
Zlib, BSL-1.0, Unlicense, SIL OFL-1.1, CC0** (and EUPL-1.2 for OciDeck itself).
|
||||||
|
Anything else — in particular GPL/AGPL/LGPL or a missing/unknown licence — is
|
||||||
|
flagged for review before it can be added.
|
||||||
|
|
||||||
|
## How to verify (repeatable)
|
||||||
|
|
||||||
|
A script scans the resolved package graph (direct **and** transitive) and
|
||||||
|
classifies each licence:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make licenses # or: dart run tool/check_licenses.dart
|
||||||
|
```
|
||||||
|
|
||||||
|
It exits non-zero if any package has an unrecognised or non-open-source licence,
|
||||||
|
so it also runs as part of `make check-full` and can be wired into CI.
|
||||||
|
|
||||||
|
> The script reads each package's `LICENSE` file from `.dart_tool/package_config.json`,
|
||||||
|
> so run `flutter pub get` first. Re-run it whenever dependencies change.
|
||||||
|
|
||||||
|
Bundled (non-package) runtime assets — the JavaScript inlined into the HTML
|
||||||
|
export and the bundled font — are tracked by hand in
|
||||||
|
[`../THIRD_PARTY_NOTICES.md`](../THIRD_PARTY_NOTICES.md).
|
||||||
|
|
||||||
|
## Latest result
|
||||||
|
|
||||||
|
All **151** resolved packages use recognised open-source licences:
|
||||||
|
|
||||||
|
| Count | Licence |
|
||||||
|
| ---: | --- |
|
||||||
|
| 108 | BSD-3-Clause |
|
||||||
|
| 30 | MIT |
|
||||||
|
| 9 | Apache-2.0 |
|
||||||
|
| 1 | MPL-2.0 (`dbus`, Linux only) |
|
||||||
|
| 1 | BSD |
|
||||||
|
| 1 | BSL-1.0 |
|
||||||
|
| 1 | EUPL-1.2 (OciDeck itself) |
|
||||||
|
|
||||||
|
Bundled assets: marked (MIT), highlight.js (BSD-3-Clause), Mermaid (MIT, bundling
|
||||||
|
DOMPurify under Apache-2.0/MPL-2.0), MathJax (Apache-2.0), and the EB Garamond
|
||||||
|
font (SIL OFL-1.1, see `assets/fonts/OFL.txt`). The OciDeck-owned brand images in
|
||||||
|
`assets/images/` and the theme in `assets/themes/` are the project's own work.
|
||||||
|
|
||||||
|
**Conclusion: no non-open-source software is included.**
|
||||||
|
|
||||||
|
## A note on Apache-2.0 and the EUPL
|
||||||
|
|
||||||
|
A few components are Apache-2.0 (e.g. MathJax in the HTML export, and some Dart
|
||||||
|
packages). Using Apache-2.0 libraries as unmodified dependencies in an EUPL-1.2
|
||||||
|
work is fine. Note, however, that Apache-2.0 is **not** on the EUPL's list of
|
||||||
|
"compatible licences" (which governs the *outbound* relicensing of derivative
|
||||||
|
works under Article 5 EUPL). This only matters if you create a combined
|
||||||
|
derivative work that must be relicensed; it does not affect bundling these
|
||||||
|
libraries as-is. If you need formal certainty for a specific distribution
|
||||||
|
scenario, have it confirmed by someone with licence expertise.
|
||||||
57
docs/SHORTCUTS.md
Normal file
57
docs/SHORTCUTS.md
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
# OciDeck — Keyboard shortcuts
|
||||||
|
|
||||||
|
`Ctrl` is shown for Windows/Linux; use `Cmd` (⌘) on macOS.
|
||||||
|
|
||||||
|
## Editor (app-wide)
|
||||||
|
|
||||||
|
| Shortcut | Action |
|
||||||
|
| --- | --- |
|
||||||
|
| `Ctrl/Cmd + O` | Open a presentation |
|
||||||
|
| `Ctrl/Cmd + S` | Save the active deck |
|
||||||
|
| `Ctrl/Cmd + Z` | Undo |
|
||||||
|
| `Ctrl/Cmd + Shift + Z` | Redo |
|
||||||
|
| `Ctrl + Y` | Redo (alternative) |
|
||||||
|
| `Ctrl/Cmd + H` | Find & replace |
|
||||||
|
|
||||||
|
## Fullscreen presenter
|
||||||
|
|
||||||
|
Navigation:
|
||||||
|
|
||||||
|
| Shortcut | Action |
|
||||||
|
| --- | --- |
|
||||||
|
| `→` · `Space` · `Page Down` · click | Next slide |
|
||||||
|
| `←` · `Page Up` | Previous slide |
|
||||||
|
| `Enter` | Next slide (or jump, if a number was typed) |
|
||||||
|
| digits, then `Enter` | Jump to that slide number |
|
||||||
|
| `Home` · `End` | First · last slide |
|
||||||
|
| `G` | Slide-grid overview (arrows + `Enter` to jump) |
|
||||||
|
|
||||||
|
View & timing:
|
||||||
|
|
||||||
|
| Shortcut | Action |
|
||||||
|
| --- | --- |
|
||||||
|
| `P` | Toggle presenter view (notes, clock, next slide) |
|
||||||
|
| `S` | Move the presentation to another screen |
|
||||||
|
| `B` · `W` | Black · white screen |
|
||||||
|
| `R` | Reset the elapsed-time counter |
|
||||||
|
| `A` | Auto-advance on/off |
|
||||||
|
| `L` | Loop (restart after the last slide) on/off |
|
||||||
|
| `M` | Advance automatically after a slide's audio finishes |
|
||||||
|
| `H` · `?` | Show the in-app shortcut cheatsheet |
|
||||||
|
|
||||||
|
Annotation tools:
|
||||||
|
|
||||||
|
| Shortcut | Action |
|
||||||
|
| --- | --- |
|
||||||
|
| `D` | Pen |
|
||||||
|
| `T` | Highlighter |
|
||||||
|
| `E` | Eraser |
|
||||||
|
| `X` | Laser pointer |
|
||||||
|
| `C` | Clear the current slide's annotations |
|
||||||
|
|
||||||
|
`Esc` is layered: it first puts away the active annotation tool, then clears a
|
||||||
|
typed slide number, then removes a black/white screen, and finally exits the
|
||||||
|
presentation.
|
||||||
|
|
||||||
|
> In **dual-screen** mode the keyboard stays with the laptop (presenter) window;
|
||||||
|
> clicks on the beamer also advance the slide.
|
||||||
107
docs/USER_GUIDE.md
Normal file
107
docs/USER_GUIDE.md
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
# OciDeck — User Guide
|
||||||
|
|
||||||
|
OciDeck builds [Marp](https://marp.app/) presentations through a structured,
|
||||||
|
slide-by-slide editor. You compose typed slides, preview them live, present them
|
||||||
|
(on one or two screens), and export to Markdown, PDF, PPTX, or a self-contained
|
||||||
|
HTML file. Files stay standard Marp Markdown, so a deck remains usable in other
|
||||||
|
Marp tools.
|
||||||
|
|
||||||
|
## Creating and opening decks
|
||||||
|
|
||||||
|
- **New / Open**: use the welcome screen or `Ctrl/Cmd + O`. Multiple decks open in
|
||||||
|
**tabs**.
|
||||||
|
- **Save**: `Ctrl/Cmd + S`. Saving lays out a tidy project folder next to your
|
||||||
|
`.md` (`images/`, `data/`, `logos/`, `themes/`) and copies assets in. See
|
||||||
|
[`FILE_FORMAT.md`](FILE_FORMAT.md).
|
||||||
|
- **Crash recovery**: unsaved work is snapshotted automatically and offered back
|
||||||
|
after an unexpected exit.
|
||||||
|
|
||||||
|
## Slide types
|
||||||
|
|
||||||
|
Add a slide and pick a type: **title**, **section** divider, **bullets**, **two
|
||||||
|
bullet columns**, **bullets + image**, **two images**, **large image**, **video**,
|
||||||
|
**audio**, **quote**, **table**, **source code**, **chart**, and **free Markdown**.
|
||||||
|
Each type has a dedicated editor on the left and a live preview on the right.
|
||||||
|
|
||||||
|
Text fields support inline Markdown (`**bold**`, `*italic*`, `` `code` ``,
|
||||||
|
`[links](…)`). Free-Markdown slides also render fenced code with syntax
|
||||||
|
highlighting and `$…$` / `$$…$$` LaTeX math.
|
||||||
|
|
||||||
|
### Source-code slides
|
||||||
|
|
||||||
|
Choose a programming language for syntax highlighting (or "plain text") and paste
|
||||||
|
your code. It renders as a dark "code sheet". Stored as a fenced code block in the
|
||||||
|
Markdown.
|
||||||
|
|
||||||
|
### Charts
|
||||||
|
|
||||||
|
Pick a type (**bar**, **line**, **pie**) and a title, then enter data in the grid:
|
||||||
|
the first column is the labels, each further column is a named series. Use **Row**
|
||||||
|
and **Series** to add data; the small ✕ removes a row/column.
|
||||||
|
|
||||||
|
- **CSV import** — click **CSV importeren**. You can either keep the data **in the
|
||||||
|
slide** (inline) or store it **as a CSV file**. A linked CSV lives in the deck's
|
||||||
|
`data/` directory and stays the source of truth (edit it in a spreadsheet); the
|
||||||
|
grid then shows it read-only until you **Ontkoppelen** (unlink).
|
||||||
|
- Charts render in the preview, presenter, PDF, and PPTX, and as inline SVG in the
|
||||||
|
HTML export.
|
||||||
|
|
||||||
|
## Per-slide options
|
||||||
|
|
||||||
|
Below each editor you can set:
|
||||||
|
|
||||||
|
- **Auto-advance** after N seconds.
|
||||||
|
- **TLP of this slide** — a Traffic Light Protocol level (see below).
|
||||||
|
- Show/hide the **logo** and **footer** on this slide.
|
||||||
|
- **Speaker notes**.
|
||||||
|
- An optional **audio** attachment.
|
||||||
|
|
||||||
|
## Traffic Light Protocol (TLP)
|
||||||
|
|
||||||
|
A deck has an overall TLP level (shown as a marking on the slides). Each slide can
|
||||||
|
*also* carry its own level. When you present or export, slides whose level is
|
||||||
|
**stricter** than the level chosen for the deck are **withheld** — so the same
|
||||||
|
deck can be shown safely to audiences with different clearances. Order, least to
|
||||||
|
most restrictive: none < CLEAR < GREEN < AMBER < AMBER+STRICT < RED.
|
||||||
|
|
||||||
|
## Presenting
|
||||||
|
|
||||||
|
Start the fullscreen presenter from the toolbar. See
|
||||||
|
[`SHORTCUTS.md`](SHORTCUTS.md) for the full key list; highlights: arrows to move,
|
||||||
|
`G` for the grid overview, `B`/`W` to blank, `P` for presenter view, `H` for the
|
||||||
|
in-app cheatsheet.
|
||||||
|
|
||||||
|
### Two screens (beamer + laptop)
|
||||||
|
|
||||||
|
When a second display is connected, OciDeck automatically shows the **slide on the
|
||||||
|
beamer** and the **presenter view on your laptop** (current slide, next slide,
|
||||||
|
notes, clock). Use an *extended* (not mirrored) display. Notes:
|
||||||
|
|
||||||
|
- The keyboard stays on the laptop; clicking the beamer also advances.
|
||||||
|
- On macOS the "external" screen is the one without the menu bar.
|
||||||
|
|
||||||
|
### Annotating while presenting
|
||||||
|
|
||||||
|
Draw on the slide live with **D** pen, **T** highlighter, **E** eraser, **X**
|
||||||
|
laser pointer, and **C** to clear; `Esc` puts the tool away. Drawings are a
|
||||||
|
separate layer (never written into the Marp Markdown), mirror live to the beamer,
|
||||||
|
and are saved in a `<name>.ink.json` sidecar so they persist with the deck.
|
||||||
|
|
||||||
|
## Exporting
|
||||||
|
|
||||||
|
Export to:
|
||||||
|
|
||||||
|
- **PDF** and **PPTX** (PPTX includes speaker notes) — rendered from the in-app
|
||||||
|
slide renderer.
|
||||||
|
- **Self-contained HTML** — one offline file; code highlighting, math, charts, and
|
||||||
|
mermaid diagrams render in the browser.
|
||||||
|
- **Portable package** (`.ocideck`) — a single zip with the Markdown and all
|
||||||
|
assets, to hand the whole deck to someone else.
|
||||||
|
|
||||||
|
## Theming and language
|
||||||
|
|
||||||
|
- **Style profiles** control deck colours, fonts, logo, and footer. The bundled
|
||||||
|
Marp theme is `assets/themes/ocideck.css`.
|
||||||
|
- **App appearance** (including a dark interface) is configurable in settings.
|
||||||
|
- The interface is available in Dutch, English, Italian, German, French, Spanish,
|
||||||
|
Frisian, and Papiamento.
|
||||||
|
|
@ -1185,13 +1185,18 @@ const _dutchSourceStrings = {
|
||||||
'Lijn': 'Linee',
|
'Lijn': 'Linee',
|
||||||
'Cirkel': 'Torta',
|
'Cirkel': 'Torta',
|
||||||
'CSV importeren': 'Importa CSV',
|
'CSV importeren': 'Importa CSV',
|
||||||
'Data (CSV: eerste rij = reeksnamen, eerste kolom = labels)': 'Dati (CSV: prima riga = nomi serie, prima colonna = etichette)',
|
'Data (CSV: eerste rij = reeksnamen, eerste kolom = labels)':
|
||||||
|
'Dati (CSV: prima riga = nomi serie, prima colonna = etichette)',
|
||||||
'Gekoppeld aan': 'Collegato a',
|
'Gekoppeld aan': 'Collegato a',
|
||||||
'Ontkoppelen': 'Scollega',
|
'Ontkoppelen': 'Scollega',
|
||||||
'Data in de slide opslaan, of als los CSV-bestand naast de presentatie bewaren?': 'Salvare i dati nella slide o tenerli come file CSV separato accanto alla presentazione?',
|
'Data in de slide opslaan, of als los CSV-bestand naast de presentatie bewaren?':
|
||||||
|
'Salvare i dati nella slide o tenerli come file CSV separato accanto alla presentazione?',
|
||||||
'In de slide': 'Nella slide',
|
'In de slide': 'Nella slide',
|
||||||
'Als CSV-bestand': 'Come file CSV',
|
'Als CSV-bestand': 'Come file CSV',
|
||||||
'Geen grafiekgegevens': 'Nessun dato del grafico',
|
'Geen grafiekgegevens': 'Nessun dato del grafico',
|
||||||
|
'Label': 'Etichetta',
|
||||||
|
'Rij': 'Riga',
|
||||||
|
'Reeks': 'Serie',
|
||||||
'Plak of typ hier je broncode...':
|
'Plak of typ hier je broncode...':
|
||||||
'Incolla o digita qui il tuo codice sorgente...',
|
'Incolla o digita qui il tuo codice sorgente...',
|
||||||
'Overgeslagen': 'Saltata',
|
'Overgeslagen': 'Saltata',
|
||||||
|
|
@ -1374,13 +1379,18 @@ const _dutchSourceStrings = {
|
||||||
'Lijn': 'Linie',
|
'Lijn': 'Linie',
|
||||||
'Cirkel': 'Kreis',
|
'Cirkel': 'Kreis',
|
||||||
'CSV importeren': 'CSV importieren',
|
'CSV importeren': 'CSV importieren',
|
||||||
'Data (CSV: eerste rij = reeksnamen, eerste kolom = labels)': 'Daten (CSV: erste Zeile = Reihennamen, erste Spalte = Beschriftungen)',
|
'Data (CSV: eerste rij = reeksnamen, eerste kolom = labels)':
|
||||||
|
'Daten (CSV: erste Zeile = Reihennamen, erste Spalte = Beschriftungen)',
|
||||||
'Gekoppeld aan': 'Verknüpft mit',
|
'Gekoppeld aan': 'Verknüpft mit',
|
||||||
'Ontkoppelen': 'Trennen',
|
'Ontkoppelen': 'Trennen',
|
||||||
'Data in de slide opslaan, of als los CSV-bestand naast de presentatie bewaren?': 'Daten in der Folie speichern oder als separate CSV-Datei neben der Präsentation behalten?',
|
'Data in de slide opslaan, of als los CSV-bestand naast de presentatie bewaren?':
|
||||||
|
'Daten in der Folie speichern oder als separate CSV-Datei neben der Präsentation behalten?',
|
||||||
'In de slide': 'In der Folie',
|
'In de slide': 'In der Folie',
|
||||||
'Als CSV-bestand': 'Als CSV-Datei',
|
'Als CSV-bestand': 'Als CSV-Datei',
|
||||||
'Geen grafiekgegevens': 'Keine Diagrammdaten',
|
'Geen grafiekgegevens': 'Keine Diagrammdaten',
|
||||||
|
'Label': 'Beschriftung',
|
||||||
|
'Rij': 'Zeile',
|
||||||
|
'Reeks': 'Reihe',
|
||||||
'Plak of typ hier je broncode...':
|
'Plak of typ hier je broncode...':
|
||||||
'Quellcode hier einfügen oder eingeben...',
|
'Quellcode hier einfügen oder eingeben...',
|
||||||
'Overgeslagen': 'Übersprungen',
|
'Overgeslagen': 'Übersprungen',
|
||||||
|
|
@ -1564,13 +1574,18 @@ const _dutchSourceStrings = {
|
||||||
'Lijn': 'Lignes',
|
'Lijn': 'Lignes',
|
||||||
'Cirkel': 'Secteurs',
|
'Cirkel': 'Secteurs',
|
||||||
'CSV importeren': 'Importer un CSV',
|
'CSV importeren': 'Importer un CSV',
|
||||||
'Data (CSV: eerste rij = reeksnamen, eerste kolom = labels)': 'Données (CSV : 1re ligne = noms de séries, 1re colonne = libellés)',
|
'Data (CSV: eerste rij = reeksnamen, eerste kolom = labels)':
|
||||||
|
'Données (CSV : 1re ligne = noms de séries, 1re colonne = libellés)',
|
||||||
'Gekoppeld aan': 'Lié à',
|
'Gekoppeld aan': 'Lié à',
|
||||||
'Ontkoppelen': 'Dissocier',
|
'Ontkoppelen': 'Dissocier',
|
||||||
'Data in de slide opslaan, of als los CSV-bestand naast de presentatie bewaren?': 'Enregistrer les données dans la diapositive, ou les conserver dans un fichier CSV séparé à côté de la présentation ?',
|
'Data in de slide opslaan, of als los CSV-bestand naast de presentatie bewaren?':
|
||||||
|
'Enregistrer les données dans la diapositive, ou les conserver dans un fichier CSV séparé à côté de la présentation ?',
|
||||||
'In de slide': 'Dans la diapositive',
|
'In de slide': 'Dans la diapositive',
|
||||||
'Als CSV-bestand': 'Comme fichier CSV',
|
'Als CSV-bestand': 'Comme fichier CSV',
|
||||||
'Geen grafiekgegevens': 'Aucune donnée de graphique',
|
'Geen grafiekgegevens': 'Aucune donnée de graphique',
|
||||||
|
'Label': 'Libellé',
|
||||||
|
'Rij': 'Ligne',
|
||||||
|
'Reeks': 'Série',
|
||||||
'Plak of typ hier je broncode...':
|
'Plak of typ hier je broncode...':
|
||||||
'Collez ou tapez votre code source ici...',
|
'Collez ou tapez votre code source ici...',
|
||||||
'Overgeslagen': 'Ignorée',
|
'Overgeslagen': 'Ignorée',
|
||||||
|
|
@ -1753,13 +1768,18 @@ const _dutchSourceStrings = {
|
||||||
'Lijn': 'Líneas',
|
'Lijn': 'Líneas',
|
||||||
'Cirkel': 'Circular',
|
'Cirkel': 'Circular',
|
||||||
'CSV importeren': 'Importar CSV',
|
'CSV importeren': 'Importar CSV',
|
||||||
'Data (CSV: eerste rij = reeksnamen, eerste kolom = labels)': 'Datos (CSV: primera fila = nombres de series, primera columna = etiquetas)',
|
'Data (CSV: eerste rij = reeksnamen, eerste kolom = labels)':
|
||||||
|
'Datos (CSV: primera fila = nombres de series, primera columna = etiquetas)',
|
||||||
'Gekoppeld aan': 'Vinculado a',
|
'Gekoppeld aan': 'Vinculado a',
|
||||||
'Ontkoppelen': 'Desvincular',
|
'Ontkoppelen': 'Desvincular',
|
||||||
'Data in de slide opslaan, of als los CSV-bestand naast de presentatie bewaren?': '¿Guardar los datos en la diapositiva o mantenerlos como archivo CSV separado junto a la presentación?',
|
'Data in de slide opslaan, of als los CSV-bestand naast de presentatie bewaren?':
|
||||||
|
'¿Guardar los datos en la diapositiva o mantenerlos como archivo CSV separado junto a la presentación?',
|
||||||
'In de slide': 'En la diapositiva',
|
'In de slide': 'En la diapositiva',
|
||||||
'Als CSV-bestand': 'Como archivo CSV',
|
'Als CSV-bestand': 'Como archivo CSV',
|
||||||
'Geen grafiekgegevens': 'Sin datos de gráfico',
|
'Geen grafiekgegevens': 'Sin datos de gráfico',
|
||||||
|
'Label': 'Etiqueta',
|
||||||
|
'Rij': 'Fila',
|
||||||
|
'Reeks': 'Serie',
|
||||||
'Plak of typ hier je broncode...':
|
'Plak of typ hier je broncode...':
|
||||||
'Pega o escribe aquí tu código fuente...',
|
'Pega o escribe aquí tu código fuente...',
|
||||||
'Overgeslagen': 'Omitida',
|
'Overgeslagen': 'Omitida',
|
||||||
|
|
@ -1943,13 +1963,18 @@ const _dutchSourceStrings = {
|
||||||
'Lijn': 'Line',
|
'Lijn': 'Line',
|
||||||
'Cirkel': 'Sirkel',
|
'Cirkel': 'Sirkel',
|
||||||
'CSV importeren': 'CSV ymportearje',
|
'CSV importeren': 'CSV ymportearje',
|
||||||
'Data (CSV: eerste rij = reeksnamen, eerste kolom = labels)': 'Data (CSV: earste rige = rige-nammen, earste kolom = labels)',
|
'Data (CSV: eerste rij = reeksnamen, eerste kolom = labels)':
|
||||||
|
'Data (CSV: earste rige = rige-nammen, earste kolom = labels)',
|
||||||
'Gekoppeld aan': 'Keppele oan',
|
'Gekoppeld aan': 'Keppele oan',
|
||||||
'Ontkoppelen': 'Ûntkeppelje',
|
'Ontkoppelen': 'Ûntkeppelje',
|
||||||
'Data in de slide opslaan, of als los CSV-bestand naast de presentatie bewaren?': 'Data yn de slide bewarje, of as los CSV-bestân neist de presintaasje hâlde?',
|
'Data in de slide opslaan, of als los CSV-bestand naast de presentatie bewaren?':
|
||||||
|
'Data yn de slide bewarje, of as los CSV-bestân neist de presintaasje hâlde?',
|
||||||
'In de slide': 'Yn de slide',
|
'In de slide': 'Yn de slide',
|
||||||
'Als CSV-bestand': 'As CSV-bestân',
|
'Als CSV-bestand': 'As CSV-bestân',
|
||||||
'Geen grafiekgegevens': 'Gjin grafykgegevens',
|
'Geen grafiekgegevens': 'Gjin grafykgegevens',
|
||||||
|
'Label': 'Label',
|
||||||
|
'Rij': 'Rige',
|
||||||
|
'Reeks': 'Rige (data)',
|
||||||
'Plak of typ hier je broncode...': 'Plak of typ hjir dyn boarnekoade...',
|
'Plak of typ hier je broncode...': 'Plak of typ hjir dyn boarnekoade...',
|
||||||
'Overgeslagen': 'Oerslein',
|
'Overgeslagen': 'Oerslein',
|
||||||
'Kopiëren': 'Kopiearje',
|
'Kopiëren': 'Kopiearje',
|
||||||
|
|
@ -2133,13 +2158,18 @@ const _dutchSourceStrings = {
|
||||||
'Lijn': 'Liña',
|
'Lijn': 'Liña',
|
||||||
'Cirkel': 'Sirkel',
|
'Cirkel': 'Sirkel',
|
||||||
'CSV importeren': 'Importá CSV',
|
'CSV importeren': 'Importá CSV',
|
||||||
'Data (CSV: eerste rij = reeksnamen, eerste kolom = labels)': 'Dato (CSV: promé fila = nòmber di serie, promé kolom = etiketnan)',
|
'Data (CSV: eerste rij = reeksnamen, eerste kolom = labels)':
|
||||||
|
'Dato (CSV: promé fila = nòmber di serie, promé kolom = etiketnan)',
|
||||||
'Gekoppeld aan': 'Konektá na',
|
'Gekoppeld aan': 'Konektá na',
|
||||||
'Ontkoppelen': 'Deskonektá',
|
'Ontkoppelen': 'Deskonektá',
|
||||||
'Data in de slide opslaan, of als los CSV-bestand naast de presentatie bewaren?': 'Warda e dato den e slide, òf keda komo un archivo CSV separá banda di e presentashon?',
|
'Data in de slide opslaan, of als los CSV-bestand naast de presentatie bewaren?':
|
||||||
|
'Warda e dato den e slide, òf keda komo un archivo CSV separá banda di e presentashon?',
|
||||||
'In de slide': 'Den e slide',
|
'In de slide': 'Den e slide',
|
||||||
'Als CSV-bestand': 'Komo archivo CSV',
|
'Als CSV-bestand': 'Komo archivo CSV',
|
||||||
'Geen grafiekgegevens': 'Sin dato di gráfiko',
|
'Geen grafiekgegevens': 'Sin dato di gráfiko',
|
||||||
|
'Label': 'Etiket',
|
||||||
|
'Rij': 'Fila',
|
||||||
|
'Reeks': 'Serie',
|
||||||
'Plak of typ hier je broncode...': 'Pega òf tek bo código fuente akinan...',
|
'Plak of typ hier je broncode...': 'Pega òf tek bo código fuente akinan...',
|
||||||
'Overgeslagen': 'Saltá',
|
'Overgeslagen': 'Saltá',
|
||||||
'Kopiëren': 'Kopia',
|
'Kopiëren': 'Kopia',
|
||||||
|
|
@ -2312,13 +2342,18 @@ const _dutchSourceStringAdditions = {
|
||||||
'Lijn': 'Line',
|
'Lijn': 'Line',
|
||||||
'Cirkel': 'Pie',
|
'Cirkel': 'Pie',
|
||||||
'CSV importeren': 'Import CSV',
|
'CSV importeren': 'Import CSV',
|
||||||
'Data (CSV: eerste rij = reeksnamen, eerste kolom = labels)': 'Data (CSV: first row = series names, first column = labels)',
|
'Data (CSV: eerste rij = reeksnamen, eerste kolom = labels)':
|
||||||
|
'Data (CSV: first row = series names, first column = labels)',
|
||||||
'Gekoppeld aan': 'Linked to',
|
'Gekoppeld aan': 'Linked to',
|
||||||
'Ontkoppelen': 'Unlink',
|
'Ontkoppelen': 'Unlink',
|
||||||
'Data in de slide opslaan, of als los CSV-bestand naast de presentatie bewaren?': 'Store the data in the slide, or keep it as a separate CSV file next to the presentation?',
|
'Data in de slide opslaan, of als los CSV-bestand naast de presentatie bewaren?':
|
||||||
|
'Store the data in the slide, or keep it as a separate CSV file next to the presentation?',
|
||||||
'In de slide': 'In the slide',
|
'In de slide': 'In the slide',
|
||||||
'Als CSV-bestand': 'As a CSV file',
|
'Als CSV-bestand': 'As a CSV file',
|
||||||
'Geen grafiekgegevens': 'No chart data',
|
'Geen grafiekgegevens': 'No chart data',
|
||||||
|
'Label': 'Label',
|
||||||
|
'Rij': 'Row',
|
||||||
|
'Reeks': 'Series',
|
||||||
'Platte tekst': 'Plain text',
|
'Platte tekst': 'Plain text',
|
||||||
'Titel (optioneel)': 'Title (optional)',
|
'Titel (optioneel)': 'Title (optional)',
|
||||||
'HTML opent in elke browser zonder internet en rendert codeblokken, wiskunde en mermaid-diagrammen.':
|
'HTML opent in elke browser zonder internet en rendert codeblokken, wiskunde en mermaid-diagrammen.':
|
||||||
|
|
|
||||||
|
|
@ -36,10 +36,7 @@ class InkStroke {
|
||||||
'color': color,
|
'color': color,
|
||||||
'width': width,
|
'width': width,
|
||||||
'points': [
|
'points': [
|
||||||
for (final p in points) ...[
|
for (final p in points) ...[_round(p.dx), _round(p.dy)],
|
||||||
_round(p.dx),
|
|
||||||
_round(p.dy),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -64,8 +61,9 @@ class InkStroke {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encode/decode a per-slide map of strokes keyed by slide id.
|
/// Encode/decode a per-slide map of strokes keyed by slide id.
|
||||||
List<Map<String, dynamic>> encodeStrokes(List<InkStroke> strokes) =>
|
List<Map<String, dynamic>> encodeStrokes(List<InkStroke> strokes) => [
|
||||||
[for (final s in strokes) s.toJson()];
|
for (final s in strokes) s.toJson(),
|
||||||
|
];
|
||||||
|
|
||||||
List<InkStroke> decodeStrokes(List<dynamic> raw) => [
|
List<InkStroke> decodeStrokes(List<dynamic> raw) => [
|
||||||
for (final e in raw) InkStroke.fromJson(Map<String, dynamic>.from(e as Map)),
|
for (final e in raw) InkStroke.fromJson(Map<String, dynamic>.from(e as Map)),
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,8 @@ class ChartSpec {
|
||||||
.toList();
|
.toList();
|
||||||
if (lines.isEmpty) return (const [], const []);
|
if (lines.isEmpty) return (const [], const []);
|
||||||
|
|
||||||
List<String> cells(String line) => line.split(',').map((c) => c.trim()).toList();
|
List<String> cells(String line) =>
|
||||||
|
line.split(',').map((c) => c.trim()).toList();
|
||||||
|
|
||||||
final header = cells(lines.first);
|
final header = cells(lines.first);
|
||||||
final seriesNames = header.length > 1 ? header.sublist(1) : <String>[];
|
final seriesNames = header.length > 1 ? header.sublist(1) : <String>[];
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,10 @@ class AnnotationCodec {
|
||||||
|
|
||||||
/// Encode the id-keyed [annotations] for [slides] into a JSON string, or null
|
/// Encode the id-keyed [annotations] for [slides] into a JSON string, or null
|
||||||
/// when there is nothing to store.
|
/// when there is nothing to store.
|
||||||
static String? encode(List<Slide> slides, Map<String, List<InkStroke>> annotations) {
|
static String? encode(
|
||||||
|
List<Slide> slides,
|
||||||
|
Map<String, List<InkStroke>> annotations,
|
||||||
|
) {
|
||||||
final entries = <Map<String, dynamic>>[];
|
final entries = <Map<String, dynamic>>[];
|
||||||
for (var i = 0; i < slides.length; i++) {
|
for (var i = 0; i < slides.length; i++) {
|
||||||
final strokes = annotations[slides[i].id];
|
final strokes = annotations[slides[i].id];
|
||||||
|
|
@ -70,9 +73,7 @@ class AnnotationCodec {
|
||||||
final entry = Map<String, dynamic>.from(e as Map);
|
final entry = Map<String, dynamic>.from(e as Map);
|
||||||
final fp = entry['fp'] as String?;
|
final fp = entry['fp'] as String?;
|
||||||
final index = (entry['index'] as num?)?.toInt() ?? -1;
|
final index = (entry['index'] as num?)?.toInt() ?? -1;
|
||||||
final strokes = decodeStrokes(
|
final strokes = decodeStrokes((entry['strokes'] as List?) ?? const []);
|
||||||
(entry['strokes'] as List?) ?? const [],
|
|
||||||
);
|
|
||||||
if (strokes.isEmpty) continue;
|
if (strokes.isEmpty) continue;
|
||||||
|
|
||||||
int target = -1;
|
int target = -1;
|
||||||
|
|
|
||||||
|
|
@ -239,7 +239,9 @@ class FileService {
|
||||||
for (final s in deck.slides) {
|
for (final s in deck.slides) {
|
||||||
if (s.type != SlideType.chart) continue;
|
if (s.type != SlideType.chart) continue;
|
||||||
final src = ChartSpec.parse(s.customMarkdown).source;
|
final src = ChartSpec.parse(s.customMarkdown).source;
|
||||||
if (src == null || p.isAbsolute(src) || deck.projectPath == null) continue;
|
if (src == null || p.isAbsolute(src) || deck.projectPath == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
final from = File(p.join(deck.projectPath!, src));
|
final from = File(p.join(deck.projectPath!, src));
|
||||||
final toPath = p.join(destDir, src);
|
final toPath = p.join(destDir, src);
|
||||||
if (from.path == toPath || !from.existsSync()) continue;
|
if (from.path == toPath || !from.existsSync()) continue;
|
||||||
|
|
|
||||||
|
|
@ -135,8 +135,7 @@ class MarpHtmlService {
|
||||||
.replaceAll('<', '<')
|
.replaceAll('<', '<')
|
||||||
.replaceAll('>', '>');
|
.replaceAll('>', '>');
|
||||||
|
|
||||||
static String _color(int i) =>
|
static String _color(int i) => _chartPalette[i % _chartPalette.length];
|
||||||
_chartPalette[i % _chartPalette.length];
|
|
||||||
|
|
||||||
static String _chartSvg(ChartSpec spec) {
|
static String _chartSvg(ChartSpec spec) {
|
||||||
if (!spec.hasInlineData) {
|
if (!spec.hasInlineData) {
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,16 @@ import 'dart:io';
|
||||||
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import '../../l10n/app_localizations.dart';
|
import '../../l10n/app_localizations.dart';
|
||||||
import '../../models/chart.dart';
|
import '../../models/chart.dart';
|
||||||
import '../../models/slide.dart';
|
import '../../models/slide.dart';
|
||||||
import '_editor_field.dart';
|
import '_editor_field.dart';
|
||||||
|
|
||||||
/// Editor for a chart slide: type, title, and the data as a CSV-style table.
|
/// Editor for a chart slide: type, title, and an editable data grid. Data can
|
||||||
/// Data can be typed/pasted, imported from a CSV (inline), or linked to a
|
/// be entered directly in the interface, imported from a CSV (inline), or
|
||||||
/// CSV file kept next to the deck (the living source).
|
/// linked to a CSV file kept in the deck's data/ directory (the living source).
|
||||||
class ChartEditor extends StatefulWidget {
|
class ChartEditor extends StatefulWidget {
|
||||||
final Slide slide;
|
final Slide slide;
|
||||||
final ValueChanged<Slide> onUpdate;
|
final ValueChanged<Slide> onUpdate;
|
||||||
|
|
@ -30,10 +31,20 @@ class ChartEditor extends StatefulWidget {
|
||||||
|
|
||||||
class _ChartEditorState extends State<ChartEditor> {
|
class _ChartEditorState extends State<ChartEditor> {
|
||||||
late final TextEditingController _title;
|
late final TextEditingController _title;
|
||||||
late final TextEditingController _csv;
|
|
||||||
late ChartType _type;
|
late ChartType _type;
|
||||||
String? _source;
|
String? _source;
|
||||||
|
|
||||||
|
// Editable grid model (strings while editing).
|
||||||
|
List<String> _xLabels = [];
|
||||||
|
List<String> _seriesNames = [];
|
||||||
|
List<List<String>> _values = []; // [row][col]
|
||||||
|
|
||||||
|
// Bumped on structural changes so cell fields rebuild with fresh values.
|
||||||
|
int _rev = 0;
|
||||||
|
|
||||||
|
static const _labelW = 130.0;
|
||||||
|
static const _cellW = 96.0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
@ -42,50 +53,99 @@ class _ChartEditorState extends State<ChartEditor> {
|
||||||
_source = spec.source;
|
_source = spec.source;
|
||||||
_title = TextEditingController(text: spec.title);
|
_title = TextEditingController(text: spec.title);
|
||||||
_title.addListener(_emit);
|
_title.addListener(_emit);
|
||||||
_csv = TextEditingController(text: _specToCsv(spec));
|
_loadFromSpec(spec);
|
||||||
_csv.addListener(_emit);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
void _loadFromSpec(ChartSpec spec) {
|
||||||
void dispose() {
|
if (spec.hasInlineData) {
|
||||||
_title.dispose();
|
_seriesNames = [for (final s in spec.series) s.name];
|
||||||
_csv.dispose();
|
_xLabels = List<String>.from(spec.x);
|
||||||
super.dispose();
|
_values = [
|
||||||
}
|
for (var r = 0; r < spec.x.length; r++)
|
||||||
|
[
|
||||||
/// Render the spec's inline data back to the CSV-style table text.
|
for (final s in spec.series)
|
||||||
static String _specToCsv(ChartSpec spec) {
|
r < s.data.length ? _fmt(s.data[r]) : '',
|
||||||
if (!spec.hasInlineData) return '';
|
],
|
||||||
final header = ['', ...spec.series.map((s) => s.name)].join(', ');
|
];
|
||||||
final rows = <String>[header];
|
} else {
|
||||||
for (var i = 0; i < spec.x.length; i++) {
|
// Sensible empty starting grid.
|
||||||
rows.add(
|
_seriesNames = ['Reeks 1'];
|
||||||
[
|
_xLabels = ['', '', ''];
|
||||||
spec.x[i],
|
_values = List.generate(3, (_) => ['']);
|
||||||
...spec.series.map(
|
|
||||||
(s) => i < s.data.length ? _fmt(s.data[i]) : '',
|
|
||||||
),
|
|
||||||
].join(', '),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return rows.join('\n');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static String _fmt(double v) =>
|
static String _fmt(double v) =>
|
||||||
v == v.roundToDouble() ? v.toInt().toString() : v.toString();
|
v == v.roundToDouble() ? v.toInt().toString() : v.toString();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_title.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
void _emit() {
|
void _emit() {
|
||||||
final parsed = parseCsv(_csv.text);
|
final series = <ChartSeries>[
|
||||||
|
for (var c = 0; c < _seriesNames.length; c++)
|
||||||
|
ChartSeries(
|
||||||
|
name: _seriesNames[c],
|
||||||
|
data: [
|
||||||
|
for (var r = 0; r < _values.length; r++)
|
||||||
|
double.tryParse(
|
||||||
|
(c < _values[r].length ? _values[r][c] : '')
|
||||||
|
.trim()
|
||||||
|
.replaceAll(',', '.'),
|
||||||
|
) ??
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
];
|
||||||
final spec = ChartSpec(
|
final spec = ChartSpec(
|
||||||
type: _type,
|
type: _type,
|
||||||
title: _title.text,
|
title: _title.text,
|
||||||
source: _source,
|
source: _source,
|
||||||
x: parsed.$1,
|
x: List<String>.from(_xLabels),
|
||||||
series: parsed.$2,
|
series: series,
|
||||||
);
|
);
|
||||||
widget.onUpdate(widget.slide.copyWith(customMarkdown: spec.toBlock()));
|
widget.onUpdate(widget.slide.copyWith(customMarkdown: spec.toBlock()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _bump() => setState(() => _rev++);
|
||||||
|
|
||||||
|
void _addColumn() {
|
||||||
|
_seriesNames.add('Reeks ${_seriesNames.length + 1}');
|
||||||
|
for (final row in _values) {
|
||||||
|
row.add('');
|
||||||
|
}
|
||||||
|
_bump();
|
||||||
|
_emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _removeColumn(int c) {
|
||||||
|
if (_seriesNames.length <= 1) return;
|
||||||
|
_seriesNames.removeAt(c);
|
||||||
|
for (final row in _values) {
|
||||||
|
if (c < row.length) row.removeAt(c);
|
||||||
|
}
|
||||||
|
_bump();
|
||||||
|
_emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _addRow() {
|
||||||
|
_xLabels.add('');
|
||||||
|
_values.add(List<String>.filled(_seriesNames.length, '', growable: true));
|
||||||
|
_bump();
|
||||||
|
_emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _removeRow(int r) {
|
||||||
|
if (_xLabels.length <= 1) return;
|
||||||
|
_xLabels.removeAt(r);
|
||||||
|
_values.removeAt(r);
|
||||||
|
_bump();
|
||||||
|
_emit();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _importCsv() async {
|
Future<void> _importCsv() async {
|
||||||
final result = await FilePicker.pickFiles(
|
final result = await FilePicker.pickFiles(
|
||||||
type: FileType.custom,
|
type: FileType.custom,
|
||||||
|
|
@ -99,8 +159,6 @@ class _ChartEditorState extends State<ChartEditor> {
|
||||||
: (file.path != null ? await File(file.path!).readAsString() : null);
|
: (file.path != null ? await File(file.path!).readAsString() : null);
|
||||||
if (text == null) return;
|
if (text == null) return;
|
||||||
|
|
||||||
// Offer to keep the CSV as an external, living source when the deck is
|
|
||||||
// saved (so it can be re-edited in a spreadsheet); otherwise inline it.
|
|
||||||
var asFile = false;
|
var asFile = false;
|
||||||
if (widget.projectPath != null && mounted) {
|
if (widget.projectPath != null && mounted) {
|
||||||
asFile =
|
asFile =
|
||||||
|
|
@ -131,15 +189,24 @@ class _ChartEditorState extends State<ChartEditor> {
|
||||||
String? source;
|
String? source;
|
||||||
if (asFile && widget.projectPath != null) {
|
if (asFile && widget.projectPath != null) {
|
||||||
final name = p.basename(file.name);
|
final name = p.basename(file.name);
|
||||||
final dir = Directory(p.join(widget.projectPath!, 'data'));
|
final dir = Directory(p.join(widget.projectPath!, chartDataDirName));
|
||||||
await dir.create(recursive: true);
|
await dir.create(recursive: true);
|
||||||
await File(p.join(dir.path, name)).writeAsString(text, flush: true);
|
await File(p.join(dir.path, name)).writeAsString(text, flush: true);
|
||||||
source = 'data/$name';
|
source = '$chartDataDirName/$name';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final parsed = parseCsv(text);
|
||||||
setState(() {
|
setState(() {
|
||||||
_source = source;
|
_source = source;
|
||||||
_csv.text = text;
|
_xLabels = parsed.$1.isEmpty ? [''] : parsed.$1;
|
||||||
|
_seriesNames = parsed.$2.isEmpty
|
||||||
|
? ['Reeks 1']
|
||||||
|
: [for (final s in parsed.$2) s.name];
|
||||||
|
_values = [
|
||||||
|
for (var r = 0; r < _xLabels.length; r++)
|
||||||
|
[for (final s in parsed.$2) r < s.data.length ? _fmt(s.data[r]) : ''],
|
||||||
|
];
|
||||||
|
_rev++;
|
||||||
});
|
});
|
||||||
_emit();
|
_emit();
|
||||||
}
|
}
|
||||||
|
|
@ -204,22 +271,9 @@ class _ChartEditorState extends State<ChartEditor> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
l10n.d('Data (CSV: eerste rij = reeksnamen, eerste kolom = labels)'),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Color(0xFF64748B),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (linked)
|
if (linked)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 6),
|
padding: const EdgeInsets.only(top: 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.link, size: 14, color: Color(0xFF0369A1)),
|
const Icon(Icons.link, size: 14, color: Color(0xFF0369A1)),
|
||||||
|
|
@ -241,25 +295,167 @@ class _ChartEditorState extends State<ChartEditor> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: SingleChildScrollView(
|
||||||
controller: _csv,
|
child: SingleChildScrollView(
|
||||||
readOnly: linked,
|
scrollDirection: Axis.horizontal,
|
||||||
maxLines: null,
|
child: _grid(enabled: !linked),
|
||||||
expands: true,
|
|
||||||
textAlignVertical: TextAlignVertical.top,
|
|
||||||
style: const TextStyle(fontFamily: 'monospace', fontSize: 13),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: ', 2025, 2026\nQ1, 10, 12\nQ2, 14, 9',
|
|
||||||
alignLabelWithHint: true,
|
|
||||||
filled: linked,
|
|
||||||
fillColor: linked ? const Color(0xFFF1F5F9) : null,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (!linked) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: _addRow,
|
||||||
|
icon: const Icon(Icons.add, size: 16),
|
||||||
|
label: Text(l10n.d('Rij')),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: _addColumn,
|
||||||
|
icon: const Icon(Icons.add, size: 16),
|
||||||
|
label: Text(l10n.d('Reeks')),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _grid({required bool enabled}) {
|
||||||
|
final cols = _seriesNames.length;
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Header row: empty label cell + series name fields.
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: _labelW,
|
||||||
|
child: _headerHint(context.l10n.d('Label')),
|
||||||
|
),
|
||||||
|
for (var c = 0; c < cols; c++)
|
||||||
|
SizedBox(
|
||||||
|
width: _cellW,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _cell(
|
||||||
|
key: ValueKey('s-$_rev-$c'),
|
||||||
|
value: _seriesNames[c],
|
||||||
|
enabled: enabled,
|
||||||
|
onChanged: (v) => _seriesNames[c] = v,
|
||||||
|
bold: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (enabled && cols > 1)
|
||||||
|
_iconBtn(Icons.close, () => _removeColumn(c)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
// Data rows.
|
||||||
|
for (var r = 0; r < _xLabels.length; r++)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: _labelW,
|
||||||
|
child: _cell(
|
||||||
|
key: ValueKey('x-$_rev-$r'),
|
||||||
|
value: _xLabels[r],
|
||||||
|
enabled: enabled,
|
||||||
|
onChanged: (v) => _xLabels[r] = v,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
for (var c = 0; c < cols; c++)
|
||||||
|
SizedBox(
|
||||||
|
width: _cellW,
|
||||||
|
child: _cell(
|
||||||
|
key: ValueKey('v-$_rev-$r-$c'),
|
||||||
|
value: c < _values[r].length ? _values[r][c] : '',
|
||||||
|
enabled: enabled,
|
||||||
|
number: true,
|
||||||
|
onChanged: (v) {
|
||||||
|
while (_values[r].length <= c) {
|
||||||
|
_values[r].add('');
|
||||||
|
}
|
||||||
|
_values[r][c] = v;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (enabled && _xLabels.length > 1)
|
||||||
|
_iconBtn(Icons.close, () => _removeRow(r)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _headerHint(String text) => Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 4, bottom: 4),
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF94A3B8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _cell({
|
||||||
|
required Key key,
|
||||||
|
required String value,
|
||||||
|
required bool enabled,
|
||||||
|
required ValueChanged<String> onChanged,
|
||||||
|
bool number = false,
|
||||||
|
bool bold = false,
|
||||||
|
}) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||||
|
child: TextFormField(
|
||||||
|
key: key,
|
||||||
|
initialValue: value,
|
||||||
|
enabled: enabled,
|
||||||
|
onChanged: (v) {
|
||||||
|
onChanged(v);
|
||||||
|
_emit();
|
||||||
|
},
|
||||||
|
keyboardType: number
|
||||||
|
? const TextInputType.numberWithOptions(decimal: true, signed: true)
|
||||||
|
: TextInputType.text,
|
||||||
|
inputFormatters: number
|
||||||
|
? [FilteringTextInputFormatter.allow(RegExp(r'[0-9.,\-]'))]
|
||||||
|
: null,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: bold ? FontWeight.w600 : FontWeight.normal,
|
||||||
|
),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _iconBtn(IconData icon, VoidCallback onTap) => IconButton(
|
||||||
|
onPressed: onTap,
|
||||||
|
icon: Icon(icon, size: 14),
|
||||||
|
color: const Color(0xFF94A3B8),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(minWidth: 24, minHeight: 24),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -708,9 +708,7 @@ class _SlideTlpControl extends StatelessWidget {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
value: level,
|
value: level,
|
||||||
child: Text(
|
child: Text(
|
||||||
level == TlpLevel.none
|
level == TlpLevel.none ? l10n.d('Geen') : level.menuLabel,
|
||||||
? l10n.d('Geen')
|
|
||||||
: level.menuLabel,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,9 @@ class _AudienceWindowAppState extends State<AudienceWindowApp> {
|
||||||
final m = Map<String, dynamic>.from(call.arguments as Map);
|
final m = Map<String, dynamic>.from(call.arguments as Map);
|
||||||
final i = (m['index'] as num?)?.toInt();
|
final i = (m['index'] as num?)?.toInt();
|
||||||
if (i == null || !mounted) return null;
|
if (i == null || !mounted) return null;
|
||||||
setState(() => _ink[i] = decodeStrokes((m['strokes'] as List?) ?? const []));
|
setState(
|
||||||
|
() => _ink[i] = decodeStrokes((m['strokes'] as List?) ?? const []),
|
||||||
|
);
|
||||||
case 'laser':
|
case 'laser':
|
||||||
final m = Map<String, dynamic>.from(call.arguments as Map);
|
final m = Map<String, dynamic>.from(call.arguments as Map);
|
||||||
final i = (m['index'] as num?)?.toInt();
|
final i = (m['index'] as num?)?.toInt();
|
||||||
|
|
|
||||||
|
|
@ -1150,10 +1150,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
('P', l10n.d('Presenter view (notities, klok)')),
|
('P', l10n.d('Presenter view (notities, klok)')),
|
||||||
('S', l10n.d('Scherm wisselen (meerdere schermen)')),
|
('S', l10n.d('Scherm wisselen (meerdere schermen)')),
|
||||||
('B · W', l10n.d('Zwart · wit scherm')),
|
('B · W', l10n.d('Zwart · wit scherm')),
|
||||||
(
|
('D · T · E', l10n.d('Pen · markeerstift · gum')),
|
||||||
'D · T · E',
|
|
||||||
l10n.d('Pen · markeerstift · gum'),
|
|
||||||
),
|
|
||||||
('X · C', l10n.d('Laser · annotaties wissen')),
|
('X · C', l10n.d('Laser · annotaties wissen')),
|
||||||
('R', l10n.d('Verstreken tijd resetten')),
|
('R', l10n.d('Verstreken tijd resetten')),
|
||||||
('A', l10n.d('Automatische modus aan/uit')),
|
('A', l10n.d('Automatische modus aan/uit')),
|
||||||
|
|
|
||||||
|
|
@ -2187,8 +2187,9 @@ class _ChartPreview extends StatelessWidget {
|
||||||
0xFF84CC16,
|
0xFF84CC16,
|
||||||
];
|
];
|
||||||
|
|
||||||
Color _seriesColor(int i) =>
|
Color _seriesColor(int i) => i == 0
|
||||||
i == 0 ? _hexColor(profile.accentColor) : Color(_palette[i % _palette.length]);
|
? _hexColor(profile.accentColor)
|
||||||
|
: Color(_palette[i % _palette.length]);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
|
||||||
|
|
@ -52,9 +52,12 @@ void main() {
|
||||||
test('re-anchors strokes to the matching slide after reordering', () {
|
test('re-anchors strokes to the matching slide after reordering', () {
|
||||||
final a = Slide.create(SlideType.bullets).copyWith(title: 'A');
|
final a = Slide.create(SlideType.bullets).copyWith(title: 'A');
|
||||||
final b = Slide.create(SlideType.bullets).copyWith(title: 'B');
|
final b = Slide.create(SlideType.bullets).copyWith(title: 'B');
|
||||||
final json = AnnotationCodec.encode([a, b], {
|
final json = AnnotationCodec.encode(
|
||||||
a.id: [stroke()],
|
[a, b],
|
||||||
})!;
|
{
|
||||||
|
a.id: [stroke()],
|
||||||
|
},
|
||||||
|
)!;
|
||||||
|
|
||||||
// Reload parses fresh slides with NEW ids but identical content, in a
|
// Reload parses fresh slides with NEW ids but identical content, in a
|
||||||
// different order.
|
// different order.
|
||||||
|
|
@ -67,9 +70,12 @@ void main() {
|
||||||
|
|
||||||
test('drops strokes when the slide content changed', () {
|
test('drops strokes when the slide content changed', () {
|
||||||
final a = Slide.create(SlideType.bullets).copyWith(title: 'A');
|
final a = Slide.create(SlideType.bullets).copyWith(title: 'A');
|
||||||
final json = AnnotationCodec.encode([a], {
|
final json = AnnotationCodec.encode(
|
||||||
a.id: [stroke()],
|
[a],
|
||||||
})!;
|
{
|
||||||
|
a.id: [stroke()],
|
||||||
|
},
|
||||||
|
)!;
|
||||||
final edited = Slide.create(
|
final edited = Slide.create(
|
||||||
SlideType.bullets,
|
SlideType.bullets,
|
||||||
).copyWith(title: 'A (changed)');
|
).copyWith(title: 'A (changed)');
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ void main() {
|
||||||
'Accent / bullets',
|
'Accent / bullets',
|
||||||
'Bullet',
|
'Bullet',
|
||||||
'Coverflow',
|
'Coverflow',
|
||||||
|
'Label',
|
||||||
'Logo',
|
'Logo',
|
||||||
'Logo px',
|
'Logo px',
|
||||||
'PREVIEW',
|
'PREVIEW',
|
||||||
|
|
@ -78,6 +79,7 @@ void main() {
|
||||||
'Accent / bullets',
|
'Accent / bullets',
|
||||||
'Bullet',
|
'Bullet',
|
||||||
'Coverflow',
|
'Coverflow',
|
||||||
|
'Label',
|
||||||
'Logo',
|
'Logo',
|
||||||
'Logo px',
|
'Logo px',
|
||||||
'PREVIEW',
|
'PREVIEW',
|
||||||
|
|
|
||||||
|
|
@ -278,13 +278,16 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('chart slide with a source keeps only the reference in markdown', () {
|
test('chart slide with a source keeps only the reference in markdown', () {
|
||||||
const block = '{"type":"line","source":"data/omzet.csv",'
|
const block =
|
||||||
|
'{"type":"line","source":"data/omzet.csv",'
|
||||||
'"x":["Q1"],"series":[{"name":"2025","data":[10]}]}';
|
'"x":["Q1"],"series":[{"name":"2025","data":[10]}]}';
|
||||||
final service = MarkdownService();
|
final service = MarkdownService();
|
||||||
final md = service.generateDeck(
|
final md = service.generateDeck(
|
||||||
Deck(
|
Deck(
|
||||||
title: 'Demo',
|
title: 'Demo',
|
||||||
slides: [Slide.create(SlideType.chart).copyWith(customMarkdown: block)],
|
slides: [
|
||||||
|
Slide.create(SlideType.chart).copyWith(customMarkdown: block),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
// The stored markdown references the CSV but does not inline the data.
|
// The stored markdown references the CSV but does not inline the data.
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,14 @@ void main() {
|
||||||
|
|
||||||
test('a slide stricter than the presentation is withheld', () {
|
test('a slide stricter than the presentation is withheld', () {
|
||||||
// Presentation at GREEN: CLEAR/GREEN shown, AMBER/RED withheld.
|
// Presentation at GREEN: CLEAR/GREEN shown, AMBER/RED withheld.
|
||||||
expect(slideVisibleAtTlp(slideAt(TlpLevel.clear), TlpLevel.green), isTrue);
|
expect(
|
||||||
expect(slideVisibleAtTlp(slideAt(TlpLevel.green), TlpLevel.green), isTrue);
|
slideVisibleAtTlp(slideAt(TlpLevel.clear), TlpLevel.green),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
slideVisibleAtTlp(slideAt(TlpLevel.green), TlpLevel.green),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
slideVisibleAtTlp(slideAt(TlpLevel.amber), TlpLevel.green),
|
slideVisibleAtTlp(slideAt(TlpLevel.amber), TlpLevel.green),
|
||||||
isFalse,
|
isFalse,
|
||||||
|
|
|
||||||
157
tool/check_licenses.dart
Normal file
157
tool/check_licenses.dart
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
// Verifies that every resolved Dart/Flutter dependency (direct and transitive)
|
||||||
|
// uses a recognised open-source licence. Bundled JS/font assets are documented
|
||||||
|
// separately in THIRD_PARTY_NOTICES.md.
|
||||||
|
//
|
||||||
|
// dart run tool/check_licenses.dart (or: make licenses)
|
||||||
|
//
|
||||||
|
// Exits non-zero if any package has an unrecognised or non-open-source licence,
|
||||||
|
// so it can run in CI.
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
/// Licence families we accept (all OSI-approved / open source).
|
||||||
|
const allowed = <String>{
|
||||||
|
'MIT',
|
||||||
|
'BSD',
|
||||||
|
'BSD-2-Clause',
|
||||||
|
'BSD-3-Clause',
|
||||||
|
'Apache-2.0',
|
||||||
|
'MPL-2.0',
|
||||||
|
'ISC',
|
||||||
|
'Zlib',
|
||||||
|
'BSL-1.0',
|
||||||
|
'Unlicense',
|
||||||
|
'OFL-1.1',
|
||||||
|
'CC0-1.0',
|
||||||
|
'EUPL-1.2', // OciDeck itself
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Packages that ship as part of the Flutter SDK without their own LICENSE file
|
||||||
|
/// (covered by the Flutter SDK licence, BSD-3-Clause).
|
||||||
|
const sdkNoLicenseOk = <String>{
|
||||||
|
'flutter',
|
||||||
|
'flutter_test',
|
||||||
|
'flutter_localizations',
|
||||||
|
'flutter_web_plugins',
|
||||||
|
'flutter_driver',
|
||||||
|
'integration_test',
|
||||||
|
'sky_engine',
|
||||||
|
};
|
||||||
|
|
||||||
|
String classify(String text) {
|
||||||
|
final t = text.toLowerCase();
|
||||||
|
if (t.contains('european union public licence') ||
|
||||||
|
t.contains('european union public license')) {
|
||||||
|
return 'EUPL-1.2';
|
||||||
|
}
|
||||||
|
// Note: the MPL-2.0 and EUPL texts *reference* the GNU licences in their
|
||||||
|
// compatibility clauses, so detect those reciprocal-but-distinct licences
|
||||||
|
// before the GNU keywords to avoid false positives.
|
||||||
|
if (t.contains('mozilla public license')) return 'MPL-2.0';
|
||||||
|
if (t.contains('apache license')) return 'Apache-2.0';
|
||||||
|
if (t.contains('gnu affero')) return 'AGPL';
|
||||||
|
if (t.contains('gnu lesser general public')) return 'LGPL';
|
||||||
|
if (t.contains('gnu general public')) return 'GPL';
|
||||||
|
if (t.contains('sil open font')) return 'OFL-1.1';
|
||||||
|
if (t.contains('isc license')) return 'ISC';
|
||||||
|
if (t.contains('boost software license')) return 'BSL-1.0';
|
||||||
|
if (t.contains('the unlicense')) return 'Unlicense';
|
||||||
|
if (t.contains('cc0')) return 'CC0-1.0';
|
||||||
|
if (t.contains('permission is hereby granted, free of charge')) return 'MIT';
|
||||||
|
if (t.contains('mit license')) return 'MIT';
|
||||||
|
if (t.contains('bsd 3-clause') ||
|
||||||
|
(t.contains('redistribution and use') &&
|
||||||
|
t.contains('neither the name'))) {
|
||||||
|
return 'BSD-3-Clause';
|
||||||
|
}
|
||||||
|
if (t.contains('redistribution and use in source and binary forms')) {
|
||||||
|
return 'BSD';
|
||||||
|
}
|
||||||
|
if (t.contains('bsd')) return 'BSD';
|
||||||
|
return 'UNKNOWN';
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
final cfgFile = File('.dart_tool/package_config.json');
|
||||||
|
if (!cfgFile.existsSync()) {
|
||||||
|
stderr.writeln(
|
||||||
|
'No .dart_tool/package_config.json — run "flutter pub get" first.',
|
||||||
|
);
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
final base = cfgFile.absolute.parent.uri;
|
||||||
|
final cfg = jsonDecode(cfgFile.readAsStringSync()) as Map<String, dynamic>;
|
||||||
|
|
||||||
|
final rows = <(String, String)>[];
|
||||||
|
final problems = <String>[];
|
||||||
|
|
||||||
|
for (final pkg in (cfg['packages'] as List)) {
|
||||||
|
final name = pkg['name'] as String;
|
||||||
|
final rootUri = pkg['rootUri'] as String;
|
||||||
|
final resolved = rootUri.startsWith('file:')
|
||||||
|
? Uri.parse(rootUri.endsWith('/') ? rootUri : '$rootUri/')
|
||||||
|
: base.resolve(rootUri.endsWith('/') ? rootUri : '$rootUri/');
|
||||||
|
final root = Directory.fromUri(resolved);
|
||||||
|
|
||||||
|
File? lic;
|
||||||
|
for (final c in [
|
||||||
|
'LICENSE',
|
||||||
|
'LICENSE.md',
|
||||||
|
'LICENSE.txt',
|
||||||
|
'COPYING',
|
||||||
|
'license',
|
||||||
|
]) {
|
||||||
|
final f = File('${root.path}/$c');
|
||||||
|
if (f.existsSync()) {
|
||||||
|
lic = f;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String kind;
|
||||||
|
if (name == 'ocideck') {
|
||||||
|
kind = 'EUPL-1.2';
|
||||||
|
} else if (lic == null) {
|
||||||
|
kind = sdkNoLicenseOk.contains(name)
|
||||||
|
? 'BSD-3-Clause (Flutter SDK)'
|
||||||
|
: 'NO LICENSE FILE';
|
||||||
|
} else {
|
||||||
|
final txt = lic.readAsStringSync();
|
||||||
|
kind = classify(txt.length > 6000 ? txt.substring(0, 6000) : txt);
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.add((name, kind));
|
||||||
|
final family = kind.split(' ').first;
|
||||||
|
if (!allowed.contains(family)) problems.add('$name → $kind');
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.sort((a, b) => a.$1.compareTo(b.$1));
|
||||||
|
|
||||||
|
final counts = <String, int>{};
|
||||||
|
for (final r in rows) {
|
||||||
|
final k = r.$2.split(' ').first;
|
||||||
|
counts[k] = (counts[k] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
stdout.writeln('Scanned ${rows.length} packages:');
|
||||||
|
final keys = counts.keys.toList()..sort((a, b) => counts[b]! - counts[a]!);
|
||||||
|
for (final k in keys) {
|
||||||
|
stdout.writeln(' ${counts[k]!.toString().padLeft(3)} $k');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (problems.isEmpty) {
|
||||||
|
stdout.writeln(
|
||||||
|
'\nOK — all dependencies use recognised open-source licences.',
|
||||||
|
);
|
||||||
|
stdout.writeln(
|
||||||
|
'Bundled JS/font assets are listed in THIRD_PARTY_NOTICES.md.',
|
||||||
|
);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
stderr.writeln('\nPROBLEM — ${problems.length} package(s) need review:');
|
||||||
|
for (final p in problems) {
|
||||||
|
stderr.writeln(' $p');
|
||||||
|
}
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue