import 'json_util.dart'; /// 笔记列表项(不含正文) /// /// 后端列表接口刻意不返回 content —— 一页 20 条正文可能有几 MB, /// 而这个后端在公网服务器上,列表页只需要摘要。 class NoteListItem { final int id; final int? folderId; final String title; final String summary; final List tags; final bool pinned; final String updateTime; const NoteListItem({ required this.id, required this.folderId, required this.title, required this.summary, required this.tags, required this.pinned, required this.updateTime, }); factory NoteListItem.fromJson(Map json) { return NoteListItem( id: asInt(json['id']), folderId: asNullableInt(json['folderId']), title: asNonEmpty(json['title'], '无标题笔记'), summary: asString(json['summary']), tags: asStringList(json['tagList']), pinned: asInt(json['isPinned']) == 1, updateTime: formatDateTime(json['updateTime']), ); } } /// 笔记详情(含正文) class NoteDetail { final int id; final int? folderId; final String title; final String content; final List tags; final bool pinned; final String createTime; final String updateTime; const NoteDetail({ required this.id, required this.folderId, required this.title, required this.content, required this.tags, required this.pinned, required this.createTime, required this.updateTime, }); factory NoteDetail.fromJson(Map json) { return NoteDetail( id: asInt(json['id']), folderId: asNullableInt(json['folderId']), title: asNonEmpty(json['title'], '无标题笔记'), content: asString(json['content']), tags: asStringList(json['tagList']), pinned: asInt(json['isPinned']) == 1, createTime: formatDateTime(json['createTime']), updateTime: formatDateTime(json['updateTime']), ); } } /// 分页结果 class NotePage { final List items; final int total; final bool hasMore; const NotePage({required this.items, required this.total, required this.hasMore}); static const empty = NotePage(items: [], total: 0, hasMore: false); factory NotePage.fromJson(Map json) { return NotePage( items: asMapList(json['list']).map(NoteListItem.fromJson).toList(), total: asInt(json['total']), hasMore: json['hasMore'] == true, ); } } /// 目录树节点 class FolderNode { final int id; final String name; final int? parentId; /// 该目录下的笔记数(**含所有子目录**),和后端列表接口的口径一致 final int noteCount; final List children; const FolderNode({ required this.id, required this.name, required this.parentId, required this.noteCount, required this.children, }); factory FolderNode.fromJson(Map json) { return FolderNode( id: asInt(json['id']), name: asNonEmpty(json['name'], '未命名'), parentId: asNullableInt(json['parentId']), noteCount: asInt(json['noteCount']), children: asMapList(json['children']).map(FolderNode.fromJson).toList(), ); } } /// 目录树接口的完整返回 class FolderTree { final List folders; final int uncategorizedCount; final int totalCount; const FolderTree({ required this.folders, required this.uncategorizedCount, required this.totalCount, }); static const empty = FolderTree(folders: [], uncategorizedCount: 0, totalCount: 0); factory FolderTree.fromJson(Map json) { return FolderTree( folders: asMapList(json['folders']).map(FolderNode.fromJson).toList(), uncategorizedCount: asInt(json['uncategorizedCount']), totalCount: asInt(json['totalCount']), ); } /// 按 id 找节点,用于拼当前目录的面包屑路径 List? findPath(int id) { List? walk(List nodes, List trail) { for (final node in nodes) { final next = [...trail, node]; if (node.id == id) return next; final found = walk(node.children, next); if (found != null) return found; } return null; } return walk(folders, []); } } /// 当前浏览的位置 /// /// 用三个 scope 对应目录树上的三种节点,而不是用 `folderId == null` /// 来表达「全部」—— 那样就没法区分「全部」和「未分类」了。 enum NoteScope { all, uncategorized, folder } class NoteLocation { final NoteScope scope; final int? folderId; final String folderName; const NoteLocation({ required this.scope, this.folderId, this.folderName = '全部', }); static const all = NoteLocation(scope: NoteScope.all, folderName: '全部'); static const uncategorized = NoteLocation(scope: NoteScope.uncategorized, folderName: '未分类'); /// 传给后端 list 接口的 scope 参数 String get apiScope { switch (scope) { case NoteScope.all: return 'all'; case NoteScope.uncategorized: return 'uncategorized'; case NoteScope.folder: return 'folder'; } } } /// 后端给的是 `yyyy-MM-dd HH:mm:ss`,这里裁成 `yyyy-MM-dd HH:mm` /// 列表里显示到秒没有意义,还会把卡片撑宽。 String formatDateTime(Object? value) { final text = asString(value); return text.length >= 16 ? text.substring(0, 16) : text; }