feat:参照研听的基础工程
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
import '../../../common/config/shorebird_service.dart';
|
||||
import '../../../common/network/auth_interceptor.dart';
|
||||
import '../../../common/network/dio_provider.dart';
|
||||
import '../../../common/router/app_router.dart';
|
||||
import '../../../common/services/device_header_service.dart';
|
||||
import '../../../common/storage/storage_keys.dart';
|
||||
import '../../../common/storage/storage_service.dart';
|
||||
import '../../../common/theme/theme_controller.dart';
|
||||
|
||||
class DebugInfoPage extends ConsumerWidget {
|
||||
const DebugInfoPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final appConfig = ref.watch(appConfigProvider);
|
||||
final dio = ref.watch(dioProvider);
|
||||
final themeMode = ref.watch(themeModeControllerProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
if (context.canPop()) {
|
||||
context.pop();
|
||||
} else {
|
||||
context.go(AppRoutes.debug);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.arrow_back_ios_new),
|
||||
),
|
||||
title: const Text('Info'),
|
||||
),
|
||||
body: FutureBuilder<_DebugInfoSnapshot>(
|
||||
future: _loadSnapshot(
|
||||
apiBaseUrl: appConfig.apiBaseUrl,
|
||||
themeMode: themeMode.name,
|
||||
dio: dio,
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
final text = snapshot.data?.text ?? 'loading...';
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: SelectableText(text, style: const TextStyle(fontSize: 12)),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<_DebugInfoSnapshot> _loadSnapshot({
|
||||
required String apiBaseUrl,
|
||||
required String themeMode,
|
||||
required Dio dio,
|
||||
}) async {
|
||||
await DeviceHeaderService.to.ensureInitialized();
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
final deviceInfo = await _readDeviceInfo();
|
||||
final storage = StorageService.to;
|
||||
final jwt = await loadJwt();
|
||||
final commonHeaders = DeviceHeaderService.to.commonHeaders();
|
||||
final platformHeaders = DeviceHeaderService.to.platformIdentityHeaders();
|
||||
final headers = <String, dynamic>{
|
||||
...commonHeaders,
|
||||
...platformHeaders,
|
||||
...dio.options.headers,
|
||||
};
|
||||
final gitVersion = const String.fromEnvironment(
|
||||
'GITVERSION',
|
||||
defaultValue: 'unknown',
|
||||
);
|
||||
final patch = ShorebirdService.instance.currentPatch;
|
||||
final jwtLine = jwt?.isNotEmpty == true ? 'Bearer <redacted>' : '';
|
||||
|
||||
final text = StringBuffer()
|
||||
..writeln('[build]')
|
||||
..writeln('app_name: ${packageInfo.appName}')
|
||||
..writeln('mode: ${kReleaseMode ? 'release' : 'debug'}')
|
||||
..writeln('package_name: ${packageInfo.packageName}')
|
||||
..writeln('version: ${packageInfo.version}')
|
||||
..writeln('build_number: ${packageInfo.buildNumber}')
|
||||
..writeln('theme_mode: $themeMode')
|
||||
..writeln('platform: ${defaultTargetPlatform.name}')
|
||||
..writeln('is_web: $kIsWeb')
|
||||
..writeln()
|
||||
..writeln('[environment]')
|
||||
..writeln('api_base: $apiBaseUrl')
|
||||
..writeln(
|
||||
'connect_timeout_ms: ${dio.options.connectTimeout?.inMilliseconds}',
|
||||
)
|
||||
..writeln(
|
||||
'receive_timeout_ms: ${dio.options.receiveTimeout?.inMilliseconds}',
|
||||
)
|
||||
..writeln('response_type: ${dio.options.responseType.name}')
|
||||
..writeln('git_version: $gitVersion')
|
||||
..writeln()
|
||||
..writeln('[storage]')
|
||||
..writeln('privacy_agreed: ${storage.getBool(storagePrivacyAgreed)}')
|
||||
..writeln('ios_startup_seen: ${storage.getBool(storageIosStartupSeen)}')
|
||||
..writeln('device_id: ${DeviceHeaderService.to.deviceId}')
|
||||
..writeln('android_id: ${storage.getString(storageAndroidId) ?? ''}')
|
||||
..writeln('device_token: ${storage.getString(storageDeviceToken) ?? ''}')
|
||||
..writeln('android_oaid: ${storage.getString(storageAndroidOaid) ?? ''}')
|
||||
..writeln('android_imei: ${storage.getString(storageAndroidImei) ?? ''}')
|
||||
..writeln('android_mac: ${storage.getString(storageAndroidMac) ?? ''}')
|
||||
..writeln('ios_idfa: ${storage.getString(storageIosIdfa) ?? ''}')
|
||||
..writeln('ios_paid: ${storage.getString(storageIosPaid) ?? ''}')
|
||||
..writeln()
|
||||
..writeln('[auth]')
|
||||
..writeln('jwt: $jwtLine')
|
||||
..writeln()
|
||||
..writeln('[headers]')
|
||||
..writeln('common: $commonHeaders')
|
||||
..writeln('platform: $platformHeaders')
|
||||
..writeln('dio: $headers')
|
||||
..writeln()
|
||||
..writeln('[shorebird]')
|
||||
..writeln('current_patch: ${patch?.number ?? 'none'}')
|
||||
..writeln('patch: ${patch ?? 'none'}')
|
||||
..writeln()
|
||||
..writeln('[device]')
|
||||
..writeln(deviceInfo);
|
||||
|
||||
return _DebugInfoSnapshot(text.toString());
|
||||
}
|
||||
|
||||
Future<String> _readDeviceInfo() async {
|
||||
try {
|
||||
if (!kIsWeb &&
|
||||
defaultTargetPlatform == TargetPlatform.android &&
|
||||
!StorageService.to.getBool(storagePrivacyAgreed)) {
|
||||
return 'privacy policy not accepted; device info not read';
|
||||
}
|
||||
final plugin = DeviceInfoPlugin();
|
||||
if (kIsWeb) {
|
||||
final info = await plugin.webBrowserInfo;
|
||||
return info.data.toString();
|
||||
}
|
||||
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
final info = await plugin.androidInfo;
|
||||
return info.data.toString();
|
||||
case TargetPlatform.iOS:
|
||||
final info = await plugin.iosInfo;
|
||||
return info.data.toString();
|
||||
case TargetPlatform.macOS:
|
||||
final info = await plugin.macOsInfo;
|
||||
return info.data.toString();
|
||||
case TargetPlatform.windows:
|
||||
final info = await plugin.windowsInfo;
|
||||
return info.data.toString();
|
||||
case TargetPlatform.linux:
|
||||
final info = await plugin.linuxInfo;
|
||||
return info.data.toString();
|
||||
case TargetPlatform.fuchsia:
|
||||
return 'fuchsia';
|
||||
}
|
||||
} catch (error) {
|
||||
return 'unavailable: $error';
|
||||
}
|
||||
}
|
||||
|
||||
class _DebugInfoSnapshot {
|
||||
const _DebugInfoSnapshot(this.text);
|
||||
|
||||
final String text;
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
import '../../../common/router/app_router.dart';
|
||||
import '../../../common/theme/app_text.dart';
|
||||
import '../../../common/theme/spacing.dart';
|
||||
|
||||
class DebugPage extends StatelessWidget {
|
||||
const DebugPage({super.key});
|
||||
|
||||
@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(
|
||||
'调试工具',
|
||||
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: () => context.pop(),
|
||||
),
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(Spacing.page, 8, Spacing.page, 24),
|
||||
children: [
|
||||
_Section(
|
||||
title: '状态',
|
||||
child: _Card(
|
||||
child: Text(
|
||||
kReleaseMode ? 'release' : 'debug',
|
||||
style: AppText.body.copyWith(color: cs.mutedForeground),
|
||||
),
|
||||
),
|
||||
),
|
||||
_Section(
|
||||
title: '入口',
|
||||
child: _Card(
|
||||
children: [
|
||||
_Row(
|
||||
icon: Icons.data_object_outlined,
|
||||
title: '请求',
|
||||
subtitle: '查看 Dio 请求和响应',
|
||||
onTap: () => context.push(AppRoutes.fancyDioInspector),
|
||||
),
|
||||
_Row(
|
||||
icon: Icons.info_outline,
|
||||
title: '应用信息',
|
||||
subtitle: '版本、包名和构建信息',
|
||||
onTap: () => context.push(AppRoutes.debugInfo),
|
||||
),
|
||||
_Row(
|
||||
icon: Icons.text_fields,
|
||||
title: 'Typography',
|
||||
subtitle: '查看字号和文本样式基线',
|
||||
onTap: () => context.push(AppRoutes.debugTypography),
|
||||
),
|
||||
_Row(
|
||||
icon: Icons.system_update_alt_outlined,
|
||||
title: 'Shorebird',
|
||||
subtitle: '查看补丁与更新状态',
|
||||
onTap: () => context.push(AppRoutes.debugShorebird),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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({this.child, this.children = const []});
|
||||
|
||||
final Widget? child;
|
||||
final List<Widget> children;
|
||||
|
||||
@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 ??
|
||||
Column(
|
||||
children: [
|
||||
for (var i = 0; i < children.length; i++) ...[
|
||||
if (i > 0) Divider(height: 1, thickness: 1, color: cs.border),
|
||||
children[i],
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Row extends StatelessWidget {
|
||||
const _Row({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = ShadTheme.of(context).colorScheme;
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 20, color: cs.foreground),
|
||||
const SizedBox(width: 13),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: AppText.body.copyWith(
|
||||
color: cs.foreground,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle,
|
||||
style: AppText.meta.copyWith(
|
||||
color: cs.mutedForeground,
|
||||
fontSize: 12.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.chevron_right, size: 18, color: cs.mutedForeground),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:fancy_dio_inspector/fancy_dio_inspector.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FancyDioInspectorPage extends StatelessWidget {
|
||||
const FancyDioInspectorPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ScaffoldMessenger(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('请求'),
|
||||
leading: Navigator.of(context).canPop()
|
||||
? IconButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
body: const FancyDioInspectorView(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
import '../../../common/theme/app_text.dart';
|
||||
import '../../../common/theme/spacing.dart';
|
||||
|
||||
class TypographyTestPage extends StatelessWidget {
|
||||
const TypographyTestPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = ShadTheme.of(context).colorScheme;
|
||||
const samples = <_Sample>[
|
||||
_Sample('appTitle', AppText.appTitle),
|
||||
_Sample('sectionTitle', AppText.sectionTitle),
|
||||
_Sample('cardTitle', AppText.cardTitle),
|
||||
_Sample('listTitle', AppText.listTitle),
|
||||
_Sample('body', AppText.body),
|
||||
_Sample('meta', AppText.meta),
|
||||
_Sample('chip', AppText.chip),
|
||||
_Sample('badge', AppText.badge),
|
||||
];
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: cs.background,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
title: Text(
|
||||
'Typography',
|
||||
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: () => context.pop(),
|
||||
),
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(Spacing.page, 8, Spacing.page, 24),
|
||||
children: [
|
||||
for (final sample in samples) ...[
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: cs.card,
|
||||
border: Border.all(color: cs.border),
|
||||
borderRadius: BorderRadius.circular(Spacing.radiusBase),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
padding: const EdgeInsets.all(16),
|
||||
margin: const EdgeInsets.only(bottom: Spacing.cardGap),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
sample.name,
|
||||
style: AppText.meta.copyWith(color: cs.mutedForeground),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text('金值 / Jinzhi / Typography', style: sample.style),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Sample {
|
||||
const _Sample(this.name, this.style);
|
||||
|
||||
final String name;
|
||||
final TextStyle style;
|
||||
}
|
||||
Reference in New Issue
Block a user