generated from dellevin/template
添加点击 最近添加
This commit is contained in:
@@ -12,14 +12,34 @@ import 'utils/sync/auto_backup_service.dart';
|
||||
import 'utils/sync/server_sync_service.dart';
|
||||
import 'utils/usage_stats_service.dart';
|
||||
import 'providers/app_provider.dart';
|
||||
import 'package:flutter/widget_previews.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await UserPrefs.init();
|
||||
final appProvider = AppProvider();
|
||||
runApp(MyApp(appProvider: appProvider));
|
||||
unawaited(_validateSyncOnStartup().then((_) => appProvider.initDatabase().then((_) => appProvider.initMainTabIndex())));
|
||||
|
||||
unawaited(_bootstrap(appProvider));
|
||||
}
|
||||
|
||||
/// 启动后台任务:先校验同步状态,再按顺序初始化数据库与主标签,
|
||||
/// 保持 UI 先渲染、数据后就绪。
|
||||
Future<void> _bootstrap(AppProvider appProvider) async {
|
||||
// 数据库优先初始化(不被 sync 阻塞)
|
||||
try {
|
||||
await appProvider.initDatabase();
|
||||
} catch (e) {
|
||||
debugPrint('[Startup] 数据库初始化失败: $e');
|
||||
}
|
||||
appProvider.initMainTabIndex();
|
||||
|
||||
// sync 校验在数据库加载完成后执行(避免阻塞本地数据展示)
|
||||
try {
|
||||
await _validateSyncOnStartup();
|
||||
} catch (e) {
|
||||
debugPrint('[Startup] 同步状态校验失败: $e');
|
||||
}
|
||||
|
||||
unawaited(_initAutoBackup());
|
||||
unawaited(_initUsageStats());
|
||||
}
|
||||
@@ -184,14 +204,3 @@ class _AppIconWrapperState extends State<_AppIconWrapper> {
|
||||
return widget.child;
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name: "MookNote App Preview")
|
||||
Widget previewMyApp() {
|
||||
final appProvider = AppProvider();
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: appProvider),
|
||||
],
|
||||
child: MyApp(appProvider: appProvider),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ class Movie {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'poster_path': posterPath,
|
||||
'release_date': releaseDate?.toUtc().toUtc().toIso8601String(),
|
||||
'release_date': releaseDate?.toUtc().toIso8601String(),
|
||||
'directors': jsonEncode(directors),
|
||||
'writers': jsonEncode(writers),
|
||||
'actors': jsonEncode(actors),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/app_provider.dart';
|
||||
@@ -27,6 +26,8 @@ class _SearchPageState extends State<SearchPage> {
|
||||
|
||||
List<_SearchResult> _results = [];
|
||||
bool _hasSearched = false;
|
||||
List<String> _matchingTags = [];
|
||||
String? _selectedTag;
|
||||
|
||||
Timer? _debounce;
|
||||
|
||||
@@ -64,7 +65,10 @@ class _SearchPageState extends State<SearchPage> {
|
||||
if (movie.title.toLowerCase().contains(lowerKeyword) ||
|
||||
movie.alternateTitles.any((t) => t.toLowerCase().contains(lowerKeyword)) ||
|
||||
(movie.summary?.toLowerCase().contains(lowerKeyword) ?? false) ||
|
||||
movie.genres.any((g) => g.toLowerCase().contains(lowerKeyword))) {
|
||||
movie.genres.any((g) => g.toLowerCase().contains(lowerKeyword)) ||
|
||||
movie.directors.any((d) => d.toLowerCase().contains(lowerKeyword)) ||
|
||||
movie.writers.any((w) => w.toLowerCase().contains(lowerKeyword)) ||
|
||||
movie.actors.any((a) => a.toLowerCase().contains(lowerKeyword))) {
|
||||
results.add(_SearchResult(type: 'movie', data: movie));
|
||||
}
|
||||
}
|
||||
@@ -81,13 +85,61 @@ class _SearchPageState extends State<SearchPage> {
|
||||
}
|
||||
if (_showNotes) {
|
||||
for (final note in provider.notes.where((n) => !n.isDeleted)) {
|
||||
if (note.content.toLowerCase().contains(lowerKeyword) ||
|
||||
if (note.title.toLowerCase().contains(lowerKeyword) ||
|
||||
note.content.toLowerCase().contains(lowerKeyword) ||
|
||||
note.tags.any((t) => t.toLowerCase().contains(lowerKeyword))) {
|
||||
results.add(_SearchResult(type: 'note', data: note));
|
||||
}
|
||||
}
|
||||
}
|
||||
setState(() { _results = results; _hasSearched = true; });
|
||||
setState(() {
|
||||
_results = results;
|
||||
_hasSearched = true;
|
||||
// 收集匹配的标签
|
||||
_matchingTags = _collectMatchingTags(lowerKeyword);
|
||||
_selectedTag = null;
|
||||
});
|
||||
}
|
||||
|
||||
/// 收集所有包含关键词的标签
|
||||
List<String> _collectMatchingTags(String lowerKeyword) {
|
||||
final provider = context.read<AppProvider>();
|
||||
final tagSet = <String>{};
|
||||
for (final m in provider.movies.where((m) => !m.isDeleted)) {
|
||||
for (final g in m.genres) {
|
||||
if (g.toLowerCase().contains(lowerKeyword)) tagSet.add(g);
|
||||
}
|
||||
}
|
||||
for (final b in provider.books.where((b) => !b.isDeleted)) {
|
||||
for (final g in b.genres) {
|
||||
if (g.toLowerCase().contains(lowerKeyword)) tagSet.add(g);
|
||||
}
|
||||
}
|
||||
for (final n in provider.notes.where((n) => !n.isDeleted)) {
|
||||
for (final t in n.tags) {
|
||||
if (t.toLowerCase().contains(lowerKeyword)) tagSet.add(t);
|
||||
}
|
||||
}
|
||||
return tagSet.toList()..sort();
|
||||
}
|
||||
|
||||
/// 按标签筛选:点击标签后只显示包含该标签的结果
|
||||
void _filterByTag(String tag) {
|
||||
final provider = context.read<AppProvider>();
|
||||
setState(() {
|
||||
_selectedTag = tag;
|
||||
final results = <_SearchResult>[];
|
||||
for (final m in provider.movies.where((m) => !m.isDeleted)) {
|
||||
if (m.genres.contains(tag)) results.add(_SearchResult(type: 'movie', data: m));
|
||||
}
|
||||
for (final b in provider.books.where((b) => !b.isDeleted)) {
|
||||
if (b.genres.contains(tag)) results.add(_SearchResult(type: 'book', data: b));
|
||||
}
|
||||
for (final n in provider.notes.where((n) => !n.isDeleted)) {
|
||||
if (n.tags.contains(tag)) results.add(_SearchResult(type: 'note', data: n));
|
||||
}
|
||||
_results = results;
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
@@ -107,7 +159,7 @@ class _SearchPageState extends State<SearchPage> {
|
||||
_buildFilterRow(),
|
||||
Expanded(
|
||||
child: _hasSearched
|
||||
? _results.isEmpty ? _buildEmptyState() : _buildResultList()
|
||||
? _results.isEmpty && _matchingTags.isEmpty ? _buildEmptyState() : _buildResultList()
|
||||
: _buildInitialState(),
|
||||
),
|
||||
]),
|
||||
@@ -155,9 +207,9 @@ class _SearchPageState extends State<SearchPage> {
|
||||
int movieCount = 0, bookCount = 0, noteCount = 0;
|
||||
if (keyword.isNotEmpty) {
|
||||
final kw = keyword.toLowerCase();
|
||||
movieCount = provider.movies.where((m) => !m.isDeleted && (m.title.toLowerCase().contains(kw) || m.alternateTitles.any((t) => t.toLowerCase().contains(kw)) || (m.summary?.toLowerCase().contains(kw) ?? false) || m.genres.any((g) => g.toLowerCase().contains(kw)))).length;
|
||||
movieCount = provider.movies.where((m) => !m.isDeleted && (m.title.toLowerCase().contains(kw) || m.alternateTitles.any((t) => t.toLowerCase().contains(kw)) || (m.summary?.toLowerCase().contains(kw) ?? false) || m.genres.any((g) => g.toLowerCase().contains(kw)) || m.directors.any((d) => d.toLowerCase().contains(kw)) || m.writers.any((w) => w.toLowerCase().contains(kw)) || m.actors.any((a) => a.toLowerCase().contains(kw)))).length;
|
||||
bookCount = provider.books.where((b) => !b.isDeleted && (b.title.toLowerCase().contains(kw) || b.alternateTitles.any((t) => t.toLowerCase().contains(kw)) || (b.summary?.toLowerCase().contains(kw) ?? false) || b.authors.any((a) => a.toLowerCase().contains(kw)))).length;
|
||||
noteCount = provider.notes.where((n) => !n.isDeleted && (n.content.toLowerCase().contains(kw) || n.tags.any((t) => t.toLowerCase().contains(kw)))).length;
|
||||
noteCount = provider.notes.where((n) => !n.isDeleted && (n.title.toLowerCase().contains(kw) || n.content.toLowerCase().contains(kw) || n.tags.any((t) => t.toLowerCase().contains(kw)))).length;
|
||||
}
|
||||
|
||||
return Padding(
|
||||
@@ -206,7 +258,7 @@ class _SearchPageState extends State<SearchPage> {
|
||||
const SizedBox(height: 20),
|
||||
Text('输入关键词搜索', style: TextStyle(fontSize: 15, color: colors.onSurface.withValues(alpha: 0.35))),
|
||||
const SizedBox(height: 4),
|
||||
Text('支持标题、作者、标签、类型', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.2))),
|
||||
Text('支持标题、导演、作者、标签、简介', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.2))),
|
||||
]),
|
||||
);
|
||||
}
|
||||
@@ -231,30 +283,85 @@ class _SearchPageState extends State<SearchPage> {
|
||||
Widget _buildResultList() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
// 标签筛选区
|
||||
if (_matchingTags.isNotEmpty) _buildTagSection(colors),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 4, 20, 8),
|
||||
child: Text('找到 ${_results.length} 条结果',
|
||||
child: Text(_selectedTag != null
|
||||
? '标签 "$_selectedTag" 共 ${_results.length} 条'
|
||||
: '找到 ${_results.length} 条结果',
|
||||
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.35))),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 24),
|
||||
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
itemCount: _results.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = _results[index];
|
||||
switch (item.type) {
|
||||
case 'movie': return _buildMovieItem(item.data as Movie);
|
||||
case 'book': return _buildBookItem(item.data as Book);
|
||||
case 'note': return _buildNoteItem(item.data as Note);
|
||||
default: return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
),
|
||||
child: _results.isEmpty
|
||||
? _buildEmptyState()
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 24),
|
||||
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
itemCount: _results.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = _results[index];
|
||||
switch (item.type) {
|
||||
case 'movie': return _buildMovieItem(item.data as Movie);
|
||||
case 'book': return _buildBookItem(item.data as Book);
|
||||
case 'note': return _buildNoteItem(item.data as Note);
|
||||
default: return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _buildTagSection(ColorScheme colors) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(16, 4, 16, 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(children: [
|
||||
Icon(Icons.label_outline, size: 14, color: colors.onSurface.withValues(alpha: 0.35)),
|
||||
const SizedBox(width: 4),
|
||||
Text('匹配标签', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.35))),
|
||||
if (_selectedTag != null) ...[
|
||||
const Spacer(),
|
||||
GestureDetector(
|
||||
onTap: () { setState(() { _selectedTag = null; }); _performSearch(); },
|
||||
child: Text('清除筛选', style: TextStyle(fontSize: 12, color: colors.primary)),
|
||||
),
|
||||
],
|
||||
]),
|
||||
const SizedBox(height: 6),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 6,
|
||||
children: _matchingTags.map((tag) {
|
||||
final isSelected = _selectedTag == tag;
|
||||
return GestureDetector(
|
||||
onTap: () => _filterByTag(tag),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? colors.primary : colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isSelected ? colors.primary : colors.outline.withValues(alpha: 0.15),
|
||||
),
|
||||
),
|
||||
child: Text(tag, style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6),
|
||||
)),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMovieItem(Movie movie) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return GestureDetector(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import '../models/data_models.dart';
|
||||
|
||||
/// 数据库帮助类 - 管理数据库的创建和版本控制
|
||||
@@ -341,11 +341,11 @@ class DatabaseHelper {
|
||||
'updated_at': row['updated_at']?.toString() ?? now,
|
||||
});
|
||||
} catch (e) {
|
||||
// 忽略迁移失败的记录
|
||||
debugPrint('[DB] 迁移笔记记录失败: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 升级books表到V3
|
||||
Future<void> _upgradeBooksTableV3(Database db) async {
|
||||
// 备份旧数据
|
||||
@@ -393,7 +393,7 @@ class DatabaseHelper {
|
||||
'is_deleted': 0,
|
||||
});
|
||||
} catch (e) {
|
||||
// 忽略迁移失败的记录
|
||||
debugPrint('[DB] 迁移书籍记录失败: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -448,7 +448,7 @@ class DatabaseHelper {
|
||||
'is_deleted': row['is_deleted'] ?? 0,
|
||||
});
|
||||
} catch (e) {
|
||||
// 忽略迁移失败的记录
|
||||
debugPrint('[DB] 迁移影视记录失败: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,8 +361,9 @@ class ServerDataService {
|
||||
final size = exists ? await file.length() : 0;
|
||||
debugPrint('[Sync] 图片: $relPath (存在=$exists 大小=$size)');
|
||||
if (exists) {
|
||||
request.files.add(await http.MultipartFile(
|
||||
'images', file.readAsBytes().asStream(), size,
|
||||
final bytes = await file.readAsBytes();
|
||||
request.files.add(http.MultipartFile.fromBytes(
|
||||
'images', bytes,
|
||||
filename: relPath,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
@@ -312,18 +311,22 @@ class ServerSyncService {
|
||||
final deviceId = _prefs.deviceId;
|
||||
final appDir = (await getApplicationDocumentsDirectory()).path;
|
||||
|
||||
// 上传数据库
|
||||
// 上传数据库(先关闭连接再读取,避免文件被占用)
|
||||
await DatabaseHelper.instance.close();
|
||||
final dbPath = await DatabaseHelper.instance.databasePath;
|
||||
final request = http.MultipartRequest('POST', Uri.parse('$url/api/sync/upload'));
|
||||
request.fields['code'] = code;
|
||||
request.fields['device_id'] = deviceId;
|
||||
if (dbPath != null && File(dbPath).existsSync()) {
|
||||
request.files.add(await http.MultipartFile(
|
||||
'database', File(dbPath).readAsBytes().asStream(), await File(dbPath).length(),
|
||||
final dbBytes = await File(dbPath).readAsBytes();
|
||||
request.files.add(http.MultipartFile.fromBytes(
|
||||
'database', dbBytes,
|
||||
filename: 'mooknote.db',
|
||||
));
|
||||
debugPrint('[Sync] 上传数据库: ${await File(dbPath).length()} bytes');
|
||||
debugPrint('[Sync] 上传数据库: ${dbBytes.length} bytes');
|
||||
}
|
||||
// 重新打开数据库
|
||||
await DatabaseHelper.instance.reopen();
|
||||
|
||||
// 收集并上传所有图片
|
||||
final imagePaths = _collectAllImageFiles(appDir);
|
||||
@@ -331,8 +334,9 @@ class ServerSyncService {
|
||||
for (final path in imagePaths) {
|
||||
final relPath = p.relative(path, from: appDir).replaceAll('\\', '/');
|
||||
if (await File(path).exists()) {
|
||||
request.files.add(await http.MultipartFile(
|
||||
'images', File(path).readAsBytes().asStream(), await File(path).length(),
|
||||
final bytes = await File(path).readAsBytes();
|
||||
request.files.add(http.MultipartFile.fromBytes(
|
||||
'images', bytes,
|
||||
filename: relPath,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@ import '../pages/stroll_page.dart';
|
||||
import '../pages/markdown_reader/md_reader_tab_page.dart';
|
||||
import '../pages/tag_management_page.dart';
|
||||
import '../pages/profile_page.dart';
|
||||
import '../pages/movies/movie_detail_page.dart';
|
||||
import '../pages/book/book_detail_page.dart';
|
||||
import '../pages/note/note_detail_page.dart';
|
||||
import '../models/data_models.dart';
|
||||
import 'fade_in_local_image.dart';
|
||||
|
||||
/// 自定义侧边栏
|
||||
@@ -368,22 +372,26 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
...recent.take(4).map((item) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
item.type == 'movie' ? Icons.movie_outlined : item.type == 'book' ? Icons.menu_book_outlined : Icons.note_outlined,
|
||||
size: 14, color: colors.onSurface.withValues(alpha: 0.3),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(item.title, maxLines: 1, overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.75))),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(_recentTimeAgo(item.date), style: TextStyle(fontSize: 10, color: colors.onSurface.withValues(alpha: 0.25))),
|
||||
],
|
||||
...recent.take(4).map((item) => InkWell(
|
||||
onTap: () => _openRecentItem(context, item),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10, top: 2),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
item.type == 'movie' ? Icons.movie_outlined : item.type == 'book' ? Icons.menu_book_outlined : Icons.note_outlined,
|
||||
size: 14, color: colors.onSurface.withValues(alpha: 0.3),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(item.title, maxLines: 1, overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.75))),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(_recentTimeAgo(item.date), style: TextStyle(fontSize: 10, color: colors.onSurface.withValues(alpha: 0.25))),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
],
|
||||
@@ -396,18 +404,30 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
||||
List<_RecentItem> _getRecentItems(AppProvider provider) {
|
||||
final items = <_RecentItem>[];
|
||||
for (final m in provider.movies.where((m) => !m.isDeleted)) {
|
||||
items.add(_RecentItem(type: 'movie', title: m.title, date: m.createdAt));
|
||||
items.add(_RecentItem(type: 'movie', title: m.title, date: m.createdAt, data: m));
|
||||
}
|
||||
for (final b in provider.books.where((b) => !b.isDeleted)) {
|
||||
items.add(_RecentItem(type: 'book', title: b.title, date: b.createdAt));
|
||||
items.add(_RecentItem(type: 'book', title: b.title, date: b.createdAt, data: b));
|
||||
}
|
||||
for (final n in provider.notes.where((n) => !n.isDeleted)) {
|
||||
items.add(_RecentItem(type: 'note', title: n.title.isNotEmpty ? n.title : '随手记', date: n.createdAt));
|
||||
items.add(_RecentItem(type: 'note', title: n.title.isNotEmpty ? n.title : '随手记', date: n.createdAt, data: n));
|
||||
}
|
||||
items.sort((a, b) => b.date.compareTo(a.date));
|
||||
return items;
|
||||
}
|
||||
|
||||
void _openRecentItem(BuildContext context, _RecentItem item) {
|
||||
Navigator.pop(context);
|
||||
switch (item.type) {
|
||||
case 'movie':
|
||||
Navigator.push(context, MaterialPageRoute(builder: (_) => MovieDetailPage(movie: item.data as Movie)));
|
||||
case 'book':
|
||||
Navigator.push(context, MaterialPageRoute(builder: (_) => BookDetailPage(book: item.data as Book)));
|
||||
case 'note':
|
||||
Navigator.push(context, MaterialPageRoute(builder: (_) => NoteDetailPage(note: item.data as Note)));
|
||||
}
|
||||
}
|
||||
|
||||
String _recentTimeAgo(DateTime date) {
|
||||
final diff = DateTime.now().difference(date);
|
||||
if (diff.inDays >= 365) return '${(diff.inDays / 365).floor()}年前';
|
||||
@@ -422,5 +442,6 @@ class _RecentItem {
|
||||
final String type;
|
||||
final String title;
|
||||
final DateTime date;
|
||||
_RecentItem({required this.type, required this.title, required this.date});
|
||||
final dynamic data;
|
||||
_RecentItem({required this.type, required this.title, required this.date, required this.data});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user