feat:参照研听的基础工程
This commit is contained in:
@@ -0,0 +1,420 @@
|
||||
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<DebugShorebirdPage> createState() => _DebugShorebirdPageState();
|
||||
}
|
||||
|
||||
class _DebugShorebirdPageState extends State<DebugShorebirdPage> {
|
||||
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<void> _loadCurrentPatch() async {
|
||||
try {
|
||||
await _shorebird.initCurrentPatch();
|
||||
if (!mounted) return;
|
||||
setState(() => _currentPatch = _shorebird.currentPatch);
|
||||
} catch (error) {
|
||||
debugPrint('Error reading current patch: $error');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _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<void> _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<UpdateTrack>(
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user