/// A backend-independent emergency notice loaded from a foreign server URL — /// frontmatter (control fields) plus a free Markdown body, so it stays /// hand-writable in an outage. See [parse] for the format. class EmergencyNotice { /// `false` renders full-screen and blocks back/barrier taps. final bool dismissible; final String? title; final String body; const EmergencyNotice({ required this.dismissible, required this.title, required this.body, }); /// Parses the raw file, or returns `null` when there is nothing to show. /// Never throws — malformed input yields `null` so a broken file can't break /// the app. /// /// ``` /// --- /// active: true # required truthy, else null; # lines are comments /// dismissible: true # default true /// title: Störung # optional /// --- /// Free **markdown** body (everything after the closing ---). /// ``` static EmergencyNotice? parse(String raw) { final lines = raw .replaceAll('\r\n', '\n') .replaceAll('\r', '\n') .split('\n'); var i = 0; while (i < lines.length && lines[i].trim().isEmpty) { i++; } if (i >= lines.length || lines[i].trim() != '---') return null; final openIndex = i; 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; } } }