491fae10f5
BREAKING CHANGE: DNS and NTP override toggles are removed; picked keys apply to every profile Changelog: Per-key DNS, NTP, sniffer and TUN settings
320 lines
9.0 KiB
Dart
320 lines
9.0 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:fl_clash/common/common.dart';
|
|
import 'package:fl_clash/enum/enum.dart';
|
|
import 'package:fl_clash/icons/icons.dart';
|
|
import 'package:fl_clash/providers/providers.dart';
|
|
import 'package:fl_clash/state.dart';
|
|
import 'package:fl_clash/widgets/list.dart';
|
|
import 'package:fl_clash/widgets/scaffold.dart';
|
|
import 'package:material_ui/material_ui.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
@immutable
|
|
class Contributor {
|
|
final String avatar;
|
|
final String name;
|
|
|
|
const Contributor({required this.avatar, required this.name});
|
|
}
|
|
|
|
class AboutView extends ConsumerWidget {
|
|
const AboutView({super.key});
|
|
|
|
static const _contributors = [
|
|
Contributor(avatar: 'assets/images/avatar/june2.jpg', name: 'June2'),
|
|
Contributor(avatar: 'assets/images/avatar/arue.jpg', name: 'Arue'),
|
|
];
|
|
|
|
Future<void> _checkUpdate(BuildContext context, WidgetRef ref) async {
|
|
if (ref.read(loadingProvider(LoadingTag.checkUpdate))) return;
|
|
final commonAction = ref.read(commonActionProvider.notifier);
|
|
final result = await globalState.loadingRun(
|
|
() async => (release: await request.checkForUpdate()),
|
|
title: context.appLocalizations.checkUpdate,
|
|
tag: LoadingTag.checkUpdate,
|
|
);
|
|
if (result == null) return;
|
|
unawaited(
|
|
commonAction.checkUpdateResultHandle(data: result.release, isUser: true),
|
|
);
|
|
}
|
|
|
|
Widget _buildLinkItem({
|
|
required Glyph glyph,
|
|
required String title,
|
|
required String url,
|
|
required String label,
|
|
}) {
|
|
return ListItem(
|
|
leading: _LinkBadge(glyph: glyph),
|
|
title: Text(title),
|
|
subtitle: Text(label, maxLines: 1, overflow: TextOverflow.ellipsis),
|
|
trailing: const GlyphIcon(AppGlyphs.openExternal),
|
|
onTap: () {
|
|
dialogs.openUrl(url);
|
|
},
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final appLocalizations = context.appLocalizations;
|
|
final isLoading = ref.watch(loadingProvider(LoadingTag.checkUpdate));
|
|
return CommonScaffold(
|
|
isLoading: isLoading,
|
|
title: appLocalizations.about,
|
|
body: ListView(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
).copyWith(top: context.contentTopPadding, bottom: 32),
|
|
children: [
|
|
_AboutHero(
|
|
isCheckingUpdate: isLoading,
|
|
onCheckUpdate: () {
|
|
_checkUpdate(context, ref);
|
|
},
|
|
onEnterDeveloperMode: () {
|
|
ref
|
|
.read(appSettingProvider.notifier)
|
|
.update((state) => state.copyWith(developerMode: true));
|
|
context.showNotifier(
|
|
appLocalizations.developerModeEnableTip,
|
|
level: MessageLevel.success,
|
|
);
|
|
},
|
|
),
|
|
const SizedBox(height: 8),
|
|
generateSectionV3(
|
|
title: appLocalizations.more,
|
|
items: [
|
|
_buildLinkItem(
|
|
glyph: AppGlyphs.code,
|
|
title: appLocalizations.project,
|
|
url: 'https://github.com/$repository',
|
|
label: 'github.com/$repository',
|
|
),
|
|
_buildLinkItem(
|
|
glyph: AppGlyphs.cpu,
|
|
title: appLocalizations.core,
|
|
url: 'https://github.com/chen08209/Clash.Meta/tree/FlClash',
|
|
label: 'github.com/chen08209/Clash.Meta',
|
|
),
|
|
_buildLinkItem(
|
|
glyph: AppGlyphs.send,
|
|
title: 'Telegram',
|
|
url: 'https://t.me/FlClash',
|
|
label: 't.me/FlClash',
|
|
),
|
|
],
|
|
),
|
|
generateSectionV3(
|
|
title: appLocalizations.otherContributors,
|
|
items: [
|
|
for (final contributor in _contributors)
|
|
ListItem(
|
|
leading: CircleAvatar(
|
|
foregroundImage: AssetImage(contributor.avatar),
|
|
),
|
|
title: Text(contributor.name),
|
|
subtitle: Text(appLocalizations.appIconDesign),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _AboutHero extends StatelessWidget {
|
|
final bool isCheckingUpdate;
|
|
final VoidCallback onCheckUpdate;
|
|
final VoidCallback onEnterDeveloperMode;
|
|
|
|
const _AboutHero({
|
|
required this.isCheckingUpdate,
|
|
required this.onCheckUpdate,
|
|
required this.onEnterDeveloperMode,
|
|
});
|
|
|
|
static const _logoSize = 96.0;
|
|
static const _logoInset = 14.0;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colorScheme = context.colorScheme;
|
|
final textTheme = context.textTheme;
|
|
final appLocalizations = context.appLocalizations;
|
|
return Padding(
|
|
padding: const EdgeInsets.fromLTRB(24, 16, 24, 0),
|
|
child: Column(
|
|
children: [
|
|
_DeveloperModeDetector(
|
|
onEnterDeveloperMode: onEnterDeveloperMode,
|
|
child: DecoratedBox(
|
|
decoration: ShapeDecoration(
|
|
color: colorScheme.surfaceContainerHigh,
|
|
shape: AppShape.all(AppCorner.fit(_logoSize)),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(_logoInset),
|
|
child: Image.asset(
|
|
'assets/images/icon.png',
|
|
width: _logoSize - _logoInset * 2,
|
|
height: _logoSize - _logoInset * 2,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
Text(
|
|
appName,
|
|
style: textTheme.headlineSmall?.copyWith(
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
alignment: WrapAlignment.center,
|
|
children: [
|
|
_Pill(
|
|
label: 'v${globalState.packageInfo.version}',
|
|
color: colorScheme.primary,
|
|
foregroundColor: colorScheme.onPrimary,
|
|
),
|
|
_Pill(
|
|
label: 'GPL-3.0',
|
|
color: colorScheme.surfaceContainerHighest,
|
|
foregroundColor: colorScheme.onSurfaceVariant,
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 420),
|
|
child: Text(
|
|
appLocalizations.desc,
|
|
textAlign: TextAlign.center,
|
|
style: textTheme.bodyMedium?.copyWith(
|
|
color: colorScheme.onSurfaceVariant,
|
|
height: 1.5,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
FilledButton.tonalIcon(
|
|
onPressed: isCheckingUpdate ? null : onCheckUpdate,
|
|
icon: const GlyphIcon(AppGlyphs.sync, fill: 1),
|
|
label: Text(appLocalizations.checkUpdate),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _Pill extends StatelessWidget {
|
|
final String label;
|
|
final Color color;
|
|
final Color foregroundColor;
|
|
|
|
const _Pill({
|
|
required this.label,
|
|
required this.color,
|
|
required this.foregroundColor,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return DecoratedBox(
|
|
decoration: ShapeDecoration(color: color, shape: AppShape.full),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
|
child: Text(
|
|
label,
|
|
style: context.textTheme.labelMedium?.copyWith(
|
|
color: foregroundColor,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _LinkBadge extends StatelessWidget {
|
|
final Glyph glyph;
|
|
|
|
const _LinkBadge({required this.glyph});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colorScheme = context.colorScheme;
|
|
return DecoratedBox(
|
|
decoration: ShapeDecoration(
|
|
color: colorScheme.secondaryContainer,
|
|
shape: AppShape.md,
|
|
),
|
|
child: SizedBox.square(
|
|
dimension: 40,
|
|
child: Center(
|
|
child: GlyphIcon(
|
|
glyph,
|
|
size: 20,
|
|
color: colorScheme.onSecondaryContainer,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _DeveloperModeDetector extends StatefulWidget {
|
|
final Widget child;
|
|
final VoidCallback onEnterDeveloperMode;
|
|
|
|
const _DeveloperModeDetector({
|
|
required this.child,
|
|
required this.onEnterDeveloperMode,
|
|
});
|
|
|
|
@override
|
|
State<_DeveloperModeDetector> createState() => _DeveloperModeDetectorState();
|
|
}
|
|
|
|
class _DeveloperModeDetectorState extends State<_DeveloperModeDetector> {
|
|
int _counter = 0;
|
|
Timer? _timer;
|
|
|
|
void _handleTap() {
|
|
_counter++;
|
|
if (_counter >= 5) {
|
|
widget.onEnterDeveloperMode();
|
|
_resetCounter();
|
|
} else {
|
|
_timer?.cancel();
|
|
_timer = Timer(const Duration(seconds: 1), _resetCounter);
|
|
}
|
|
}
|
|
|
|
void _resetCounter() {
|
|
_counter = 0;
|
|
_timer?.cancel();
|
|
_timer = null;
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_timer?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GestureDetector(onTap: _handleTap, child: widget.child);
|
|
}
|
|
}
|