/// The addresses of the client's own pages, under the `mews:` scheme. /// One spelling of the scheme, shared by the navigator and the page builders. library; /// Internal addresses come in two shapes. A page address is a navigation and /// joins the walk history; an action address under `do/` changes something and /// leaves the reader where they are. /// /// View state (sort, search, tag filter) rides in the query string rather than /// in a signal, because swipe previews are built from the address alone and /// going back should undo a sort. abstract final class MewsUrls { static const scheme = 'mews'; static const hub = 'mews:hub'; static const bookmarks = 'mews:bookmarks'; static const subscriptions = 'mews:subscriptions'; static const history = 'mews:history'; static const settings = 'mews:settings'; /// Whether [url] belongs to the internal scheme. static bool isInternal(String url) => Uri.tryParse(url)?.scheme == scheme; /// Whether [url] changes something instead of going somewhere, e.g. /// mews:do/set-theme?value=dark. static bool isAction(String url) { final uri = Uri.tryParse(url); if (uri == null || uri.scheme != scheme) return false; final segments = uri.pathSegments; return segments.length == 2 && segments.first == 'do'; } /// The verb of an action address, or null when [url] is not one. static String? verbOf(String url) => isAction(url) ? Uri.parse(url).pathSegments[1] : null; /// A single spelling for every internal address, so the walk history entry /// and the built page's url always compare equal. Bare `mews:` is the hub. static String canonical(String url) { if (!isInternal(url)) return url; final uri = Uri.parse(url); if (uri.path.isEmpty) return hub; return uri.removeFragment().toString(); } /// An action address carrying [args] as its query string. static String action(String verb, [Map args = const {}]) => Uri( scheme: scheme, path: 'do/$verb', queryParameters: args.isEmpty ? null : args, ).toString(); /// [base] with [changes] merged over its query string. An empty or null /// value drops the parameter, so clearing a search box returns the whole /// list. Merging rather than replacing keeps the other parameters, which /// `Uri.replace(queryParameters:)` would discard. static String page(Uri base, [Map changes = const {}]) { final params = {...base.queryParameters}; for (final change in changes.entries) { final value = change.value; if (value == null || value.isEmpty) { params.remove(change.key); } else { params[change.key] = value; } } return Uri( scheme: scheme, path: base.path, queryParameters: params.isEmpty ? null : params, ).toString(); } }