diff --git a/README.md b/README.md index acd4e61..9772bff 100644 --- a/README.md +++ b/README.md @@ -260,7 +260,8 @@ flutter pub get ```bash # 构建 Release APK flutter build apk --release - +# 推荐只 arm64-v8a +flutter build apk --release --target-platform android-arm64 # 构建 App Bundle(Google Play) flutter build appbundle --release ``` diff --git a/lib/models/data_models.dart b/lib/models/data_models.dart index 37802da..0c2f288 100644 --- a/lib/models/data_models.dart +++ b/lib/models/data_models.dart @@ -192,6 +192,7 @@ class Book { final String title; // 书籍名称 final String? coverPath; // 本地封面路径 final List authors; // 作者列表 + final List translators; // 译者列表 final List alternateTitles; // 别名 final String? publisher; // 出版社 final List genres; // 类型 @@ -212,6 +213,7 @@ class Book { required this.title, this.coverPath, this.authors = const [], + this.translators = const [], this.alternateTitles = const [], this.publisher, this.genres = const [], @@ -234,6 +236,7 @@ class Book { title: json['title'] ?? '', coverPath: json['cover_path'], authors: Movie.parseStringList(json['authors']), + translators: Movie.parseStringList(json['translators']), alternateTitles: Movie.parseStringList(json['alternate_titles']), publisher: json['publisher'], genres: Movie.parseStringList(json['genres']), @@ -257,6 +260,7 @@ class Book { 'title': title, 'cover_path': coverPath, 'authors': jsonEncode(authors), + 'translators': jsonEncode(translators), 'alternate_titles': jsonEncode(alternateTitles), 'publisher': publisher, 'genres': jsonEncode(genres), @@ -286,6 +290,7 @@ class Book { String? title, Object? coverPath = _copyWithNull, List? authors, + List? translators, List? alternateTitles, String? publisher, List? genres, @@ -306,6 +311,7 @@ class Book { title: title ?? this.title, coverPath: coverPath is _CopyWithNullSentinel ? this.coverPath : (coverPath as String?), authors: authors ?? this.authors, + translators: translators ?? this.translators, alternateTitles: alternateTitles ?? this.alternateTitles, publisher: publisher ?? this.publisher, genres: genres ?? this.genres, diff --git a/lib/pages/book/book_detail_page.dart b/lib/pages/book/book_detail_page.dart index 40bd45b..00638ea 100644 --- a/lib/pages/book/book_detail_page.dart +++ b/lib/pages/book/book_detail_page.dart @@ -89,6 +89,7 @@ class _BookDetailPageState extends State { _buildBasicInfo(book), Divider(height: 0.5, thickness: 0.5, color: colors.outline), _buildAuthorsSection(book), + if (book.translators.isNotEmpty) _buildTranslatorsSection(book), if (book.genres.isNotEmpty) _buildGenresSection(book), if (book.isbn != null && book.isbn!.isNotEmpty) _buildIsbnSection(book), if (book.publisher != null && book.publisher!.isNotEmpty) _buildPublisherSection(book), @@ -190,6 +191,7 @@ class _BookDetailPageState extends State { const SizedBox(height: 20), // 信息区块:无毛玻璃 _buildAuthorsSection(book), + if (book.translators.isNotEmpty) _buildTranslatorsSection(book), if (book.isbn != null && book.isbn!.isNotEmpty) _buildIsbnSection(book), if (book.publisher != null && book.publisher!.isNotEmpty) _buildPublisherSection(book), if (book.publishDate != null) _buildPublishDateSection(book), @@ -764,6 +766,39 @@ class _BookDetailPageState extends State { ); } + 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) { final isOverlay = _detailStyle == 1; final colors = Theme.of(context).colorScheme; diff --git a/lib/pages/book/book_form_page.dart b/lib/pages/book/book_form_page.dart index b250600..32a4171 100644 --- a/lib/pages/book/book_form_page.dart +++ b/lib/pages/book/book_form_page.dart @@ -43,6 +43,7 @@ class _BookFormPageState extends State { late TextEditingController _isbnController; List _authors = []; + List _translators = []; List _alternateTitles = []; List _genres = []; String? _coverPath; @@ -74,6 +75,7 @@ class _BookFormPageState extends State { if (book != null) { _authors = List.from(book.authors); + _translators = List.from(book.translators); _alternateTitles = List.from(book.alternateTitles); _genres = List.from(book.genres); _coverPath = book.coverPath; @@ -165,7 +167,7 @@ class _BookFormPageState extends State { }, ), - // 作者 + 出版社 + // 作者 + 译者 _halfCard('作者', _authors.isEmpty ? '' : '${_authors.length}人:${_authors.join('、')}', Icons.person_outline, onTap: () async { final provider = context.read(); @@ -174,6 +176,14 @@ class _BookFormPageState extends State { if (r != null) setState(() => _authors = r); }, ), + _halfCard('译者', _translators.isEmpty ? '' : '${_translators.length}人:${_translators.join('、')}', Icons.translate, + onTap: () async { + final provider = context.read(); + 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, onTap: () async { final r = await TextInputPanel.show(context: context, title: '出版社', initialValue: _publisherController.text, hint: '请输入出版社'); @@ -572,7 +582,7 @@ class _BookFormPageState extends State { if (_isbnController.text.trim().isNotEmpty) return true; if (_publisherController.text.trim().isNotEmpty) 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 (_startDate != null) return true; if (_finishDate != null) return true; @@ -616,7 +626,7 @@ class _BookFormPageState extends State { } final newBook = Book( 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, isbn: _isbnController.text.trim().isNotEmpty ? _isbnController.text.trim() : null, publishDate: _publishDate, startDate: _startDate, finishDate: _finishDate, createdAt: now, updatedAt: now, @@ -625,7 +635,7 @@ class _BookFormPageState extends State { } else { final updatedBook = widget.book!.copyWith( 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, isbn: _isbnController.text.trim().isNotEmpty ? _isbnController.text.trim() : null, publishDate: _publishDate, startDate: _startDate, finishDate: _finishDate, updatedAt: now, diff --git a/lib/pages/book/book_reviews_page.dart b/lib/pages/book/book_reviews_page.dart index 9e4483e..413780d 100644 --- a/lib/pages/book/book_reviews_page.dart +++ b/lib/pages/book/book_reviews_page.dart @@ -140,24 +140,6 @@ class _BookReviewsPageState extends State { ), ), 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, - ), - ), - ), - ), ], ), ); diff --git a/lib/pages/movies/movie_posters_page.dart b/lib/pages/movies/movie_posters_page.dart index 0868fae..2d05290 100644 --- a/lib/pages/movies/movie_posters_page.dart +++ b/lib/pages/movies/movie_posters_page.dart @@ -91,24 +91,6 @@ class _MoviePostersPageState extends State { ), ), 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, - ), - ), - ), - ), ], ), ); diff --git a/lib/pages/movies/movie_reviews_page.dart b/lib/pages/movies/movie_reviews_page.dart index 52c8f4c..bc7513e 100644 --- a/lib/pages/movies/movie_reviews_page.dart +++ b/lib/pages/movies/movie_reviews_page.dart @@ -140,24 +140,6 @@ class _MovieReviewsPageState extends State { ), ), 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, - ), - ), - ), - ), ], ), ); diff --git a/lib/pages/person_list_page.dart b/lib/pages/person_list_page.dart index 793e8de..5341b3f 100644 --- a/lib/pages/person_list_page.dart +++ b/lib/pages/person_list_page.dart @@ -66,6 +66,7 @@ class _PersonListPageState extends State { } for (final b in provider.books.where((b) => !b.isDeleted)) { 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(); @@ -119,7 +120,7 @@ class _PersonListPageState extends State { style: TextStyle(fontSize: 15, color: colors.onSurface), cursorColor: colors.primary, decoration: InputDecoration( - hintText: '搜索导演、编剧、演员、作者', + hintText: '搜索导演、编剧、演员、作者、译者', hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.3)), isDense: true, contentPadding: const EdgeInsets.symmetric(vertical: 12), @@ -156,7 +157,7 @@ class _PersonListPageState extends State { padding: const EdgeInsets.fromLTRB(20, 10, 20, 4), child: Row( children: [ - for (final f in ['all', '导演', '编剧', '主演', '作者']) + for (final f in ['all', '导演', '编剧', '主演', '作者', '译者']) Padding( padding: const EdgeInsets.only(right: 6), child: GestureDetector( @@ -208,6 +209,7 @@ class _PersonListPageState extends State { '编剧': const Color(0xFF009688), '主演': const Color(0xFFE91E63), '作者': const Color(0xFF7E57C2), + '译者': const Color(0xFFFF7043), }; final totalWorks = person.movies.length + person.books.length; return ListTile( @@ -262,6 +264,7 @@ class _PersonDetailPage extends StatelessWidget { '编剧': const Color(0xFF009688), '主演': const Color(0xFFE91E63), '作者': const Color(0xFF7E57C2), + '译者': const Color(0xFFFF7043), }; final movieItems = <_WorkItem>[]; @@ -275,7 +278,10 @@ class _PersonDetailPage extends StatelessWidget { movieItems.add(_WorkItem(title: m.title, roles: roles, path: m.posterPath, data: m)); } for (final b in person.books) { - bookItems.add(_WorkItem(title: b.title, roles: const ['作者'], path: b.coverPath, data: b)); + final roles = []; + 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( diff --git a/lib/pages/stroll_page.dart b/lib/pages/stroll_page.dart index 52bd50d..abe42c0 100644 --- a/lib/pages/stroll_page.dart +++ b/lib/pages/stroll_page.dart @@ -340,7 +340,7 @@ class _StrollPageState extends State { /// 有图片的卡片:全屏沉浸式 Widget _buildImmersiveCard(_StrollItem item, ColorScheme colors) { return Container( - margin: const EdgeInsets.symmetric(vertical: 56, horizontal: 8), + margin: const EdgeInsets.symmetric(vertical: 80, horizontal: 8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 20, offset: const Offset(0, 8))], @@ -382,7 +382,7 @@ class _StrollPageState extends State { /// 无图片的卡片:内容从顶部开始 Widget _buildContentCard(_StrollItem item, ColorScheme colors) { return Container( - margin: const EdgeInsets.symmetric(vertical: 56, horizontal: 8), + margin: const EdgeInsets.symmetric(vertical: 80, horizontal: 8), decoration: BoxDecoration( color: colors.surface, borderRadius: BorderRadius.circular(20), diff --git a/lib/utils/database_helper.dart b/lib/utils/database_helper.dart index e0f3fa0..cf90d8b 100644 --- a/lib/utils/database_helper.dart +++ b/lib/utils/database_helper.dart @@ -72,7 +72,7 @@ class DatabaseHelper { return await openDatabase( path, - version: 28, + version: 29, onCreate: _createDB, onUpgrade: _onUpgrade, ); @@ -255,6 +255,13 @@ class DatabaseHelper { // 移除 Note Plus 功能,删除 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(添加阅读始末日期字段) @@ -665,6 +672,7 @@ class DatabaseHelper { title TEXT NOT NULL, cover_path TEXT, authors TEXT, + translators TEXT, alternate_titles TEXT, publisher TEXT, genres TEXT, diff --git a/lib/utils/usage_stats_service.dart b/lib/utils/usage_stats_service.dart index 48d5fd4..fc930e8 100644 --- a/lib/utils/usage_stats_service.dart +++ b/lib/utils/usage_stats_service.dart @@ -1,9 +1,11 @@ import 'dart:async'; import 'dart:convert'; 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:http/http.dart' as http; +import 'package:package_info_plus/package_info_plus.dart'; import 'user_prefs.dart'; import 'server_config.dart'; @@ -59,16 +61,40 @@ class UsageStatsService with WidgetsBindingObserver { /// 确保设备有匿名ID Future _ensureDeviceId() async { if (_prefs.deviceId.isEmpty) { - final id = _generateDeviceId(); + final id = await _generateDeviceId(); await _prefs.setDeviceId(id); } } - /// 生成匿名设备标识 - String _generateDeviceId() { - final random = Random.secure(); - final bytes = List.generate(16, (_) => random.nextInt(256)); - final hex = bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); + /// 基于设备硬件信息生成匿名设备标识(SHA-256 哈希) + Future _generateDeviceId() async { + final deviceInfo = DeviceInfoPlugin(); + String rawId; + + 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)}-' '${hex.substring(8, 12)}-' '${hex.substring(12, 16)}-' @@ -83,6 +109,12 @@ class UsageStatsService with WidgetsBindingObserver { if (deviceId.isEmpty) return; try { + String appVersion = ''; + try { + final pkgInfo = await PackageInfo.fromPlatform(); + appVersion = '${pkgInfo.version}+${pkgInfo.buildNumber}'; + } catch (_) {} + await http .post( Uri.parse('$serverUrl/api/heartbeat'), @@ -93,6 +125,7 @@ class UsageStatsService with WidgetsBindingObserver { Platform.operatingSystem, // android/ios/windows/macos/linux 'device_name': '${Platform.operatingSystem} ${Platform.operatingSystemVersion}', + 'app_version': appVersion, }), ) .timeout(const Duration(seconds: 5)); diff --git a/lib/widgets/tag_side_panel.dart b/lib/widgets/tag_side_panel.dart index 2a73cde..ea25c77 100644 --- a/lib/widgets/tag_side_panel.dart +++ b/lib/widgets/tag_side_panel.dart @@ -210,12 +210,14 @@ class _TagSidePanelState extends State { Expanded( child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(16, 8, 16, 24), - child: Wrap( - spacing: 8, - runSpacing: 8, - alignment: WrapAlignment.start, - crossAxisAlignment: WrapCrossAlignment.start, - children: widget.allAvailableTags.map((tag) { + child: SizedBox( + width: double.infinity, + child: Wrap( + spacing: 8, + runSpacing: 8, + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + children: widget.allAvailableTags.map((tag) { final isSelected = _selected.contains(tag); return GestureDetector( onTap: () => _toggleTag(tag), @@ -240,6 +242,7 @@ class _TagSidePanelState extends State { ), ); }).toList(), + ), ), ), ), diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 7fb92d3..8080fcd 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import device_info_plus import dynamic_color import file_picker import file_selector_macos @@ -16,6 +17,7 @@ import sqflite_darwin import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 7b373ba..474bec1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -90,13 +90,29 @@ packages: source: hosted version: "0.3.5+2" crypto: - dependency: transitive + dependency: "direct main" description: name: crypto sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf url: "https://pub.dev" source: hosted 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: dependency: "direct main" description: @@ -1042,6 +1058,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 159e54f..99acc80 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,6 +34,8 @@ dependencies: uuid: ^4.5.0 expandable: ^5.0.1 sqflite_common_ffi: ^2.3.0 + device_info_plus: ^11.2.0 + crypto: ^3.0.6 dev_dependencies: flutter_test: