feat: align shared app foundations
This commit is contained in:
1 parent
bd6bb0ffce
commit
c5653a3699
21 files changed
+894
-66
No files matched your search
@@ -0,0 +1,155 @@
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../../common/services/device_header_service.dart';
|
||||
import '../../auth/data/auth_models.dart';
|
||||
|
||||
class FeedbackEmailService {
|
||||
const FeedbackEmailService._();
|
||||
|
||||
static const recipient = 'business@yqzhice.top';
|
||||
static const productName = '金值';
|
||||
|
||||
static Future<void> send(AuthState? authState) async {
|
||||
final email = await _buildEmail(authState);
|
||||
final uri = composeMailtoUri(
|
||||
recipient: email.recipient,
|
||||
subject: email.subject,
|
||||
body: email.body,
|
||||
);
|
||||
try {
|
||||
final launched = await launchUrl(
|
||||
uri,
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
if (!launched) throw const FeedbackEmailUnavailableException();
|
||||
} catch (_) {
|
||||
throw const FeedbackEmailUnavailableException();
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
static Uri composeMailtoUri({
|
||||
required String recipient,
|
||||
required String subject,
|
||||
required String body,
|
||||
}) => Uri(
|
||||
scheme: 'mailto',
|
||||
path: recipient,
|
||||
query: _encodeQueryParameters({'subject': subject, 'body': body}),
|
||||
);
|
||||
|
||||
static String _encodeQueryParameters(
|
||||
Map<String, String> parameters,
|
||||
) => parameters.entries
|
||||
.map(
|
||||
(entry) =>
|
||||
'${Uri.encodeComponent(entry.key)}=${Uri.encodeComponent(entry.value)}',
|
||||
)
|
||||
.join('&');
|
||||
|
||||
static Future<_FeedbackEmail> _buildEmail(AuthState? authState) async {
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
final device = await _loadDeviceInfo();
|
||||
try {
|
||||
await DeviceHeaderService.to.ensureInitialized();
|
||||
} catch (_) {}
|
||||
|
||||
final headers = DeviceHeaderService.to.commonHeaders();
|
||||
final loggedIn = authState is LoggedInAuthState ? authState : null;
|
||||
final version = '${packageInfo.version}+${packageInfo.buildNumber}';
|
||||
final diagnostics = [
|
||||
'App version: $version',
|
||||
'Channel: ${headers['User-Channel'] ?? ''}',
|
||||
'OS version: ${device.osVersion}',
|
||||
'Device manufacturer: ${device.manufacturer}',
|
||||
'Device model: ${device.model}',
|
||||
'Login type: ${loggedIn == null ? 'guest' : 'phone'}',
|
||||
'Phone: ${loggedIn?.user.phone ?? ''}',
|
||||
].join('\n');
|
||||
return _FeedbackEmail(
|
||||
recipient: recipient,
|
||||
subject:
|
||||
'$productName反馈[$version]-[${device.manufacturer}]-[${device.osVersion}]',
|
||||
body: [
|
||||
'$productName团队你好,',
|
||||
'请描述你的问题,可附上截图或视频:',
|
||||
'',
|
||||
'',
|
||||
'----',
|
||||
diagnostics,
|
||||
].join('\n'),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<_FeedbackDeviceInfo> _loadDeviceInfo() async {
|
||||
final plugin = DeviceInfoPlugin();
|
||||
try {
|
||||
if (kIsWeb) {
|
||||
final info = await plugin.webBrowserInfo;
|
||||
return _FeedbackDeviceInfo(
|
||||
osVersion: info.platform ?? 'web',
|
||||
manufacturer: info.browserName.name,
|
||||
model: info.userAgent ?? '',
|
||||
);
|
||||
}
|
||||
if (defaultTargetPlatform == TargetPlatform.android) {
|
||||
final info = await plugin.androidInfo;
|
||||
return _FeedbackDeviceInfo(
|
||||
osVersion: info.version.release,
|
||||
manufacturer: info.manufacturer,
|
||||
model: info.model,
|
||||
);
|
||||
}
|
||||
if (defaultTargetPlatform == TargetPlatform.iOS) {
|
||||
final info = await plugin.iosInfo;
|
||||
return _FeedbackDeviceInfo(
|
||||
osVersion: info.systemVersion,
|
||||
manufacturer: 'Apple',
|
||||
model: info.utsname.machine,
|
||||
);
|
||||
}
|
||||
return const _FeedbackDeviceInfo(
|
||||
osVersion: '',
|
||||
manufacturer: '',
|
||||
model: '',
|
||||
);
|
||||
} catch (_) {
|
||||
return const _FeedbackDeviceInfo(
|
||||
osVersion: '',
|
||||
manufacturer: '',
|
||||
model: '',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FeedbackEmailUnavailableException implements Exception {
|
||||
const FeedbackEmailUnavailableException();
|
||||
}
|
||||
|
||||
class _FeedbackEmail {
|
||||
const _FeedbackEmail({
|
||||
required this.recipient,
|
||||
required this.subject,
|
||||
required this.body,
|
||||
});
|
||||
|
||||
final String recipient;
|
||||
final String subject;
|
||||
final String body;
|
||||
}
|
||||
|
||||
class _FeedbackDeviceInfo {
|
||||
const _FeedbackDeviceInfo({
|
||||
required this.osVersion,
|
||||
required this.manufacturer,
|
||||
required this.model,
|
||||
});
|
||||
|
||||
final String osVersion;
|
||||
final String manufacturer;
|
||||
final String model;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_remix/flutter_remix.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@@ -20,6 +19,7 @@ import '../../auth/application/auth_controller.dart';
|
||||
import '../../auth/data/auth_models.dart';
|
||||
import '../../profile/application/profile_settings_controller.dart';
|
||||
import '../../profile/data/profile_models.dart';
|
||||
import '../application/feedback_email_service.dart';
|
||||
|
||||
class SettingsPage extends HookConsumerWidget {
|
||||
const SettingsPage({super.key});
|
||||
@@ -31,8 +31,6 @@ class SettingsPage extends HookConsumerWidget {
|
||||
final profileSettings = ref.watch(profileSettingsControllerProvider);
|
||||
final authAsync = ref.watch(authProvider);
|
||||
final loggedIn = authAsync.valueOrNull is LoggedInAuthState;
|
||||
final contactDebugTapCount = useState(0);
|
||||
final contactLastTapAt = useRef<DateTime?>(null);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: cs.background,
|
||||
@@ -199,21 +197,12 @@ class SettingsPage extends HookConsumerWidget {
|
||||
_NavRow(
|
||||
icon: FlutterRemix.mail_line,
|
||||
label: '联系我们',
|
||||
trailing: 'business@hnsiderui.cn',
|
||||
onTap: () {
|
||||
final now = DateTime.now();
|
||||
final lastTapAt = contactLastTapAt.value;
|
||||
if (lastTapAt == null ||
|
||||
now.difference(lastTapAt) >
|
||||
const Duration(milliseconds: 500)) {
|
||||
contactDebugTapCount.value = 0;
|
||||
}
|
||||
contactLastTapAt.value = now;
|
||||
contactDebugTapCount.value += 1;
|
||||
if (contactDebugTapCount.value >= 10) {
|
||||
contactDebugTapCount.value = 0;
|
||||
contactLastTapAt.value = null;
|
||||
context.push(AppRoutes.debug);
|
||||
trailing: FeedbackEmailService.recipient,
|
||||
onTap: () async {
|
||||
try {
|
||||
await FeedbackEmailService.send(authAsync.valueOrNull);
|
||||
} catch (_) {
|
||||
if (context.mounted) _showFeedbackEmailFailDialog(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -337,13 +326,15 @@ class SettingsPage extends HookConsumerWidget {
|
||||
_ChoiceButton(
|
||||
label: PriceColorMode.redUpGreenDown.label,
|
||||
selected: current == PriceColorMode.redUpGreenDown,
|
||||
onTap: () => Navigator.of(ctx).pop(PriceColorMode.redUpGreenDown),
|
||||
onTap: () =>
|
||||
Navigator.of(ctx).pop(PriceColorMode.redUpGreenDown),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_ChoiceButton(
|
||||
label: PriceColorMode.greenUpRedDown.label,
|
||||
selected: current == PriceColorMode.greenUpRedDown,
|
||||
onTap: () => Navigator.of(ctx).pop(PriceColorMode.greenUpRedDown),
|
||||
onTap: () =>
|
||||
Navigator.of(ctx).pop(PriceColorMode.greenUpRedDown),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -351,9 +342,9 @@ class SettingsPage extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
if (selected != null && context.mounted) {
|
||||
ref.read(profileSettingsControllerProvider.notifier).setPriceColorMode(
|
||||
selected,
|
||||
);
|
||||
ref
|
||||
.read(profileSettingsControllerProvider.notifier)
|
||||
.setPriceColorMode(selected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -720,6 +711,24 @@ class SettingsPage extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
void _showFeedbackEmailFailDialog(BuildContext context) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('无法发送反馈'),
|
||||
content: const Text(
|
||||
'请配置并登录系统邮箱,或使用其他邮箱发送邮件至 ${FeedbackEmailService.recipient}。',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _Section extends StatelessWidget {
|
||||
const _Section(this.title);
|
||||
|
||||
@@ -1081,9 +1090,7 @@ class _ChoiceButton extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? cs.primary : cs.secondary,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: selected ? cs.primary : cs.border,
|
||||
),
|
||||
border: Border.all(color: selected ? cs.primary : cs.border),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
@@ -1182,10 +1189,7 @@ class _ValueRow extends StatelessWidget {
|
||||
style: TextStyle(fontSize: 15, color: cs.foreground),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(fontSize: 13, color: cs.mutedForeground),
|
||||
),
|
||||
Text(value, style: TextStyle(fontSize: 13, color: cs.mutedForeground)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user