修复:禁止截屏功能

This commit is contained in:
ivan 2026-04-14 15:25:53 +08:00
parent 3edf55cacd
commit 65439a66b2
11 changed files with 198 additions and 110 deletions

View File

@ -71,6 +71,7 @@
"firstPurchase": "qlw4fp",
"purchase": "b2ms4n",
"register": "2k7vm5",
"paymentFailed": "",
"price_599": "mtzzqk",
"price_999": "m9ivl1",
"price_1999": "kp7a52",
@ -80,7 +81,7 @@
"extConfig": {
"keys": {
"showVideoMenu": ["go_run", "need_wait"],
"allowScreenshot": ["screen"],
"forbidScreenshot": ["screen"],
"blockScreenshot": ["safe_area"],
"allowThirdPartyPayment": ["san_fang", "lucky"],
"privacyUrl": ["privacy"],

View File

@ -171,7 +171,7 @@ lib/
|------|------|
| `ExtConfigRuntime.data` | `ValueNotifier<ExtConfigData?>`,监听后刷新首页 Tab / Grid |
| `ExtConfigRuntime.commonInfoSucceeded` | `true` / `false` / `null`:是否成功拉到 common_info**建议仅在为 `true` 时展示核心业务 UI** |
| `ExtConfigData` | `showVideoMenu``allowScreenshot`、`allowThirdPartyPayment``privacyUrl``agreementUrl``items` |
| `ExtConfigData` | `showVideoMenu``forbidScreenshot`、`allowThirdPartyPayment``privacyUrl``agreementUrl``items` |
| `ExtConfigItem` | 单项:`image``image_fix``img_need``cost``title``params` / `detail``taskExt``params ?? detail` |
| `kExtConfigItemsCategoryId` | 固定 `-1`,作「静态 items Tab」分类 id |
| `mergeHomeTabsWithExtConfigItems<T>()` | `showVideoMenu == true` 时在 API Tab 列表 **末尾** 追加静态 Tab |
@ -182,7 +182,7 @@ lib/
| 语义 | 默认候选键(首个存在则生效) |
|------|------------------------------|
| 展示顶部 Video Tab 栏 + items 固定最后一格 | `go_run``need_wait` |
| 允许截屏 | `screen`;若无则看 `safe_area``true` ⇒ 不允许截屏 |
| 是否禁止截屏(逻辑 [ExtConfigData.forbidScreenshot] | `screen`;若无则看 `safe_area`(均为 **`true` = 禁止截屏** |
| 允许第三方支付 | `san_fang``lucky` |
| 隐私 / 协议 URL | `privacy``agreement` |
| items 数组 | `items` |
@ -211,7 +211,7 @@ lib/
}
```
首页逻辑建议(对齐 `app_client``await FrameworkAuthService.loginComplete` 后判断 `ExtConfigRuntime.commonInfoSucceeded.value == true` 再进入主页;`showVideoMenu == true` 时展示顶部 Tab分类列表用 `mergeHomeTabsWithExtConfigItems` 把静态 Tab 放在最后,**该 Tab 的 Grid 数据源为 `ExtConfigRuntime.data.value?.items`**。第三方支付入口用 `allowThirdPartyPayment`;截屏策略用 `shouldPreventCapture` 或自行根据 `allowScreenshot` 调用宿主侧防护(框架不强制依赖 `screen_secure`)。
首页逻辑建议(对齐 `app_client``await FrameworkAuthService.loginComplete` 后判断 `ExtConfigRuntime.commonInfoSucceeded.value == true` 再进入主页;`showVideoMenu == true` 时展示顶部 Tab分类列表用 `mergeHomeTabsWithExtConfigItems` 把静态 Tab 放在最后,**该 Tab 的 Grid 数据源为 `ExtConfigRuntime.data.value?.items`**。第三方支付入口用 `allowThirdPartyPayment`;截屏策略用 `forbidScreenshot` 驱动宿主侧防护(如 `screen_secure`;框架不强制依赖)。
---

View File

@ -70,7 +70,7 @@
| 子节 | 说明 |
|------|------|
| `keys.showVideoMenu` | 字符串数组,如 `["go_run","need_wait"]`,依次为布尔字段候选键 |
| `keys.allowScreenshot` | 直接表示「允许截屏」的键,如 `["screen"]` |
| `keys.forbidScreenshot` | 线网为 `true` 时表示禁止截屏的键,如 `["screen"]`(兼容旧键名 `allowScreenshot` |
| `keys.blockScreenshot` | 为 `true` 时表示**禁止**截屏的键,如 `["safe_area"]` |
| `keys.allowThirdPartyPayment` | 如 `["san_fang","lucky"]` |
| `keys.privacyUrl` / `agreementUrl` / `items` | 隐私、协议 URL、items 数组所在键名 |

View File

@ -1,5 +1,7 @@
import 'dart:async';
import 'package:client_proxy_framework/client_proxy_framework.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
@ -8,15 +10,50 @@ import 'core/theme/app_colors.dart';
import 'core/theme/app_theme.dart';
import 'features/shell/main_screen.dart';
class App extends StatelessWidget {
class App extends StatefulWidget {
const App({super.key, required this.title});
final String title;
@override
State<App> createState() => _AppState();
}
class _AppState extends State<App> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
WidgetsBinding.instance.addPostFrameCallback((_) {
_reportFacebookActivateApp('first_frame');
});
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
/// app_client FB App Events [FacebookService.activateApp]
void _reportFacebookActivateApp(String reason) {
FacebookService.activateApp();
if (kDebugMode) {
debugPrint('[App] Facebook activateApp ($reason)');
}
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_reportFacebookActivateApp('lifecycle_resumed');
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: title,
title: widget.title,
debugShowCheckedModeBanner: false,
theme: buildFunyMeeTheme(),
home: const MainScreen(),

View File

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
@ -6,6 +7,7 @@ import 'package:client_proxy_framework/client_proxy_framework.dart';
import 'package:crypto/crypto.dart' show md5;
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:screen_secure/screen_secure.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../user/user_state.dart';
@ -58,6 +60,11 @@ class AppAuthCallbacks implements AuthServiceCallbacks {
avatar: data.avatar,
userName: data.userName,
);
unawaited(
AnalyticsEvents.trackRegisterIfNeeded(
firstRegister: data.firstRegister == true,
),
);
}
@override
@ -77,11 +84,77 @@ class AppAuthCallbacks implements AuthServiceCallbacks {
class AuthService {
static final _authCallbacks = AppAuthCallbacks();
static var _screenSecureListenerBound = false;
/// [ExtConfigRuntime.data] `null` `_applyScreenSecure(null)`
/// common_info `_applyScreenSecure()`
static int _screenSecureApplyGeneration = 0;
static Future<void> init() async {
FrameworkAuthService.init(_authCallbacks);
_bindScreenSecureToExtConfig();
await FrameworkAuthService.start();
}
/// [app_client] `AuthService.runWithNativeMediaPicker` /
/// [ExtConfigRuntime]
static Future<T> runWithNativeMediaPicker<T>(Future<T> Function() action) async {
if (defaultTargetPlatform != TargetPlatform.android &&
defaultTargetPlatform != TargetPlatform.iOS) {
return await action();
}
try {
await ScreenSecure.disableScreenshotBlock();
await ScreenSecure.disableScreenRecordBlock();
} on ScreenSecureException catch (e) {
debugPrint('[AuthService] native media picker: disable failed: ${e.message}');
}
try {
return await action();
} finally {
unawaited(_applyScreenSecure(ExtConfigRuntime.data.value));
}
}
static void _bindScreenSecureToExtConfig() {
if (_screenSecureListenerBound) return;
_screenSecureListenerBound = true;
void listener() {
unawaited(_applyScreenSecure(ExtConfigRuntime.data.value));
}
ExtConfigRuntime.data.addListener(listener);
// listener()`data == null` disable common_info enable
}
/// [ExtConfigData]`screen` / `safe_area` [ExtConfigKeySchema] [ExtConfigData.forbidScreenshot]
/// `true` / app_client `safe_area`
static Future<void> _applyScreenSecure(ExtConfigData? ext) async {
if (defaultTargetPlatform != TargetPlatform.android &&
defaultTargetPlatform != TargetPlatform.iOS) {
return;
}
final gen = ++_screenSecureApplyGeneration;
final block = ext?.forbidScreenshot == true;
try {
await ScreenSecure.init(screenshotBlock: false, screenRecordBlock: false);
if (gen != _screenSecureApplyGeneration) return;
if (block) {
await ScreenSecure.enableScreenshotBlock();
await ScreenSecure.enableScreenRecordBlock();
} else {
await ScreenSecure.disableScreenshotBlock();
await ScreenSecure.disableScreenRecordBlock();
}
if (gen != _screenSecureApplyGeneration) return;
if (kDebugMode && block) {
debugPrint('[AuthService] ScreenSecure: enabled (forbidScreenshot)');
}
} on ScreenSecureException catch (e) {
debugPrint('[AuthService] ScreenSecure: ${e.message}');
}
}
static Future<void> get loginComplete => FrameworkAuthService.loginComplete;
/// fast_login + common_info [loginComplete] Future null

View File

@ -9,6 +9,7 @@ import 'package:google_fonts/google_fonts.dart';
import 'package:image_picker/image_picker.dart';
import '../../core/app_env.dart';
import '../../core/auth/auth_service.dart';
import '../../core/open_purchase_store.dart';
import '../../core/user/user_state.dart';
import '../../design/pencil_theme.dart';
@ -142,7 +143,9 @@ class _GenerateScreenState extends State<GenerateScreen> {
if (!mounted) return;
final source = await _showPickImageSourceSheet(context);
if (source == null || !mounted) return;
final x = await _picker.pickImage(source: source, imageQuality: 92);
final x = await AuthService.runWithNativeMediaPicker(
() => _picker.pickImage(source: source, imageQuality: 92),
);
if (x == null || !mounted) return;
setState(() {
if (slot == 0) {
@ -619,7 +622,7 @@ class _GenerateScreenState extends State<GenerateScreen> {
),
const SizedBox(height: 12),
Text(
'Est. cost · $_estimatedCost credits',
'cost · $_estimatedCost credits',
textAlign: TextAlign.center,
style: GoogleFonts.inter(
fontSize: 13,
@ -627,22 +630,6 @@ class _GenerateScreenState extends State<GenerateScreen> {
color: PencilTheme.stone600,
),
),
const SizedBox(height: 8),
InkWell(
onTap: () => openPurchaseStore(context),
child: Text(
'Balance · ${credits.toStringAsFixed(2)}',
textAlign: TextAlign.center,
style: GoogleFonts.inter(
fontSize: 12,
color: PencilTheme.inkSoft,
decoration: TextDecoration.underline,
decorationColor: PencilTheme.inkSoft.withValues(
alpha: 0.45,
),
),
),
),
],
],
),

View File

@ -73,7 +73,8 @@ class _ProfileScreenState extends State<ProfileScreen> {
Expanded(
child: ListView(
padding: EdgeInsets.only(
bottom: 28 +
bottom:
28 +
(widget.isRootTab
? PencilTheme.mainTabBottomChromeReserve(context)
: 0),
@ -154,59 +155,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
padding: const EdgeInsets.fromLTRB(20, 20, 20, 12),
child: _menuCard(context),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const PurchaseScreen(),
),
);
},
child: Text(
'Buy credits',
style: GoogleFonts.inter(
fontWeight: FontWeight.w600,
color: PencilTheme.underlineGold,
),
),
),
TextButton(
onPressed: () async {
await UserAccountRefresh.fetchAndNotify(
app: currentBackendAppType(),
userId: UserState.userId.value,
onAccount: (a) {
if (a.credits != null) {
UserState.setCredits(a.credits!);
}
if (a.avatar != null) {
UserState.setAvatar(a.avatar!);
}
if (a.userName != null) {
UserState.setUserName(a.userName!);
}
},
onFailure: (m) {
if (context.mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(m)));
}
},
);
},
child: Text(
'Refresh',
style: GoogleFonts.inter(
fontWeight: FontWeight.w600,
color: PencilTheme.stone600,
),
),
),
],
),
const SizedBox(height: 24),
],
),
@ -219,8 +168,11 @@ class _ProfileScreenState extends State<ProfileScreen> {
}
Widget _avatarFallback() {
return Icon(Icons.person_rounded,
size: 44, color: PencilTheme.profileAvatarIcon);
return Icon(
Icons.person_rounded,
size: 44,
color: PencilTheme.profileAvatarIcon,
);
}
String _formatCredits(int c) {
@ -235,17 +187,14 @@ class _ProfileScreenState extends State<ProfileScreen> {
required String? url,
}) {
if (url == null || url.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Link not configured')),
);
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Link not configured')));
return;
}
Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (_) => AppWebViewScreen(
title: title,
initialUrl: url.trim(),
),
builder: (_) => AppWebViewScreen(title: title, initialUrl: url.trim()),
),
);
}
@ -288,17 +237,24 @@ class _ProfileScreenState extends State<ProfileScreen> {
_divider(),
_row('Version', value: 'v$_version'),
_divider(),
_row('Delete account',
danger: true,
trailing: Icons.chevron_right_rounded,
onTap: () => _delete(context)),
_row(
'Delete account',
danger: true,
trailing: Icons.chevron_right_rounded,
onTap: () => _delete(context),
),
],
),
);
}
Widget _row(String title,
{IconData? trailing, String? value, bool danger = false, VoidCallback? onTap}) {
Widget _row(
String title, {
IconData? trailing,
String? value,
bool danger = false,
VoidCallback? onTap,
}) {
return ListTile(
onTap: onTap,
title: Text(
@ -310,17 +266,21 @@ class _ProfileScreenState extends State<ProfileScreen> {
),
),
trailing: value != null
? Text(value,
? Text(
value,
style: GoogleFonts.inter(
fontSize: 14,
color: const Color(0xFF78716C)))
: Icon(trailing,
color: danger ? const Color(0xFFFCA292) : const Color(0xFFA8A29E)),
fontSize: 14,
color: const Color(0xFF78716C),
),
)
: Icon(
trailing,
color: danger ? const Color(0xFFFCA292) : const Color(0xFFA8A29E),
),
);
}
Widget _divider() =>
Container(height: 1, color: const Color(0xFFF5F5F4));
Widget _divider() => Container(height: 1, color: const Color(0xFFF5F5F4));
Future<void> _delete(BuildContext context) async {
final ok = await showDeleteAccountConfirmationFlow(context);
@ -335,12 +295,16 @@ class _ProfileScreenState extends State<ProfileScreen> {
UserState.clear();
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Account deleted. Please restart the app and sign in again.')),
const SnackBar(
content: Text(
'Account deleted. Please restart the app and sign in again.',
),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(res.msg)),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(res.msg)));
}
}
}

View File

@ -212,6 +212,7 @@ class _PurchaseScreenState extends State<PurchaseScreen> {
_paying = false;
_selectedIndex = null;
});
AnalyticsEvents.trackPaymentFailed();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
@ -230,6 +231,7 @@ class _PurchaseScreenState extends State<PurchaseScreen> {
_paying = false;
_selectedIndex = null;
});
AnalyticsEvents.trackPaymentFailed();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('No payment methods available.')),
);
@ -256,7 +258,7 @@ class _PurchaseScreenState extends State<PurchaseScreen> {
_selectedIndex = index;
});
final sink = _PurchaseSink(
final innerSink = _PurchaseSink(
context: context,
onRefresh: () {
if (mounted) {
@ -272,6 +274,10 @@ class _PurchaseScreenState extends State<PurchaseScreen> {
if (mounted) setState(() {});
},
);
final sink = PaymentSettlementSinkWithAnalytics(
inner: innerSink,
analyticsProduct: item,
);
final outcome = await ThirdPartyCheckoutCoordinator.createOrder(
userId: uid,
@ -288,6 +294,7 @@ class _PurchaseScreenState extends State<PurchaseScreen> {
_paying = false;
_selectedIndex = null;
});
AnalyticsEvents.trackPaymentFailed();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(outcome.message ?? 'Create order failed')),
);
@ -319,6 +326,7 @@ class _PurchaseScreenState extends State<PurchaseScreen> {
_paying = false;
_selectedIndex = null;
});
AnalyticsEvents.trackPaymentFailed();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Invalid payment response.')),
);
@ -367,6 +375,8 @@ class _PurchaseScreenState extends State<PurchaseScreen> {
return;
}
AnalyticsEvents.trackTierSelection(item);
final ext = ExtConfigRuntime.data.value;
if (_useThirdPartyFromExt(ext)) {
await _onBuyThirdParty(item, index);
@ -389,7 +399,7 @@ class _PurchaseScreenState extends State<PurchaseScreen> {
_selectedIndex = index;
});
final sink = _PurchaseSink(
final innerSink = _PurchaseSink(
context: context,
onRefresh: () {
if (mounted) {
@ -403,6 +413,10 @@ class _PurchaseScreenState extends State<PurchaseScreen> {
if (mounted) setState(() {});
},
);
final sink = PaymentSettlementSinkWithAnalytics(
inner: innerSink,
analyticsProduct: item,
);
await NativeIapCoordinator.purchaseGooglePlay(
sink: sink,

View File

@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:image_picker/image_picker.dart';
import '../../core/auth/auth_service.dart';
import '../../design/pencil_theme.dart';
import '../../widgets/pencil_chrome.dart';
import 'report_feedback_upload.dart';
@ -39,9 +40,11 @@ class _ReportScreenState extends State<ReportScreen> {
Future<void> _pickImage() async {
if (_submitting) return;
final x = await _picker.pickImage(
source: ImageSource.gallery,
imageQuality: 85,
final x = await AuthService.runWithNativeMediaPicker(
() => _picker.pickImage(
source: ImageSource.gallery,
imageQuality: 85,
),
);
if (x == null || !mounted) return;
setState(() => _imageFile = File(x.path));

View File

@ -743,6 +743,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.28.0"
screen_secure:
dependency: "direct main"
description:
name: screen_secure
sha256: "9660d0a285f7e27d482333779153e3cb17c5fd929e15373e51abad63810df7b4"
url: "https://pub.dev"
source: hosted
version: "1.0.3"
shared_preferences:
dependency: "direct main"
description:

View File

@ -29,6 +29,7 @@ dependencies:
package_info_plus: ^8.1.2
webview_flutter: ^4.13.1
gal: ^2.3.2
screen_secure: ^1.0.3
dev_dependencies:
flutter_test: