9 Commits
v1.0.0 ... main

Author SHA1 Message Date
f39dc3b204 update readme 2025-10-21 01:54:20 +08:00
41ae738ad7 update version 2025-10-21 00:49:04 +08:00
23d4953d89 add search 2025-08-28 16:58:18 +08:00
66779421c7 remove unused import 2025-08-28 14:39:20 +08:00
97c59aac8e update ci 2025-07-24 03:04:06 +08:00
a35f1ebc52 update ci 2025-07-24 03:03:27 +08:00
b668d8c218 fix android 2025-07-24 03:00:53 +08:00
768bd4b71b fix android 2025-07-24 02:59:58 +08:00
53171e900d docs 2025-07-24 00:55:18 +08:00
12 changed files with 421 additions and 103 deletions

54
.github/workflows/build-android.yml vendored Normal file
View File

@@ -0,0 +1,54 @@
# 由于安卓构建需要较长时间,故创建备用的独立工作流
name: Build Android App
on:
workflow_dispatch:
# 允许手动触发工作流
jobs:
#-------------------------------------------------
# Android (Unchanged, as it builds a universal app)
#-------------------------------------------------
build_android:
name: Build Android
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
- name: Get Flutter dependencies
run: flutter pub get
- name: Decode Keystore
run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/app/upload-keystore.jks
- name: Create key.properties file
run: |
echo "storeFile=$(pwd)/android/app/upload-keystore.jks" > android/key.properties
echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/key.properties
echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> android/key.properties
echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/key.properties
- name: Build Flutter App Bundle and APK
run: |
flutter build appbundle --release
flutter build apk --release
- name: Upload Android Artifacts
uses: actions/upload-artifact@v4
with:
name: android-builds
path: |
build/app/outputs/bundle/release/app-release.aab
build/app/outputs/flutter-apk/app-release.apk

View File

