add search

This commit is contained in:
2025-08-28 16:58:18 +08:00
parent 66779421c7
commit 23d4953d89
6 changed files with 244 additions and 41 deletions

View File

@@ -40,6 +40,36 @@ class ApiClient {
return headers;
}
Future<List<SearchResultBook>> searchBooks(String keyword) async {
final params = {
'extend': '',
'tab': '0',
'gender': '0',
'refresh_state': '8',
'page': '1',
'wd': keyword,
'is_short_story_user': '0'
};
params['sign'] = _generateSignature(params, _signKey);
final response = await _dio.get(
"$_baseUrlBc/search/v1/words",
queryParameters: params,
options: Options(headers: _getHeaders('00000000')),
);
if (response.statusCode == 200 && response.data['data'] != null) {
List<dynamic> books = response.data['data']['books'] ?? [];
return books
.where((json) =>
json['id'] != null && json['id'].toString().isNotEmpty)
.map((json) => SearchResultBook.fromSearchJson(json))
.toList();
} else {
throw Exception('搜索失败: ${response.data['message']}');
}
}
Future<Book> fetchBookInfo(String bookId) async {
final params = {'id': bookId, 'imei_ip': '2937357107', 'teeny_mode': '0'};
params['sign'] = _generateSignature(params, _signKey);

View File

@@ -84,4 +84,33 @@ class BookChapter {
sort: _parseInt(json['chapter_sort']),
);
}
}
class SearchResultBook {
final String id;
final String title;
final String author;
final bool isOver;
SearchResultBook({
required this.id,
required this.title,
required this.author,
required this.isOver,
});
factory SearchResultBook.fromSearchJson(Map<String, dynamic> json) {
// Helper function to remove HTML tags
String _removeHtmlTags(String htmlText) {
RegExp exp = RegExp(r"<[^>]*>", multiLine: true, caseSensitive: true);
return htmlText.replaceAll(exp, '');
}
return SearchResultBook(
id: json['id']?.toString() ?? '',
title: _removeHtmlTags(json['title'] ?? '无书名'),
author: _removeHtmlTags(json['author'] ?? '未知作者'),
isOver: json['is_over'] == '1',
);
}
}

View File

@@ -0,0 +1,67 @@
// lib/providers/search_provider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/book.dart';
import '../core/api_client.dart';
class SearchState {
final List<SearchResultBook> searchResults;
final bool isLoading;
final String? error;
SearchState({
this.searchResults = const [],
this.isLoading = false,
this.error,
});
SearchState copyWith({
List<SearchResultBook>? searchResults,
bool? isLoading,
String? error,
}) {
return SearchState(
searchResults: searchResults ?? this.searchResults,
isLoading: isLoading ?? this.isLoading,
error: error,
);
}
}
class SearchNotifier extends StateNotifier<SearchState> {
final ApiClient _apiClient;
SearchNotifier(this._apiClient) : super(SearchState());
Future<void> searchBooks(String keyword) async {
if (keyword.trim().isEmpty) {
state = SearchState(searchResults: []);
return;
}
state = state.copyWith(isLoading: true, error: null);
try {
final results = await _apiClient.searchBooks(keyword);
state = state.copyWith(
searchResults: results,
isLoading: false,
);
} catch (e) {
state = state.copyWith(
error: e.toString(),
isLoading: false,
);
}
}
void clearSearch() {
state = SearchState();
}
}
final apiClientProvider = Provider((ref) => ApiClient());
final searchProvider = StateNotifierProvider<SearchNotifier, SearchState>((ref) {
final apiClient = ref.watch(apiClientProvider);
return SearchNotifier(apiClient);
});

View File

