import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:shorebird_code_push/shorebird_code_push.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import '../../../common/config/shorebird_service.dart'; import '../../../common/router/app_router.dart'; import '../../../common/theme/app_text.dart'; import '../../../common/theme/spacing.dart'; class DebugShorebirdPage extends StatefulWidget { const DebugShorebirdPage({super.key}); @override State createState() => _DebugShorebirdPageState(); } class _DebugShorebirdPageState extends State { final ShorebirdService _shorebird = ShorebirdService.instance; late final bool _isUpdaterAvailable; var _currentTrack = UpdateTrack.stable; var _isCheckingForUpdates = false; Patch? _currentPatch; @override void initState() { super.initState(); _isUpdaterAvailable = _shorebird.isAvailable; _loadCurrentPatch(); } Future _loadCurrentPatch() async { try { await _shorebird.initCurrentPatch(); if (!mounted) return; setState(() => _currentPatch = _shorebird.currentPatch); } catch (error) { debugPrint('Error reading current patch: $error'); } } Future _checkForUpdate() async { if (_isCheckingForUpdates) return; try { setState(() => _isCheckingForUpdates = true); final status = await _shorebird.checkForUpdate(track: _currentTrack); if (!mounted) return; switch (status) { case UpdateStatus.upToDate: _showNoUpdateAvailableBanner(); break; case UpdateStatus.outdated: _showUpdateAvailableBanner(); break; case UpdateStatus.restartRequired: _showRestartBanner(); break; case UpdateStatus.unavailable: _showUnavailableBanner(); break; } } catch (error) { debugPrint('Error checking for update: $error'); _showErrorBanner(error); } finally { if (mounted) { setState(() => _isCheckingForUpdates = false); } } } void _showDownloadingBanner() { ScaffoldMessenger.of(context) ..hideCurrentMaterialBanner() ..showMaterialBanner( const MaterialBanner( content: Text('正在下载更新...'), actions: [ SizedBox(height: 14, width: 14, child: CircularProgressIndicator()), ], ), ); } void _showUpdateAvailableBanner() { ScaffoldMessenger.of(context) ..hideCurrentMaterialBanner() ..showMaterialBanner( MaterialBanner( content: Text('当前 ${_currentTrack.name} track 有可用更新。'), actions: [ TextButton( onPressed: () async { ScaffoldMessenger.of(context).hideCurrentMaterialBanner(); await _downloadUpdate(); if (!mounted) return; ScaffoldMessenger.of(context).hideCurrentMaterialBanner(); }, child: const Text('Download'), ), ], ), ); } void _showNoUpdateAvailableBanner() { ScaffoldMessenger.of(context) ..hideCurrentMaterialBanner() ..showMaterialBanner( MaterialBanner( content: Text('当前 ${_currentTrack.name} track 没有可用更新。'), actions: [ TextButton( onPressed: () { ScaffoldMessenger.of(context).hideCurrentMaterialBanner(); }, child: const Text('Dismiss'), ), ], ), ); } void _showRestartBanner() { ScaffoldMessenger.of(context) ..hideCurrentMaterialBanner() ..showMaterialBanner( MaterialBanner( content: const Text('新的 patch 已准备好,请重启应用。'), actions: [ TextButton( onPressed: () { ScaffoldMessenger.of(context).hideCurrentMaterialBanner(); }, child: const Text('Dismiss'), ), ], ), ); } void _showUnavailableBanner() { ScaffoldMessenger.of(context) ..hideCurrentMaterialBanner() ..showMaterialBanner( MaterialBanner( content: const Text('当前构建不可用 Shorebird。'), actions: [ TextButton( onPressed: () { ScaffoldMessenger.of(context).hideCurrentMaterialBanner(); }, child: const Text('Dismiss'), ), ], ), ); } void _showErrorBanner(Object error) { ScaffoldMessenger.of(context) ..hideCurrentMaterialBanner() ..showMaterialBanner( MaterialBanner( content: Text('检查更新时发生错误:$error'), actions: [ TextButton( onPressed: () { ScaffoldMessenger.of(context).hideCurrentMaterialBanner(); }, child: const Text('Dismiss'), ), ], ), ); } Future _downloadUpdate() async { _showDownloadingBanner(); try { await _shorebird.downloadUpdate(track: _currentTrack); if (!mounted) return; setState(() => _currentPatch = _shorebird.currentPatch); _showRestartBanner(); } on UpdateException catch (error) { _showErrorBanner(error.message); } catch (error) { _showErrorBanner(error); } } @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; return Scaffold( appBar: AppBar( backgroundColor: cs.background, surfaceTintColor: Colors.transparent, elevation: 0, scrolledUnderElevation: 0, title: Text( 'Shorebird', style: TextStyle( fontSize: 17, fontWeight: FontWeight.w700, color: cs.foreground, ), ), leading: IconButton( icon: Icon(Icons.arrow_back_ios_new, color: cs.foreground, size: 18), onPressed: () { if (context.canPop()) { context.pop(); } else { context.go(AppRoutes.debug); } }, ), ), body: ListView( padding: const EdgeInsets.fromLTRB(Spacing.page, 8, Spacing.page, 24), children: [ if (!_isUpdaterAvailable) ...[ _Card( child: Text( '当前构建未接入 Shorebird。请确认应用是通过 `shorebird release` 生成的 release 包,然后再检查更新与 patch 状态。', style: AppText.body.copyWith(color: cs.mutedForeground), ), ), const SizedBox(height: Spacing.cardGap), ], _Section( title: '当前状态', child: _Card( child: Column( children: [ _InfoTile( title: '当前 patch 版本', value: _currentPatch != null ? '${_currentPatch!.number}' : '未安装 patch', ), Divider(height: 1, thickness: 1, color: cs.border), _InfoTile( title: '更新器可用', value: _isUpdaterAvailable ? '是' : '否', ), ], ), ), ), _Section( title: 'Track 选择', child: _Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '选择要检查和下载的 track。', style: AppText.body.copyWith(color: cs.mutedForeground), ), const SizedBox(height: 12), SegmentedButton( segments: const [ ButtonSegment( label: Text('Stable'), value: UpdateTrack.stable, ), ButtonSegment( label: Text('Beta'), value: UpdateTrack.beta, ), ButtonSegment( label: Text('Staging'), value: UpdateTrack.staging, ), ], selected: {_currentTrack}, onSelectionChanged: (tracks) { setState(() => _currentTrack = tracks.single); }, ), ], ), ), ), ), _Section( title: '操作', child: _Card( child: Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Expanded( child: FilledButton( onPressed: _isCheckingForUpdates ? null : _checkForUpdate, child: _isCheckingForUpdates ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, ), ) : const Text('检查更新'), ), ), const SizedBox(width: 12), Expanded( child: OutlinedButton( onPressed: _downloadUpdate, child: const Text('下载更新'), ), ), ], ), ), ), ), ], ), ); } } class _Section extends StatelessWidget { const _Section({required this.title, required this.child}); final String title; final Widget child; @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; return Padding( padding: const EdgeInsets.only(bottom: Spacing.sectionGap), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(left: 4, bottom: 10), child: Text( title, style: AppText.meta.copyWith( fontSize: 13, fontWeight: FontWeight.w600, color: cs.mutedForeground, ), ), ), child, ], ), ); } } class _Card extends StatelessWidget { const _Card({required this.child}); final Widget child; @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; return Container( decoration: BoxDecoration( color: cs.card, border: Border.all(color: cs.border), borderRadius: BorderRadius.circular(Spacing.radiusBase), ), clipBehavior: Clip.antiAlias, child: child, ); } } class _InfoTile extends StatelessWidget { const _InfoTile({required this.title, required this.value}); final String title; final String value; @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: AppText.meta.copyWith(color: cs.mutedForeground), ), const SizedBox(height: 4), Text( value, style: AppText.body.copyWith( color: cs.foreground, fontWeight: FontWeight.w600, ), ), ], ), ), ], ), ); } }