generated from dellevin/template
界面优化
This commit is contained in:
@@ -260,7 +260,8 @@ flutter pub get
|
|||||||
```bash
|
```bash
|
||||||
# 构建 Release APK
|
# 构建 Release APK
|
||||||
flutter build apk --release
|
flutter build apk --release
|
||||||
|
# 推荐只 arm64-v8a
|
||||||
|
flutter build apk --release --target-platform android-arm64
|
||||||
# 构建 App Bundle(Google Play)
|
# 构建 App Bundle(Google Play)
|
||||||
flutter build appbundle --release
|
flutter build appbundle --release
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ class Book {
|
|||||||
final String title; // 书籍名称
|
final String title; // 书籍名称
|
||||||
final String? coverPath; // 本地封面路径
|
final String? coverPath; // 本地封面路径
|
||||||
final List<String> authors; // 作者列表
|
final List<String> authors; // 作者列表
|
||||||
|
final List<String> translators; // 译者列表
|
||||||
final List<String> alternateTitles; // 别名
|
final List<String> alternateTitles; // 别名
|
||||||
final String? publisher; // 出版社
|
final String? publisher; // 出版社
|
||||||
final List<String> genres; // 类型
|
final List<String> genres; // 类型
|
||||||
@@ -212,6 +213,7 @@ class Book {
|
|||||||
required this.title,
|
required this.title,
|
||||||
this.coverPath,
|
this.coverPath,
|
||||||
this.authors = const [],
|
this.authors = const [],
|
||||||
|
this.translators = const [],
|
||||||
this.alternateTitles = const [],
|
this.alternateTitles = const [],
|
||||||
this.publisher,
|
this.publisher,
|
||||||
this.genres = const [],
|
this.genres = const [],
|
||||||
@@ -234,6 +236,7 @@ class Book {
|
|||||||
title: json['title'] ?? '',
|
title: json['title'] ?? '',
|
||||||
coverPath: json['cover_path'],
|
coverPath: json['cover_path'],
|
||||||
authors: Movie.parseStringList(json['authors']),
|
authors: Movie.parseStringList(json['authors']),
|
||||||
|
translators: Movie.parseStringList(json['translators']),
|
||||||
alternateTitles: Movie.parseStringList(json['alternate_titles']),
|
alternateTitles: Movie.parseStringList(json['alternate_titles']),
|
||||||
publisher: json['publisher'],
|
publisher: json['publisher'],
|
||||||
genres: Movie.parseStringList(json['genres']),
|
genres: Movie.parseStringList(json['genres']),
|
||||||
@@ -257,6 +260,7 @@ class Book {
|
|||||||
'title': title,
|
'title': title,
|
||||||
'cover_path': coverPath,
|
'cover_path': coverPath,
|
||||||
'authors': jsonEncode(authors),
|
'authors': jsonEncode(authors),
|
||||||
|
'translators': jsonEncode(translators),
|
||||||
'alternate_titles': jsonEncode(alternateTitles),
|
'alternate_titles': jsonEncode(alternateTitles),
|
||||||
'publisher': publisher,
|
'publisher': publisher,
|
||||||
'genres': jsonEncode(genres),
|
'genres': jsonEncode(genres),
|
||||||
@@ -286,6 +290,7 @@ class Book {
|
|||||||
String? title,
|
String? title,
|
||||||
Object? coverPath = _copyWithNull,
|
Object? coverPath = _copyWithNull,
|
||||||
List<String>? authors,
|
List<String>? authors,
|
||||||
|
List<String>? translators,
|
||||||
List<String>? alternateTitles,
|
List<String>? alternateTitles,
|
||||||
String? publisher,
|
String? publisher,
|
||||||
List<String>? genres,
|
List<String>? genres,
|
||||||
@@ -306,6 +311,7 @@ class Book {
|
|||||||
title: title ?? this.title,
|
title: title ?? this.title,
|
||||||
coverPath: coverPath is _CopyWithNullSentinel ? this.coverPath : (coverPath as String?),
|
coverPath: coverPath is _CopyWithNullSentinel ? this.coverPath : (coverPath as String?),
|
||||||
authors: authors ?? this.authors,
|
authors: authors ?? this.authors,
|
||||||
|
translators: translators ?? this.translators,
|
||||||
alternateTitles: alternateTitles ?? this.alternateTitles,
|
alternateTitles: alternateTitles ?? this.alternateTitles,
|
||||||
publisher: publisher ?? this.publisher,
|
publisher: publisher ?? this.publisher,
|
||||||
genres: genres ?? this.genres,
|
genres: genres ?? this.genres,
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
_buildBasicInfo(book),
|
_buildBasicInfo(book),
|
||||||
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
|
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
|
||||||
_buildAuthorsSection(book),
|
_buildAuthorsSection(book),
|
||||||
|
if (book.translators.isNotEmpty) _buildTranslatorsSection(book),
|
||||||
if (book.genres.isNotEmpty) _buildGenresSection(book),
|
if (book.genres.isNotEmpty) _buildGenresSection(book),
|
||||||
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),
|
||||||
@@ -190,6 +191,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
// 信息区块:无毛玻璃
|
// 信息区块:无毛玻璃
|
||||||
_buildAuthorsSection(book),
|
_buildAuthorsSection(book),
|
||||||
|
if (book.translators.isNotEmpty) _buildTranslatorsSection(book),
|
||||||
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),
|
||||||
@@ -764,6 +766,39 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildTranslatorsSection(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: Text(
|
||||||
|
book.translators.join(','),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
color: isOverlay ? Colors.white : colors.onSurface,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildIsbnSection(Book book) {
|
Widget _buildIsbnSection(Book book) {
|
||||||
final isOverlay = _detailStyle == 1;
|
final isOverlay = _detailStyle == 1;
|
||||||
final colors = Theme.of(context).colorScheme;
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class _BookFormPageState extends State<BookFormPage> {
|
|||||||
late TextEditingController _isbnController;
|
late TextEditingController _isbnController;
|
||||||
|
|
||||||
List<String> _authors = [];
|
List<String> _authors = [];
|
||||||
|
List<String> _translators = [];
|
||||||
List<String> _alternateTitles = [];
|
List<String> _alternateTitles = [];
|
||||||
List<String> _genres = [];
|
List<String> _genres = [];
|
||||||
String? _coverPath;
|
String? _coverPath;
|
||||||
@@ -74,6 +75,7 @@ class _BookFormPageState extends State<BookFormPage> {
|
|||||||
|
|
||||||
if (book != null) {
|
if (book != null) {
|
||||||
_authors = List.from(book.authors);
|
_authors = List.from(book.authors);
|
||||||
|
_translators = List.from(book.translators);
|
||||||
_alternateTitles = List.from(book.alternateTitles);
|
_alternateTitles = List.from(book.alternateTitles);
|
||||||
_genres = List.from(book.genres);
|
_genres = List.from(book.genres);
|
||||||
_coverPath = book.coverPath;
|
_coverPath = book.coverPath;
|
||||||
@@ -165,7 +167,7 @@ class _BookFormPageState extends State<BookFormPage> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
// 作者 + 出版社
|
// 作者 + 译者
|
||||||
_halfCard('作者', _authors.isEmpty ? '' : '${_authors.length}人:${_authors.join('、')}', Icons.person_outline,
|
_halfCard('作者', _authors.isEmpty ? '' : '${_authors.length}人:${_authors.join('、')}', Icons.person_outline,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final provider = context.read<AppProvider>();
|
final provider = context.read<AppProvider>();
|
||||||
@@ -174,6 +176,14 @@ class _BookFormPageState extends State<BookFormPage> {
|
|||||||
if (r != null) setState(() => _authors = r);
|
if (r != null) setState(() => _authors = r);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
_halfCard('译者', _translators.isEmpty ? '' : '${_translators.length}人:${_translators.join('、')}', Icons.translate,
|
||||||
|
onTap: () async {
|
||||||
|
final provider = context.read<AppProvider>();
|
||||||
|
final data = provider.books.map((b) => b.translators).toList();
|
||||||
|
final r = await GenreSelectorPage.show(context: context, title: '选择译者', existingTagsFuture: compute(_collectUnique, data), initialSelected: _translators, hint: '如:李继宏、许钧');
|
||||||
|
if (r != null) setState(() => _translators = r);
|
||||||
|
},
|
||||||
|
),
|
||||||
_halfCard('出版社', _publisherController.text, Icons.business_outlined,
|
_halfCard('出版社', _publisherController.text, Icons.business_outlined,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final r = await TextInputPanel.show(context: context, title: '出版社', initialValue: _publisherController.text, hint: '请输入出版社');
|
final r = await TextInputPanel.show(context: context, title: '出版社', initialValue: _publisherController.text, hint: '请输入出版社');
|
||||||
@@ -572,7 +582,7 @@ class _BookFormPageState extends State<BookFormPage> {
|
|||||||
if (_isbnController.text.trim().isNotEmpty) return true;
|
if (_isbnController.text.trim().isNotEmpty) return true;
|
||||||
if (_publisherController.text.trim().isNotEmpty) return true;
|
if (_publisherController.text.trim().isNotEmpty) return true;
|
||||||
if (_coverPath != null) return true;
|
if (_coverPath != null) return true;
|
||||||
if (_authors.isNotEmpty || _alternateTitles.isNotEmpty || _genres.isNotEmpty) return true;
|
if (_authors.isNotEmpty || _translators.isNotEmpty || _alternateTitles.isNotEmpty || _genres.isNotEmpty) return true;
|
||||||
if (_publishDate != null) return true;
|
if (_publishDate != null) return true;
|
||||||
if (_startDate != null) return true;
|
if (_startDate != null) return true;
|
||||||
if (_finishDate != null) return true;
|
if (_finishDate != null) return true;
|
||||||
@@ -616,7 +626,7 @@ class _BookFormPageState extends State<BookFormPage> {
|
|||||||
}
|
}
|
||||||
final newBook = Book(
|
final newBook = Book(
|
||||||
id: newBookId, title: _titleController.text.trim(), coverPath: finalCoverPath,
|
id: newBookId, title: _titleController.text.trim(), coverPath: finalCoverPath,
|
||||||
authors: _authors, alternateTitles: _alternateTitles, publisher: _publisherController.text.trim(),
|
authors: _authors, translators: _translators, 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, startDate: _startDate, finishDate: _finishDate, createdAt: now, updatedAt: now,
|
publishDate: _publishDate, startDate: _startDate, finishDate: _finishDate, createdAt: now, updatedAt: now,
|
||||||
@@ -625,7 +635,7 @@ class _BookFormPageState extends State<BookFormPage> {
|
|||||||
} else {
|
} else {
|
||||||
final updatedBook = widget.book!.copyWith(
|
final updatedBook = widget.book!.copyWith(
|
||||||
title: _titleController.text.trim(), coverPath: _coverPath,
|
title: _titleController.text.trim(), coverPath: _coverPath,
|
||||||
authors: _authors, alternateTitles: _alternateTitles, publisher: _publisherController.text.trim(),
|
authors: _authors, translators: _translators, 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, startDate: _startDate, finishDate: _finishDate, updatedAt: now,
|
publishDate: _publishDate, startDate: _startDate, finishDate: _finishDate, updatedAt: now,
|
||||||
|
|||||||
@@ -140,24 +140,6 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
InkWell(
|
|
||||||
onTap: () => _navigateToAddReview(),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: colors.primary,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'添加记录',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: colors.onPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -91,24 +91,6 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
InkWell(
|
|
||||||
onTap: _pickPoster,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: colors.primary,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'添加记录',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: colors.onPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -140,24 +140,6 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
InkWell(
|
|
||||||
onTap: () => _navigateToAddReview(),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: colors.primary,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'添加记录',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: colors.onPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ class _PersonListPageState extends State<PersonListPage> {
|
|||||||
}
|
}
|
||||||
for (final b in provider.books.where((b) => !b.isDeleted)) {
|
for (final b in provider.books.where((b) => !b.isDeleted)) {
|
||||||
for (final a in b.authors) addRole(a, '作者', book: b);
|
for (final a in b.authors) addRole(a, '作者', book: b);
|
||||||
|
for (final t in b.translators) addRole(t, '译者', book: b);
|
||||||
}
|
}
|
||||||
|
|
||||||
var list = map.values.toList();
|
var list = map.values.toList();
|
||||||
@@ -119,7 +120,7 @@ class _PersonListPageState extends State<PersonListPage> {
|
|||||||
style: TextStyle(fontSize: 15, color: colors.onSurface),
|
style: TextStyle(fontSize: 15, color: colors.onSurface),
|
||||||
cursorColor: colors.primary,
|
cursorColor: colors.primary,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: '搜索导演、编剧、演员、作者',
|
hintText: '搜索导演、编剧、演员、作者、译者',
|
||||||
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.3)),
|
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.3)),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
contentPadding: const EdgeInsets.symmetric(vertical: 12),
|
contentPadding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
@@ -156,7 +157,7 @@ class _PersonListPageState extends State<PersonListPage> {
|
|||||||
padding: const EdgeInsets.fromLTRB(20, 10, 20, 4),
|
padding: const EdgeInsets.fromLTRB(20, 10, 20, 4),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
for (final f in ['all', '导演', '编剧', '主演', '作者'])
|
for (final f in ['all', '导演', '编剧', '主演', '作者', '译者'])
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 6),
|
padding: const EdgeInsets.only(right: 6),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
@@ -208,6 +209,7 @@ class _PersonListPageState extends State<PersonListPage> {
|
|||||||
'编剧': const Color(0xFF009688),
|
'编剧': const Color(0xFF009688),
|
||||||
'主演': const Color(0xFFE91E63),
|
'主演': const Color(0xFFE91E63),
|
||||||
'作者': const Color(0xFF7E57C2),
|
'作者': const Color(0xFF7E57C2),
|
||||||
|
'译者': const Color(0xFFFF7043),
|
||||||
};
|
};
|
||||||
final totalWorks = person.movies.length + person.books.length;
|
final totalWorks = person.movies.length + person.books.length;
|
||||||
return ListTile(
|
return ListTile(
|
||||||
@@ -262,6 +264,7 @@ class _PersonDetailPage extends StatelessWidget {
|
|||||||
'编剧': const Color(0xFF009688),
|
'编剧': const Color(0xFF009688),
|
||||||
'主演': const Color(0xFFE91E63),
|
'主演': const Color(0xFFE91E63),
|
||||||
'作者': const Color(0xFF7E57C2),
|
'作者': const Color(0xFF7E57C2),
|
||||||
|
'译者': const Color(0xFFFF7043),
|
||||||
};
|
};
|
||||||
|
|
||||||
final movieItems = <_WorkItem>[];
|
final movieItems = <_WorkItem>[];
|
||||||
@@ -275,7 +278,10 @@ class _PersonDetailPage extends StatelessWidget {
|
|||||||
movieItems.add(_WorkItem(title: m.title, roles: roles, path: m.posterPath, data: m));
|
movieItems.add(_WorkItem(title: m.title, roles: roles, path: m.posterPath, data: m));
|
||||||
}
|
}
|
||||||
for (final b in person.books) {
|
for (final b in person.books) {
|
||||||
bookItems.add(_WorkItem(title: b.title, roles: const ['作者'], path: b.coverPath, data: b));
|
final roles = <String>[];
|
||||||
|
if (b.authors.contains(person.name)) roles.add('作者');
|
||||||
|
if (b.translators.contains(person.name)) roles.add('译者');
|
||||||
|
bookItems.add(_WorkItem(title: b.title, roles: roles, path: b.coverPath, data: b));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ class _StrollPageState extends State<StrollPage> {
|
|||||||
/// 有图片的卡片:全屏沉浸式
|
/// 有图片的卡片:全屏沉浸式
|
||||||
Widget _buildImmersiveCard(_StrollItem item, ColorScheme colors) {
|
Widget _buildImmersiveCard(_StrollItem item, ColorScheme colors) {
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(vertical: 56, horizontal: 8),
|
margin: const EdgeInsets.symmetric(vertical: 80, horizontal: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 20, offset: const Offset(0, 8))],
|
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 20, offset: const Offset(0, 8))],
|
||||||
@@ -382,7 +382,7 @@ class _StrollPageState extends State<StrollPage> {
|
|||||||
/// 无图片的卡片:内容从顶部开始
|
/// 无图片的卡片:内容从顶部开始
|
||||||
Widget _buildContentCard(_StrollItem item, ColorScheme colors) {
|
Widget _buildContentCard(_StrollItem item, ColorScheme colors) {
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(vertical: 56, horizontal: 8),
|
margin: const EdgeInsets.symmetric(vertical: 80, horizontal: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: colors.surface,
|
color: colors.surface,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class DatabaseHelper {
|
|||||||
|
|
||||||
return await openDatabase(
|
return await openDatabase(
|
||||||
path,
|
path,
|
||||||
version: 28,
|
version: 29,
|
||||||
onCreate: _createDB,
|
onCreate: _createDB,
|
||||||
onUpgrade: _onUpgrade,
|
onUpgrade: _onUpgrade,
|
||||||
);
|
);
|
||||||
@@ -255,6 +255,13 @@ class DatabaseHelper {
|
|||||||
// 移除 Note Plus 功能,删除 note_plus 表
|
// 移除 Note Plus 功能,删除 note_plus 表
|
||||||
await db.execute('DROP TABLE IF EXISTS note_plus');
|
await db.execute('DROP TABLE IF EXISTS note_plus');
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 29) {
|
||||||
|
// 为书籍表添加译者字段
|
||||||
|
final bookCols = await db.rawQuery('PRAGMA table_info(books)');
|
||||||
|
if (!bookCols.any((col) => col['name'] == 'translators')) {
|
||||||
|
await db.execute('ALTER TABLE books ADD COLUMN translators TEXT');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 升级books表到V27(添加阅读始末日期字段)
|
/// 升级books表到V27(添加阅读始末日期字段)
|
||||||
@@ -665,6 +672,7 @@ class DatabaseHelper {
|
|||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
cover_path TEXT,
|
cover_path TEXT,
|
||||||
authors TEXT,
|
authors TEXT,
|
||||||
|
translators TEXT,
|
||||||
alternate_titles TEXT,
|
alternate_titles TEXT,
|
||||||
publisher TEXT,
|
publisher TEXT,
|
||||||
genres TEXT,
|
genres TEXT,
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io' show Platform;
|
import 'dart:io' show Platform;
|
||||||
import 'dart:math';
|
import 'package:crypto/crypto.dart';
|
||||||
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'user_prefs.dart';
|
import 'user_prefs.dart';
|
||||||
import 'server_config.dart';
|
import 'server_config.dart';
|
||||||
|
|
||||||
@@ -59,16 +61,40 @@ class UsageStatsService with WidgetsBindingObserver {
|
|||||||
/// 确保设备有匿名ID
|
/// 确保设备有匿名ID
|
||||||
Future<void> _ensureDeviceId() async {
|
Future<void> _ensureDeviceId() async {
|
||||||
if (_prefs.deviceId.isEmpty) {
|
if (_prefs.deviceId.isEmpty) {
|
||||||
final id = _generateDeviceId();
|
final id = await _generateDeviceId();
|
||||||
await _prefs.setDeviceId(id);
|
await _prefs.setDeviceId(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 生成匿名设备标识
|
/// 基于设备硬件信息生成匿名设备标识(SHA-256 哈希)
|
||||||
String _generateDeviceId() {
|
Future<String> _generateDeviceId() async {
|
||||||
final random = Random.secure();
|
final deviceInfo = DeviceInfoPlugin();
|
||||||
final bytes = List<int>.generate(16, (_) => random.nextInt(256));
|
String rawId;
|
||||||
final hex = bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
final info = await deviceInfo.androidInfo;
|
||||||
|
rawId = info.id; // Settings.Secure.ANDROID_ID
|
||||||
|
} else if (Platform.isIOS) {
|
||||||
|
final info = await deviceInfo.iosInfo;
|
||||||
|
rawId = info.identifierForVendor ?? '';
|
||||||
|
} else if (Platform.isWindows) {
|
||||||
|
final info = await deviceInfo.windowsInfo;
|
||||||
|
rawId = '${info.computerName}-${info.numberOfCores}';
|
||||||
|
} else if (Platform.isMacOS) {
|
||||||
|
final info = await deviceInfo.macOsInfo;
|
||||||
|
rawId = '${info.computerName}-${info.systemGUID ?? ''}';
|
||||||
|
} else if (Platform.isLinux) {
|
||||||
|
final info = await deviceInfo.linuxInfo;
|
||||||
|
rawId = '${info.name}-${info.id}';
|
||||||
|
} else {
|
||||||
|
rawId = DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
final bytes = utf8.encode(rawId);
|
||||||
|
final hash = sha256.convert(bytes);
|
||||||
|
final hex = hash.toString();
|
||||||
|
|
||||||
|
// 格式化为 UUID 样式
|
||||||
return '${hex.substring(0, 8)}-'
|
return '${hex.substring(0, 8)}-'
|
||||||
'${hex.substring(8, 12)}-'
|
'${hex.substring(8, 12)}-'
|
||||||
'${hex.substring(12, 16)}-'
|
'${hex.substring(12, 16)}-'
|
||||||
@@ -83,6 +109,12 @@ class UsageStatsService with WidgetsBindingObserver {
|
|||||||
if (deviceId.isEmpty) return;
|
if (deviceId.isEmpty) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
String appVersion = '';
|
||||||
|
try {
|
||||||
|
final pkgInfo = await PackageInfo.fromPlatform();
|
||||||
|
appVersion = '${pkgInfo.version}+${pkgInfo.buildNumber}';
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
await http
|
await http
|
||||||
.post(
|
.post(
|
||||||
Uri.parse('$serverUrl/api/heartbeat'),
|
Uri.parse('$serverUrl/api/heartbeat'),
|
||||||
@@ -93,6 +125,7 @@ class UsageStatsService with WidgetsBindingObserver {
|
|||||||
Platform.operatingSystem, // android/ios/windows/macos/linux
|
Platform.operatingSystem, // android/ios/windows/macos/linux
|
||||||
'device_name':
|
'device_name':
|
||||||
'${Platform.operatingSystem} ${Platform.operatingSystemVersion}',
|
'${Platform.operatingSystem} ${Platform.operatingSystemVersion}',
|
||||||
|
'app_version': appVersion,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.timeout(const Duration(seconds: 5));
|
.timeout(const Duration(seconds: 5));
|
||||||
|
|||||||
@@ -210,6 +210,8 @@ class _TagSidePanelState extends State<TagSidePanel> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
@@ -243,6 +245,7 @@ class _TagSidePanelState extends State<TagSidePanel> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import device_info_plus
|
||||||
import dynamic_color
|
import dynamic_color
|
||||||
import file_picker
|
import file_picker
|
||||||
import file_selector_macos
|
import file_selector_macos
|
||||||
@@ -16,6 +17,7 @@ import sqflite_darwin
|
|||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||||
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
|
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
|
||||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
|
|||||||
26
pubspec.lock
26
pubspec.lock
@@ -90,13 +90,29 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.5+2"
|
version: "0.3.5+2"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: crypto
|
name: crypto
|
||||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.7"
|
version: "3.0.7"
|
||||||
|
device_info_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: device_info_plus
|
||||||
|
sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.5.0"
|
||||||
|
device_info_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: device_info_plus_platform_interface
|
||||||
|
sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.3"
|
||||||
dynamic_color:
|
dynamic_color:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1042,6 +1058,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.15.0"
|
version: "5.15.0"
|
||||||
|
win32_registry:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32_registry
|
||||||
|
sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ dependencies:
|
|||||||
uuid: ^4.5.0
|
uuid: ^4.5.0
|
||||||
expandable: ^5.0.1
|
expandable: ^5.0.1
|
||||||
sqflite_common_ffi: ^2.3.0
|
sqflite_common_ffi: ^2.3.0
|
||||||
|
device_info_plus: ^11.2.0
|
||||||
|
crypto: ^3.0.6
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user