mirror of
https://mirror.ghproxy.com/https://github.com/StarCitizenToolBox/app.git
synced 2024-12-23 05:23:44 +08:00
feat:riverpod 迁移 HomeUI
This commit is contained in:
parent
248b416277
commit
e70fca4899
@ -22,7 +22,6 @@ import 'api/api.dart';
|
||||
import 'common/helper/system_helper.dart';
|
||||
import 'common/io/rs_http.dart';
|
||||
import 'common/rust/frb_generated.dart';
|
||||
import 'common/utils/base_utils.dart';
|
||||
import 'data/app_version_data.dart';
|
||||
import 'ui/index_ui.dart';
|
||||
import 'ui/settings/upgrade_dialog.dart';
|
||||
|
738
lib/ui/home/home_ui.dart
Normal file
738
lib/ui/home/home_ui.dart
Normal file
@ -0,0 +1,738 @@
|
||||
import 'package:card_swiper/card_swiper.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:flutter_tilt/flutter_tilt.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:starcitizen_doctor/api/analytics.dart';
|
||||
import 'package:starcitizen_doctor/common/helper/system_helper.dart';
|
||||
import 'package:starcitizen_doctor/widgets/widgets.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
import 'home_ui_model.dart';
|
||||
|
||||
class HomeUI extends HookConsumerWidget {
|
||||
const HomeUI({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final homeState = ref.watch(homeUIModelProvider);
|
||||
final model = ref.watch(homeUIModelProvider.notifier);
|
||||
return Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (homeState.appPlacardData != null) ...[
|
||||
InfoBar(
|
||||
title: Text("${homeState.appPlacardData?.title}"),
|
||||
content: Text("${homeState.appPlacardData?.content}"),
|
||||
severity: InfoBarSeverity.info,
|
||||
action: homeState.appPlacardData?.link == null
|
||||
? null
|
||||
: Button(
|
||||
child: const Text('查看详情'),
|
||||
onPressed: () => _showPlacard(),
|
||||
),
|
||||
onClose: homeState.appPlacardData?.alwaysShow == true
|
||||
? null
|
||||
: () => model.closePlacard(),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
],
|
||||
...makeIndex(context, model, homeState)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (homeState.isFixing)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withAlpha(150),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const ProgressRing(),
|
||||
const SizedBox(height: 12),
|
||||
Text(homeState.isFixingString.isNotEmpty
|
||||
? homeState.isFixingString
|
||||
: "正在处理..."),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> makeIndex(
|
||||
BuildContext context, HomeUIModel model, HomeUIModelState homeState) {
|
||||
const double width = 280;
|
||||
return [
|
||||
Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 64, bottom: 0),
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 30),
|
||||
child: Image.asset(
|
||||
"assets/sc_logo.png",
|
||||
fit: BoxFit.fitHeight,
|
||||
height: 260,
|
||||
),
|
||||
),
|
||||
makeGameStatusCard(context, model, 340, homeState)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 24,
|
||||
child: makeLeftColumn(context, model, width, homeState),
|
||||
),
|
||||
Positioned(
|
||||
right: 24,
|
||||
top: 0,
|
||||
child: makeNewsCard(context, model, homeState),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 24, right: 24),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text("安装位置:"),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: ComboBox<String>(
|
||||
value: homeState.scInstalledPath,
|
||||
items: [
|
||||
const ComboBoxItem(
|
||||
value: "not_install",
|
||||
child: Text("未安装 或 安装失败"),
|
||||
),
|
||||
for (final path in homeState.scInstallPaths)
|
||||
ComboBoxItem(
|
||||
value: path,
|
||||
child: Text(path),
|
||||
)
|
||||
],
|
||||
onChanged: model.onChangeInstallPath,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Button(
|
||||
onPressed: homeState.webLocalizationVersionsData == null
|
||||
? null
|
||||
: () => model.launchRSI(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: Icon(
|
||||
homeState.isCurGameRunning
|
||||
? FluentIcons.stop_solid
|
||||
: FluentIcons.play,
|
||||
color: homeState.isCurGameRunning
|
||||
? Colors.red.withOpacity(.8)
|
||||
: null,
|
||||
),
|
||||
)),
|
||||
const SizedBox(width: 12),
|
||||
Button(
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(6),
|
||||
child: Icon(FluentIcons.folder_open),
|
||||
),
|
||||
onPressed: () => SystemHelper.openDir(homeState.scInstalledPath),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Button(
|
||||
onPressed: model.reScanPath,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(6),
|
||||
child: Icon(FluentIcons.refresh),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(homeState.lastScreenInfo, maxLines: 1),
|
||||
makeIndexActionLists(context, model, homeState),
|
||||
];
|
||||
}
|
||||
|
||||
Widget makeLeftColumn(BuildContext context, HomeUIModel model, double width,
|
||||
HomeUIModelState homeState) {
|
||||
return Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: FluentTheme.of(context).cardColor.withOpacity(.03),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
makeWebViewButton(context, model,
|
||||
icon: SvgPicture.asset(
|
||||
"assets/rsi.svg",
|
||||
colorFilter: makeSvgColor(Colors.white),
|
||||
height: 18,
|
||||
),
|
||||
name: "星际公民官网汉化",
|
||||
webTitle: "星际公民官网汉化",
|
||||
webURL: "https://robertsspaceindustries.com",
|
||||
info: "罗伯茨航天工业公司,万物的起源",
|
||||
useLocalization: true,
|
||||
width: width,
|
||||
touchKey: "webLocalization_rsi"),
|
||||
const SizedBox(height: 12),
|
||||
makeWebViewButton(context, model,
|
||||
icon: Row(
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
"assets/uex.svg",
|
||||
height: 18,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
),
|
||||
name: "UEX 汉化",
|
||||
webTitle: "UEX 汉化",
|
||||
webURL: "https://uexcorp.space/",
|
||||
info: "采矿、精炼、贸易计算器、价格、船信息",
|
||||
useLocalization: true,
|
||||
width: width,
|
||||
touchKey: "webLocalization_uex"),
|
||||
const SizedBox(height: 12),
|
||||
makeWebViewButton(context, model,
|
||||
icon: Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
"assets/dps.png",
|
||||
height: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
),
|
||||
name: "DPS计算器汉化",
|
||||
webTitle: "DPS计算器汉化",
|
||||
webURL: "https://www.erkul.games/live/calculator",
|
||||
info: "在线改船,查询伤害数值和配件购买地点",
|
||||
useLocalization: true,
|
||||
width: width,
|
||||
touchKey: "webLocalization_dps"),
|
||||
const SizedBox(height: 12),
|
||||
const Text("外部浏览器拓展:"),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Button(
|
||||
child:
|
||||
const FaIcon(FontAwesomeIcons.chrome, size: 18),
|
||||
onPressed: () {
|
||||
launchUrlString(
|
||||
"https://chrome.google.com/webstore/detail/gocnjckojmledijgmadmacoikibcggja?authuser=0&hl=zh-CN");
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Button(
|
||||
child: const FaIcon(FontAwesomeIcons.edge, size: 18),
|
||||
onPressed: () {
|
||||
launchUrlString(
|
||||
"https://microsoftedge.microsoft.com/addons/detail/lipbbcckldklpdcpfagicipecaacikgi");
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Button(
|
||||
child: const FaIcon(FontAwesomeIcons.firefoxBrowser,
|
||||
size: 18),
|
||||
onPressed: () {
|
||||
launchUrlString(
|
||||
"https://addons.mozilla.org/zh-CN/firefox/"
|
||||
"addon/%E6%98%9F%E9%99%85%E5%85%AC%E6%B0%91%E7%9B%92%E5%AD%90%E6%B5%8F%E8%A7%88%E5%99%A8%E6%8B%93%E5%B1%95/");
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Button(
|
||||
child:
|
||||
const FaIcon(FontAwesomeIcons.github, size: 18),
|
||||
onPressed: () {
|
||||
launchUrlString(
|
||||
"https://github.com/StarCitizenToolBox/StarCitizenBoxBrowserEx");
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
makeActivityBanner(context, model, width, homeState),
|
||||
],
|
||||
),
|
||||
if (homeState.webLocalizationVersionsData == null)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(.3),
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
child: const Center(
|
||||
child: ProgressRing(),
|
||||
),
|
||||
))
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget makeNewsCard(
|
||||
BuildContext context, HomeUIModel model, HomeUIModelState homeState) {
|
||||
return ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
||||
child: Container(
|
||||
width: 316,
|
||||
height: 386,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(.1),
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 190,
|
||||
width: 316,
|
||||
child: Tilt(
|
||||
shadowConfig: const ShadowConfig(maxIntensity: .3),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
topRight: Radius.circular(12),
|
||||
),
|
||||
child: homeState.rssVideoItems == null
|
||||
? Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(.1)),
|
||||
child: makeLoading(context),
|
||||
)
|
||||
: Swiper(
|
||||
itemCount: homeState.rssVideoItems?.length ?? 0,
|
||||
itemBuilder: (context, index) {
|
||||
final item = homeState.rssVideoItems![index];
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (item.link != null) {
|
||||
launchUrlString(item.link!);
|
||||
}
|
||||
},
|
||||
child: CacheNetImage(
|
||||
url: model.getRssImage(item),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
},
|
||||
autoplay: true,
|
||||
),
|
||||
)),
|
||||
const SizedBox(height: 1),
|
||||
if (homeState.rssTextItems == null)
|
||||
makeLoading(context)
|
||||
else
|
||||
ListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final item = homeState.rssTextItems![index];
|
||||
return Tilt(
|
||||
shadowConfig: const ShadowConfig(maxIntensity: .3),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (item.link != null) {
|
||||
launchUrlString(item.link!);
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12, right: 12, top: 4, bottom: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
getRssIcon(item.link ?? ""),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
model.handleTitle(item.title),
|
||||
textAlign: TextAlign.start,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 12.2),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Icon(
|
||||
FluentIcons.chevron_right,
|
||||
size: 12,
|
||||
color: Colors.white.withOpacity(.4),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
},
|
||||
itemCount: homeState.rssTextItems?.length,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget getRssIcon(String url) {
|
||||
if (url.startsWith("https://tieba.baidu.com")) {
|
||||
return SvgPicture.asset("assets/tieba.svg", width: 14, height: 14);
|
||||
}
|
||||
|
||||
if (url.startsWith("https://www.bilibili.com")) {
|
||||
return const FaIcon(
|
||||
FontAwesomeIcons.bilibili,
|
||||
size: 14,
|
||||
color: Color.fromRGBO(0, 161, 214, 1),
|
||||
);
|
||||
}
|
||||
|
||||
return const FaIcon(FontAwesomeIcons.rss, size: 14);
|
||||
}
|
||||
|
||||
Widget makeIndexActionLists(
|
||||
BuildContext context, HomeUIModel model, HomeUIModelState homeState) {
|
||||
final items = [
|
||||
_HomeItemData("auto_check", "一键诊断", "一键诊断星际公民常见问题",
|
||||
FluentIcons.auto_deploy_settings),
|
||||
_HomeItemData(
|
||||
"localization", "汉化管理", "快捷安装汉化资源", FluentIcons.locale_language),
|
||||
_HomeItemData("performance", "性能优化", "调整引擎配置文件,优化游戏性能",
|
||||
FluentIcons.process_meta_task),
|
||||
];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: AlignedGridView.count(
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
itemCount: items.length,
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
final item = items.elementAt(index);
|
||||
return HoverButton(
|
||||
onPressed: () => model.onMenuTap(item.key),
|
||||
builder: (BuildContext context, Set<ButtonStates> states) {
|
||||
return Container(
|
||||
width: 300,
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: states.isHovering
|
||||
? FluentTheme.of(context).cardColor.withOpacity(.1)
|
||||
: FluentTheme.of(context).cardColor,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(.2),
|
||||
borderRadius: BorderRadius.circular(1000)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Icon(
|
||||
item.icon,
|
||||
size: 26,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.name,
|
||||
style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(item.infoString),
|
||||
],
|
||||
)),
|
||||
if (item.key == "localization" &&
|
||||
homeState.localizationUpdateInfo != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 3, bottom: 3, left: 8, right: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
child: Text(
|
||||
homeState.localizationUpdateInfo?.key ?? " "),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Icon(
|
||||
FluentIcons.chevron_right,
|
||||
size: 16,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget makeWebViewButton(BuildContext context, HomeUIModel model,
|
||||
{required Widget icon,
|
||||
required String name,
|
||||
required String webTitle,
|
||||
required String webURL,
|
||||
required bool useLocalization,
|
||||
required double width,
|
||||
String? info,
|
||||
String? touchKey}) {
|
||||
return Tilt(
|
||||
shadowConfig: const ShadowConfig(maxIntensity: .3),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (touchKey != null) {
|
||||
AnalyticsApi.touch(touchKey);
|
||||
}
|
||||
model.goWebView(webTitle, webURL, useLocalization: true);
|
||||
},
|
||||
child: Container(
|
||||
width: width,
|
||||
decoration: BoxDecoration(
|
||||
color: FluentTheme.of(context).cardColor,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
icon,
|
||||
Text(
|
||||
name,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (info != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
info,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white.withOpacity(.6)),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Icon(
|
||||
FluentIcons.chevron_right,
|
||||
size: 14,
|
||||
color: Colors.white.withOpacity(.6),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget makeGameStatusCard(BuildContext context, HomeUIModel model,
|
||||
double width, HomeUIModelState homeState) {
|
||||
const statusCnName = {
|
||||
"Platform": "平台",
|
||||
"Persistent Universe": "持续宇宙",
|
||||
"Electronic Access": "电子访问",
|
||||
"Arena Commander": "竞技场指挥官"
|
||||
};
|
||||
|
||||
return Tilt(
|
||||
shadowConfig: const ShadowConfig(maxIntensity: .2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
model.goWebView(
|
||||
"RSI 服务器状态", "https://status.robertsspaceindustries.com/",
|
||||
useLocalization: true);
|
||||
},
|
||||
child: Container(
|
||||
width: width,
|
||||
decoration: BoxDecoration(
|
||||
color: FluentTheme.of(context).cardColor,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(children: [
|
||||
if (homeState.scServerStatus == null)
|
||||
makeLoading(context, width: 20)
|
||||
else
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text("状态:"),
|
||||
for (final item in homeState.scServerStatus ?? [])
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 14,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
FontAwesomeIcons.solidCircle,
|
||||
color: model.isRSIServerStatusOK(item)
|
||||
? Colors.green
|
||||
: Colors.red,
|
||||
size: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
Text(
|
||||
"${statusCnName[item["name"]] ?? item["name"]}",
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
Icon(
|
||||
FluentIcons.chevron_right,
|
||||
size: 12,
|
||||
color: Colors.white.withOpacity(.4),
|
||||
)
|
||||
],
|
||||
)
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget makeActivityBanner(BuildContext context, HomeUIModel model,
|
||||
double width, HomeUIModelState homeState) {
|
||||
return Tilt(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
shadowConfig: const ShadowConfig(disable: true),
|
||||
child: GestureDetector(
|
||||
onTap: () => _onTapFestival(),
|
||||
child: Container(
|
||||
width: width + 24,
|
||||
decoration: BoxDecoration(color: FluentTheme.of(context).cardColor),
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 12, right: 12, top: 8, bottom: 8),
|
||||
child: (homeState.countdownFestivalListData == null)
|
||||
? SizedBox(
|
||||
width: width,
|
||||
height: 62,
|
||||
child: const Center(
|
||||
child: ProgressRing(),
|
||||
),
|
||||
)
|
||||
: SizedBox(
|
||||
width: width,
|
||||
height: 62,
|
||||
child: Swiper(
|
||||
itemCount: homeState.countdownFestivalListData!.length,
|
||||
autoplay: true,
|
||||
autoplayDelay: 5000,
|
||||
itemBuilder: (context, index) {
|
||||
final item =
|
||||
homeState.countdownFestivalListData![index];
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
if (item.icon != null && item.icon != "") ...[
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(1000),
|
||||
child: Image.asset(
|
||||
"assets/countdown/${item.icon}",
|
||||
width: 48,
|
||||
height: 48,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
],
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
item.name ?? "",
|
||||
style: const TextStyle(fontSize: 15),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
CountdownTimeText(
|
||||
targetTime:
|
||||
DateTime.fromMillisecondsSinceEpoch(
|
||||
item.time ?? 0),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Icon(
|
||||
FluentIcons.chevron_right,
|
||||
size: 14,
|
||||
color: Colors.white.withOpacity(.6),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_showPlacard() {}
|
||||
|
||||
_onTapFestival() {}
|
||||
}
|
||||
|
||||
class _HomeItemData {
|
||||
String key;
|
||||
|
||||
_HomeItemData(this.key, this.name, this.infoString, this.icon);
|
||||
|
||||
String name;
|
||||
String infoString;
|
||||
IconData icon;
|
||||
}
|
246
lib/ui/home/home_ui_model.dart
Normal file
246
lib/ui/home/home_ui_model.dart
Normal file
@ -0,0 +1,246 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dart_rss/domain/rss_item.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:starcitizen_doctor/api/analytics.dart';
|
||||
import 'package:starcitizen_doctor/api/api.dart';
|
||||
import 'package:starcitizen_doctor/api/rss.dart';
|
||||
import 'package:starcitizen_doctor/common/conf/const_conf.dart';
|
||||
import 'package:starcitizen_doctor/common/conf/url_conf.dart';
|
||||
import 'package:starcitizen_doctor/common/helper/log_helper.dart';
|
||||
import 'package:starcitizen_doctor/common/helper/system_helper.dart';
|
||||
import 'package:starcitizen_doctor/common/io/rs_http.dart';
|
||||
import 'package:starcitizen_doctor/common/utils/base_utils.dart';
|
||||
import 'package:starcitizen_doctor/common/utils/log.dart';
|
||||
import 'package:starcitizen_doctor/common/utils/provider.dart';
|
||||
import 'package:starcitizen_doctor/data/app_placard_data.dart';
|
||||
import 'package:starcitizen_doctor/data/app_web_localization_versions_data.dart';
|
||||
import 'package:starcitizen_doctor/data/countdown_festival_item_data.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:html/parser.dart' as html;
|
||||
import 'package:html/dom.dart' as html_dom;
|
||||
|
||||
part 'home_ui_model.freezed.dart';
|
||||
|
||||
part 'home_ui_model.g.dart';
|
||||
|
||||
@freezed
|
||||
class HomeUIModelState with _$HomeUIModelState {
|
||||
factory HomeUIModelState({
|
||||
AppPlacardData? appPlacardData,
|
||||
@Default(false) bool isFixing,
|
||||
@Default("") String isFixingString,
|
||||
String? scInstalledPath,
|
||||
@Default([]) List<String> scInstallPaths,
|
||||
AppWebLocalizationVersionsData? webLocalizationVersionsData,
|
||||
@Default(false) bool isCurGameRunning,
|
||||
@Default("") String lastScreenInfo,
|
||||
List<RssItem>? rssVideoItems,
|
||||
List<RssItem>? rssTextItems,
|
||||
MapEntry<String, bool>? localizationUpdateInfo,
|
||||
List? scServerStatus,
|
||||
List<CountdownFestivalItemData>? countdownFestivalListData,
|
||||
}) = _HomeUIModelState;
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class HomeUIModel extends _$HomeUIModel {
|
||||
@override
|
||||
HomeUIModelState build() {
|
||||
state = HomeUIModelState();
|
||||
_init();
|
||||
_loadData();
|
||||
return state;
|
||||
}
|
||||
|
||||
closePlacard() async {
|
||||
final box = await Hive.openBox("app_conf");
|
||||
await box.put("close_placard", state.appPlacardData?.version);
|
||||
state = state.copyWith(appPlacardData: null);
|
||||
}
|
||||
|
||||
Future<void> reScanPath() async {
|
||||
state = state.copyWith(
|
||||
scInstalledPath: "not_install", lastScreenInfo: "正在扫描 ...");
|
||||
try {
|
||||
final listData = await SCLoggerHelper.getLauncherLogList();
|
||||
if (listData == null) {
|
||||
state = state.copyWith(scInstalledPath: "not_install");
|
||||
return;
|
||||
}
|
||||
final scInstallPaths = await SCLoggerHelper.getGameInstallPath(listData,
|
||||
withVersion: ["LIVE", "PTU", "EPTU"], checkExists: true);
|
||||
String? scInstalledPath;
|
||||
if (scInstallPaths.isNotEmpty) {
|
||||
scInstalledPath = scInstallPaths.first;
|
||||
}
|
||||
final lastScreenInfo = "扫描完毕,共找到 ${scInstallPaths.length} 个有效安装目录";
|
||||
state = state.copyWith(
|
||||
scInstalledPath: scInstalledPath,
|
||||
scInstallPaths: scInstallPaths,
|
||||
lastScreenInfo: lastScreenInfo);
|
||||
} catch (e) {
|
||||
state = state.copyWith(
|
||||
scInstalledPath: "not_install", lastScreenInfo: "解析 log 文件失败!");
|
||||
AnalyticsApi.touch("error_launchLogs");
|
||||
// showToast(context!,
|
||||
// "解析 log 文件失败! \n请关闭游戏,退出RSI启动器后重试,若仍有问题,请使用工具箱中的 RSI Launcher log 修复。");
|
||||
}
|
||||
}
|
||||
|
||||
String getRssImage(RssItem item) {
|
||||
final h = html.parse(item.description ?? "");
|
||||
if (h.body == null) return "";
|
||||
for (var node in h.body!.nodes) {
|
||||
if (node is html_dom.Element) {
|
||||
if (node.localName == "img") {
|
||||
return node.attributes["src"]?.trim() ?? "";
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
String handleTitle(String? title) {
|
||||
if (title == null) return "";
|
||||
title = title.replaceAll("【", "[ ");
|
||||
title = title.replaceAll("】", " ] ");
|
||||
return title;
|
||||
}
|
||||
|
||||
onMenuTap(String key) {}
|
||||
|
||||
void goWebView(String webTitle, String webURL,
|
||||
{required bool useLocalization}) {}
|
||||
|
||||
bool isRSIServerStatusOK(Map map) {
|
||||
return (map["status"] == "ok" || map["status"] == "operational");
|
||||
}
|
||||
|
||||
Timer? _serverUpdateTimer;
|
||||
Timer? _appUpdateTimer;
|
||||
|
||||
void _init() {
|
||||
reScanPath();
|
||||
_serverUpdateTimer = Timer.periodic(
|
||||
const Duration(minutes: 10),
|
||||
(timer) {
|
||||
_updateSCServerStatus();
|
||||
},
|
||||
);
|
||||
|
||||
_appUpdateTimer = Timer.periodic(const Duration(minutes: 30), (timer) {
|
||||
_checkLocalizationUpdate();
|
||||
});
|
||||
|
||||
ref.onDispose(() {
|
||||
_serverUpdateTimer?.cancel();
|
||||
_serverUpdateTimer = null;
|
||||
_appUpdateTimer?.cancel();
|
||||
_appUpdateTimer = null;
|
||||
});
|
||||
}
|
||||
|
||||
void _loadData() async {
|
||||
if (appGlobalState.networkVersionData == null) return;
|
||||
try {
|
||||
final r = await Api.getAppPlacard();
|
||||
final box = await Hive.openBox("app_conf");
|
||||
final version = box.get("close_placard", defaultValue: "");
|
||||
if (r.enable == true) {
|
||||
if (r.alwaysShow != true && version == r.version) {
|
||||
} else {
|
||||
state = state.copyWith(appPlacardData: r);
|
||||
}
|
||||
}
|
||||
|
||||
final appWebLocalizationVersionsData =
|
||||
AppWebLocalizationVersionsData.fromJson(json.decode(
|
||||
(await RSHttp.getText(
|
||||
"${URLConf.webTranslateHomeUrl}/versions.json"))));
|
||||
final countdownFestivalListData = await Api.getFestivalCountdownList();
|
||||
state = state.copyWith(
|
||||
webLocalizationVersionsData: appWebLocalizationVersionsData,
|
||||
countdownFestivalListData: countdownFestivalListData);
|
||||
_updateSCServerStatus();
|
||||
_loadRRS();
|
||||
} catch (e) {
|
||||
dPrint(e);
|
||||
}
|
||||
// check Localization update
|
||||
_checkLocalizationUpdate();
|
||||
}
|
||||
|
||||
Future<void> _updateSCServerStatus() async {
|
||||
try {
|
||||
final s = await Api.getScServerStatus();
|
||||
dPrint("updateSCServerStatus===$s");
|
||||
state = state.copyWith(scServerStatus: s);
|
||||
} catch (e) {
|
||||
dPrint(e);
|
||||
}
|
||||
}
|
||||
|
||||
Future _loadRRS() async {
|
||||
try {
|
||||
state = state.copyWith(
|
||||
rssVideoItems: await RSSApi.getRssVideo(),
|
||||
rssTextItems: await RSSApi.getRssText());
|
||||
dPrint("RSS update Success !");
|
||||
} catch (e) {
|
||||
dPrint("_loadRRS Error:$e");
|
||||
}
|
||||
}
|
||||
|
||||
void _checkLocalizationUpdate() {}
|
||||
|
||||
// ignore: avoid_build_context_in_providers
|
||||
launchRSI(BuildContext context) async {
|
||||
if (state.scInstalledPath == "not_install") {
|
||||
showToast(context, "该功能需要一个有效的安装位置");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ConstConf.isMSE) {
|
||||
if (state.isCurGameRunning) {
|
||||
await Process.run(
|
||||
SystemHelper.powershellPath, ["ps \"StarCitizen\" | kill"]);
|
||||
return;
|
||||
}
|
||||
AnalyticsApi.touch("gameLaunch");
|
||||
// showDialog(
|
||||
// context: context,
|
||||
// dismissWithEsc: false,
|
||||
// builder: (context) {
|
||||
// return BaseUIContainer(
|
||||
// uiCreate: () => LoginDialog(),
|
||||
// modelCreate: () => LoginDialogModel(scInstalledPath, this));
|
||||
// });
|
||||
} else {
|
||||
final ok = await showConfirmDialogs(
|
||||
context,
|
||||
"一键启动功能提示",
|
||||
const Text("为确保账户安全,一键启动功能已在开发版中禁用,我们将在微软商店版本中提供此功能。"
|
||||
"\n\n微软商店版由微软提供可靠的分发下载与数字签名,可有效防止软件被恶意篡改。\n\n提示:您无需使用盒子启动游戏也可使用汉化。"),
|
||||
confirm: "安装微软商店版本",
|
||||
cancel: "取消");
|
||||
if (ok == true) {
|
||||
await launchUrlString(
|
||||
"https://apps.microsoft.com/detail/9NF3SWFWNKL1?launch=true");
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onChangeInstallPath(String? value) {
|
||||
if (value == null) return;
|
||||
state = state.copyWith(scInstalledPath: value);
|
||||
}
|
||||
}
|
458
lib/ui/home/home_ui_model.freezed.dart
Normal file
458
lib/ui/home/home_ui_model.freezed.dart
Normal file
@ -0,0 +1,458 @@
|
||||
// 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 'home_ui_model.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 _$HomeUIModelState {
|
||||
AppPlacardData? get appPlacardData => throw _privateConstructorUsedError;
|
||||
bool get isFixing => throw _privateConstructorUsedError;
|
||||
String get isFixingString => throw _privateConstructorUsedError;
|
||||
String? get scInstalledPath => throw _privateConstructorUsedError;
|
||||
List<String> get scInstallPaths => throw _privateConstructorUsedError;
|
||||
AppWebLocalizationVersionsData? get webLocalizationVersionsData =>
|
||||
throw _privateConstructorUsedError;
|
||||
bool get isCurGameRunning => throw _privateConstructorUsedError;
|
||||
String get lastScreenInfo => throw _privateConstructorUsedError;
|
||||
List<RssItem>? get rssVideoItems => throw _privateConstructorUsedError;
|
||||
List<RssItem>? get rssTextItems => throw _privateConstructorUsedError;
|
||||
MapEntry<String, bool>? get localizationUpdateInfo =>
|
||||
throw _privateConstructorUsedError;
|
||||
List<dynamic>? get scServerStatus => throw _privateConstructorUsedError;
|
||||
List<CountdownFestivalItemData>? get countdownFestivalListData =>
|
||||
throw _privateConstructorUsedError;
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
$HomeUIModelStateCopyWith<HomeUIModelState> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $HomeUIModelStateCopyWith<$Res> {
|
||||
factory $HomeUIModelStateCopyWith(
|
||||
HomeUIModelState value, $Res Function(HomeUIModelState) then) =
|
||||
_$HomeUIModelStateCopyWithImpl<$Res, HomeUIModelState>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{AppPlacardData? appPlacardData,
|
||||
bool isFixing,
|
||||
String isFixingString,
|
||||
String? scInstalledPath,
|
||||
List<String> scInstallPaths,
|
||||
AppWebLocalizationVersionsData? webLocalizationVersionsData,
|
||||
bool isCurGameRunning,
|
||||
String lastScreenInfo,
|
||||
List<RssItem>? rssVideoItems,
|
||||
List<RssItem>? rssTextItems,
|
||||
MapEntry<String, bool>? localizationUpdateInfo,
|
||||
List<dynamic>? scServerStatus,
|
||||
List<CountdownFestivalItemData>? countdownFestivalListData});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$HomeUIModelStateCopyWithImpl<$Res, $Val extends HomeUIModelState>
|
||||
implements $HomeUIModelStateCopyWith<$Res> {
|
||||
_$HomeUIModelStateCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? appPlacardData = freezed,
|
||||
Object? isFixing = null,
|
||||
Object? isFixingString = null,
|
||||
Object? scInstalledPath = freezed,
|
||||
Object? scInstallPaths = null,
|
||||
Object? webLocalizationVersionsData = freezed,
|
||||
Object? isCurGameRunning = null,
|
||||
Object? lastScreenInfo = null,
|
||||
Object? rssVideoItems = freezed,
|
||||
Object? rssTextItems = freezed,
|
||||
Object? localizationUpdateInfo = freezed,
|
||||
Object? scServerStatus = freezed,
|
||||
Object? countdownFestivalListData = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
appPlacardData: freezed == appPlacardData
|
||||
? _value.appPlacardData
|
||||
: appPlacardData // ignore: cast_nullable_to_non_nullable
|
||||
as AppPlacardData?,
|
||||
isFixing: null == isFixing
|
||||
? _value.isFixing
|
||||
: isFixing // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
isFixingString: null == isFixingString
|
||||
? _value.isFixingString
|
||||
: isFixingString // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
scInstalledPath: freezed == scInstalledPath
|
||||
? _value.scInstalledPath
|
||||
: scInstalledPath // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
scInstallPaths: null == scInstallPaths
|
||||
? _value.scInstallPaths
|
||||
: scInstallPaths // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
webLocalizationVersionsData: freezed == webLocalizationVersionsData
|
||||
? _value.webLocalizationVersionsData
|
||||
: webLocalizationVersionsData // ignore: cast_nullable_to_non_nullable
|
||||
as AppWebLocalizationVersionsData?,
|
||||
isCurGameRunning: null == isCurGameRunning
|
||||
? _value.isCurGameRunning
|
||||
: isCurGameRunning // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
lastScreenInfo: null == lastScreenInfo
|
||||
? _value.lastScreenInfo
|
||||
: lastScreenInfo // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
rssVideoItems: freezed == rssVideoItems
|
||||
? _value.rssVideoItems
|
||||
: rssVideoItems // ignore: cast_nullable_to_non_nullable
|
||||
as List<RssItem>?,
|
||||
rssTextItems: freezed == rssTextItems
|
||||
? _value.rssTextItems
|
||||
: rssTextItems // ignore: cast_nullable_to_non_nullable
|
||||
as List<RssItem>?,
|
||||
localizationUpdateInfo: freezed == localizationUpdateInfo
|
||||
? _value.localizationUpdateInfo
|
||||
: localizationUpdateInfo // ignore: cast_nullable_to_non_nullable
|
||||
as MapEntry<String, bool>?,
|
||||
scServerStatus: freezed == scServerStatus
|
||||
? _value.scServerStatus
|
||||
: scServerStatus // ignore: cast_nullable_to_non_nullable
|
||||
as List<dynamic>?,
|
||||
countdownFestivalListData: freezed == countdownFestivalListData
|
||||
? _value.countdownFestivalListData
|
||||
: countdownFestivalListData // ignore: cast_nullable_to_non_nullable
|
||||
as List<CountdownFestivalItemData>?,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$HomeUIModelStateImplCopyWith<$Res>
|
||||
implements $HomeUIModelStateCopyWith<$Res> {
|
||||
factory _$$HomeUIModelStateImplCopyWith(_$HomeUIModelStateImpl value,
|
||||
$Res Function(_$HomeUIModelStateImpl) then) =
|
||||
__$$HomeUIModelStateImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{AppPlacardData? appPlacardData,
|
||||
bool isFixing,
|
||||
String isFixingString,
|
||||
String? scInstalledPath,
|
||||
List<String> scInstallPaths,
|
||||
AppWebLocalizationVersionsData? webLocalizationVersionsData,
|
||||
bool isCurGameRunning,
|
||||
String lastScreenInfo,
|
||||
List<RssItem>? rssVideoItems,
|
||||
List<RssItem>? rssTextItems,
|
||||
MapEntry<String, bool>? localizationUpdateInfo,
|
||||
List<dynamic>? scServerStatus,
|
||||
List<CountdownFestivalItemData>? countdownFestivalListData});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$HomeUIModelStateImplCopyWithImpl<$Res>
|
||||
extends _$HomeUIModelStateCopyWithImpl<$Res, _$HomeUIModelStateImpl>
|
||||
implements _$$HomeUIModelStateImplCopyWith<$Res> {
|
||||
__$$HomeUIModelStateImplCopyWithImpl(_$HomeUIModelStateImpl _value,
|
||||
$Res Function(_$HomeUIModelStateImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? appPlacardData = freezed,
|
||||
Object? isFixing = null,
|
||||
Object? isFixingString = null,
|
||||
Object? scInstalledPath = freezed,
|
||||
Object? scInstallPaths = null,
|
||||
Object? webLocalizationVersionsData = freezed,
|
||||
Object? isCurGameRunning = null,
|
||||
Object? lastScreenInfo = null,
|
||||
Object? rssVideoItems = freezed,
|
||||
Object? rssTextItems = freezed,
|
||||
Object? localizationUpdateInfo = freezed,
|
||||
Object? scServerStatus = freezed,
|
||||
Object? countdownFestivalListData = freezed,
|
||||
}) {
|
||||
return _then(_$HomeUIModelStateImpl(
|
||||
appPlacardData: freezed == appPlacardData
|
||||
? _value.appPlacardData
|
||||
: appPlacardData // ignore: cast_nullable_to_non_nullable
|
||||
as AppPlacardData?,
|
||||
isFixing: null == isFixing
|
||||
? _value.isFixing
|
||||
: isFixing // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
isFixingString: null == isFixingString
|
||||
? _value.isFixingString
|
||||
: isFixingString // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
scInstalledPath: freezed == scInstalledPath
|
||||
? _value.scInstalledPath
|
||||
: scInstalledPath // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
scInstallPaths: null == scInstallPaths
|
||||
? _value._scInstallPaths
|
||||
: scInstallPaths // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
webLocalizationVersionsData: freezed == webLocalizationVersionsData
|
||||
? _value.webLocalizationVersionsData
|
||||
: webLocalizationVersionsData // ignore: cast_nullable_to_non_nullable
|
||||
as AppWebLocalizationVersionsData?,
|
||||
isCurGameRunning: null == isCurGameRunning
|
||||
? _value.isCurGameRunning
|
||||
: isCurGameRunning // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
lastScreenInfo: null == lastScreenInfo
|
||||
? _value.lastScreenInfo
|
||||
: lastScreenInfo // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
rssVideoItems: freezed == rssVideoItems
|
||||
? _value._rssVideoItems
|
||||
: rssVideoItems // ignore: cast_nullable_to_non_nullable
|
||||
as List<RssItem>?,
|
||||
rssTextItems: freezed == rssTextItems
|
||||
? _value._rssTextItems
|
||||
: rssTextItems // ignore: cast_nullable_to_non_nullable
|
||||
as List<RssItem>?,
|
||||
localizationUpdateInfo: freezed == localizationUpdateInfo
|
||||
? _value.localizationUpdateInfo
|
||||
: localizationUpdateInfo // ignore: cast_nullable_to_non_nullable
|
||||
as MapEntry<String, bool>?,
|
||||
scServerStatus: freezed == scServerStatus
|
||||
? _value._scServerStatus
|
||||
: scServerStatus // ignore: cast_nullable_to_non_nullable
|
||||
as List<dynamic>?,
|
||||
countdownFestivalListData: freezed == countdownFestivalListData
|
||||
? _value._countdownFestivalListData
|
||||
: countdownFestivalListData // ignore: cast_nullable_to_non_nullable
|
||||
as List<CountdownFestivalItemData>?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$HomeUIModelStateImpl implements _HomeUIModelState {
|
||||
_$HomeUIModelStateImpl(
|
||||
{this.appPlacardData,
|
||||
this.isFixing = false,
|
||||
this.isFixingString = "",
|
||||
this.scInstalledPath,
|
||||
final List<String> scInstallPaths = const [],
|
||||
this.webLocalizationVersionsData,
|
||||
this.isCurGameRunning = false,
|
||||
this.lastScreenInfo = "",
|
||||
final List<RssItem>? rssVideoItems,
|
||||
final List<RssItem>? rssTextItems,
|
||||
this.localizationUpdateInfo,
|
||||
final List<dynamic>? scServerStatus,
|
||||
final List<CountdownFestivalItemData>? countdownFestivalListData})
|
||||
: _scInstallPaths = scInstallPaths,
|
||||
_rssVideoItems = rssVideoItems,
|
||||
_rssTextItems = rssTextItems,
|
||||
_scServerStatus = scServerStatus,
|
||||
_countdownFestivalListData = countdownFestivalListData;
|
||||
|
||||
@override
|
||||
final AppPlacardData? appPlacardData;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool isFixing;
|
||||
@override
|
||||
@JsonKey()
|
||||
final String isFixingString;
|
||||
@override
|
||||
final String? scInstalledPath;
|
||||
final List<String> _scInstallPaths;
|
||||
@override
|
||||
@JsonKey()
|
||||
List<String> get scInstallPaths {
|
||||
if (_scInstallPaths is EqualUnmodifiableListView) return _scInstallPaths;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_scInstallPaths);
|
||||
}
|
||||
|
||||
@override
|
||||
final AppWebLocalizationVersionsData? webLocalizationVersionsData;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool isCurGameRunning;
|
||||
@override
|
||||
@JsonKey()
|
||||
final String lastScreenInfo;
|
||||
final List<RssItem>? _rssVideoItems;
|
||||
@override
|
||||
List<RssItem>? get rssVideoItems {
|
||||
final value = _rssVideoItems;
|
||||
if (value == null) return null;
|
||||
if (_rssVideoItems is EqualUnmodifiableListView) return _rssVideoItems;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(value);
|
||||
}
|
||||
|
||||
final List<RssItem>? _rssTextItems;
|
||||
@override
|
||||
List<RssItem>? get rssTextItems {
|
||||
final value = _rssTextItems;
|
||||
if (value == null) return null;
|
||||
if (_rssTextItems is EqualUnmodifiableListView) return _rssTextItems;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(value);
|
||||
}
|
||||
|
||||
@override
|
||||
final MapEntry<String, bool>? localizationUpdateInfo;
|
||||
final List<dynamic>? _scServerStatus;
|
||||
@override
|
||||
List<dynamic>? get scServerStatus {
|
||||
final value = _scServerStatus;
|
||||
if (value == null) return null;
|
||||
if (_scServerStatus is EqualUnmodifiableListView) return _scServerStatus;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(value);
|
||||
}
|
||||
|
||||
final List<CountdownFestivalItemData>? _countdownFestivalListData;
|
||||
@override
|
||||
List<CountdownFestivalItemData>? get countdownFestivalListData {
|
||||
final value = _countdownFestivalListData;
|
||||
if (value == null) return null;
|
||||
if (_countdownFestivalListData is EqualUnmodifiableListView)
|
||||
return _countdownFestivalListData;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(value);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'HomeUIModelState(appPlacardData: $appPlacardData, isFixing: $isFixing, isFixingString: $isFixingString, scInstalledPath: $scInstalledPath, scInstallPaths: $scInstallPaths, webLocalizationVersionsData: $webLocalizationVersionsData, isCurGameRunning: $isCurGameRunning, lastScreenInfo: $lastScreenInfo, rssVideoItems: $rssVideoItems, rssTextItems: $rssTextItems, localizationUpdateInfo: $localizationUpdateInfo, scServerStatus: $scServerStatus, countdownFestivalListData: $countdownFestivalListData)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$HomeUIModelStateImpl &&
|
||||
(identical(other.appPlacardData, appPlacardData) ||
|
||||
other.appPlacardData == appPlacardData) &&
|
||||
(identical(other.isFixing, isFixing) ||
|
||||
other.isFixing == isFixing) &&
|
||||
(identical(other.isFixingString, isFixingString) ||
|
||||
other.isFixingString == isFixingString) &&
|
||||
(identical(other.scInstalledPath, scInstalledPath) ||
|
||||
other.scInstalledPath == scInstalledPath) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._scInstallPaths, _scInstallPaths) &&
|
||||
(identical(other.webLocalizationVersionsData,
|
||||
webLocalizationVersionsData) ||
|
||||
other.webLocalizationVersionsData ==
|
||||
webLocalizationVersionsData) &&
|
||||
(identical(other.isCurGameRunning, isCurGameRunning) ||
|
||||
other.isCurGameRunning == isCurGameRunning) &&
|
||||
(identical(other.lastScreenInfo, lastScreenInfo) ||
|
||||
other.lastScreenInfo == lastScreenInfo) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._rssVideoItems, _rssVideoItems) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._rssTextItems, _rssTextItems) &&
|
||||
(identical(other.localizationUpdateInfo, localizationUpdateInfo) ||
|
||||
other.localizationUpdateInfo == localizationUpdateInfo) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._scServerStatus, _scServerStatus) &&
|
||||
const DeepCollectionEquality().equals(
|
||||
other._countdownFestivalListData, _countdownFestivalListData));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
appPlacardData,
|
||||
isFixing,
|
||||
isFixingString,
|
||||
scInstalledPath,
|
||||
const DeepCollectionEquality().hash(_scInstallPaths),
|
||||
webLocalizationVersionsData,
|
||||
isCurGameRunning,
|
||||
lastScreenInfo,
|
||||
const DeepCollectionEquality().hash(_rssVideoItems),
|
||||
const DeepCollectionEquality().hash(_rssTextItems),
|
||||
localizationUpdateInfo,
|
||||
const DeepCollectionEquality().hash(_scServerStatus),
|
||||
const DeepCollectionEquality().hash(_countdownFestivalListData));
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$HomeUIModelStateImplCopyWith<_$HomeUIModelStateImpl> get copyWith =>
|
||||
__$$HomeUIModelStateImplCopyWithImpl<_$HomeUIModelStateImpl>(
|
||||
this, _$identity);
|
||||
}
|
||||
|
||||
abstract class _HomeUIModelState implements HomeUIModelState {
|
||||
factory _HomeUIModelState(
|
||||
{final AppPlacardData? appPlacardData,
|
||||
final bool isFixing,
|
||||
final String isFixingString,
|
||||
final String? scInstalledPath,
|
||||
final List<String> scInstallPaths,
|
||||
final AppWebLocalizationVersionsData? webLocalizationVersionsData,
|
||||
final bool isCurGameRunning,
|
||||
final String lastScreenInfo,
|
||||
final List<RssItem>? rssVideoItems,
|
||||
final List<RssItem>? rssTextItems,
|
||||
final MapEntry<String, bool>? localizationUpdateInfo,
|
||||
final List<dynamic>? scServerStatus,
|
||||
final List<CountdownFestivalItemData>? countdownFestivalListData}) =
|
||||
_$HomeUIModelStateImpl;
|
||||
|
||||
@override
|
||||
AppPlacardData? get appPlacardData;
|
||||
@override
|
||||
bool get isFixing;
|
||||
@override
|
||||
String get isFixingString;
|
||||
@override
|
||||
String? get scInstalledPath;
|
||||
@override
|
||||
List<String> get scInstallPaths;
|
||||
@override
|
||||
AppWebLocalizationVersionsData? get webLocalizationVersionsData;
|
||||
@override
|
||||
bool get isCurGameRunning;
|
||||
@override
|
||||
String get lastScreenInfo;
|
||||
@override
|
||||
List<RssItem>? get rssVideoItems;
|
||||
@override
|
||||
List<RssItem>? get rssTextItems;
|
||||
@override
|
||||
MapEntry<String, bool>? get localizationUpdateInfo;
|
||||
@override
|
||||
List<dynamic>? get scServerStatus;
|
||||
@override
|
||||
List<CountdownFestivalItemData>? get countdownFestivalListData;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$HomeUIModelStateImplCopyWith<_$HomeUIModelStateImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
25
lib/ui/home/home_ui_model.g.dart
Normal file
25
lib/ui/home/home_ui_model.g.dart
Normal file
@ -0,0 +1,25 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'home_ui_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$homeUIModelHash() => r'7ab7b3721ff81a18d67717c9bc91632226c516f6';
|
||||
|
||||
/// See also [HomeUIModel].
|
||||
@ProviderFor(HomeUIModel)
|
||||
final homeUIModelProvider =
|
||||
AutoDisposeNotifierProvider<HomeUIModel, HomeUIModelState>.internal(
|
||||
HomeUIModel.new,
|
||||
name: r'homeUIModelProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$homeUIModelHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$HomeUIModel = AutoDisposeNotifier<HomeUIModelState>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
@ -3,6 +3,8 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:starcitizen_doctor/common/conf/const_conf.dart';
|
||||
import 'package:starcitizen_doctor/provider/aria2c.dart';
|
||||
import 'package:starcitizen_doctor/ui/home/home_ui.dart';
|
||||
import 'package:starcitizen_doctor/ui/home/home_ui_model.dart';
|
||||
import 'package:starcitizen_doctor/widgets/widgets.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
@ -11,6 +13,9 @@ class IndexUI extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// pre init child
|
||||
ref.watch(homeUIModelProvider.select((value) => null));
|
||||
|
||||
final curIndex = useState(0);
|
||||
return NavigationView(
|
||||
appBar: NavigationAppBar(
|
||||
@ -107,10 +112,15 @@ class IndexUI extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
Widget getPage(int value) {
|
||||
switch (value) {
|
||||
case 0:
|
||||
return const HomeUI();
|
||||
default:
|
||||
return Center(
|
||||
child: Text("$value"),
|
||||
child: Text("UnimplPage $value"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onTapIndexMenu(String value, ValueNotifier<int> curIndexState) {
|
||||
final pageIndex =
|
||||
|
@ -11,7 +11,6 @@ import 'package:starcitizen_doctor/app.dart';
|
||||
import 'package:starcitizen_doctor/common/conf/const_conf.dart';
|
||||
import 'package:starcitizen_doctor/common/conf/url_conf.dart';
|
||||
import 'package:starcitizen_doctor/common/helper/system_helper.dart';
|
||||
import 'package:starcitizen_doctor/common/utils/base_utils.dart';
|
||||
import 'package:starcitizen_doctor/common/utils/log.dart';
|
||||
import 'package:starcitizen_doctor/widgets/widgets.dart';
|
||||
import 'package:html/parser.dart' as html_parser;
|
||||
|
41
lib/widgets/src/cache_image.dart
Normal file
41
lib/widgets/src/cache_image.dart
Normal file
@ -0,0 +1,41 @@
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
class CacheNetImage extends StatelessWidget {
|
||||
final String url;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final BoxFit? fit;
|
||||
|
||||
const CacheNetImage(
|
||||
{super.key, required this.url, this.width, this.height, this.fit});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ExtendedImage.network(
|
||||
url,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
loadStateChanged: (ExtendedImageState state) {
|
||||
switch (state.extendedImageLoadState) {
|
||||
case LoadState.loading:
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
ProgressRing(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
case LoadState.failed:
|
||||
return const Text("Loading Image error");
|
||||
case LoadState.completed:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
91
lib/widgets/src/countdown_time_text.dart
Normal file
91
lib/widgets/src/countdown_time_text.dart
Normal file
@ -0,0 +1,91 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
class CountdownTimeText extends StatefulWidget {
|
||||
final DateTime targetTime;
|
||||
|
||||
const CountdownTimeText({super.key, required this.targetTime});
|
||||
|
||||
@override
|
||||
State<CountdownTimeText> createState() => _CountdownTimeTextState();
|
||||
}
|
||||
|
||||
class _CountdownTimeTextState extends State<CountdownTimeText> {
|
||||
Timer? _timer;
|
||||
|
||||
Widget? textWidget;
|
||||
|
||||
bool stopTimer = false;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
_onUpdateTime(null);
|
||||
if (!stopTimer) {
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), _onUpdateTime);
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
_onUpdateTime(_) {
|
||||
final now = DateTime.now();
|
||||
final dur = widget.targetTime.difference(now);
|
||||
setState(() {
|
||||
textWidget = _chineseTimeText(dur);
|
||||
});
|
||||
// 时间到,停止计时,并向宿主传递超时信息
|
||||
if (dur.inMilliseconds <= 0) {
|
||||
stopTimer = true;
|
||||
setState(() {});
|
||||
}
|
||||
if (stopTimer) {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _chineseTimeText(Duration duration) {
|
||||
final surplus = duration;
|
||||
int day = (surplus.inSeconds ~/ 3600) ~/ 24;
|
||||
int hour = (surplus.inSeconds ~/ 3600) % 24;
|
||||
int minute = surplus.inSeconds % 3600 ~/ 60;
|
||||
int second = surplus.inSeconds % 60;
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"$day天 ",
|
||||
style: TextStyle(
|
||||
fontSize: 24, color: day < 30 ? Colors.red : Colors.white),
|
||||
),
|
||||
Text("${timePart(hour)}:${timePart(minute)}:${timePart(second)}"),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String timePart(int p) {
|
||||
if (p.toString().length == 1) return "0$p";
|
||||
return "$p";
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (stopTimer) {
|
||||
return const Text(
|
||||
"正在进行中",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Color.fromRGBO(32, 220, 89, 1.0),
|
||||
fontWeight: FontWeight.bold),
|
||||
);
|
||||
}
|
||||
return textWidget ?? const Text("");
|
||||
}
|
||||
}
|
@ -5,6 +5,25 @@ import 'package:window_manager/window_manager.dart';
|
||||
import 'package:markdown_widget/config/all.dart';
|
||||
import 'package:markdown_widget/widget/all.dart';
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
export 'src/cache_image.dart';
|
||||
export 'src/countdown_time_text.dart';
|
||||
export '../common/utils/base_utils.dart';
|
||||
|
||||
Widget makeLoading(
|
||||
BuildContext context, {
|
||||
double? width,
|
||||
}) {
|
||||
width ??= 30;
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
height: width,
|
||||
child: const ProgressRing(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget makeDefaultPage(BuildContext context,
|
||||
{Widget? titleRow,
|
||||
@ -99,6 +118,10 @@ List<Widget> makeMarkdownView(String description, {String? attachmentsUrl}) {
|
||||
]));
|
||||
}
|
||||
|
||||
ColorFilter makeSvgColor(Color color) {
|
||||
return ui.ColorFilter.mode(color, ui.BlendMode.srcIn);
|
||||
}
|
||||
|
||||
CustomTransitionPage<T> myPageBuilder<T>(
|
||||
BuildContext context, GoRouterState state, Widget child) {
|
||||
return CustomTransitionPage(
|
||||
|
Loading…
Reference in New Issue
Block a user