feat: 42kit Nav

feat: Animation Optimization
This commit is contained in:
xkeyC 2025-05-04 14:07:56 +08:00
parent a2de310d84
commit 03c941c970
23 changed files with 5618 additions and 493 deletions

View File

@ -29,6 +29,10 @@ linter:
analyzer:
plugins:
- custom_lint
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"
errors:
invalid_annotation_target: ignore
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

14
lib/api/udb.dart Normal file
View File

@ -0,0 +1,14 @@
import 'dart:convert';
import 'package:starcitizen_doctor/common/io/rs_http.dart';
import 'package:starcitizen_doctor/data/nav_api_data.dart';
class UDBNavApi {
static Future<NavApiData> getNavItems({int pageNo = 1}) async {
final r = await RSHttp.getText(
"https://payload.citizenwiki.cn/api/community-navs?sort=is_sponsored&depth=2&page=$pageNo&limit=1000");
if (r.isEmpty) throw "Network Error";
final result = NavApiData.fromJson(jsonDecode(r));
return result;
}
}

View File

@ -22,7 +22,7 @@ final routerProvider = AutoDisposeProvider<GoRouter>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef RouterRef = AutoDisposeProviderRef<GoRouter>;
String _$appGlobalModelHash() => r'8aa468bda409c425a76e3ef9e7739ca4ed055d2b';
String _$appGlobalModelHash() => r'eb06413ab3a70f26712d897cee745ee62e89e75e';
/// See also [AppGlobalModel].
@ProviderFor(AppGlobalModel)

View File

@ -0,0 +1,101 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:starcitizen_doctor/common/io/rs_http.dart';
class FileCacheUtils {
//
static final Map<String, Future<File>> _downloadingTasks = {};
//
static Directory? _cacheDir;
///
static Future<Directory> _getCacheDirectory() async {
if (_cacheDir != null) return _cacheDir!;
final tempDir = await getTemporaryDirectory();
_cacheDir = Directory(path.join(tempDir.path, 'ScToolbox_File_Cache'));
if (!await _cacheDir!.exists()) {
await _cacheDir!.create(recursive: true);
}
return _cacheDir!;
}
/// URL获取文件
static Future<File> getFile(String url) async {
//
if (_downloadingTasks.containsKey(url)) {
return _downloadingTasks[url]!;
}
final fileTask = _downloadFile(url);
_downloadingTasks[url] = fileTask;
try {
final file = await fileTask;
return file;
} finally {
//
_downloadingTasks.remove(url);
}
}
///
static Future<File> _downloadFile(String url) async {
// (使URL的MD5哈希作为文件名)
final filename = md5.convert(utf8.encode(url)).toString();
final cacheDir = await _getCacheDirectory();
final file = File(path.join(cacheDir.path, filename));
//
if (await file.exists()) {
return file;
}
//
final response = await RSHttp.get(url);
if (response.statusCode == 200) {
await file.writeAsBytes(response.data ?? []);
return file;
} else {
throw Exception('Failed to download file: ${response.statusCode}');
}
}
/// URL的缓存
static Future<bool> clearCache(String url) async {
try {
final filename = md5.convert(utf8.encode(url)).toString();
final cacheDir = await _getCacheDirectory();
final file = File(path.join(cacheDir.path, filename));
if (await file.exists()) {
await file.delete();
return true;
}
return false;
} catch (e) {
return false;
}
}
///
static Future<void> clearAllCache() async {
try {
final cacheDir = await _getCacheDirectory();
if (await cacheDir.exists()) {
await cacheDir.delete(recursive: true);
await cacheDir.create();
}
} catch (e) {
debugPrint('清除缓存失败: $e');
}
}
}

246
lib/data/nav_api_data.dart Normal file
View File

@ -0,0 +1,246 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'nav_api_data.freezed.dart';
part 'nav_api_data.g.dart';
@freezed
class NavApiDocsItemData with _$NavApiDocsItemData {
const factory NavApiDocsItemData({
@Default('') @JsonKey(name: 'id') String id,
@Default('') @JsonKey(name: 'name') String name,
@Default('') @JsonKey(name: 'slug') String slug,
@Default('') @JsonKey(name: 'abstract') String abstract_,
@Default('') @JsonKey(name: 'description') String description,
@Default(NavApiDocsItemImageData())
@JsonKey(name: 'image')
NavApiDocsItemImageData image,
@Default('') @JsonKey(name: 'link') String link,
@Default(false) @JsonKey(name: 'is_sponsored') bool isSponsored,
@Default(<NavApiDocsItemTagsItemData>[])
@JsonKey(name: 'tags')
List<NavApiDocsItemTagsItemData> tags,
@Default('') @JsonKey(name: 'updatedAt') String updatedAt,
@Default('') @JsonKey(name: 'createdAt') String createdAt,
}) = _NavApiDocsItemData;
const NavApiDocsItemData._();
factory NavApiDocsItemData.fromJson(Map<String, Object?> json) =>
_$NavApiDocsItemDataFromJson(json);
}
@freezed
class NavApiDocsItemImageData with _$NavApiDocsItemImageData {
const factory NavApiDocsItemImageData({
@Default('') @JsonKey(name: 'id') String id,
@Default(NavApiDocsItemImageCreatedByData())
@JsonKey(name: 'createdBy')
NavApiDocsItemImageCreatedByData createdBy,
@Default('') @JsonKey(name: 'title') String title,
@Default(false) @JsonKey(name: 'original') bool original,
@Default('') @JsonKey(name: 'credit') String credit,
@Default('') @JsonKey(name: 'source') String source,
@Default('') @JsonKey(name: 'license') String license,
@JsonKey(name: 'caption') dynamic caption,
@Default('') @JsonKey(name: 'updatedAt') String updatedAt,
@Default('') @JsonKey(name: 'createdAt') String createdAt,
@Default('') @JsonKey(name: 'url') String url,
@Default('') @JsonKey(name: 'filename') String filename,
@Default('') @JsonKey(name: 'mimeType') String mimeType,
@Default(0) @JsonKey(name: 'filesize') int filesize,
@Default(0) @JsonKey(name: 'width') int width,
@Default(0) @JsonKey(name: 'height') int height,
@Default(NavApiDocsItemImageSizesData())
@JsonKey(name: 'sizes')
NavApiDocsItemImageSizesData sizes,
}) = _NavApiDocsItemImageData;
const NavApiDocsItemImageData._();
factory NavApiDocsItemImageData.fromJson(Map<String, Object?> json) =>
_$NavApiDocsItemImageDataFromJson(json);
}
@freezed
class NavApiDocsItemImageCreatedByData with _$NavApiDocsItemImageCreatedByData {
const factory NavApiDocsItemImageCreatedByData({
@Default('') @JsonKey(name: 'id') String id,
@Default('') @JsonKey(name: 'sub') String sub,
@Default('') @JsonKey(name: 'external_provider') String externalProvider,
@Default('') @JsonKey(name: 'username') String username,
@Default('') @JsonKey(name: 'name') String name,
@Default(<String>[]) @JsonKey(name: 'roles') List<String> roles,
@Default('') @JsonKey(name: 'avatar_url') String avatarUrl,
@Default('') @JsonKey(name: 'updatedAt') String updatedAt,
@Default('') @JsonKey(name: 'createdAt') String createdAt,
@Default('') @JsonKey(name: 'email') String email,
@Default(0) @JsonKey(name: 'loginAttempts') int loginAttempts,
@Default('') @JsonKey(name: 'avatar') String avatar,
}) = _NavApiDocsItemImageCreatedByData;
const NavApiDocsItemImageCreatedByData._();
factory NavApiDocsItemImageCreatedByData.fromJson(
Map<String, Object?> json) =>
_$NavApiDocsItemImageCreatedByDataFromJson(json);
}
@freezed
class NavApiDocsItemImageSizesThumbnailData
with _$NavApiDocsItemImageSizesThumbnailData {
const factory NavApiDocsItemImageSizesThumbnailData({
@Default('') @JsonKey(name: 'url') String url,
@Default(0) @JsonKey(name: 'width') int width,
@Default(0) @JsonKey(name: 'height') int height,
@Default('') @JsonKey(name: 'mimeType') String mimeType,
@Default(0) @JsonKey(name: 'filesize') int filesize,
@Default('') @JsonKey(name: 'filename') String filename,
}) = _NavApiDocsItemImageSizesThumbnailData;
const NavApiDocsItemImageSizesThumbnailData._();
factory NavApiDocsItemImageSizesThumbnailData.fromJson(
Map<String, Object?> json) =>
_$NavApiDocsItemImageSizesThumbnailDataFromJson(json);
}
@freezed
class NavApiDocsItemImageSizesData with _$NavApiDocsItemImageSizesData {
const factory NavApiDocsItemImageSizesData({
@Default(NavApiDocsItemImageSizesThumbnailData())
@JsonKey(name: 'thumbnail')
NavApiDocsItemImageSizesThumbnailData thumbnail,
@Default(NavApiDocsItemImageSizesPreloadData())
@JsonKey(name: 'preload')
NavApiDocsItemImageSizesPreloadData preload,
@Default(NavApiDocsItemImageSizesCardData())
@JsonKey(name: 'card')
NavApiDocsItemImageSizesCardData card,
@Default(NavApiDocsItemImageSizesTabletData())
@JsonKey(name: 'tablet')
NavApiDocsItemImageSizesTabletData tablet,
@Default(NavApiDocsItemImageSizesAvatarData())
@JsonKey(name: 'avatar')
NavApiDocsItemImageSizesAvatarData avatar,
}) = _NavApiDocsItemImageSizesData;
const NavApiDocsItemImageSizesData._();
factory NavApiDocsItemImageSizesData.fromJson(Map<String, Object?> json) =>
_$NavApiDocsItemImageSizesDataFromJson(json);
}
@freezed
class NavApiDocsItemImageSizesPreloadData
with _$NavApiDocsItemImageSizesPreloadData {
const factory NavApiDocsItemImageSizesPreloadData({
@JsonKey(name: 'url') dynamic url,
@JsonKey(name: 'width') dynamic width,
@JsonKey(name: 'height') dynamic height,
@JsonKey(name: 'mimeType') dynamic mimeType,
@JsonKey(name: 'filesize') dynamic filesize,
@JsonKey(name: 'filename') dynamic filename,
}) = _NavApiDocsItemImageSizesPreloadData;
const NavApiDocsItemImageSizesPreloadData._();
factory NavApiDocsItemImageSizesPreloadData.fromJson(
Map<String, Object?> json) =>
_$NavApiDocsItemImageSizesPreloadDataFromJson(json);
}
@freezed
class NavApiDocsItemImageSizesCardData with _$NavApiDocsItemImageSizesCardData {
const factory NavApiDocsItemImageSizesCardData({
@Default('') @JsonKey(name: 'url') String url,
@Default(0) @JsonKey(name: 'width') int width,
@Default(0) @JsonKey(name: 'height') int height,
@Default('') @JsonKey(name: 'mimeType') String mimeType,
@Default(0) @JsonKey(name: 'filesize') int filesize,
@Default('') @JsonKey(name: 'filename') String filename,
}) = _NavApiDocsItemImageSizesCardData;
const NavApiDocsItemImageSizesCardData._();
factory NavApiDocsItemImageSizesCardData.fromJson(
Map<String, Object?> json) =>
_$NavApiDocsItemImageSizesCardDataFromJson(json);
}
@freezed
class NavApiDocsItemImageSizesTabletData
with _$NavApiDocsItemImageSizesTabletData {
const factory NavApiDocsItemImageSizesTabletData({
@Default('') @JsonKey(name: 'url') String url,
@Default(0) @JsonKey(name: 'width') int width,
@Default(0) @JsonKey(name: 'height') int height,
@Default('') @JsonKey(name: 'mimeType') String mimeType,
@Default(0) @JsonKey(name: 'filesize') int filesize,
@Default('') @JsonKey(name: 'filename') String filename,
}) = _NavApiDocsItemImageSizesTabletData;
const NavApiDocsItemImageSizesTabletData._();
factory NavApiDocsItemImageSizesTabletData.fromJson(
Map<String, Object?> json) =>
_$NavApiDocsItemImageSizesTabletDataFromJson(json);
}
@freezed
class NavApiDocsItemImageSizesAvatarData
with _$NavApiDocsItemImageSizesAvatarData {
const factory NavApiDocsItemImageSizesAvatarData({
@Default('') @JsonKey(name: 'url') String url,
@Default(0) @JsonKey(name: 'width') int width,
@Default(0) @JsonKey(name: 'height') int height,
@Default('') @JsonKey(name: 'mimeType') String mimeType,
@Default(0) @JsonKey(name: 'filesize') int filesize,
@Default('') @JsonKey(name: 'filename') String filename,
}) = _NavApiDocsItemImageSizesAvatarData;
const NavApiDocsItemImageSizesAvatarData._();
factory NavApiDocsItemImageSizesAvatarData.fromJson(
Map<String, Object?> json) =>
_$NavApiDocsItemImageSizesAvatarDataFromJson(json);
}
@freezed
class NavApiDocsItemTagsItemData with _$NavApiDocsItemTagsItemData {
const factory NavApiDocsItemTagsItemData({
@Default('') @JsonKey(name: 'id') String id,
@Default('') @JsonKey(name: 'name') String name,
@Default('') @JsonKey(name: 'slug') String slug,
@Default('') @JsonKey(name: 'updatedAt') String updatedAt,
@Default('') @JsonKey(name: 'createdAt') String createdAt,
}) = _NavApiDocsItemTagsItemData;
const NavApiDocsItemTagsItemData._();
factory NavApiDocsItemTagsItemData.fromJson(Map<String, Object?> json) =>
_$NavApiDocsItemTagsItemDataFromJson(json);
}
@freezed
class NavApiData with _$NavApiData {
const factory NavApiData({
@Default(<NavApiDocsItemData>[])
@JsonKey(name: 'docs')
List<NavApiDocsItemData> docs,
@Default(false) @JsonKey(name: 'hasNextPage') bool hasNextPage,
@Default(false) @JsonKey(name: 'hasPrevPage') bool hasPrevPage,
@Default(0) @JsonKey(name: 'limit') int limit,
@JsonKey(name: 'nextPage') dynamic nextPage,
@Default(0) @JsonKey(name: 'page') int page,
@Default(0) @JsonKey(name: 'pagingCounter') int pagingCounter,
@JsonKey(name: 'prevPage') dynamic prevPage,
@Default(0) @JsonKey(name: 'totalDocs') int totalDocs,
@Default(0) @JsonKey(name: 'totalPages') int totalPages,
}) = _NavApiData;
const NavApiData._();
factory NavApiData.fromJson(Map<String, Object?> json) =>
_$NavApiDataFromJson(json);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,336 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'nav_api_data.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$NavApiDocsItemDataImpl _$$NavApiDocsItemDataImplFromJson(
Map<String, dynamic> json) =>
_$NavApiDocsItemDataImpl(
id: json['id'] as String? ?? '',
name: json['name'] as String? ?? '',
slug: json['slug'] as String? ?? '',
abstract_: json['abstract'] as String? ?? '',
description: json['description'] as String? ?? '',
image: json['image'] == null
? const NavApiDocsItemImageData()
: NavApiDocsItemImageData.fromJson(
json['image'] as Map<String, dynamic>),
link: json['link'] as String? ?? '',
isSponsored: json['is_sponsored'] as bool? ?? false,
tags: (json['tags'] as List<dynamic>?)
?.map((e) => NavApiDocsItemTagsItemData.fromJson(
e as Map<String, dynamic>))
.toList() ??
const <NavApiDocsItemTagsItemData>[],
updatedAt: json['updatedAt'] as String? ?? '',
createdAt: json['createdAt'] as String? ?? '',
);
Map<String, dynamic> _$$NavApiDocsItemDataImplToJson(
_$NavApiDocsItemDataImpl instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'slug': instance.slug,
'abstract': instance.abstract_,
'description': instance.description,
'image': instance.image,
'link': instance.link,
'is_sponsored': instance.isSponsored,
'tags': instance.tags,
'updatedAt': instance.updatedAt,
'createdAt': instance.createdAt,
};
_$NavApiDocsItemImageDataImpl _$$NavApiDocsItemImageDataImplFromJson(
Map<String, dynamic> json) =>
_$NavApiDocsItemImageDataImpl(
id: json['id'] as String? ?? '',
createdBy: json['createdBy'] == null
? const NavApiDocsItemImageCreatedByData()
: NavApiDocsItemImageCreatedByData.fromJson(
json['createdBy'] as Map<String, dynamic>),
title: json['title'] as String? ?? '',
original: json['original'] as bool? ?? false,
credit: json['credit'] as String? ?? '',
source: json['source'] as String? ?? '',
license: json['license'] as String? ?? '',
caption: json['caption'],
updatedAt: json['updatedAt'] as String? ?? '',
createdAt: json['createdAt'] as String? ?? '',
url: json['url'] as String? ?? '',
filename: json['filename'] as String? ?? '',
mimeType: json['mimeType'] as String? ?? '',
filesize: (json['filesize'] as num?)?.toInt() ?? 0,
width: (json['width'] as num?)?.toInt() ?? 0,
height: (json['height'] as num?)?.toInt() ?? 0,
sizes: json['sizes'] == null
? const NavApiDocsItemImageSizesData()
: NavApiDocsItemImageSizesData.fromJson(
json['sizes'] as Map<String, dynamic>),
);
Map<String, dynamic> _$$NavApiDocsItemImageDataImplToJson(
_$NavApiDocsItemImageDataImpl instance) =>
<String, dynamic>{
'id': instance.id,
'createdBy': instance.createdBy,
'title': instance.title,
'original': instance.original,
'credit': instance.credit,
'source': instance.source,
'license': instance.license,
'caption': instance.caption,
'updatedAt': instance.updatedAt,
'createdAt': instance.createdAt,
'url': instance.url,
'filename': instance.filename,
'mimeType': instance.mimeType,
'filesize': instance.filesize,
'width': instance.width,
'height': instance.height,
'sizes': instance.sizes,
};
_$NavApiDocsItemImageCreatedByDataImpl
_$$NavApiDocsItemImageCreatedByDataImplFromJson(
Map<String, dynamic> json) =>
_$NavApiDocsItemImageCreatedByDataImpl(
id: json['id'] as String? ?? '',
sub: json['sub'] as String? ?? '',
externalProvider: json['external_provider'] as String? ?? '',
username: json['username'] as String? ?? '',
name: json['name'] as String? ?? '',
roles: (json['roles'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const <String>[],
avatarUrl: json['avatar_url'] as String? ?? '',
updatedAt: json['updatedAt'] as String? ?? '',
createdAt: json['createdAt'] as String? ?? '',
email: json['email'] as String? ?? '',
loginAttempts: (json['loginAttempts'] as num?)?.toInt() ?? 0,
avatar: json['avatar'] as String? ?? '',
);
Map<String, dynamic> _$$NavApiDocsItemImageCreatedByDataImplToJson(
_$NavApiDocsItemImageCreatedByDataImpl instance) =>
<String, dynamic>{
'id': instance.id,
'sub': instance.sub,
'external_provider': instance.externalProvider,
'username': instance.username,
'name': instance.name,
'roles': instance.roles,
'avatar_url': instance.avatarUrl,
'updatedAt': instance.updatedAt,
'createdAt': instance.createdAt,
'email': instance.email,
'loginAttempts': instance.loginAttempts,
'avatar': instance.avatar,
};
_$NavApiDocsItemImageSizesThumbnailDataImpl
_$$NavApiDocsItemImageSizesThumbnailDataImplFromJson(
Map<String, dynamic> json) =>
_$NavApiDocsItemImageSizesThumbnailDataImpl(
url: json['url'] as String? ?? '',
width: (json['width'] as num?)?.toInt() ?? 0,
height: (json['height'] as num?)?.toInt() ?? 0,
mimeType: json['mimeType'] as String? ?? '',
filesize: (json['filesize'] as num?)?.toInt() ?? 0,
filename: json['filename'] as String? ?? '',
);
Map<String, dynamic> _$$NavApiDocsItemImageSizesThumbnailDataImplToJson(
_$NavApiDocsItemImageSizesThumbnailDataImpl instance) =>
<String, dynamic>{
'url': instance.url,
'width': instance.width,
'height': instance.height,
'mimeType': instance.mimeType,
'filesize': instance.filesize,
'filename': instance.filename,
};
_$NavApiDocsItemImageSizesDataImpl _$$NavApiDocsItemImageSizesDataImplFromJson(
Map<String, dynamic> json) =>
_$NavApiDocsItemImageSizesDataImpl(
thumbnail: json['thumbnail'] == null
? const NavApiDocsItemImageSizesThumbnailData()
: NavApiDocsItemImageSizesThumbnailData.fromJson(
json['thumbnail'] as Map<String, dynamic>),
preload: json['preload'] == null
? const NavApiDocsItemImageSizesPreloadData()
: NavApiDocsItemImageSizesPreloadData.fromJson(
json['preload'] as Map<String, dynamic>),
card: json['card'] == null
? const NavApiDocsItemImageSizesCardData()
: NavApiDocsItemImageSizesCardData.fromJson(
json['card'] as Map<String, dynamic>),
tablet: json['tablet'] == null
? const NavApiDocsItemImageSizesTabletData()
: NavApiDocsItemImageSizesTabletData.fromJson(
json['tablet'] as Map<String, dynamic>),
avatar: json['avatar'] == null
? const NavApiDocsItemImageSizesAvatarData()
: NavApiDocsItemImageSizesAvatarData.fromJson(
json['avatar'] as Map<String, dynamic>),
);
Map<String, dynamic> _$$NavApiDocsItemImageSizesDataImplToJson(
_$NavApiDocsItemImageSizesDataImpl instance) =>
<String, dynamic>{
'thumbnail': instance.thumbnail,
'preload': instance.preload,
'card': instance.card,
'tablet': instance.tablet,
'avatar': instance.avatar,
};
_$NavApiDocsItemImageSizesPreloadDataImpl
_$$NavApiDocsItemImageSizesPreloadDataImplFromJson(
Map<String, dynamic> json) =>
_$NavApiDocsItemImageSizesPreloadDataImpl(
url: json['url'],
width: json['width'],
height: json['height'],
mimeType: json['mimeType'],
filesize: json['filesize'],
filename: json['filename'],
);
Map<String, dynamic> _$$NavApiDocsItemImageSizesPreloadDataImplToJson(
_$NavApiDocsItemImageSizesPreloadDataImpl instance) =>
<String, dynamic>{
'url': instance.url,
'width': instance.width,
'height': instance.height,
'mimeType': instance.mimeType,
'filesize': instance.filesize,
'filename': instance.filename,
};
_$NavApiDocsItemImageSizesCardDataImpl
_$$NavApiDocsItemImageSizesCardDataImplFromJson(
Map<String, dynamic> json) =>
_$NavApiDocsItemImageSizesCardDataImpl(
url: json['url'] as String? ?? '',
width: (json['width'] as num?)?.toInt() ?? 0,
height: (json['height'] as num?)?.toInt() ?? 0,
mimeType: json['mimeType'] as String? ?? '',
filesize: (json['filesize'] as num?)?.toInt() ?? 0,
filename: json['filename'] as String? ?? '',
);
Map<String, dynamic> _$$NavApiDocsItemImageSizesCardDataImplToJson(
_$NavApiDocsItemImageSizesCardDataImpl instance) =>
<String, dynamic>{
'url': instance.url,
'width': instance.width,
'height': instance.height,
'mimeType': instance.mimeType,
'filesize': instance.filesize,
'filename': instance.filename,
};
_$NavApiDocsItemImageSizesTabletDataImpl
_$$NavApiDocsItemImageSizesTabletDataImplFromJson(
Map<String, dynamic> json) =>
_$NavApiDocsItemImageSizesTabletDataImpl(
url: json['url'] as String? ?? '',
width: (json['width'] as num?)?.toInt() ?? 0,
height: (json['height'] as num?)?.toInt() ?? 0,
mimeType: json['mimeType'] as String? ?? '',
filesize: (json['filesize'] as num?)?.toInt() ?? 0,
filename: json['filename'] as String? ?? '',
);
Map<String, dynamic> _$$NavApiDocsItemImageSizesTabletDataImplToJson(
_$NavApiDocsItemImageSizesTabletDataImpl instance) =>
<String, dynamic>{
'url': instance.url,
'width': instance.width,
'height': instance.height,
'mimeType': instance.mimeType,
'filesize': instance.filesize,
'filename': instance.filename,
};
_$NavApiDocsItemImageSizesAvatarDataImpl
_$$NavApiDocsItemImageSizesAvatarDataImplFromJson(
Map<String, dynamic> json) =>
_$NavApiDocsItemImageSizesAvatarDataImpl(
url: json['url'] as String? ?? '',
width: (json['width'] as num?)?.toInt() ?? 0,
height: (json['height'] as num?)?.toInt() ?? 0,
mimeType: json['mimeType'] as String? ?? '',
filesize: (json['filesize'] as num?)?.toInt() ?? 0,
filename: json['filename'] as String? ?? '',
);
Map<String, dynamic> _$$NavApiDocsItemImageSizesAvatarDataImplToJson(
_$NavApiDocsItemImageSizesAvatarDataImpl instance) =>
<String, dynamic>{
'url': instance.url,
'width': instance.width,
'height': instance.height,
'mimeType': instance.mimeType,
'filesize': instance.filesize,
'filename': instance.filename,
};
_$NavApiDocsItemTagsItemDataImpl _$$NavApiDocsItemTagsItemDataImplFromJson(
Map<String, dynamic> json) =>
_$NavApiDocsItemTagsItemDataImpl(
id: json['id'] as String? ?? '',
name: json['name'] as String? ?? '',
slug: json['slug'] as String? ?? '',
updatedAt: json['updatedAt'] as String? ?? '',
createdAt: json['createdAt'] as String? ?? '',
);
Map<String, dynamic> _$$NavApiDocsItemTagsItemDataImplToJson(
_$NavApiDocsItemTagsItemDataImpl instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'slug': instance.slug,
'updatedAt': instance.updatedAt,
'createdAt': instance.createdAt,
};
_$NavApiDataImpl _$$NavApiDataImplFromJson(Map<String, dynamic> json) =>
_$NavApiDataImpl(
docs: (json['docs'] as List<dynamic>?)
?.map(
(e) => NavApiDocsItemData.fromJson(e as Map<String, dynamic>))
.toList() ??
const <NavApiDocsItemData>[],
hasNextPage: json['hasNextPage'] as bool? ?? false,
hasPrevPage: json['hasPrevPage'] as bool? ?? false,
limit: (json['limit'] as num?)?.toInt() ?? 0,
nextPage: json['nextPage'],
page: (json['page'] as num?)?.toInt() ?? 0,
pagingCounter: (json['pagingCounter'] as num?)?.toInt() ?? 0,
prevPage: json['prevPage'],
totalDocs: (json['totalDocs'] as num?)?.toInt() ?? 0,
totalPages: (json['totalPages'] as num?)?.toInt() ?? 0,
);
Map<String, dynamic> _$$NavApiDataImplToJson(_$NavApiDataImpl instance) =>
<String, dynamic>{
'docs': instance.docs,
'hasNextPage': instance.hasNextPage,
'hasPrevPage': instance.hasPrevPage,
'limit': instance.limit,
'nextPage': instance.nextPage,
'page': instance.page,
'pagingCounter': instance.pagingCounter,
'prevPage': instance.prevPage,
'totalDocs': instance.totalDocs,
'totalPages': instance.totalPages,
};

View File

@ -30,8 +30,7 @@ class AboutUI extends HookConsumerWidget {
);
}
Widget _makeAbout(BuildContext context, WidgetRef ref,
ValueNotifier<bool> isTipTextCn, PageController pageCtrl) {
Widget _makeAbout(BuildContext context, WidgetRef ref, ValueNotifier<bool> isTipTextCn, PageController pageCtrl) {
return Stack(
children: [
Center(
@ -42,9 +41,7 @@ class AboutUI extends HookConsumerWidget {
const SizedBox(height: 32),
Image.asset("assets/app_logo.png", width: 128, height: 128),
const SizedBox(height: 6),
Text(
S.current.app_index_version_info(
ConstConf.appVersion, ConstConf.isMSE ? "" : " Dev"),
Text(S.current.app_index_version_info(ConstConf.appVersion, ConstConf.isMSE ? "" : " Dev"),
style: const TextStyle(fontSize: 18)),
const SizedBox(height: 12),
Button(
@ -56,18 +53,15 @@ class AboutUI extends HookConsumerWidget {
const SizedBox(height: 32),
Container(
margin: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor,
borderRadius: BorderRadius.circular(12)),
decoration:
BoxDecoration(color: FluentTheme.of(context).cardColor, borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
Text(
S.current.about_app_description,
style: TextStyle(
fontSize: 14,
color: Colors.white.withValues(alpha: .9)),
style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .9)),
),
],
),
@ -86,9 +80,7 @@ class AboutUI extends HookConsumerWidget {
child: Container(
width: MediaQuery.of(context).size.width * .35,
decoration: BoxDecoration(
color: FluentTheme.of(context)
.cardColor
.withValues(alpha: .06),
color: FluentTheme.of(context).cardColor.withValues(alpha: .06),
borderRadius: BorderRadius.circular(12)),
child: IconButton(
icon: Padding(
@ -96,9 +88,7 @@ class AboutUI extends HookConsumerWidget {
child: Text(
isTipTextCn.value ? tipTextCN : tipTextEN,
textAlign: TextAlign.start,
style: TextStyle(
fontSize: 12,
color: Colors.white.withValues(alpha: .9)),
style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .9)),
),
),
onPressed: () {
@ -126,8 +116,7 @@ class AboutUI extends HookConsumerWidget {
);
}
Widget _makeDonate(
BuildContext context, WidgetRef ref, PageController pageCtrl) {
Widget _makeDonate(BuildContext context, WidgetRef ref, PageController pageCtrl) {
final donationTypeNotifier = useState('alipay');
final bubbleMessages = [
S.current.support_dev_thanks_message,
@ -171,8 +160,7 @@ class AboutUI extends HookConsumerWidget {
for (var i = 0; i < bubbleMessages.length; i++)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: SelectionArea(
child: ChatBubble(message: bubbleMessages[i])),
child: SelectionArea(child: ChatBubble(message: bubbleMessages[i])),
),
],
),
@ -261,14 +249,10 @@ class AboutUI extends HookConsumerWidget {
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Button(
style: ButtonStyle(
backgroundColor: WidgetStateProperty.resolveWith((states) =>
isSelected
? ButtonThemeData.buttonColor(context, states)
.withAlpha((255.0 * 0.08).round())
: ButtonThemeData.buttonColor(context, states)
.withAlpha((255.0 * 0.005).round())),
padding: WidgetStateProperty.all(
EdgeInsets.symmetric(horizontal: 16, vertical: 8)),
backgroundColor: WidgetStateProperty.resolveWith((states) => isSelected
? ButtonThemeData.buttonColor(context, states).withAlpha((255.0 * 0.08).round())
: ButtonThemeData.buttonColor(context, states).withAlpha((255.0 * 0.005).round())),
padding: WidgetStateProperty.all(EdgeInsets.symmetric(horizontal: 16, vertical: 8)),
),
onPressed: onTap,
child: Column(
@ -328,16 +312,13 @@ class AboutUI extends HookConsumerWidget {
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: FluentTheme.of(context)
.cardColor
.withAlpha((255 * .1).round()),
color: FluentTheme.of(context).cardColor.withAlpha((255 * .1).round()),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(S.current.support_dev_in_game_id,
style: TextStyle(fontSize: 16)),
Text(S.current.support_dev_in_game_id, style: TextStyle(fontSize: 16)),
const SizedBox(width: 12),
Button(
onPressed: () {
@ -386,8 +367,7 @@ class AboutUI extends HookConsumerWidget {
return Column(
key: ValueKey(type),
children: [
Text(title,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
Text(title, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
Container(
width: 200,
@ -413,19 +393,13 @@ class AboutUI extends HookConsumerWidget {
icon: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
pageIndex == 0
? FluentIcons.chevron_up
: FluentIcons.chevron_down,
size: 12),
Icon(pageIndex == 0 ? FluentIcons.chevron_up : FluentIcons.chevron_down, size: 12),
SizedBox(width: 8),
Text(pageIndex == 0
? S.current.support_dev_back_button
: S.current.support_dev_scroll_hint),
Text(pageIndex == 0 ? S.current.support_dev_back_button : S.current.support_dev_scroll_hint),
],
),
onPressed: () => pageCtrl.animateToPage(pageIndex,
duration: const Duration(milliseconds: 300), curve: Curves.ease),
onPressed: () =>
pageCtrl.animateToPage(pageIndex, duration: const Duration(milliseconds: 300), curve: Curves.ease),
);
}
@ -440,8 +414,7 @@ class AboutUI extends HookConsumerWidget {
const SizedBox(width: 8),
Text(
S.current.about_action_btn_faq,
style: TextStyle(
fontSize: 14, color: Colors.white.withValues(alpha: .6)),
style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .6)),
),
],
),
@ -457,8 +430,7 @@ class AboutUI extends HookConsumerWidget {
const SizedBox(width: 8),
Text(
S.current.about_online_feedback,
style: TextStyle(
fontSize: 14, color: Colors.white.withValues(alpha: .6)),
style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .6)),
),
],
),
@ -474,8 +446,7 @@ class AboutUI extends HookConsumerWidget {
const SizedBox(width: 8),
Text(
S.current.about_action_qq_group,
style: TextStyle(
fontSize: 14, color: Colors.white.withValues(alpha: .6)),
style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .6)),
),
],
),
@ -492,8 +463,7 @@ class AboutUI extends HookConsumerWidget {
const SizedBox(width: 8),
Text(
S.current.about_action_email,
style: TextStyle(
fontSize: 14, color: Colors.white.withValues(alpha: .6)),
style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .6)),
),
],
),
@ -509,8 +479,7 @@ class AboutUI extends HookConsumerWidget {
const SizedBox(width: 8),
Text(
S.current.about_action_open_source,
style: TextStyle(
fontSize: 14, color: Colors.white.withValues(alpha: .6)),
style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .6)),
),
],
),
@ -528,6 +497,7 @@ class AboutUI extends HookConsumerWidget {
static String get tipTextCN => S.current.about_disclaimer;
Widget makeAnalyticsWidget(BuildContext context) {
var buildIndex = 0;
return LoadingWidget(
onLoadData: AnalyticsApi.getAnalyticsData,
autoRefreshDuration: const Duration(seconds: 60),
@ -547,20 +517,18 @@ class AboutUI extends HookConsumerWidget {
"performance_apply",
"p4k_download",
].contains(item["Type"]))
makeAnalyticsItem(
context: context,
name: item["Type"] as String,
value: item["Count"] as int)
GridItemAnimator(
index: buildIndex++,
child: makeAnalyticsItem(
context: context, name: item["Type"] as String, value: item["Count"] as int),
)
],
);
},
);
}
Widget makeAnalyticsItem(
{required BuildContext context,
required String name,
required int value}) {
Widget makeAnalyticsItem({required BuildContext context, required String name, required int value}) {
final names = {
"launch": S.current.about_analytics_launch,
"gameLaunch": S.current.about_analytics_launch_game,
@ -573,14 +541,12 @@ class AboutUI extends HookConsumerWidget {
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(left: 18, right: 18),
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor.withValues(alpha: .06),
borderRadius: BorderRadius.circular(12)),
color: FluentTheme.of(context).cardColor.withValues(alpha: .06), borderRadius: BorderRadius.circular(12)),
child: Column(
children: [
Text(
names[name] ?? name,
style: TextStyle(
fontSize: 13, color: Colors.white.withValues(alpha: .6)),
style: TextStyle(fontSize: 13, color: Colors.white.withValues(alpha: .6)),
),
const SizedBox(height: 4),
Row(
@ -606,8 +572,7 @@ class AboutUI extends HookConsumerWidget {
launchUrlString("ms-windows-store://pdp/?productid=9NF3SWFWNKL1");
return;
} else {
final hasUpdate =
await ref.read(appGlobalModelProvider.notifier).checkUpdate(context);
final hasUpdate = await ref.read(appGlobalModelProvider.notifier).checkUpdate(context);
if (!hasUpdate) {
if (!context.mounted) return;
showToast(context, S.current.about_info_latest_version);
@ -626,8 +591,7 @@ class ChatBubble extends StatelessWidget {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color:
FluentTheme.of(context).accentColor.withAlpha((255.0 * .2).round()),
color: FluentTheme.of(context).accentColor.withAlpha((255.0 * .2).round()),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(4),
topRight: Radius.circular(18),
@ -645,8 +609,7 @@ class ChatBubble extends StatelessWidget {
class DonationQrCodeData {
static const alipay = "https://qr.alipay.com/tsx16308c4uai0ticmz4j96";
static const wechat =
"wxp://f2f0J40rTCX7Vt79yooWNbiqH3U6UmwGJkqjcAYnrv9OZVzKyS5_W6trp8mo3KP-CTQ5";
static const wechat = "wxp://f2f0J40rTCX7Vt79yooWNbiqH3U6UmwGJkqjcAYnrv9OZVzKyS5_W6trp8mo3KP-CTQ5";
static const qq =
"https://i.qianbao.qq.com/wallet/sqrcode.htm?m=tenpay&f=wallet&a=1&u=3334969096&n=xkeyC&ac=CAEQiK6etgwY8ZuKvgYyGOa1geWKqOaRiuS9jee7j-iQpeaUtuasvjgBQiAzY2Y4NWY3MDI1MWUxYWEwMGYyN2Q0OTM4Y2U2ODFlMw%3D%3D_xxx_sign";
}

View File

@ -28,8 +28,7 @@ class AdvancedLocalizationUI extends HookConsumerWidget {
onSwitchFile() async {
final sb = await showDialog(
context: context,
builder: (BuildContext context) =>
const LocalizationFromFileDialogUI(isInAdvancedMode: true),
builder: (BuildContext context) => const LocalizationFromFileDialogUI(isInAdvancedMode: true),
);
if (sb is (StringBuffer, bool)) {
model.setCustomizeGlobalIni(sb.$1.toString());
@ -42,8 +41,7 @@ class AdvancedLocalizationUI extends HookConsumerWidget {
}, const []);
return makeDefaultPage(
title: S.current.home_localization_advanced_title(
homeUIState.scInstalledPath ?? "-"),
title: S.current.home_localization_advanced_title(homeUIState.scInstalledPath ?? "-"),
context,
content: state.workingText.isNotEmpty
? Center(
@ -71,15 +69,13 @@ class AdvancedLocalizationUI extends HookConsumerWidget {
children: [
Text(
S.current.home_localization_advanced_msg_version(
state.apiLocalizationData?.versionName ??
"-"),
state.apiLocalizationData?.versionName ?? "-"),
),
const SizedBox(width: 12),
Button(
onPressed: onSwitchFile,
child: const Padding(
padding: EdgeInsets.symmetric(
horizontal: 6, vertical: 3),
padding: EdgeInsets.symmetric(horizontal: 6, vertical: 3),
child: Icon(FluentIcons.switch_widget),
)),
if (state.customizeGlobalIni != null) ...[
@ -89,23 +85,19 @@ class AdvancedLocalizationUI extends HookConsumerWidget {
model.setCustomizeGlobalIni(null);
},
child: const Padding(
padding: EdgeInsets.symmetric(
horizontal: 6, vertical: 3),
padding: EdgeInsets.symmetric(horizontal: 6, vertical: 3),
child: Icon(FluentIcons.delete),
)),
]
],
)),
Text(S.current.home_localization_advanced_title_msg(
state.serverGlobalIniLines,
state.p4kGlobalIniLines)),
Text(S.current
.home_localization_advanced_title_msg(state.serverGlobalIniLines, state.p4kGlobalIniLines)),
const SizedBox(width: 32),
Button(
child: Padding(
padding: const EdgeInsets.only(
left: 12, right: 12, top: 4, bottom: 4),
child: Text(S.current
.home_localization_advanced_action_install),
padding: const EdgeInsets.only(left: 12, right: 12, top: 4, bottom: 4),
child: Text(S.current.home_localization_advanced_action_install),
),
onPressed: () {
model.onInstall(context);
@ -113,19 +105,13 @@ class AdvancedLocalizationUI extends HookConsumerWidget {
const SizedBox(width: 12),
],
),
Expanded(
child:
_makeBody(context, homeUIState, state, ref, model)),
Expanded(child: _makeBody(context, homeUIState, state, ref, model)),
]
],
));
}
Widget _makeBody(
BuildContext context,
HomeUIModelState homeUIState,
AdvancedLocalizationUIState state,
WidgetRef ref,
Widget _makeBody(BuildContext context, HomeUIModelState homeUIState, AdvancedLocalizationUIState state, WidgetRef ref,
AdvancedLocalizationUIModel model) {
return AlignedGridView.count(
crossAxisCount: 4,
@ -134,7 +120,9 @@ class AdvancedLocalizationUI extends HookConsumerWidget {
padding: const EdgeInsets.all(12),
itemBuilder: (BuildContext context, int index) {
final item = state.classMap!.values.elementAt(index);
return Container(
return GridItemAnimator(
index: index,
child: Container(
padding: const EdgeInsets.only(top: 6, bottom: 12),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: .05),
@ -144,8 +132,7 @@ class AdvancedLocalizationUI extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
IconButton(
onPressed:
item.isWorking ? null : () => _showContent(context, item),
onPressed: item.isWorking ? null : () => _showContent(context, item),
icon: Padding(
padding: const EdgeInsets.only(left: 12, right: 12),
child: Row(
@ -153,8 +140,7 @@ class AdvancedLocalizationUI extends HookConsumerWidget {
Expanded(
child: Text(
"${item.className}",
style: const TextStyle(
fontSize: 16, fontWeight: FontWeight.bold),
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
textAlign: TextAlign.start,
)),
Text(
@ -185,8 +171,7 @@ class AdvancedLocalizationUI extends HookConsumerWidget {
children: [
makeLoading(context),
const SizedBox(height: 6),
Text(
S.current.home_localization_advanced_action_mod_change),
Text(S.current.home_localization_advanced_action_mod_change),
],
)
else ...[
@ -194,15 +179,11 @@ class AdvancedLocalizationUI extends HookConsumerWidget {
padding: const EdgeInsets.only(left: 12, right: 12),
child: Row(
children: [
Expanded(
child: Text(S
.current.home_localization_advanced_action_mode)),
Expanded(child: Text(S.current.home_localization_advanced_action_mode)),
ComboBox(
value: item.mode,
items: [
for (final type
in AppAdvancedLocalizationClassKeysDataMode
.values)
for (final type in AppAdvancedLocalizationClassKeysDataMode.values)
ComboBoxItem(
value: type,
child: Text(state.typeNames[type] ?? "-"),
@ -210,8 +191,7 @@ class AdvancedLocalizationUI extends HookConsumerWidget {
],
onChanged: item.lockMod
? null
: (v) => model.onChangeMod(item,
v as AppAdvancedLocalizationClassKeysDataMode),
: (v) => model.onChangeMod(item, v as AppAdvancedLocalizationClassKeysDataMode),
),
],
),
@ -238,14 +218,14 @@ class AdvancedLocalizationUI extends HookConsumerWidget {
],
],
),
),
);
},
itemCount: state.classMap?.length ?? 0,
);
}
_showContent(
BuildContext context, AppAdvancedLocalizationClassKeysData item) {
_showContent(BuildContext context, AppAdvancedLocalizationClassKeysData item) {
showDialog(
context: context,
builder: (BuildContext context) {
@ -282,8 +262,7 @@ class AdvancedLocalizationUI extends HookConsumerWidget {
const SizedBox(
width: 24,
),
Text(S.current.home_localization_advanced_title_preview(
item.className ?? "-")),
Text(S.current.home_localization_advanced_title_preview(item.className ?? "-")),
],
),
content: textData.value.isEmpty
@ -295,13 +274,10 @@ class AdvancedLocalizationUI extends HookConsumerWidget {
),
child: CodeEditor(
readOnly: true,
controller:
CodeLineEditingController.fromText(textData.value),
controller: CodeLineEditingController.fromText(textData.value),
style: CodeEditorStyle(
codeTheme: CodeHighlightTheme(
languages: {
'ini': CodeHighlightThemeMode(mode: langIni)
},
languages: {'ini': CodeHighlightThemeMode(mode: langIni)},
theme: vs2015Theme,
),
),

View File

@ -30,8 +30,7 @@ class LocalizationDialogUI extends HookConsumerWidget {
return ContentDialog(
title: makeTitle(context, model, state),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * .7,
minHeight: MediaQuery.of(context).size.height * .9),
maxWidth: MediaQuery.of(context).size.width * .7, minHeight: MediaQuery.of(context).size.height * .9),
content: Padding(
padding: const EdgeInsets.only(left: 12, right: 12, top: 12),
child: SingleChildScrollView(
@ -40,18 +39,15 @@ class LocalizationDialogUI extends HookConsumerWidget {
AnimatedSize(
duration: const Duration(milliseconds: 130),
child: state.patchStatus?.key == true &&
state.patchStatus?.value ==
S.current.home_action_info_game_built_in
state.patchStatus?.value == S.current.home_action_info_game_built_in
? Padding(
padding: const EdgeInsets.only(bottom: 12),
child: InfoBar(
title: Text(S.current.home_action_info_warning),
content: Text(S.current
.localization_info_machine_translation_warning),
content: Text(S.current.localization_info_machine_translation_warning),
severity: InfoBarSeverity.info,
style: InfoBarThemeData(decoration: (severity) {
return const BoxDecoration(
color: Color.fromRGBO(155, 7, 7, 1.0));
return const BoxDecoration(color: Color.fromRGBO(155, 7, 7, 1.0));
}, iconColor: (severity) {
return Colors.white;
}),
@ -65,10 +61,8 @@ class LocalizationDialogUI extends HookConsumerWidget {
Padding(
padding: const EdgeInsets.only(bottom: 12),
child: InfoBar(
title: Text(S.current
.home_localization_ptu_advanced_localization_tip_title),
content: Text(S.current
.home_localization_ptu_advanced_localization_tip_title_info),
title: Text(S.current.home_localization_ptu_advanced_localization_tip_title),
content: Text(S.current.home_localization_ptu_advanced_localization_tip_title_info),
severity: InfoBarSeverity.info,
style: InfoBarThemeData(decoration: (severity) {
return BoxDecoration(color: Colors.orange);
@ -89,9 +83,7 @@ class LocalizationDialogUI extends HookConsumerWidget {
children: [
Center(
child: Text(S.current.localization_info_enabled(
LocalizationUIModel.languageSupport[
state.selectedLanguage] ??
"")),
LocalizationUIModel.languageSupport[state.selectedLanguage] ?? "")),
),
const Spacer(),
ToggleSwitch(
@ -109,20 +101,15 @@ class LocalizationDialogUI extends HookConsumerWidget {
Text(S.current.localization_info_installed_version(
"${state.patchStatus?.value ?? ""} ${(state.isInstalledAdvanced ?? false) ? S.current.home_localization_msg_version_advanced : ""}")),
SizedBox(width: 24),
if (state
.installedCommunityInputMethodSupportVersion !=
null)
if (state.installedCommunityInputMethodSupportVersion != null)
Text(
S.current
.input_method_community_input_method_support_version(
state.installedCommunityInputMethodSupportVersion ??
"?"),
S.current.input_method_community_input_method_support_version(
state.installedCommunityInputMethodSupportVersion ?? "?"),
)
],
),
),
if (state.patchStatus?.value !=
S.current.home_action_info_game_built_in)
if (state.patchStatus?.value != S.current.home_action_info_game_built_in)
Row(
children: [
Button(
@ -133,8 +120,7 @@ class LocalizationDialogUI extends HookConsumerWidget {
children: [
const Icon(FluentIcons.feedback),
const SizedBox(width: 6),
Text(S.current
.localization_action_translation_feedback),
Text(S.current.localization_action_translation_feedback),
],
),
)),
@ -147,8 +133,7 @@ class LocalizationDialogUI extends HookConsumerWidget {
children: [
const Icon(FluentIcons.delete),
const SizedBox(width: 6),
Text(S.current
.localization_action_uninstall_translation),
Text(S.current.localization_action_uninstall_translation),
],
),
)),
@ -167,11 +152,8 @@ class LocalizationDialogUI extends HookConsumerWidget {
else if (state.apiLocalizationData!.isEmpty)
Center(
child: Text(
S.current
.localization_info_no_translation_available,
style: TextStyle(
fontSize: 13,
color: Colors.white.withValues(alpha: .8)),
S.current.localization_info_no_translation_available,
style: TextStyle(fontSize: 13, color: Colors.white.withValues(alpha: .8)),
),
)
else
@ -180,9 +162,8 @@ class LocalizationDialogUI extends HookConsumerWidget {
crossAxisSpacing: 12,
mainAxisSpacing: 12,
itemBuilder: (BuildContext context, int index) {
final item = state.apiLocalizationData!.entries
.elementAt(index);
return makeRemoteList(context, model, item, state);
final item = state.apiLocalizationData!.entries.elementAt(index);
return makeRemoteList(context, model, item, state, index);
},
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
@ -198,22 +179,21 @@ class LocalizationDialogUI extends HookConsumerWidget {
);
}
Widget makeRemoteList(BuildContext context, LocalizationUIModel model,
MapEntry<String, ScLocalizationData> item, LocalizationUIState state) {
Widget makeRemoteList(BuildContext context, LocalizationUIModel model, MapEntry<String, ScLocalizationData> item,
LocalizationUIState state, int index) {
final isWorking = state.workingVersion.isNotEmpty;
final isMineWorking = state.workingVersion == item.key;
final isInstalled = state.patchStatus?.value == item.key;
final isItemEnabled = ((item.value.enable ?? false));
final tapDisabled =
isInstalled || isWorking || !isItemEnabled || isMineWorking;
return Tilt(
final tapDisabled = isInstalled || isWorking || !isItemEnabled || isMineWorking;
return GridItemAnimator(
index: index,
child: Tilt(
shadowConfig: const ShadowConfig(maxIntensity: .3),
borderRadius: BorderRadius.circular(7),
disable: tapDisabled,
child: GestureDetector(
onTap: tapDisabled
? null
: () => model.onRemoteInsTall(context, item, state),
onTap: tapDisabled ? null : () => model.onRemoteInsTall(context, item, state),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
@ -235,24 +215,18 @@ class LocalizationDialogUI extends HookConsumerWidget {
),
const SizedBox(height: 4),
Text(
S.current.localization_info_version_number(
item.value.versionName ?? ""),
style: TextStyle(
color: Colors.white.withValues(alpha: .6)),
S.current.localization_info_version_number(item.value.versionName ?? ""),
style: TextStyle(color: Colors.white.withValues(alpha: .6)),
),
const SizedBox(height: 4),
Text(
S.current.localization_info_channel(
item.value.gameChannel ?? ""),
style: TextStyle(
color: Colors.white.withValues(alpha: .6)),
S.current.localization_info_channel(item.value.gameChannel ?? ""),
style: TextStyle(color: Colors.white.withValues(alpha: .6)),
),
const SizedBox(height: 4),
Text(
S.current.localization_info_update_time(
item.value.updateAt ?? ""),
style: TextStyle(
color: Colors.white.withValues(alpha: .6)),
S.current.localization_info_update_time(item.value.updateAt ?? ""),
style: TextStyle(color: Colors.white.withValues(alpha: .6)),
),
],
),
@ -309,25 +283,20 @@ class LocalizationDialogUI extends HookConsumerWidget {
),
),
),
),
);
}
Widget makeListContainer(
String title, List<Widget> children, BuildContext context,
{List<Widget> actions = const [],
bool gridViewMode = false,
int gridViewCrossAxisCount = 2}) {
Widget makeListContainer(String title, List<Widget> children, BuildContext context,
{List<Widget> actions = const [], bool gridViewMode = false, int gridViewCrossAxisCount = 2}) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: AnimatedSize(
duration: const Duration(milliseconds: 130),
child: Container(
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor,
borderRadius: BorderRadius.circular(7)),
decoration: BoxDecoration(color: FluentTheme.of(context).cardColor, borderRadius: BorderRadius.circular(7)),
child: Padding(
padding:
const EdgeInsets.only(top: 12, bottom: 12, left: 24, right: 24),
padding: const EdgeInsets.only(top: 12, bottom: 12, left: 24, right: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -371,8 +340,7 @@ class LocalizationDialogUI extends HookConsumerWidget {
);
}
Widget makeTitle(BuildContext context, LocalizationUIModel model,
LocalizationUIState state) {
Widget makeTitle(BuildContext context, LocalizationUIModel model, LocalizationUIState state) {
return Row(
children: [
IconButton(
@ -401,8 +369,7 @@ class LocalizationDialogUI extends HookConsumerWidget {
ComboBox<String>(
value: state.selectedLanguage,
items: [
for (final lang
in LocalizationUIModel.languageSupport.entries)
for (final lang in LocalizationUIModel.languageSupport.entries)
ComboBoxItem(
value: lang.key,
child: Text(lang.value),
@ -429,8 +396,7 @@ class LocalizationDialogUI extends HookConsumerWidget {
);
}
Widget makeToolsListContainer(BuildContext context, LocalizationUIModel model,
LocalizationUIState state) {
Widget makeToolsListContainer(BuildContext context, LocalizationUIModel model, LocalizationUIState state) {
final toolsMenu = {
"launcher_mod": (
const Icon(FluentIcons.c_plus_plus, size: 24),
@ -469,8 +435,7 @@ class LocalizationDialogUI extends HookConsumerWidget {
case "custom_files":
final sb = await showDialog(
context: context,
builder: (BuildContext context) =>
const LocalizationFromFileDialogUI(),
builder: (BuildContext context) => const LocalizationFromFileDialogUI(),
);
if (sb is (StringBuffer, bool)) {
await model.installFormString(

View File

@ -12,6 +12,7 @@ import 'package:window_manager/window_manager.dart';
import 'about/about_ui.dart';
import 'home/home_ui.dart';
import 'nav/nav_ui.dart';
import 'settings/settings_ui.dart';
import 'tools/tools_ui.dart';
@ -42,8 +43,7 @@ class IndexUI extends HookConsumerWidget {
fit: BoxFit.cover,
),
const SizedBox(width: 12),
Text(S.current.app_index_version_info(
ConstConf.appVersion, ConstConf.isMSE ? "" : " Dev")),
Text(S.current.app_index_version_info(ConstConf.appVersion, ConstConf.isMSE ? "" : " Dev")),
],
),
),
@ -77,8 +77,7 @@ class IndexUI extends HookConsumerWidget {
key: Key("NavigationPane_${S.current.app_language_code}"),
selected: curIndex.value,
items: getNavigationPaneItems(curIndex),
size: NavigationPaneSize(
openWidth: S.current.app_language_code.startsWith("zh") ? 64 : 74),
size: NavigationPaneSize(openWidth: S.current.app_language_code.startsWith("zh") ? 64 : 74),
),
paneBodyBuilder: (item, child) {
return item!.body;
@ -95,18 +94,15 @@ class IndexUI extends HookConsumerWidget {
S.current.app_index_menu_tools,
const ToolsUI(),
),
FluentIcons.settings: (
S.current.app_index_menu_settings,
const SettingsUI()
),
FluentIcons.power_apps: ("导航", const NavUI()),
FluentIcons.settings: (S.current.app_index_menu_settings, const SettingsUI()),
FluentIcons.info: (
S.current.app_index_menu_about,
const AboutUI(),
),
};
List<NavigationPaneItem> getNavigationPaneItems(
ValueNotifier<int> curIndexState) {
List<NavigationPaneItem> getNavigationPaneItems(ValueNotifier<int> curIndexState) {
// width = 64
return [
for (final kv in pageMenus.entries)
@ -136,8 +132,7 @@ class IndexUI extends HookConsumerWidget {
}
void _onTapIndexMenu(String value, ValueNotifier<int> curIndexState) {
final pageIndex =
pageMenus.values.toList().indexWhere((element) => element.$1 == value);
final pageIndex = pageMenus.values.toList().indexWhere((element) => element.$1 == value);
curIndexState.value = pageIndex;
}
@ -156,8 +151,7 @@ class IndexUI extends HookConsumerWidget {
color: Colors.red,
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.only(
left: 6, right: 6, bottom: 1.5, top: 1.5),
padding: const EdgeInsets.only(left: 6, right: 6, bottom: 1.5, top: 1.5),
child: Text(
"${aria2cState.aria2TotalTaskNum}",
style: const TextStyle(

41
lib/ui/nav/nav_state.dart Normal file
View File

@ -0,0 +1,41 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:starcitizen_doctor/api/udb.dart';
import 'package:starcitizen_doctor/data/nav_api_data.dart';
part 'nav_state.freezed.dart';
part 'nav_state.g.dart';
@freezed
class NavState with _$NavState {
const factory NavState({
List<NavApiDocsItemData>? items,
@Default("") String errorInfo,
}) = _NavState;
}
@riverpod
class Nav extends _$Nav {
bool _mounted = true;
@override
NavState build() {
state = NavState();
loadData(1);
ref.onDispose(() {
_mounted = false;
});
return state;
}
void loadData(int pageNo) async {
if (!_mounted) return;
try {
final r = await UDBNavApi.getNavItems(pageNo: pageNo);
state = state.copyWith(items: r.docs, errorInfo: "");
} catch (e) {
state = state.copyWith(errorInfo: e.toString());
}
}
}

View File

@ -0,0 +1,173 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'nav_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$NavState {
List<NavApiDocsItemData>? get items => throw _privateConstructorUsedError;
String get errorInfo => throw _privateConstructorUsedError;
/// Create a copy of NavState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$NavStateCopyWith<NavState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $NavStateCopyWith<$Res> {
factory $NavStateCopyWith(NavState value, $Res Function(NavState) then) =
_$NavStateCopyWithImpl<$Res, NavState>;
@useResult
$Res call({List<NavApiDocsItemData>? items, String errorInfo});
}
/// @nodoc
class _$NavStateCopyWithImpl<$Res, $Val extends NavState>
implements $NavStateCopyWith<$Res> {
_$NavStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of NavState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? items = freezed,
Object? errorInfo = null,
}) {
return _then(_value.copyWith(
items: freezed == items
? _value.items
: items // ignore: cast_nullable_to_non_nullable
as List<NavApiDocsItemData>?,
errorInfo: null == errorInfo
? _value.errorInfo
: errorInfo // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
}
/// @nodoc
abstract class _$$NavStateImplCopyWith<$Res>
implements $NavStateCopyWith<$Res> {
factory _$$NavStateImplCopyWith(
_$NavStateImpl value, $Res Function(_$NavStateImpl) then) =
__$$NavStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({List<NavApiDocsItemData>? items, String errorInfo});
}
/// @nodoc
class __$$NavStateImplCopyWithImpl<$Res>
extends _$NavStateCopyWithImpl<$Res, _$NavStateImpl>
implements _$$NavStateImplCopyWith<$Res> {
__$$NavStateImplCopyWithImpl(
_$NavStateImpl _value, $Res Function(_$NavStateImpl) _then)
: super(_value, _then);
/// Create a copy of NavState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? items = freezed,
Object? errorInfo = null,
}) {
return _then(_$NavStateImpl(
items: freezed == items
? _value._items
: items // ignore: cast_nullable_to_non_nullable
as List<NavApiDocsItemData>?,
errorInfo: null == errorInfo
? _value.errorInfo
: errorInfo // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
class _$NavStateImpl implements _NavState {
const _$NavStateImpl(
{final List<NavApiDocsItemData>? items, this.errorInfo = ""})
: _items = items;
final List<NavApiDocsItemData>? _items;
@override
List<NavApiDocsItemData>? get items {
final value = _items;
if (value == null) return null;
if (_items is EqualUnmodifiableListView) return _items;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override
@JsonKey()
final String errorInfo;
@override
String toString() {
return 'NavState(items: $items, errorInfo: $errorInfo)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$NavStateImpl &&
const DeepCollectionEquality().equals(other._items, _items) &&
(identical(other.errorInfo, errorInfo) ||
other.errorInfo == errorInfo));
}
@override
int get hashCode => Object.hash(
runtimeType, const DeepCollectionEquality().hash(_items), errorInfo);
/// Create a copy of NavState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$NavStateImplCopyWith<_$NavStateImpl> get copyWith =>
__$$NavStateImplCopyWithImpl<_$NavStateImpl>(this, _$identity);
}
abstract class _NavState implements NavState {
const factory _NavState(
{final List<NavApiDocsItemData>? items,
final String errorInfo}) = _$NavStateImpl;
@override
List<NavApiDocsItemData>? get items;
@override
String get errorInfo;
/// Create a copy of NavState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$NavStateImplCopyWith<_$NavStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,24 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'nav_state.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$navHash() => r'2019b3f675fbaec4be794049d900bf2dcc8d5e37';
/// See also [Nav].
@ProviderFor(Nav)
final navProvider = AutoDisposeNotifierProvider<Nav, NavState>.internal(
Nav.new,
name: r'navProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$navHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$Nav = AutoDisposeNotifier<NavState>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

203
lib/ui/nav/nav_ui.dart Normal file
View File

@ -0,0 +1,203 @@
import 'dart:ui';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/gestures.dart' show TapGestureRecognizer;
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:flutter_tilt/flutter_tilt.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:starcitizen_doctor/ui/nav/nav_state.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart';
class NavUI extends HookConsumerWidget {
const NavUI({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Column(
children: [
Expanded(
child: buildBody(context, ref),
),
SizedBox(height: 6),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text.rich(
TextSpan(children: [
TextSpan(text: "*对应链接指向的服务由第三方提供,我们不对其做任何担保,请用户自行判断使用风险 | "),
TextSpan(text: "网站导航数据由"),
TextSpan(
text: " 42kit ",
style: TextStyle(
color: Colors.white,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()
..onTap = () {
launchUrlString("https://42kit.citizenwiki.cn/nav");
},
),
TextSpan(text: "提供"),
]),
style: TextStyle(
fontSize: 13,
color: Colors.white.withValues(alpha: .6),
),
),
SizedBox(width: 12),
],
),
],
),
);
}
Widget buildBody(BuildContext context, WidgetRef ref) {
final data = ref.watch(navProvider);
if (data.errorInfo.isNotEmpty) {
return Center(
child: Text(
data.errorInfo,
style: TextStyle(color: Colors.red),
),
);
}
if (data.items == null) {
return const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ProgressRing(),
SizedBox(height: 12),
Text("正在获取数据..."),
],
));
}
return MasonryGridView.count(
crossAxisCount: 3,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
itemCount: data.items!.length,
padding: EdgeInsets.only(left: 12, right: 12, bottom: 12),
itemBuilder: (BuildContext context, int index) {
const itemHeight = 160.0;
final item = data.items![index];
final itemName = item.name;
final itemImage = item.image.url;
return GridItemAnimator(
index: index,
child: GestureDetector(
onTap: () {
launchUrlString(item.link);
},
child: Tilt(
shadowConfig: const ShadowConfig(maxIntensity: .3),
borderRadius: BorderRadius.circular(12),
clipBehavior: Clip.hardEdge,
child: SizedBox(
height: itemHeight,
width: double.infinity,
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
clipBehavior: Clip.hardEdge,
child: Stack(
children: [
Center(
child: CacheNetImage(
height: itemHeight,
width: double.infinity,
url: itemImage,
fit: BoxFit.fitWidth,
),
),
Container(
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: .55),
),
),
ClipRect(
clipBehavior: Clip.hardEdge,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 15.0, sigmaY: 15.0),
blendMode: BlendMode.srcOver,
child: SizedBox(
width: double.infinity,
height: itemHeight,
),
),
),
Container(
decoration: BoxDecoration(),
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: CacheNetImage(
url: itemImage,
height: 48,
width: 48,
fit: BoxFit.cover,
),
),
SizedBox(width: 12),
Flexible(
child: Text(
itemName,
style: TextStyle(
fontSize: 18,
),
),
),
],
),
SizedBox(height: 12),
Expanded(
child: Text(
item.abstract_,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: Colors.white.withValues(alpha: .75)),
),
),
Row(
children: [
for (var value in item.tags)
Container(
decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: .6),
borderRadius: BorderRadius.circular(12)),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 3),
margin: EdgeInsets.only(right: 6),
child: Text(
value.name,
style: TextStyle(
fontSize: 13,
color: Colors.white,
),
),
)
],
)
],
),
),
],
),
),
),
),
),
);
},
);
}
}

View File

@ -40,13 +40,9 @@ class ToolsUI extends HookConsumerWidget {
),
const SizedBox(width: 12),
Button(
onPressed: state.working
? null
: () =>
model.loadToolsCard(context, skipPathScan: false),
onPressed: state.working ? null : () => model.loadToolsCard(context, skipPathScan: false),
child: const Padding(
padding: EdgeInsets.only(
top: 30, bottom: 30, left: 12, right: 12),
padding: EdgeInsets.only(top: 30, bottom: 30, left: 12, right: 12),
child: Icon(FluentIcons.refresh),
),
),
@ -75,23 +71,26 @@ class ToolsUI extends HookConsumerWidget {
crossAxisCount: 3,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
itemCount: (state.isItemLoading)
? state.items.length + 1
: state.items.length,
itemCount: (state.isItemLoading) ? state.items.length + 1 : state.items.length,
shrinkWrap: true,
itemBuilder: (context, index) {
if (index == state.items.length) {
return Container(
return GridItemAnimator(
index: index,
child: Container(
width: 300,
height: 200,
height: 160,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: FluentTheme.of(context).cardColor,
),
child: makeLoading(context));
child: makeLoading(context)),
);
}
final item = state.items[index];
return Container(
return GridItemAnimator(
index: index,
child: Container(
width: 300,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
@ -106,10 +105,8 @@ class ToolsUI extends HookConsumerWidget {
children: [
Container(
decoration: BoxDecoration(
color:
Colors.white.withValues(alpha: .2),
borderRadius:
BorderRadius.circular(1000)),
color: Colors.white.withValues(alpha: .2),
borderRadius: BorderRadius.circular(1000)),
child: Padding(
padding: const EdgeInsets.all(12),
child: item.icon,
@ -127,9 +124,7 @@ class ToolsUI extends HookConsumerWidget {
const SizedBox(height: 12),
Text(
item.infoString,
style: TextStyle(
fontSize: 14,
color: Colors.white.withValues(alpha: .6)),
style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .6)),
),
const SizedBox(height: 12),
Row(
@ -144,11 +139,7 @@ class ToolsUI extends HookConsumerWidget {
try {
item.onTap?.call();
} catch (e) {
showToast(
context,
S.current
.tools_info_processing_failed(
e));
showToast(context, S.current.tools_info_processing_failed(e));
}
},
child: const Padding(
@ -161,6 +152,7 @@ class ToolsUI extends HookConsumerWidget {
],
),
),
),
);
},
),
@ -188,8 +180,7 @@ class ToolsUI extends HookConsumerWidget {
);
}
Widget makeGamePathSelect(
BuildContext context, ToolsUIModel model, ToolsUIState state) {
Widget makeGamePathSelect(BuildContext context, ToolsUIModel model, ToolsUIState state) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
@ -223,8 +214,7 @@ class ToolsUI extends HookConsumerWidget {
),
onPressed: () {
if (state.scInstalledPath.trim().isEmpty) {
showToast(context,
S.current.tools_action_info_star_citizen_not_found);
showToast(context, S.current.tools_action_info_star_citizen_not_found);
return;
}
model.openDir(state.scInstalledPath);
@ -233,8 +223,7 @@ class ToolsUI extends HookConsumerWidget {
);
}
Widget makeGameLauncherPathSelect(
BuildContext context, ToolsUIModel model, ToolsUIState state) {
Widget makeGameLauncherPathSelect(BuildContext context, ToolsUIModel model, ToolsUIState state) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
@ -268,10 +257,7 @@ class ToolsUI extends HookConsumerWidget {
),
onPressed: () {
if (state.scInstalledPath.trim().isEmpty) {
showToast(
context,
S.current
.tools_rsi_launcher_enhance_msg_error_launcher_notfound);
showToast(context, S.current.tools_rsi_launcher_enhance_msg_error_launcher_notfound);
return;
}
model.openDir(state.rsiLauncherInstalledPath);

View File

@ -6,7 +6,7 @@ part of 'tools_ui_model.dart';
// RiverpodGenerator
// **************************************************************************
String _$toolsUIModelHash() => r'cd72f7833fa5696baf9022d16d10d7951387df7e';
String _$toolsUIModelHash() => r'c8830e26df6c0ee572dd5e78c4ccef3317f8b4e6';
/// See also [ToolsUIModel].
@ProviderFor(ToolsUIModel)

View File

@ -1,5 +1,8 @@
import 'package:extended_image/extended_image.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'cache_svg_image.dart';
class CacheNetImage extends StatelessWidget {
final String url;
@ -7,11 +10,18 @@ class CacheNetImage extends StatelessWidget {
final double? height;
final BoxFit? fit;
const CacheNetImage(
{super.key, required this.url, this.width, this.height, this.fit});
const CacheNetImage({super.key, required this.url, this.width, this.height, this.fit});
@override
Widget build(BuildContext context) {
if (url.endsWith(".svg")) {
return CachedSvgImage(
url,
width: width,
height: height,
fit: fit ?? BoxFit.contain,
);
}
return ExtendedImage.network(
url,
width: width,
@ -20,14 +30,11 @@ class CacheNetImage extends StatelessWidget {
loadStateChanged: (ExtendedImageState state) {
switch (state.extendedImageLoadState) {
case LoadState.loading:
return const Center(
child: Padding(
padding: EdgeInsets.all(8.0),
child: Column(
children: [
ProgressRing(),
],
),
return SizedBox(
width: width,
height: height,
child: Center(
child: ProgressRing(),
),
);
case LoadState.failed:

View File

@ -0,0 +1,64 @@
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_svg/svg.dart';
import 'package:starcitizen_doctor/common/utils/file_cache_utils.dart';
class CachedSvgImage extends HookWidget {
final String url;
final double? width;
final double? height;
final BoxFit? fit;
const CachedSvgImage(this.url, {super.key, this.width, this.height, this.fit});
@override
Widget build(BuildContext context) {
final cachedFile = useState<File?>(null);
final errorInfo = useState<String?>(null);
useEffect(
() {
() async {
try {
cachedFile.value = await FileCacheUtils.getFile(url);
} catch (e) {
debugPrint("Error loading SVG: $e");
errorInfo.value = "Error loading SVG: $e";
}
}();
return null;
},
[url],
);
if (errorInfo.value != null) {
return SizedBox(
width: width,
height: height,
child: Center(
child: Text(
errorInfo.value!,
style: TextStyle(color: Colors.red),
),
),
);
}
return cachedFile.value != null
? SvgPicture.file(
cachedFile.value!,
width: width,
height: height,
fit: fit ?? BoxFit.contain,
)
: SizedBox(
width: width,
height: height,
child: Center(
child: ProgressRing(),
),
);
}
}

View File

@ -0,0 +1,71 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
class GridItemAnimator extends HookWidget {
final Widget child; //
final int index; //
final Duration duration; //
final Duration delayPerItem; //
final double slideOffset; //
const GridItemAnimator({
super.key,
required this.child,
required this.index,
this.duration = const Duration(milliseconds: 230),
this.delayPerItem = const Duration(milliseconds: 50),
this.slideOffset = 20.0,
});
@override
Widget build(BuildContext context) {
//
final animationController = useAnimationController(
duration: duration,
);
//
final opacityAnimation = useAnimation(
Tween<double>(
begin: 0.0, //
end: 1.0, //
).animate(CurvedAnimation(
parent: animationController,
curve: Curves.easeOut,
)),
);
//
final slideAnimation = useAnimation(
Tween<double>(
begin: 1.0, //
end: 0.0, //
).animate(CurvedAnimation(
parent: animationController,
curve: Curves.easeOutCubic,
)),
);
//
useEffect(() {
//
final delay = delayPerItem * index;
Future.delayed(delay, () {
if (animationController.status != AnimationStatus.completed) {
animationController.forward();
}
});
return null;
}, const []);
//
return Opacity(
opacity: opacityAnimation,
child: Transform.translate(
offset: Offset(0, slideOffset * slideAnimation), //
child: child,
),
);
}
}

View File

@ -15,6 +15,9 @@ import 'dart:ui' as ui;
export 'src/cache_image.dart';
export 'src/countdown_time_text.dart';
export 'src/cache_svg_image.dart';
export 'src/grid_item_animator.dart';
export '../common/utils/async.dart';
export '../common/utils/base_utils.dart';
export 'package:starcitizen_doctor/generated/l10n.dart';
@ -137,14 +140,13 @@ ColorFilter makeSvgColor(Color color) {
return ui.ColorFilter.mode(color, ui.BlendMode.srcIn);
}
CustomTransitionPage<T> myPageBuilder<T>(
BuildContext context, GoRouterState state, Widget child) {
CustomTransitionPage<T> myPageBuilder<T>(BuildContext context, GoRouterState state, Widget child) {
return CustomTransitionPage(
child: child,
transitionDuration: const Duration(milliseconds: 150),
reverseTransitionDuration: const Duration(milliseconds: 150),
transitionsBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
transitionsBuilder:
(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.0, 1.0),
@ -164,12 +166,7 @@ class LoadingWidget<T> extends HookConsumerWidget {
final Widget Function(BuildContext context, T data) childBuilder;
final Duration? autoRefreshDuration;
const LoadingWidget(
{super.key,
this.data,
required this.childBuilder,
this.onLoadData,
this.autoRefreshDuration});
const LoadingWidget({super.key, this.data, required this.childBuilder, this.onLoadData, this.autoRefreshDuration});
@override
Widget build(BuildContext context, WidgetRef ref) {
@ -204,8 +201,7 @@ class LoadingWidget<T> extends HookConsumerWidget {
return childBuilder(context, (data ?? dataState.value) as T);
}
void _loadData(
ValueNotifier<T?> dataState, ValueNotifier<String> errorMsg) async {
void _loadData(ValueNotifier<T?> dataState, ValueNotifier<String> errorMsg) async {
errorMsg.value = "";
try {
final r = await onLoadData!();

View File

@ -232,7 +232,7 @@ packages:
source: hosted
version: "0.3.4+2"
crypto:
dependency: transitive
dependency: "direct main"
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
@ -916,7 +916,7 @@ packages:
source: hosted
version: "2.2.0"
path:
dependency: transitive
dependency: "direct main"
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"

View File

@ -68,6 +68,8 @@ dependencies:
qr_flutter: ^4.1.0
desktop_multi_window: ^0.2.1
watcher: ^1.1.1
path: ^1.9.1
crypto: ^3.0.6
dependency_overrides:
http: ^1.1.2