/// 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 = {}; 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; } } }