@@ -1,4 +1,4 @@
# SwiftCat Downloader Flutter
# 灵猫小说下载器 Flutter
一个用于下载七猫小说的工具。
@@ -6,14 +6,57 @@
**Flutter 版本是技术测试版本,不保证稳定性。**
**Flutter 版本不支持Epub下载格式。**
## 特性
- [x] 支持保存为TXT格式
- [x] 支持保存为单文件或按章节保存*
- [x] 极快的下载速度
- [x] 漂亮的用户界面
- [x] 全平台支持*
- [x] 通过书名搜索小说
- [ ] 下载为EPUB格式
*按章节保存支持平台Android、iOS、macOS、Windows、Linux
*全平台支持Android、iOS、macOS、Windows、Linux、Web
## 特定于平台的说明
#### Android
由于 Android 10 及以上版本的存储权限限制,目前 Android 版本只能保存到 Download 文件夹。
支持的架构arm64-v8a、armeabi-v7a、x86_64
#### iOS
由于签名需要向Apple缴纳开发者费用iOS 版本提供的ipa文件需要您自行使用第三方工具如 爱思助手)进行签名后才能安装。
我们不提供关于签名的技术支持,请您自行查找相关资料。
#### macOS
macOS 版本由于需要自定义文件保存路径,故运行时未使用沙盒模式,请您仅从信任的来源下载应用。
您可能需要执行以下操作来在新版本的 macOS 上运行应用:
1. 打开“终端”应用
2. 输入并回车执行 `sudo spctl --global-disable`
输入您的管理员密码(输入时不会显示)并回车;
3. 打开 系统设置 > 隐私与安全性 > 常规,
在“允许以下来源的应用程序”下选择“任何来源”,输入密码并同意。
4. 在“终端”应用中输入并回车执行 `sudo xattr -r -d com.apple.quarantine /Applications/灵猫小说下载器.app`
5. 运行应用。
支持的架构arm64 (Apple Silicon)、x86_64 (Intel)
#### Linux
支持的架构arm64、x86_64
#### Web
Web 版本支持 Chrome、Edge、Firefox 等现代浏览器,您可能需要在浏览器设置中打开硬件加速功能以避免白屏。
由于浏览器限制,不支持分章节保存模式,且仅能保存至浏览器下载目录。
## 许可
基于 星缘工作室软件共享许可证A 1.0 (SSLA 1.0) 发布,详情见 [LICENSE.md](https://github.com/shing-yu/swiftcat-downloader-flutter/blob/main/LICENSE.md)。
仅供个人使用,严禁用于任何商业目的。
仅供个人学习研究使用,严禁用于任何商业目的请于下载后24小时内删除小说文件
书籍内容著作权归原作者所有,使用本软件下载书籍内容前请确保您遵循当地法律法规。
本项目作者、贡献者不对因用户使用本软件而导致的任何直接、间接、偶然、特殊或后果性损害承担责任。
## 技术支持
社区Q群690736066
如果您认为本程序侵犯了您的权益,请通过 shyu@staredges.cn 联系我们。

View File

@@ -1,10 +1,13 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<application
android:label="灵猫小说下载器"
android:name="${applicationName}"
android:requestLegacyExternalStorage="true"
android:icon="@mipmap/launcher_icon">
<activity
android:name=".MainActivity"

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

@@ -1,5 +1,4 @@
// lib/models/book.dart
import 'package:flutter/foundation.dart';
// --- 新增的辅助函数 ---
// 这个函数能安全地将任何动态类型的值转换为整数。
@@ -85,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

@@ -1,14 +1,14 @@
// lib/ui/widgets/book_detail_view.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:file_picker/file_picker.dart';
import 'package:open_file/open_file.dart';
import 'package:flutter/foundation.dart';
import 'dart:io' show Platform , File, Directory;
import 'dart:io' show Platform , Directory;
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:file_saver/file_saver.dart';
import 'package:device_info_plus/device_info_plus.dart'; // 新增: 用于获取设备信息
import '../../core/book_downloader.dart';
import '../../models/book.dart';
@@ -17,23 +17,6 @@ import '../../providers/book_provider.dart';
final bool isAndroid = !kIsWeb && Platform.isAndroid;
final bool isIOS = !kIsWeb && Platform.isIOS;
/// (仅安卓) 检查文件路径是否存在,如果存在,则返回一个新的、不冲突的文件路径。
Future<String> _getUniqueFilePath(String filePath) async {
String newPath = filePath;
int counter = 1;
final context = p.Context(style: p.Style.posix);
while (await File(newPath).exists() || await Directory(newPath).exists()) {
final String directory = context.dirname(filePath);
final String extension = context.extension(filePath);
final String filenameWithoutExt = context.basenameWithoutExtension(filePath);
newPath = context.join(directory, '$filenameWithoutExt($counter)$extension');
counter++;
}
return newPath;
}
class BookDetailView extends ConsumerStatefulWidget {
const BookDetailView({super.key});
@@ -44,19 +27,15 @@ class BookDetailView extends ConsumerStatefulWidget {
class _BookDetailViewState extends ConsumerState<BookDetailView> {
DownloadFormat _selectedFormat = DownloadFormat.singleTxt;
// --- 新增: 状态变量,用于在异步方法和 build 方法之间传递数据 ---
String? _lastDownloadedPath;
Future<String?> _getMobileDownloadsDirectory() async {
Directory? directory;
try {
if (Platform.isIOS) {
// iOS 平台保存到应用文档目录
directory = await getApplicationDocumentsDirectory();
} else {
// Android 平台,首先尝试公共的 Download 目录
directory = Directory('/storage/emulated/0/Download');
// 如果这个目录因为某些原因不存在,则回退到应用外部存储的根目录
if (!await directory.exists()) {
directory = await getExternalStorageDirectory();
}
@@ -75,7 +54,6 @@ class _BookDetailViewState extends ConsumerState<BookDetailView> {
try {
if (kIsWeb) {
// --- Web 平台逻辑 ---
if (_selectedFormat == DownloadFormat.chapterTxt) {
showDialog(
context: context,
@@ -92,44 +70,59 @@ class _BookDetailViewState extends ConsumerState<BookDetailView> {
);
return;
} else {
// 对于 Web我们不需要真实的目录。我们只用文件名作为标识。
// 下载器将会在内存中处理数据,而不是写入文件。
String extension = _selectedFormat == DownloadFormat.singleTxt ? 'txt' : 'epub';
outputPath = '$fileName.$extension';
}
} else if (isAndroid || isIOS) {
// --- 移动平台 (Android/iOS) 逻辑 ---
var hasPermission = true;
if (isAndroid) {
// 只在 Android 上请求权限
var status = await Permission.storage.status;
if (status.isDenied) {
status = await Permission.storage.request();
// --- 修改点: 根据安卓版本请求权限 ---
final deviceInfo = await DeviceInfoPlugin().androidInfo;
PermissionStatus status;
// Android 11 (API 30) 或更高版本
if (deviceInfo.version.sdkInt >= 30) {
status = await Permission.manageExternalStorage.status;
if (!status.isGranted) {
status = await Permission.manageExternalStorage.request();
}
} else { // Android 10 (API 29) 或更低版本
status = await Permission.storage.status;
if (!status.isGranted) {
status = await Permission.storage.request();
}
}
hasPermission = status.isGranted;
}
if (hasPermission) {
// --- 核心修改点: 使用新的路径获取方法 ---
final String? downloadsPath = await _getMobileDownloadsDirectory();
if (downloadsPath != null) {
String initialPath;
if (_selectedFormat == DownloadFormat.chapterTxt) {
initialPath = '$downloadsPath/$fileName';
outputPath = '$downloadsPath/$fileName';
} else {
String extension = _selectedFormat == DownloadFormat.singleTxt ? 'txt' : 'epub';
initialPath = '$downloadsPath/$fileName.$extension';
outputPath = '$downloadsPath/$fileName.$extension';
}
// 同样只在 Android 上处理同名问题
outputPath = isAndroid ? await _getUniqueFilePath(initialPath) : initialPath;
// --- 已移除: 不再调用 _getUniqueFilePath ---
} else {
throw Exception("无法获取下载目录。");
}
} else {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('需要存储权限才能下载文件。')),
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('权限不足'),
content: const Text('需要存储权限才能下载文件'),
actions: [
TextButton(
child: const Text('确定'),
onPressed: () => Navigator.of(context).pop(),
),
],
),
);
return;
}
@@ -153,11 +146,9 @@ class _BookDetailViewState extends ConsumerState<BookDetailView> {
}
if (outputPath != null) {
// --- 修改点: 在启动下载前,保存文件路径到状态变量 ---
setState(() {
_lastDownloadedPath = outputPath;
});
// --- 修改点: 移除了这里的 ref.listen ---
ref.read(downloadProvider.notifier).startDownload(
book: book,
format: _selectedFormat,
@@ -173,31 +164,23 @@ class _BookDetailViewState extends ConsumerState<BookDetailView> {
@override
Widget build(BuildContext context) {
// --- 核心修改点: 调整 ref.listen 的内部逻辑 ---
ref.listen<DownloadState>(downloadProvider, (previous, next) {
if (previous?.isDownloading == true && !next.isDownloading && next.status.contains('成功')) {
// 检查路径是否存在
if (_lastDownloadedPath != null) {
// --- 解决方案: 创建一个局部 final 变量来捕获当前路径的值 ---
final String path = _lastDownloadedPath!;
final String fileName = p.basename(path); // 从完整路径中提取文件名
final String fileName = p.basename(path);
// 显示 SnackBar
if (kIsWeb) {
// --- Web 平台: 直接从 state 获取 bytes 并触发浏览器下载 ---
if (next.data != null) {
FileSaver.instance.saveFile(
name: p.basenameWithoutExtension(fileName), // 文件名 (无扩展名)
bytes: next.data!, // 文件内容
fileExtension: p.extension(fileName).replaceFirst('.', ''), // 扩展名 (去掉点)
mimeType: MimeType.text
name: p.basenameWithoutExtension(fileName),
bytes: next.data!,
fileExtension: p.extension(fileName).replaceFirst('.', ''),
mimeType: MimeType.text
);
// --- 新增: 保存文件后,调用清理方法 ---
ref.read(downloadProvider.notifier).clearDownloadData();
}
} else {
// --- 移动/桌面平台: 显示带“打开”按钮的 SnackBar ---
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('下载完成: $path'),
@@ -210,7 +193,6 @@ class _BookDetailViewState extends ConsumerState<BookDetailView> {
)
);
}
// 现在可以安全地清空成员变量,为下一次下载做准备了
_lastDownloadedPath = null;
}
}
@@ -301,7 +283,6 @@ class _BookDetailViewState extends ConsumerState<BookDetailView> {
);
}
// _buildDownloadControls 方法保持不变
Widget _buildDownloadControls(Book book) {
final downloadState = ref.watch(downloadProvider);
@@ -385,4 +366,4 @@ class _BookDetailViewState extends ConsumerState<BookDetailView> {
},
);
}
}
}

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

@@ -5,6 +5,7 @@
import FlutterMacOS
import Foundation
import device_info_plus
import file_picker
import file_saver
import open_file_mac
@@ -14,6 +15,7 @@ import url_launcher_macos
import window_size
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin"))
OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin"))

View File

@@ -121,6 +121,22 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.8"
device_info_plus:
dependency: "direct main"
description:
name: device_info_plus
sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a"
url: "https://pub.flutter-io.cn"
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.flutter-io.cn"
source: hosted
version: "7.0.3"
dio:
dependency: "direct main"
description:
@@ -161,6 +177,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.4"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.0.1"
file_picker:
dependency: "direct main"
description:
@@ -738,6 +762,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.14.0"
win32_registry:
dependency: transitive
description:
name: win32_registry
sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.0"
window_size:
dependency: "direct main"
description:
@@ -772,4 +804,4 @@ packages:
version: "3.1.3"
sdks:
dart: ">=3.8.1 <4.0.0"
flutter: ">=3.27.0"
flutter: ">=3.29.0"

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+202507230
version: 1.0.1+202510210
environment:
sdk: ^3.8.1
@@ -58,6 +58,7 @@ dependencies:
window_size: ^0.1.0
url_launcher: ^6.0.0
package_info_plus: ^8.3.0
device_info_plus: ^11.5.0
dev_dependencies:
flutter_test: