修复:禁止截屏功能

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

View File

@ -171,7 +171,7 @@ lib/
|------|------| |------|------|
| `ExtConfigRuntime.data` | `ValueNotifier<ExtConfigData?>`,监听后刷新首页 Tab / Grid | | `ExtConfigRuntime.data` | `ValueNotifier<ExtConfigData?>`,监听后刷新首页 Tab / Grid |
| `ExtConfigRuntime.commonInfoSucceeded` | `true` / `false` / `null`:是否成功拉到 common_info**建议仅在为 `true` 时展示核心业务 UI** | | `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` | | `ExtConfigItem` | 单项:`image``image_fix``img_need``cost``title``params` / `detail``taskExt``params ?? detail` |
| `kExtConfigItemsCategoryId` | 固定 `-1`,作「静态 items Tab」分类 id | | `kExtConfigItemsCategoryId` | 固定 `-1`,作「静态 items Tab」分类 id |
| `mergeHomeTabsWithExtConfigItems<T>()` | `showVideoMenu == true` 时在 API Tab 列表 **末尾** 追加静态 Tab | | `mergeHomeTabsWithExtConfigItems<T>()` | `showVideoMenu == true` 时在 API Tab 列表 **末尾** 追加静态 Tab |
@ -182,7 +182,7 @@ lib/
| 语义 | 默认候选键(首个存在则生效) | | 语义 | 默认候选键(首个存在则生效) |
|------|------------------------------| |------|------------------------------|
| 展示顶部 Video Tab 栏 + items 固定最后一格 | `go_run``need_wait` | | 展示顶部 Video Tab 栏 + items 固定最后一格 | `go_run``need_wait` |
| 允许截屏 | `screen`;若无则看 `safe_area``true` ⇒ 不允许截屏 | | 是否禁止截屏(逻辑 [ExtConfigData.forbidScreenshot] | `screen`;若无则看 `safe_area`(均为 **`true` = 禁止截屏** |
| 允许第三方支付 | `san_fang``lucky` | | 允许第三方支付 | `san_fang``lucky` |
| 隐私 / 协议 URL | `privacy``agreement` | | 隐私 / 协议 URL | `privacy``agreement` |
| items 数组 | `items` | | 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.showVideoMenu` | 字符串数组,如 `["go_run","need_wait"]`,依次为布尔字段候选键 |
| `keys.allowScreenshot` | 直接表示「允许截屏」的键,如 `["screen"]` | | `keys.forbidScreenshot` | 线网为 `true` 时表示禁止截屏的键,如 `["screen"]`(兼容旧键名 `allowScreenshot` |
| `keys.blockScreenshot` | 为 `true` 时表示**禁止**截屏的键,如 `["safe_area"]` | | `keys.blockScreenshot` | 为 `true` 时表示**禁止**截屏的键,如 `["safe_area"]` |
| `keys.allowThirdPartyPayment` | 如 `["san_fang","lucky"]` | | `keys.allowThirdPartyPayment` | 如 `["san_fang","lucky"]` |
| `keys.privacyUrl` / `agreementUrl` / `items` | 隐私、协议 URL、items 数组所在键名 | | `keys.privacyUrl` / `agreementUrl` / `items` | 隐私、协议 URL、items 数组所在键名 |

View File

@ -1,5 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:client_proxy_framework/client_proxy_framework.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.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 'core/theme/app_theme.dart';
import 'features/shell/main_screen.dart'; import 'features/shell/main_screen.dart';
class App extends StatelessWidget { class App extends StatefulWidget {
const App({super.key, required this.title}); const App({super.key, required this.title});
final String 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
title: title, title: widget.title,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: buildFunyMeeTheme(), theme: buildFunyMeeTheme(),
home: const MainScreen(), home: const MainScreen(),

View File

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:math'; 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:crypto/crypto.dart' show md5;
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:screen_secure/screen_secure.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../user/user_state.dart'; import '../user/user_state.dart';
@ -58,6 +60,11 @@ class AppAuthCallbacks implements AuthServiceCallbacks {
avatar: data.avatar, avatar: data.avatar,
userName: data.userName, userName: data.userName,
); );
unawaited(
AnalyticsEvents.trackRegisterIfNeeded(
firstRegister: data.firstRegister == true,
),
);
} }
@override @override
@ -77,11 +84,77 @@ class AppAuthCallbacks implements AuthServiceCallbacks {
class AuthService { class AuthService {
static final _authCallbacks = AppAuthCallbacks(); 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 { static Future<void> init() async {
FrameworkAuthService.init(_authCallbacks); FrameworkAuthService.init(_authCallbacks);
_bindScreenSecureToExtConfig();
await FrameworkAuthService.start(); 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; static Future<void> get loginComplete => FrameworkAuthService.loginComplete;
/// fast_login + common_info [loginComplete] Future null /// 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 'package:image_picker/image_picker.dart';
import '../../core/app_env.dart'; import '../../core/app_env.dart';
import '../../core/auth/auth_service.dart';
import '../../core/open_purchase_store.dart'; import '../../core/open_purchase_store.dart';
import '../../core/user/user_state.dart'; import '../../core/user/user_state.dart';
import '../../design/pencil_theme.dart'; import '../../design/pencil_theme.dart';
@ -142,7 +143,9 @@ class _GenerateScreenState extends State<GenerateScreen> {
if (!mounted) return; if (!mounted) return;
final source = await _showPickImageSourceSheet(context); final source = await _showPickImageSourceSheet(context);
if (source == null || !mounted) return; 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; if (x == null || !mounted) return;
setState(() { setState(() {
if (slot == 0) { if (slot == 0) {
@ -619,7 +622,7 @@ class _GenerateScreenState extends State<GenerateScreen> {
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Text( Text(
'Est. cost · $_estimatedCost credits', 'cost · $_estimatedCost credits',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: GoogleFonts.inter( style: GoogleFonts.inter(
fontSize: 13, fontSize: 13,
@ -627,22 +630,6 @@ class _GenerateScreenState extends State<GenerateScreen> {
color: PencilTheme.stone600, 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( Expanded(
child: ListView( child: ListView(
padding: EdgeInsets.only( padding: EdgeInsets.only(
bottom: 28 + bottom:
28 +
(widget.isRootTab (widget.isRootTab
? PencilTheme.mainTabBottomChromeReserve(context) ? PencilTheme.mainTabBottomChromeReserve(context)
: 0), : 0),
@ -154,59 +155,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
padding: const EdgeInsets.fromLTRB(20, 20, 20, 12), padding: const EdgeInsets.fromLTRB(20, 20, 20, 12),
child: _menuCard(context), 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), const SizedBox(height: 24),
], ],
), ),
@ -219,8 +168,11 @@ class _ProfileScreenState extends State<ProfileScreen> {
} }
Widget _avatarFallback() { Widget _avatarFallback() {
return Icon(Icons.person_rounded, return Icon(
size: 44, color: PencilTheme.profileAvatarIcon); Icons.person_rounded,
size: 44,
color: PencilTheme.profileAvatarIcon,
);
} }
String _formatCredits(int c) { String _formatCredits(int c) {
@ -235,17 +187,14 @@ class _ProfileScreenState extends State<ProfileScreen> {
required String? url, required String? url,
}) { }) {
if (url == null || url.trim().isEmpty) { if (url == null || url.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
const SnackBar(content: Text('Link not configured')), context,
); ).showSnackBar(const SnackBar(content: Text('Link not configured')));
return; return;
} }
Navigator.of(context).push<void>( Navigator.of(context).push<void>(
MaterialPageRoute<void>( MaterialPageRoute<void>(
builder: (_) => AppWebViewScreen( builder: (_) => AppWebViewScreen(title: title, initialUrl: url.trim()),
title: title,
initialUrl: url.trim(),
),
), ),
); );
} }
@ -288,17 +237,24 @@ class _ProfileScreenState extends State<ProfileScreen> {
_divider(), _divider(),
_row('Version', value: 'v$_version'), _row('Version', value: 'v$_version'),
_divider(), _divider(),
_row('Delete account', _row(
'Delete account',
danger: true, danger: true,
trailing: Icons.chevron_right_rounded, trailing: Icons.chevron_right_rounded,
onTap: () => _delete(context)), onTap: () => _delete(context),
),
], ],
), ),
); );
} }
Widget _row(String title, Widget _row(
{IconData? trailing, String? value, bool danger = false, VoidCallback? onTap}) { String title, {
IconData? trailing,
String? value,
bool danger = false,
VoidCallback? onTap,
}) {
return ListTile( return ListTile(
onTap: onTap, onTap: onTap,
title: Text( title: Text(
@ -310,17 +266,21 @@ class _ProfileScreenState extends State<ProfileScreen> {
), ),
), ),
trailing: value != null trailing: value != null
? Text(value, ? Text(
value,
style: GoogleFonts.inter( style: GoogleFonts.inter(
fontSize: 14, fontSize: 14,
color: const Color(0xFF78716C))) color: const Color(0xFF78716C),
: Icon(trailing, ),
color: danger ? const Color(0xFFFCA292) : const Color(0xFFA8A29E)), )
: Icon(
trailing,
color: danger ? const Color(0xFFFCA292) : const Color(0xFFA8A29E),
),
); );
} }
Widget _divider() => Widget _divider() => Container(height: 1, color: const Color(0xFFF5F5F4));
Container(height: 1, color: const Color(0xFFF5F5F4));
Future<void> _delete(BuildContext context) async { Future<void> _delete(BuildContext context) async {
final ok = await showDeleteAccountConfirmationFlow(context); final ok = await showDeleteAccountConfirmationFlow(context);
@ -335,12 +295,16 @@ class _ProfileScreenState extends State<ProfileScreen> {
UserState.clear(); UserState.clear();
Navigator.of(context).pop(); Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar( 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 { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
SnackBar(content: Text(res.msg)), context,
); ).showSnackBar(SnackBar(content: Text(res.msg)));
} }
} }
} }

View File

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

View File

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

View File

@ -743,6 +743,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.28.0" 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: shared_preferences:
dependency: "direct main" dependency: "direct main"
description: description:

View File

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