@@ -5,9 +5,11 @@ import 'package:url_launcher/url_launcher.dart';
import 'package:flutter/gestures.dart';
import '../../providers/book_provider.dart';
import '../../providers/theme_provider.dart';
import '../../providers/search_provider.dart';
import '../widgets/book_detail_view.dart';
import '../widgets/responsive_layout.dart';
import '../widgets/status_bar.dart';
import '../widgets/search_result_view.dart';
import '../../globals.dart';
String? _parseBookIdInput(String input) {
@@ -45,51 +47,53 @@ class HomeScreen extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final searchController = TextEditingController();
ref.listen<SearchState>(searchProvider, (previous, next) {
final isMobile = MediaQuery.of(context).size.width < 600;
if (isMobile && (previous?.isLoading ?? false) && !next.isLoading) {
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('搜索结果'),
content: SizedBox(
width: double.maxFinite,
child: SearchResultView(
onResultSelected: () {
Navigator.of(dialogContext).pop();
},
),
),
actions: [
TextButton(
child: const Text('关闭'),
onPressed: () => Navigator.of(dialogContext).pop(),
),
],
),
);
}
});
// --- 核心修改点: 重构 performSearch 方法 ---
void performSearch() {
final rawInput = searchController.text.trim();
if (rawInput.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请输入小说ID链接')),
const SnackBar(content: Text('请输入小说ID链接或关键词')),
);
return;
}
// 调用解析函数
final String? parsedId = _parseBookIdInput(rawInput);
// 根据解析结果执行操作
if (parsedId != null) {
// --- 逻辑分支 1 & 2: 成功解析 ---
// 如果解析出的ID与原始输入不同说明是从URL中提取的则更新输入框
if (parsedId != rawInput) {
searchController.text = parsedId;
}
// 使用干净的ID执行搜索
ref.read(bookProvider.notifier).fetchBook(parsedId);
ref.read(searchProvider.notifier).clearSearch();
} else {
// --- 逻辑分支 3: 解析失败 ---
// 弹窗报错
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('输入无效'),
content: const Text('请输入纯数字ID或有效的七猫小说链接。'),
actions: [
TextButton(
child: const Text('确定'),
onPressed: () => Navigator.of(context).pop(),
),
],
),
);
// 清除输入框内容
searchController.clear();
ref.read(searchKeywordProvider.notifier).state = rawInput;
ref.read(searchProvider.notifier).searchBooks(rawInput);
}
}
@@ -99,11 +103,10 @@ class HomeScreen extends ConsumerWidget {
child: TextField(
controller: searchController,
decoration: InputDecoration(
labelText: '小说ID',
hintText: '在此输入七猫小说ID',
// 设置所有边框状态的圆角
labelText: '搜索',
hintText: '输入小说ID、链接或关键词',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24.0), // 从默认值增大
borderRadius: BorderRadius.circular(24.0),
),
suffixIcon: IconButton(
icon: const Icon(Icons.search),
@@ -212,14 +215,9 @@ class HomeScreen extends ConsumerWidget {
child: Column(
children: [
searchBar,
// 可以在这里添加搜索历史或推荐列表
const Padding(
padding: EdgeInsets.all(16.0),
child: Text(
'输入小说ID后详细信息将显示在右侧。',
textAlign: TextAlign.center,
),
)
const Expanded(
child: SearchResultView(),
),
],
),
),

View File

@@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:swiftcat_downloader/providers/book_provider.dart';
import 'package:swiftcat_downloader/providers/search_provider.dart';
class SearchResultView extends ConsumerWidget {
final VoidCallback? onResultSelected;
const SearchResultView({super.key, this.onResultSelected});
@override
Widget build(BuildContext context, WidgetRef ref) {
final searchState = ref.watch(searchProvider);
final selectedBookId = ref.watch(selectedBookIdProvider);
if (searchState.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (searchState.error != null) {
return Center(child: Text('搜索出错: ${searchState.error}'));
}
if (searchState.searchResults.isEmpty) {
final searchKeyword = ref.watch(searchKeywordProvider);
if (searchKeyword.isNotEmpty) {
return Center(child: Text('没有找到与“$searchKeyword”相关的结果。'));
}
return const Center(child: Text('没有搜索结果。'));
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Text('共找到 ${searchState.searchResults.length} 条结果。', style: Theme.of(context).textTheme.titleMedium),
),
Expanded(
child: ListView.builder(
itemCount: searchState.searchResults.length,
itemBuilder: (context, index) {
final book = searchState.searchResults[index];
final status = book.isOver ? '完结' : '连载中';
return RadioListTile<String>(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${book.title}'),
Text(
book.author,
style: Theme.of(context).textTheme.bodySmall,
),
Text(
status,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
value: book.id,
groupValue: selectedBookId,
onChanged: (value) {
if (value != null) {
ref.read(selectedBookIdProvider.notifier).state = value;
ref.read(bookProvider.notifier).fetchBook(value);
onResultSelected?.call();
}
},
);
},
),
),
],
);
}
}
final selectedBookIdProvider = StateProvider<String?>((ref) => null);
final searchKeywordProvider = StateProvider<String>((ref) => '');

View File

@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+202507240
version: 1.0.1+202508280
environment:
sdk: ^3.8.1