generated from dellevin/template
更新数据库版本
This commit is contained in:
@@ -200,6 +200,8 @@ class Book {
|
|||||||
final String status; // read/reading/want_to_read
|
final String status; // read/reading/want_to_read
|
||||||
final String? isbn; // ISBN编号
|
final String? isbn; // ISBN编号
|
||||||
final DateTime? publishDate; // 出版时间
|
final DateTime? publishDate; // 出版时间
|
||||||
|
final DateTime? startDate; // 开始阅读日期
|
||||||
|
final DateTime? finishDate; // 读完日期
|
||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
final DateTime updatedAt;
|
final DateTime updatedAt;
|
||||||
final bool isDeleted;
|
final bool isDeleted;
|
||||||
@@ -218,6 +220,8 @@ class Book {
|
|||||||
required this.status,
|
required this.status,
|
||||||
this.isbn,
|
this.isbn,
|
||||||
this.publishDate,
|
this.publishDate,
|
||||||
|
this.startDate,
|
||||||
|
this.finishDate,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
this.isDeleted = false,
|
this.isDeleted = false,
|
||||||
@@ -238,6 +242,8 @@ class Book {
|
|||||||
status: json['status'] ?? 'want_to_read',
|
status: json['status'] ?? 'want_to_read',
|
||||||
isbn: json['isbn'],
|
isbn: json['isbn'],
|
||||||
publishDate: _safeParseDate(json['publish_date']),
|
publishDate: _safeParseDate(json['publish_date']),
|
||||||
|
startDate: _safeParseDate(json['start_date']),
|
||||||
|
finishDate: _safeParseDate(json['finish_date']),
|
||||||
createdAt: _safeParseDate(json['created_at'], fallback: DateTime.now())!,
|
createdAt: _safeParseDate(json['created_at'], fallback: DateTime.now())!,
|
||||||
updatedAt: _safeParseDate(json['updated_at'], fallback: DateTime.now())!,
|
updatedAt: _safeParseDate(json['updated_at'], fallback: DateTime.now())!,
|
||||||
isDeleted: json['is_deleted'] == 1 || json['is_deleted'] == true,
|
isDeleted: json['is_deleted'] == 1 || json['is_deleted'] == true,
|
||||||
@@ -259,6 +265,8 @@ class Book {
|
|||||||
'status': status,
|
'status': status,
|
||||||
'isbn': isbn,
|
'isbn': isbn,
|
||||||
'publish_date': publishDate?.toUtc().toIso8601String(),
|
'publish_date': publishDate?.toUtc().toIso8601String(),
|
||||||
|
'start_date': startDate?.toUtc().toIso8601String(),
|
||||||
|
'finish_date': finishDate?.toUtc().toIso8601String(),
|
||||||
'created_at': createdAt.toUtc().toIso8601String(),
|
'created_at': createdAt.toUtc().toIso8601String(),
|
||||||
'updated_at': updatedAt.toUtc().toIso8601String(),
|
'updated_at': updatedAt.toUtc().toIso8601String(),
|
||||||
'is_deleted': isDeleted ? 1 : 0,
|
'is_deleted': isDeleted ? 1 : 0,
|
||||||
@@ -286,6 +294,8 @@ class Book {
|
|||||||
String? status,
|
String? status,
|
||||||
Object? isbn = _copyWithNull,
|
Object? isbn = _copyWithNull,
|
||||||
DateTime? publishDate,
|
DateTime? publishDate,
|
||||||
|
DateTime? startDate,
|
||||||
|
DateTime? finishDate,
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
DateTime? updatedAt,
|
DateTime? updatedAt,
|
||||||
bool? isDeleted,
|
bool? isDeleted,
|
||||||
@@ -304,6 +314,8 @@ class Book {
|
|||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
isbn: isbn is _CopyWithNullSentinel ? this.isbn : (isbn as String?),
|
isbn: isbn is _CopyWithNullSentinel ? this.isbn : (isbn as String?),
|
||||||
publishDate: publishDate ?? this.publishDate,
|
publishDate: publishDate ?? this.publishDate,
|
||||||
|
startDate: startDate ?? this.startDate,
|
||||||
|
finishDate: finishDate ?? this.finishDate,
|
||||||
createdAt: createdAt ?? this.createdAt,
|
createdAt: createdAt ?? this.createdAt,
|
||||||
updatedAt: updatedAt ?? this.updatedAt,
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
isDeleted: isDeleted ?? this.isDeleted,
|
isDeleted: isDeleted ?? this.isDeleted,
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
if (book.isbn != null && book.isbn!.isNotEmpty) _buildIsbnSection(book),
|
if (book.isbn != null && book.isbn!.isNotEmpty) _buildIsbnSection(book),
|
||||||
if (book.publisher != null && book.publisher!.isNotEmpty) _buildPublisherSection(book),
|
if (book.publisher != null && book.publisher!.isNotEmpty) _buildPublisherSection(book),
|
||||||
if (book.publishDate != null) _buildPublishDateSection(book),
|
if (book.publishDate != null) _buildPublishDateSection(book),
|
||||||
|
if (book.startDate != null || book.finishDate != null) _buildReadingDatesSection(book),
|
||||||
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
|
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
|
||||||
if (book.summary != null && book.summary!.isNotEmpty) _buildSummarySection(book),
|
if (book.summary != null && book.summary!.isNotEmpty) _buildSummarySection(book),
|
||||||
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
|
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
|
||||||
@@ -192,6 +193,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
if (book.isbn != null && book.isbn!.isNotEmpty) _buildIsbnSection(book),
|
if (book.isbn != null && book.isbn!.isNotEmpty) _buildIsbnSection(book),
|
||||||
if (book.publisher != null && book.publisher!.isNotEmpty) _buildPublisherSection(book),
|
if (book.publisher != null && book.publisher!.isNotEmpty) _buildPublisherSection(book),
|
||||||
if (book.publishDate != null) _buildPublishDateSection(book),
|
if (book.publishDate != null) _buildPublishDateSection(book),
|
||||||
|
if (book.startDate != null || book.finishDate != null) _buildReadingDatesSection(book),
|
||||||
// 类型标签毛玻璃
|
// 类型标签毛玻璃
|
||||||
if (book.genres.isNotEmpty) _buildGenresSection(book),
|
if (book.genres.isNotEmpty) _buildGenresSection(book),
|
||||||
// 简介:内部已有毛玻璃卡片
|
// 简介:内部已有毛玻璃卡片
|
||||||
@@ -855,6 +857,59 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildReadingDatesSection(Book book) {
|
||||||
|
final isOverlay = _detailStyle == 1;
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 24, vertical: isOverlay ? 5 : 16),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 64,
|
||||||
|
child: Text(
|
||||||
|
'阅读日期',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: isOverlay ? const Color(0x66FFFFFF) : colors.onSurface.withValues(alpha: 0.4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 12,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: [
|
||||||
|
if (book.startDate != null)
|
||||||
|
_buildDateChip('开始', book.startDate!, isOverlay),
|
||||||
|
if (book.finishDate != null)
|
||||||
|
_buildDateChip('读完', book.finishDate!, isOverlay),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDateChip(String label, DateTime date, bool isOverlay) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isOverlay ? Colors.white.withValues(alpha: 0.12) : colors.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'$label ${_formatDate(date)}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: isOverlay ? Colors.white.withValues(alpha: 0.85) : colors.onSurface.withValues(alpha: 0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildGenresSection(Book book) {
|
Widget _buildGenresSection(Book book) {
|
||||||
final isOverlay = _detailStyle == 1;
|
final isOverlay = _detailStyle == 1;
|
||||||
return Padding(
|
return Padding(
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ class _BookFormPageState extends State<BookFormPage> {
|
|||||||
String? _coverPath;
|
String? _coverPath;
|
||||||
String _status = 'want_to_read';
|
String _status = 'want_to_read';
|
||||||
DateTime? _publishDate;
|
DateTime? _publishDate;
|
||||||
|
DateTime? _startDate;
|
||||||
|
DateTime? _finishDate;
|
||||||
bool _isDownloading = false;
|
bool _isDownloading = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -77,6 +79,8 @@ class _BookFormPageState extends State<BookFormPage> {
|
|||||||
_coverPath = book.coverPath;
|
_coverPath = book.coverPath;
|
||||||
_status = book.status;
|
_status = book.status;
|
||||||
_publishDate = book.publishDate;
|
_publishDate = book.publishDate;
|
||||||
|
_startDate = book.startDate;
|
||||||
|
_finishDate = book.finishDate;
|
||||||
} else if (widget.initialStatus != null) {
|
} else if (widget.initialStatus != null) {
|
||||||
_status = widget.initialStatus!;
|
_status = widget.initialStatus!;
|
||||||
}
|
}
|
||||||
@@ -208,6 +212,14 @@ class _BookFormPageState extends State<BookFormPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// 开始阅读日期 + 读完日期(同一行)
|
||||||
|
_halfCard('开始阅读', _startDate != null ? '${_startDate!.year}.${_startDate!.month.toString().padLeft(2, '0')}.${_startDate!.day.toString().padLeft(2, '0')}' : '', Icons.play_circle_outlined,
|
||||||
|
onTap: () => _selectStartDate(),
|
||||||
|
),
|
||||||
|
_halfCard('读完日期', _finishDate != null ? '${_finishDate!.year}.${_finishDate!.month.toString().padLeft(2, '0')}.${_finishDate!.day.toString().padLeft(2, '0')}' : '', Icons.check_circle_outlined,
|
||||||
|
onTap: () => _selectFinishDate(),
|
||||||
|
),
|
||||||
|
|
||||||
// 书籍简介
|
// 书籍简介
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@@ -536,6 +548,16 @@ class _BookFormPageState extends State<BookFormPage> {
|
|||||||
if (picked != null) setState(() => _publishDate = picked);
|
if (picked != null) setState(() => _publishDate = picked);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _selectStartDate() async {
|
||||||
|
final picked = await showDatePicker(context: context, initialDate: _startDate ?? DateTime.now(), firstDate: DateTime(1900), lastDate: DateTime.now().add(const Duration(days: 365 * 5)));
|
||||||
|
if (picked != null) setState(() => _startDate = picked);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _selectFinishDate() async {
|
||||||
|
final picked = await showDatePicker(context: context, initialDate: _finishDate ?? DateTime.now(), firstDate: DateTime(1900), lastDate: DateTime.now().add(const Duration(days: 365 * 5)));
|
||||||
|
if (picked != null) setState(() => _finishDate = picked);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _editSummary() async {
|
Future<void> _editSummary() async {
|
||||||
final result = await Navigator.push<String>(context, MaterialPageRoute(builder: (_) => _SummaryEditorPage(initialText: _summaryController.text)));
|
final result = await Navigator.push<String>(context, MaterialPageRoute(builder: (_) => _SummaryEditorPage(initialText: _summaryController.text)));
|
||||||
if (result != null) setState(() => _summaryController.text = result);
|
if (result != null) setState(() => _summaryController.text = result);
|
||||||
@@ -551,6 +573,8 @@ class _BookFormPageState extends State<BookFormPage> {
|
|||||||
if (_coverPath != null) return true;
|
if (_coverPath != null) return true;
|
||||||
if (_authors.isNotEmpty || _alternateTitles.isNotEmpty || _genres.isNotEmpty) return true;
|
if (_authors.isNotEmpty || _alternateTitles.isNotEmpty || _genres.isNotEmpty) return true;
|
||||||
if (_publishDate != null) return true;
|
if (_publishDate != null) return true;
|
||||||
|
if (_startDate != null) return true;
|
||||||
|
if (_finishDate != null) return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,7 +618,7 @@ class _BookFormPageState extends State<BookFormPage> {
|
|||||||
authors: _authors, alternateTitles: _alternateTitles, publisher: _publisherController.text.trim(),
|
authors: _authors, alternateTitles: _alternateTitles, publisher: _publisherController.text.trim(),
|
||||||
genres: _genres, summary: _summaryController.text.trim(), rating: rating, status: _status,
|
genres: _genres, summary: _summaryController.text.trim(), rating: rating, status: _status,
|
||||||
isbn: _isbnController.text.trim().isNotEmpty ? _isbnController.text.trim() : null,
|
isbn: _isbnController.text.trim().isNotEmpty ? _isbnController.text.trim() : null,
|
||||||
publishDate: _publishDate, createdAt: now, updatedAt: now,
|
publishDate: _publishDate, startDate: _startDate, finishDate: _finishDate, createdAt: now, updatedAt: now,
|
||||||
);
|
);
|
||||||
await context.read<AppProvider>().addBook(newBook);
|
await context.read<AppProvider>().addBook(newBook);
|
||||||
} else {
|
} else {
|
||||||
@@ -603,7 +627,7 @@ class _BookFormPageState extends State<BookFormPage> {
|
|||||||
authors: _authors, alternateTitles: _alternateTitles, publisher: _publisherController.text.trim(),
|
authors: _authors, alternateTitles: _alternateTitles, publisher: _publisherController.text.trim(),
|
||||||
genres: _genres, summary: _summaryController.text.trim(), rating: rating, status: _status,
|
genres: _genres, summary: _summaryController.text.trim(), rating: rating, status: _status,
|
||||||
isbn: _isbnController.text.trim().isNotEmpty ? _isbnController.text.trim() : null,
|
isbn: _isbnController.text.trim().isNotEmpty ? _isbnController.text.trim() : null,
|
||||||
publishDate: _publishDate, updatedAt: now,
|
publishDate: _publishDate, startDate: _startDate, finishDate: _finishDate, updatedAt: now,
|
||||||
);
|
);
|
||||||
await context.read<AppProvider>().updateBook(updatedBook);
|
await context.read<AppProvider>().updateBook(updatedBook);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,12 +44,14 @@ class _EpubDetailPageState extends State<EpubDetailPage> {
|
|||||||
bool _descriptionExpanded = false;
|
bool _descriptionExpanded = false;
|
||||||
Future<List<BookExcerpt>>? _excerptsFuture;
|
Future<List<BookExcerpt>>? _excerptsFuture;
|
||||||
Future<List<Map<String, dynamic>>>? _highlightsFuture;
|
Future<List<Map<String, dynamic>>>? _highlightsFuture;
|
||||||
|
String? _linkedBookCoverPath;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_book = widget.book;
|
_book = widget.book;
|
||||||
_loadBookInfo();
|
_loadBookInfo();
|
||||||
|
_loadLinkedBookCover();
|
||||||
final linkedBookId = _book['book_id'] as String? ?? '';
|
final linkedBookId = _book['book_id'] as String? ?? '';
|
||||||
if (linkedBookId.isNotEmpty) {
|
if (linkedBookId.isNotEmpty) {
|
||||||
_excerptsFuture = BookExcerptDao().getExcerptsByBookId(linkedBookId);
|
_excerptsFuture = BookExcerptDao().getExcerptsByBookId(linkedBookId);
|
||||||
@@ -64,12 +66,22 @@ class _EpubDetailPageState extends State<EpubDetailPage> {
|
|||||||
if (mounted && info != null) setState(() => _bookInfo = info);
|
if (mounted && info != null) setState(() => _bookInfo = info);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadLinkedBookCover() async {
|
||||||
|
final linkedBookId = _book['book_id'] as String? ?? '';
|
||||||
|
if (linkedBookId.isEmpty) return;
|
||||||
|
final book = await _bookDao.getBookById(linkedBookId);
|
||||||
|
if (mounted && book != null && book.coverPath != null && book.coverPath!.isNotEmpty) {
|
||||||
|
setState(() => _linkedBookCoverPath = book.coverPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _refreshBook() async {
|
Future<void> _refreshBook() async {
|
||||||
final updated = await _dao.getReaderBookById(widget.bookId);
|
final updated = await _dao.getReaderBookById(widget.bookId);
|
||||||
if (mounted && updated != null) {
|
if (mounted && updated != null) {
|
||||||
final linkedBookId = updated['book_id'] as String? ?? '';
|
final linkedBookId = updated['book_id'] as String? ?? '';
|
||||||
setState(() {
|
setState(() {
|
||||||
_book = updated;
|
_book = updated;
|
||||||
|
_linkedBookCoverPath = null;
|
||||||
_highlightsFuture = _dao.getHighlightsByBookId(widget.bookId);
|
_highlightsFuture = _dao.getHighlightsByBookId(widget.bookId);
|
||||||
if (linkedBookId.isNotEmpty) {
|
if (linkedBookId.isNotEmpty) {
|
||||||
_excerptsFuture = BookExcerptDao().getExcerptsByBookId(linkedBookId);
|
_excerptsFuture = BookExcerptDao().getExcerptsByBookId(linkedBookId);
|
||||||
@@ -77,10 +89,12 @@ class _EpubDetailPageState extends State<EpubDetailPage> {
|
|||||||
_excerptsFuture = null;
|
_excerptsFuture = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
_loadLinkedBookCover();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _navigateToReader() {
|
void _navigateToReader() {
|
||||||
|
final coverPath = _linkedBookCoverPath ?? _book['cover_path'] as String?;
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
@@ -88,7 +102,7 @@ class _EpubDetailPageState extends State<EpubDetailPage> {
|
|||||||
bookId: _book['id'] as String,
|
bookId: _book['id'] as String,
|
||||||
filePath: _book['file_path'] as String,
|
filePath: _book['file_path'] as String,
|
||||||
title: _book['title'] as String? ?? '',
|
title: _book['title'] as String? ?? '',
|
||||||
coverPath: _book['cover_path'] as String?,
|
coverPath: coverPath,
|
||||||
bookData: _book,
|
bookData: _book,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -113,7 +127,7 @@ class _EpubDetailPageState extends State<EpubDetailPage> {
|
|||||||
final progress = (_book['reading_percentage'] as num?)?.toDouble() ?? 0.0;
|
final progress = (_book['reading_percentage'] as num?)?.toDouble() ?? 0.0;
|
||||||
final title = _book['title'] as String? ?? '';
|
final title = _book['title'] as String? ?? '';
|
||||||
final author = _book['author'] as String? ?? '';
|
final author = _book['author'] as String? ?? '';
|
||||||
final coverPath = _book['cover_path'] as String?;
|
final coverPath = _linkedBookCoverPath ?? _book['cover_path'] as String?;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: colors.surface,
|
backgroundColor: colors.surface,
|
||||||
@@ -755,6 +769,7 @@ class _EpubDetailPageState extends State<EpubDetailPage> {
|
|||||||
final chapter = int.tryParse(highlight['chapter'] as String? ?? '') ?? 0;
|
final chapter = int.tryParse(highlight['chapter'] as String? ?? '') ?? 0;
|
||||||
final xpath = _extractStartXPath(highlight['cfi'] as String? ?? '');
|
final xpath = _extractStartXPath(highlight['cfi'] as String? ?? '');
|
||||||
final text = highlight['content'] as String? ?? '';
|
final text = highlight['content'] as String? ?? '';
|
||||||
|
final coverPath = _linkedBookCoverPath ?? _book['cover_path'] as String?;
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
@@ -762,7 +777,7 @@ class _EpubDetailPageState extends State<EpubDetailPage> {
|
|||||||
bookId: _book['id'] as String,
|
bookId: _book['id'] as String,
|
||||||
filePath: _book['file_path'] as String,
|
filePath: _book['file_path'] as String,
|
||||||
title: _book['title'] as String? ?? '',
|
title: _book['title'] as String? ?? '',
|
||||||
coverPath: _book['cover_path'] as String?,
|
coverPath: coverPath,
|
||||||
bookData: _book,
|
bookData: _book,
|
||||||
initialSpineIndex: chapter,
|
initialSpineIndex: chapter,
|
||||||
scrollToXPath: xpath,
|
scrollToXPath: xpath,
|
||||||
|
|||||||
@@ -844,6 +844,62 @@ class _ProfilePageState extends State<ProfilePage> with RouteAware {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.surfaceContainerHighest.withValues(alpha: 0.5),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.group_outlined,
|
||||||
|
size: 20, color: colors.primary.withValues(alpha: 0.8)),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('QQ 群',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: colors.onSurface.withValues(alpha: 0.5))),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text('1087203310',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: colors.onSurface)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
Clipboard.setData(ClipboardData(text: '1087203310'));
|
||||||
|
ToastUtil.show(context, '已复制到剪贴板');
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.primary.withValues(alpha: 0.08),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.copy, size: 14, color: colors.primary),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text('复制', style: TextStyle(fontSize: 12, color: colors.primary, fontWeight: FontWeight.w600)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class DatabaseHelper {
|
|||||||
|
|
||||||
return await openDatabase(
|
return await openDatabase(
|
||||||
path,
|
path,
|
||||||
version: 26,
|
version: 27,
|
||||||
onCreate: _createDB,
|
onCreate: _createDB,
|
||||||
onUpgrade: _onUpgrade,
|
onUpgrade: _onUpgrade,
|
||||||
);
|
);
|
||||||
@@ -245,6 +245,40 @@ class DatabaseHelper {
|
|||||||
await db.execute("ALTER TABLE movies ADD COLUMN category TEXT NOT NULL DEFAULT 'movie'");
|
await db.execute("ALTER TABLE movies ADD COLUMN category TEXT NOT NULL DEFAULT 'movie'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 26) {
|
||||||
|
await _upgradeBooksTableV26(db);
|
||||||
|
}
|
||||||
|
if (oldVersion < 27) {
|
||||||
|
await _upgradeBooksTableV27(db);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 升级books表到V27(添加阅读始末日期字段)
|
||||||
|
Future<void> _upgradeBooksTableV27(Database db) async {
|
||||||
|
final columns = await db.rawQuery('PRAGMA table_info(books)');
|
||||||
|
final hasStartDate = columns.any((col) => col['name'] == 'start_date');
|
||||||
|
final hasFinishDate = columns.any((col) => col['name'] == 'finish_date');
|
||||||
|
|
||||||
|
if (!hasStartDate) {
|
||||||
|
await db.execute('ALTER TABLE books ADD COLUMN start_date TEXT');
|
||||||
|
}
|
||||||
|
if (!hasFinishDate) {
|
||||||
|
await db.execute('ALTER TABLE books ADD COLUMN finish_date TEXT');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 升级books表到V26(添加阅读始末日期字段)
|
||||||
|
Future<void> _upgradeBooksTableV26(Database db) async {
|
||||||
|
final columns = await db.rawQuery('PRAGMA table_info(books)');
|
||||||
|
final hasStartDate = columns.any((col) => col['name'] == 'start_date');
|
||||||
|
final hasFinishDate = columns.any((col) => col['name'] == 'finish_date');
|
||||||
|
|
||||||
|
if (!hasStartDate) {
|
||||||
|
await db.execute('ALTER TABLE books ADD COLUMN start_date TEXT');
|
||||||
|
}
|
||||||
|
if (!hasFinishDate) {
|
||||||
|
await db.execute('ALTER TABLE books ADD COLUMN finish_date TEXT');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 升级books表到V11(添加ISBN和出版时间字段)
|
/// 升级books表到V11(添加ISBN和出版时间字段)
|
||||||
@@ -635,6 +669,8 @@ class DatabaseHelper {
|
|||||||
status TEXT NOT NULL,
|
status TEXT NOT NULL,
|
||||||
isbn TEXT,
|
isbn TEXT,
|
||||||
publish_date TEXT,
|
publish_date TEXT,
|
||||||
|
start_date TEXT,
|
||||||
|
finish_date TEXT,
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
updated_at TEXT NOT NULL,
|
updated_at TEXT NOT NULL,
|
||||||
is_deleted INTEGER DEFAULT 0,
|
is_deleted INTEGER DEFAULT 0,
|
||||||
|
|||||||
@@ -6,14 +6,27 @@ class ReaderDao {
|
|||||||
|
|
||||||
// ─── reader_books ─────────────────────────────────────────────────
|
// ─── reader_books ─────────────────────────────────────────────────
|
||||||
|
|
||||||
/// 获取所有未删除的阅读记录
|
/// 获取所有未删除的阅读记录(包含关联书籍封面)
|
||||||
Future<List<Map<String, dynamic>>> getAllReaderBooks() async {
|
Future<List<Map<String, dynamic>>> getAllReaderBooks() async {
|
||||||
final db = await _db.database;
|
final db = await _db.database;
|
||||||
return db.query(
|
final results = await db.rawQuery('''
|
||||||
'reader_books',
|
SELECT rb.*,
|
||||||
where: 'is_deleted = 0',
|
COALESCE(b.cover_path, rb.cover_path) as display_cover_path
|
||||||
orderBy: 'updated_at DESC',
|
FROM reader_books rb
|
||||||
);
|
LEFT JOIN books b ON rb.book_id = b.id AND b.is_deleted = 0
|
||||||
|
WHERE rb.is_deleted = 0
|
||||||
|
ORDER BY rb.updated_at DESC
|
||||||
|
''');
|
||||||
|
return results.map((row) {
|
||||||
|
final map = Map<String, dynamic>.from(row);
|
||||||
|
// 如果有关联封面,覆盖 cover_path 供 UI 使用
|
||||||
|
final displayCover = map['display_cover_path'] as String?;
|
||||||
|
if (displayCover != null && displayCover.isNotEmpty) {
|
||||||
|
map['cover_path'] = displayCover;
|
||||||
|
}
|
||||||
|
map.remove('display_cover_path');
|
||||||
|
return map;
|
||||||
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 根据 ID 获取阅读记录
|
/// 根据 ID 获取阅读记录
|
||||||
|
|||||||
Reference in New Issue
Block a user