117 lines
3.5 KiB
Dart
117 lines
3.5 KiB
Dart
/// A backend-independent emergency notice, loaded from a foreign server URL.
|
|
///
|
|
/// Deliberately decoupled from MarianumConnect: if that backend is unreachable
|
|
/// (the disaster case this exists for), a plain text file on any other host can
|
|
/// still surface a message. The file is a small YAML-ish frontmatter followed
|
|
/// by a free Markdown body, so it stays hand-writable in an emergency — no JSON
|
|
/// escaping of the (multi-line) content.
|
|
///
|
|
/// See [parse] for the exact format.
|
|
class EmergencyNotice {
|
|
/// Whether the notice can be dismissed. `false` renders it full-screen and
|
|
/// blocks back/barrier taps.
|
|
final bool dismissible;
|
|
|
|
/// Optional heading shown above the body.
|
|
final String? title;
|
|
|
|
/// Markdown body (the message itself).
|
|
final String body;
|
|
|
|
const EmergencyNotice({
|
|
required this.dismissible,
|
|
required this.title,
|
|
required this.body,
|
|
});
|
|
|
|
/// Parses the raw file into a displayable notice, or returns `null` when
|
|
/// there is nothing to show. Never throws — any malformed input yields `null`
|
|
/// so a broken file can never break the app.
|
|
///
|
|
/// Format:
|
|
/// ```
|
|
/// ---
|
|
/// active: true
|
|
/// # dismissible: false <- comment lines (# ...) are ignored
|
|
/// dismissible: true
|
|
/// title: Störung
|
|
/// ---
|
|
/// # Markdown heading
|
|
/// Free **markdown** body.
|
|
/// ```
|
|
///
|
|
/// Rules:
|
|
/// - The frontmatter is everything between the first `---` line and the next
|
|
/// `---` line. Both delimiters are required.
|
|
/// - Frontmatter entries are `key: value`; a line whose trimmed form starts
|
|
/// with `#` is a comment. Unknown keys are ignored.
|
|
/// - `active` (default `false`) must be explicitly true, else `null`.
|
|
/// - `dismissible` defaults to `true`.
|
|
/// - `title` is optional. The rest after the closing `---` is the Markdown
|
|
/// body; an empty body yields `null`.
|
|
static EmergencyNotice? parse(String raw) {
|
|
final lines = raw
|
|
.replaceAll('\r\n', '\n')
|
|
.replaceAll('\r', '\n')
|
|
.split('\n');
|
|
|
|
// Locate the opening `---` (skipping any leading blank lines).
|
|
var i = 0;
|
|
while (i < lines.length && lines[i].trim().isEmpty) {
|
|
i++;
|
|
}
|
|
if (i >= lines.length || lines[i].trim() != '---') return null;
|
|
final openIndex = i;
|
|
|
|
// Locate the closing `---`.
|
|
var closeIndex = -1;
|
|
for (var j = openIndex + 1; j < lines.length; j++) {
|
|
if (lines[j].trim() == '---') {
|
|
closeIndex = j;
|
|
break;
|
|
}
|
|
}
|
|
if (closeIndex == -1) return null;
|
|
|
|
final meta = <String, String>{};
|
|
for (var j = openIndex + 1; j < closeIndex; j++) {
|
|
final line = lines[j].trim();
|
|
if (line.isEmpty || line.startsWith('#')) continue;
|
|
final sep = line.indexOf(':');
|
|
if (sep <= 0) continue;
|
|
final key = line.substring(0, sep).trim().toLowerCase();
|
|
final value = line.substring(sep + 1).trim();
|
|
meta[key] = value;
|
|
}
|
|
|
|
if (_parseBool(meta['active']) != true) return null;
|
|
|
|
final body = lines.sublist(closeIndex + 1).join('\n').trim();
|
|
if (body.isEmpty) return null;
|
|
|
|
final title = meta['title'];
|
|
return EmergencyNotice(
|
|
dismissible: _parseBool(meta['dismissible']) ?? true,
|
|
title: (title == null || title.isEmpty) ? null : title,
|
|
body: body,
|
|
);
|
|
}
|
|
|
|
static bool? _parseBool(String? value) {
|
|
switch (value?.trim().toLowerCase()) {
|
|
case 'true':
|
|
case 'yes':
|
|
case '1':
|
|
case 'on':
|
|
return true;
|
|
case 'false':
|
|
case 'no':
|
|
case '0':
|
|
case 'off':
|
|
return false;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
}
|