mirror of
https://github.com/StarCitizenToolBox/app.git
synced 2026-02-12 10:10:23 +00:00
feat: web yearly_report
This commit is contained in:
@@ -17,6 +17,7 @@ import 'home/home_ui.dart';
|
||||
import 'nav/nav_ui.dart';
|
||||
import 'settings/settings_ui.dart';
|
||||
import 'tools/tools_ui.dart';
|
||||
import 'tools/yearly_report/yearly_report_entry.dart';
|
||||
|
||||
class IndexUI extends HookConsumerWidget {
|
||||
const IndexUI({super.key});
|
||||
@@ -145,6 +146,8 @@ class IndexUI extends HookConsumerWidget {
|
||||
Map<IconData, (String, Widget)> get pageMenus => {
|
||||
FluentIcons.home: (S.current.app_index_menu_home, const HomeUI()),
|
||||
if (!kIsWeb) FluentIcons.toolbox: (S.current.app_index_menu_tools, const ToolsUI()),
|
||||
if (kIsWeb && isYearlyReportPeriod())
|
||||
FluentIcons.chart: (S.current.yearly_report_menu_title, const YearlyReportEntryUI()),
|
||||
FluentIcons.power_apps: ((S.current.nav_title), const NavUI()),
|
||||
FluentIcons.settings: (S.current.app_index_menu_settings, const SettingsUI()),
|
||||
FluentIcons.info: (S.current.app_index_menu_about, const AboutUI()),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hive_ce/hive.dart';
|
||||
@@ -64,6 +65,16 @@ class SplashUI extends HookConsumerWidget {
|
||||
final appConf = await Hive.openBox("app_conf");
|
||||
final v = appConf.get("splash_alert_info_version", defaultValue: 0);
|
||||
AnalyticsApi.touch("launch");
|
||||
|
||||
// 检查 Web 特殊路由
|
||||
if (kIsWeb) {
|
||||
final uri = Uri.base;
|
||||
if (uri.path.contains('yearly_report') || uri.queryParameters.containsKey('yearly_report')) {
|
||||
if (!context.mounted) return;
|
||||
context.go("/tools/yearly_report");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (v < _alertInfoVersion) {
|
||||
if (!context.mounted) return;
|
||||
await _showAlert(context, appConf);
|
||||
|
||||
442
lib/ui/tools/yearly_report/yearly_report_entry.dart
Normal file
442
lib/ui/tools/yearly_report/yearly_report_entry.dart
Normal file
@@ -0,0 +1,442 @@
|
||||
import 'dart:async';
|
||||
import 'dart:js_interop';
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:starcitizen_doctor/app.dart';
|
||||
import 'package:starcitizen_doctor/widgets/widgets.dart';
|
||||
import 'package:web/web.dart' as web;
|
||||
|
||||
import 'yearly_report_ui.dart';
|
||||
|
||||
// JS interop extensions for File System Access API
|
||||
@JS('showDirectoryPicker')
|
||||
external JSPromise<JSObject> _showDirectoryPicker();
|
||||
|
||||
extension type FileSystemDirectoryHandleJS(JSObject _) implements JSObject {
|
||||
external JSPromise<JSObject> getDirectoryHandle(JSString name);
|
||||
external JSPromise<JSObject> getFileHandle(JSString name);
|
||||
external JSObject values();
|
||||
}
|
||||
|
||||
extension type FileSystemFileHandleJS(JSObject _) implements JSObject {
|
||||
external JSPromise<web.File> getFile();
|
||||
external JSString get kind;
|
||||
external JSString get name;
|
||||
}
|
||||
|
||||
extension type AsyncIteratorJS(JSObject _) implements JSObject {
|
||||
external JSPromise<IteratorResultJS> next();
|
||||
}
|
||||
|
||||
extension type IteratorResultJS(JSObject _) implements JSObject {
|
||||
external JSBoolean get done;
|
||||
external JSObject? get value;
|
||||
}
|
||||
|
||||
/// 检查当前日期是否在年度报告展示期间(12月20日 - 次年1月20日)
|
||||
bool isYearlyReportPeriod() {
|
||||
final now = DateTime.now();
|
||||
// 12月20日及之后
|
||||
if (now.month == 12 && now.day >= 20) return true;
|
||||
// 1月1日至1月20日
|
||||
if (now.month == 1 && now.day <= 20) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// 获取年度报告的年份(1月时使用上一年)
|
||||
int getReportYear() {
|
||||
final now = DateTime.now();
|
||||
// 如果是 1 月,年份减 1
|
||||
if (now.month == 1) {
|
||||
return now.year - 1;
|
||||
}
|
||||
return now.year;
|
||||
}
|
||||
|
||||
/// Web 平台年度报告入口页面(用于通过 URL 直接访问)
|
||||
class YearlyReportEntryUIRoute extends HookConsumerWidget {
|
||||
const YearlyReportEntryUIRoute({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final globalState = ref.watch(appGlobalModelProvider);
|
||||
|
||||
// 使用类似 index_ui.dart 的背景图片布局
|
||||
return Container(
|
||||
color: const Color(0xFF0a0a12), // 深色背景色作为底色
|
||||
child: Stack(
|
||||
children: [
|
||||
// 背景图片
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(seconds: 3),
|
||||
switchInCurve: Curves.easeInOut,
|
||||
switchOutCurve: Curves.easeInOut,
|
||||
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||
return FadeTransition(opacity: animation, child: child);
|
||||
},
|
||||
child: ExtendedImage.asset(
|
||||
key: ValueKey(globalState.backgroundImageAssetsPath),
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
globalState.backgroundImageAssetsPath,
|
||||
fit: BoxFit.cover,
|
||||
cacheWidth: null,
|
||||
cacheHeight: null,
|
||||
loadStateChanged: (state) {
|
||||
if (state.extendedImageLoadState == LoadState.completed) {
|
||||
return state.completedWidget;
|
||||
}
|
||||
return Container(width: double.infinity, height: double.infinity, color: const Color(0xFF0a0a12));
|
||||
},
|
||||
),
|
||||
),
|
||||
// 半透明遮罩,增加可读性
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Colors.black.withValues(alpha: .6), Colors.black.withValues(alpha: .75)],
|
||||
),
|
||||
),
|
||||
),
|
||||
// 内容区域
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 1440, maxHeight: 920),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
child: BlurOvalWidget(child: const YearlyReportEntryUI()),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Web 平台年度报告入口页面
|
||||
class YearlyReportEntryUI extends HookConsumerWidget {
|
||||
const YearlyReportEntryUI({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final appGlobalState = ref.watch(appGlobalModelProvider);
|
||||
final appGlobalModel = ref.read(appGlobalModelProvider.notifier);
|
||||
|
||||
// 自动计算年份
|
||||
final reportYear = getReportYear();
|
||||
final isLoading = useState(false);
|
||||
final loadingMessage = useState("");
|
||||
final loadingProgress = useState(0.0);
|
||||
final logContents = useState<List<String>?>(null);
|
||||
final errorMessage = useState<String?>(null);
|
||||
|
||||
// 如果已加载日志内容,显示报告
|
||||
if (logContents.value != null) {
|
||||
return YearlyReportUI(logContents: logContents.value!, year: reportYear);
|
||||
}
|
||||
|
||||
// 构建内容
|
||||
Widget contentWidget;
|
||||
if (isLoading.value) {
|
||||
contentWidget = _buildLoadingState(context, loadingMessage.value, loadingProgress.value);
|
||||
} else {
|
||||
contentWidget = _buildSelectionState(
|
||||
context,
|
||||
reportYear,
|
||||
isLoading,
|
||||
loadingMessage,
|
||||
loadingProgress,
|
||||
logContents,
|
||||
errorMessage,
|
||||
appGlobalState,
|
||||
appGlobalModel,
|
||||
);
|
||||
}
|
||||
|
||||
return contentWidget;
|
||||
}
|
||||
|
||||
Widget _buildLoadingState(BuildContext context, String message, double progress) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const ProgressRing(),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
message.isEmpty ? S.current.yearly_report_web_reading_files : message,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
if (progress > 0) ...[
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(width: 300, child: ProgressBar(value: progress * 100)),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"${(progress * 100).toStringAsFixed(1)}%",
|
||||
style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .7)),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSelectionState(
|
||||
BuildContext context,
|
||||
int reportYear,
|
||||
ValueNotifier<bool> isLoading,
|
||||
ValueNotifier<String> loadingMessage,
|
||||
ValueNotifier<double> loadingProgress,
|
||||
ValueNotifier<List<String>?> logContents,
|
||||
ValueNotifier<String?> errorMessage,
|
||||
AppGlobalState appGlobalState,
|
||||
AppGlobalModel appGlobalModel,
|
||||
) {
|
||||
return Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(FontAwesomeIcons.chartLine, size: 64, color: FluentTheme.of(context).accentColor),
|
||||
const SizedBox(height: 32),
|
||||
Text(
|
||||
S.current.yearly_report_title(reportYear.toString()),
|
||||
style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
S.current.yearly_report_web_select_folder_desc,
|
||||
style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .7)),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
// 语言选择器
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(FontAwesomeIcons.language, size: 16),
|
||||
const SizedBox(width: 12),
|
||||
Text("Language", style: const TextStyle(fontSize: 14)),
|
||||
const SizedBox(width: 12),
|
||||
SizedBox(
|
||||
height: 36,
|
||||
child: ComboBox(
|
||||
value: appGlobalState.appLocale ?? const Locale("auto"),
|
||||
items: [
|
||||
for (final mkv in AppGlobalModel.appLocaleSupport.entries)
|
||||
ComboBoxItem(value: mkv.key, child: Text(mkv.value)),
|
||||
],
|
||||
onChanged: appGlobalModel.changeLocale,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
if (errorMessage.value != null) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
constraints: const BoxConstraints(maxWidth: 500),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.withValues(alpha: .2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(errorMessage.value!, style: TextStyle(color: Colors.red)),
|
||||
),
|
||||
],
|
||||
FilledButton(
|
||||
onPressed: () => _selectFolderAndGenerateReport(
|
||||
context,
|
||||
reportYear,
|
||||
isLoading,
|
||||
loadingMessage,
|
||||
loadingProgress,
|
||||
logContents,
|
||||
errorMessage,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(FluentIcons.folder_open, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Text(S.current.yearly_report_web_select_folder, style: const TextStyle(fontSize: 16)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _selectFolderAndGenerateReport(
|
||||
BuildContext context,
|
||||
int year,
|
||||
ValueNotifier<bool> isLoading,
|
||||
ValueNotifier<String> loadingMessage,
|
||||
ValueNotifier<double> loadingProgress,
|
||||
ValueNotifier<List<String>?> logContents,
|
||||
ValueNotifier<String?> errorMessage,
|
||||
) async {
|
||||
if (!kIsWeb) return;
|
||||
|
||||
// 检查浏览器是否支持 showDirectoryPicker
|
||||
if (!_isDirectoryPickerSupported()) {
|
||||
errorMessage.value = S.current.yearly_report_web_browser_not_supported;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
loadingMessage.value = S.current.yearly_report_web_reading_files;
|
||||
loadingProgress.value = 0;
|
||||
errorMessage.value = null;
|
||||
|
||||
final contents = await _readLogFilesFromDirectory(loadingMessage, loadingProgress);
|
||||
|
||||
if (contents.isEmpty) {
|
||||
errorMessage.value = S.current.yearly_report_web_no_logs_found;
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
logContents.value = contents;
|
||||
} catch (e) {
|
||||
errorMessage.value = e.toString();
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool _isDirectoryPickerSupported() {
|
||||
try {
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<String>> _readLogFilesFromDirectory(
|
||||
ValueNotifier<String> loadingMessage,
|
||||
ValueNotifier<double> loadingProgress,
|
||||
) async {
|
||||
final logContents = <String>[];
|
||||
|
||||
try {
|
||||
final dirHandle = await _showDirectoryPickerWrapper();
|
||||
if (dirHandle == null) return [];
|
||||
|
||||
final dirHandleJS = FileSystemDirectoryHandleJS(dirHandle);
|
||||
|
||||
// 尝试获取 LIVE 目录
|
||||
FileSystemDirectoryHandleJS? liveHandle;
|
||||
try {
|
||||
final livePromise = dirHandleJS.getDirectoryHandle('LIVE'.toJS);
|
||||
final liveResult = await livePromise.toDart;
|
||||
liveHandle = FileSystemDirectoryHandleJS(liveResult);
|
||||
} catch (_) {
|
||||
liveHandle = dirHandleJS;
|
||||
}
|
||||
|
||||
// 先收集所有需要读取的文件
|
||||
final filesToRead = <FileSystemFileHandleJS>[];
|
||||
|
||||
// 读取 Game.log
|
||||
try {
|
||||
final gameLogPromise = liveHandle.getFileHandle('Game.log'.toJS);
|
||||
final gameLogResult = await gameLogPromise.toDart;
|
||||
filesToRead.add(FileSystemFileHandleJS(gameLogResult));
|
||||
} catch (_) {}
|
||||
|
||||
// 收集 logbackups 目录中的文件
|
||||
try {
|
||||
final logbackupsPromise = liveHandle.getDirectoryHandle('logbackups'.toJS);
|
||||
final logbackupsResult = await logbackupsPromise.toDart;
|
||||
final logbackupsHandle = FileSystemDirectoryHandleJS(logbackupsResult);
|
||||
await _collectLogbackupsFiles(logbackupsHandle, filesToRead);
|
||||
} catch (_) {}
|
||||
|
||||
// 依次读取文件,更新进度
|
||||
for (var i = 0; i < filesToRead.length; i++) {
|
||||
final fileHandle = filesToRead[i];
|
||||
final fileName = fileHandle.name.toDart;
|
||||
loadingMessage.value = "${S.current.yearly_report_web_reading_files} ($fileName)";
|
||||
loadingProgress.value = i / filesToRead.length;
|
||||
|
||||
try {
|
||||
await Future.delayed(Duration.zero);
|
||||
|
||||
final content = await _readFileContent(fileHandle);
|
||||
if (content.isNotEmpty) {
|
||||
logContents.add(content);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
loadingProgress.value = 1.0;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
|
||||
return logContents;
|
||||
}
|
||||
|
||||
Future<void> _collectLogbackupsFiles(
|
||||
FileSystemDirectoryHandleJS dirHandle,
|
||||
List<FileSystemFileHandleJS> filesToRead,
|
||||
) async {
|
||||
try {
|
||||
final valuesIterator = AsyncIteratorJS(dirHandle.values());
|
||||
|
||||
while (true) {
|
||||
final nextPromise = valuesIterator.next();
|
||||
final result = await nextPromise.toDart;
|
||||
|
||||
if (result.done.toDart) break;
|
||||
|
||||
final entry = result.value;
|
||||
if (entry == null) continue;
|
||||
|
||||
final handleJS = FileSystemFileHandleJS(entry);
|
||||
final name = handleJS.name.toDart;
|
||||
final kind = handleJS.kind.toDart;
|
||||
|
||||
if (kind == 'file' && name.endsWith('.log')) {
|
||||
filesToRead.add(handleJS);
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
Future<JSObject?> _showDirectoryPickerWrapper() async {
|
||||
try {
|
||||
final promise = _showDirectoryPicker();
|
||||
final result = await promise.toDart;
|
||||
return result;
|
||||
} catch (e) {
|
||||
if (e.toString().contains('AbortError')) {
|
||||
return null;
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _readFileContent(FileSystemFileHandleJS fileHandle) async {
|
||||
final filePromise = fileHandle.getFile();
|
||||
final file = await filePromise.toDart;
|
||||
final textPromise = file.text();
|
||||
final text = await textPromise.toDart;
|
||||
return text.toDart;
|
||||
}
|
||||
}
|
||||
1489
lib/ui/tools/yearly_report/yearly_report_pages.dart
Normal file
1489
lib/ui/tools/yearly_report/yearly_report_pages.dart
Normal file
File diff suppressed because it is too large
Load Diff
252
lib/ui/tools/yearly_report/yearly_report_ui.dart
Normal file
252
lib/ui/tools/yearly_report/yearly_report_ui.dart
Normal file
@@ -0,0 +1,252 @@
|
||||
import 'package:animate_do/animate_do.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:starcitizen_doctor/common/helper/yearly_report_analyzer.dart';
|
||||
import 'package:starcitizen_doctor/widgets/widgets.dart';
|
||||
|
||||
part 'yearly_report_pages.dart';
|
||||
|
||||
class YearlyReportUI extends HookConsumerWidget {
|
||||
final List<String> logContents;
|
||||
final int year;
|
||||
|
||||
const YearlyReportUI({super.key, required this.logContents, required this.year});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final reportData = useState<YearlyReportData?>(null);
|
||||
final isLoading = useState(true);
|
||||
final currentPage = useState(0);
|
||||
final loadingProgress = useState(0.0);
|
||||
final pageController = usePageController();
|
||||
|
||||
useEffect(() {
|
||||
_loadReport(reportData, isLoading, loadingProgress);
|
||||
return null;
|
||||
}, const []);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: isLoading.value
|
||||
? _buildLoadingPage(context, loadingProgress.value)
|
||||
: reportData.value == null
|
||||
? _buildErrorPage(context)
|
||||
: _buildReportPages(context, reportData.value!, currentPage, pageController),
|
||||
),
|
||||
_buildDisclaimer(context),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadReport(
|
||||
ValueNotifier<YearlyReportData?> reportData,
|
||||
ValueNotifier<bool> isLoading,
|
||||
ValueNotifier<double> loadingProgress,
|
||||
) async {
|
||||
try {
|
||||
bool isGenerating = true;
|
||||
Future<void> progressAnimation() async {
|
||||
while (isGenerating && loadingProgress.value < 0.9) {
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
if (isGenerating) {
|
||||
final remaining = 0.9 - loadingProgress.value;
|
||||
loadingProgress.value += remaining * 0.02;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final progressFuture = progressAnimation();
|
||||
final report = await YearlyReportAnalyzer.generateReportFromContents(logContents, year);
|
||||
|
||||
isGenerating = false;
|
||||
await progressFuture;
|
||||
|
||||
while (loadingProgress.value < 1.0) {
|
||||
await Future.delayed(const Duration(milliseconds: 20));
|
||||
loadingProgress.value = (loadingProgress.value + 0.05).clamp(0.0, 1.0);
|
||||
}
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
reportData.value = report;
|
||||
isLoading.value = false;
|
||||
} catch (e) {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildLoadingPage(BuildContext context, double progress) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SpinPerfect(
|
||||
infinite: true,
|
||||
duration: const Duration(seconds: 3),
|
||||
child: FadeIn(child: Icon(FontAwesomeIcons.rocket, size: 80, color: FluentTheme.of(context).accentColor)),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
FadeInUp(
|
||||
delay: const Duration(milliseconds: 300),
|
||||
child: Text(
|
||||
S.current.yearly_report_generating,
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FadeInUp(
|
||||
delay: const Duration(milliseconds: 500),
|
||||
child: Text(
|
||||
S.current.yearly_report_analyzing_logs,
|
||||
style: TextStyle(fontSize: 16, color: Colors.white.withValues(alpha: .6)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
FadeInUp(
|
||||
delay: const Duration(milliseconds: 700),
|
||||
child: SizedBox(width: 300, child: ProgressBar(value: progress * 100)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorPage(BuildContext context) {
|
||||
return Center(
|
||||
child: FadeInUp(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(FluentIcons.error, size: 80, color: Colors.red),
|
||||
const SizedBox(height: 24),
|
||||
Text(S.current.yearly_report_error_title, style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
S.current.yearly_report_error_description,
|
||||
style: TextStyle(fontSize: 16, color: Colors.white.withValues(alpha: .6)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReportPages(
|
||||
BuildContext context,
|
||||
YearlyReportData data,
|
||||
ValueNotifier<int> currentPage,
|
||||
PageController pageController,
|
||||
) {
|
||||
final pages = _buildPageList(context, data);
|
||||
return Stack(
|
||||
children: [
|
||||
PageView.builder(
|
||||
scrollDirection: Axis.vertical,
|
||||
controller: pageController,
|
||||
onPageChanged: (index) => currentPage.value = index,
|
||||
itemCount: pages.length,
|
||||
itemBuilder: (context, index) => pages[index],
|
||||
),
|
||||
if (currentPage.value > 0)
|
||||
Positioned(
|
||||
top: 12,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(child: _makeNavButton(pageController, currentPage.value - 1, true)),
|
||||
),
|
||||
if (currentPage.value < pages.length - 1)
|
||||
Positioned(
|
||||
bottom: 12,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(child: _makeNavButton(pageController, currentPage.value + 1, false)),
|
||||
),
|
||||
Positioned(
|
||||
right: 16,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: List.generate(
|
||||
pages.length,
|
||||
(index) => GestureDetector(
|
||||
onTap: () => pageController.animateToPage(
|
||||
index,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
child: Container(
|
||||
width: 8,
|
||||
height: currentPage.value == index ? 24 : 8,
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: currentPage.value == index
|
||||
? FluentTheme.of(context).accentColor
|
||||
: Colors.white.withValues(alpha: .3),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _makeNavButton(PageController pageCtrl, int pageIndex, bool isUp) {
|
||||
return Bounce(
|
||||
child: IconButton(
|
||||
icon: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(isUp ? FluentIcons.chevron_up : FluentIcons.chevron_down, size: 12),
|
||||
const SizedBox(width: 8),
|
||||
Text(isUp ? S.current.yearly_report_nav_prev : S.current.yearly_report_nav_next),
|
||||
],
|
||||
),
|
||||
onPressed: () =>
|
||||
pageCtrl.animateToPage(pageIndex, duration: const Duration(milliseconds: 300), curve: Curves.ease),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildPageList(BuildContext context, YearlyReportData data) {
|
||||
return [
|
||||
_WelcomePage(year: year),
|
||||
_LaunchCountPage(data: data),
|
||||
_PlayTimePage(data: data),
|
||||
_SessionStatsPage(data: data),
|
||||
_MonthlyStatsPage(data: data),
|
||||
_StreakStatsPage(data: data),
|
||||
_CrashCountPage(data: data),
|
||||
_KillDeathPage(data: data),
|
||||
_EarliestPlayPage(data: data),
|
||||
_LatestPlayPage(data: data),
|
||||
_VehicleDestructionPage(data: data),
|
||||
_VehiclePilotedPage(data: data),
|
||||
_LocationStatsPage(data: data),
|
||||
_AccountStatsPage(data: data),
|
||||
_SummaryPage(year: year),
|
||||
_DataSummaryPage(data: data, year: year),
|
||||
];
|
||||
}
|
||||
|
||||
Widget _buildDisclaimer(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 24),
|
||||
decoration: BoxDecoration(color: FluentTheme.of(context).cardColor.withValues(alpha: .15)),
|
||||
child: Text(
|
||||
S.current.yearly_report_disclaimer,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 11, color: Colors.white.withValues(alpha: .7)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user