feat: 扫码商品详情页重设计 + 宣传主页 + nginx 路由完善
- public_product_screen: 1:1 复刻设计稿,新增商品介绍、批次防伪(自定义圆形印章)、完整商品参数表、门店营业时间、页脚链接 - app_theme: 完整品牌色系(brand/gray/accent/semantic) - shop model: 新增 business_hours 字段,public 接口同步返回 - nginx: 新增 /product/ → Flutter app 扫码入口,修复 /docs /download html 后缀兼容 - web/: 新增宣传主页、功能页、文档页、扫码 HTML 及设计资源 - app_shell: 界面优化 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -37,16 +37,17 @@ class _RouterNotifier extends ChangeNotifier {
|
||||
String? redirect(BuildContext context, GoRouterState state) {
|
||||
final authState = _ref.read(authStateProvider);
|
||||
final isLoggedIn = authState.isLoggedIn;
|
||||
final isLoginRoute = state.matchedLocation == '/login';
|
||||
final isPublicRoute = state.matchedLocation.startsWith('/product/');
|
||||
final loc = state.matchedLocation;
|
||||
final isPublicRoute = loc == '/login' ||
|
||||
loc.startsWith('/product/');
|
||||
final result = !authState.initialized
|
||||
? null
|
||||
: (!isLoggedIn && !isLoginRoute && !isPublicRoute)
|
||||
: (!isLoggedIn && !isPublicRoute)
|
||||
? '/login'
|
||||
: (isLoggedIn && isLoginRoute)
|
||||
: (isLoggedIn && loc == '/login')
|
||||
? '/stock-in'
|
||||
: null;
|
||||
debugPrint('[Router] redirect: location=${state.matchedLocation}'
|
||||
debugPrint('[Router] redirect: location=$loc'
|
||||
' initialized=${authState.initialized}'
|
||||
' isLoggedIn=$isLoggedIn'
|
||||
' → ${result ?? "null (no redirect)"}');
|
||||
@@ -71,7 +72,7 @@ final appRouterProvider = Provider<GoRouter>((ref) {
|
||||
refreshListenable: notifier,
|
||||
redirect: notifier.redirect,
|
||||
routes: [
|
||||
// Public route — no auth, no shell nav bar
|
||||
// Public product scan — no auth, no shell nav bar
|
||||
GoRoute(
|
||||
path: '/product/:public_id',
|
||||
builder: (context, state) =>
|
||||
|
||||
@@ -1,17 +1,110 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppTheme {
|
||||
static const Color primary = Color(0xFF1565C0);
|
||||
static const Color primaryDark = Color(0xFF0D47A1);
|
||||
static const Color primaryLight = Color(0xFF1976D2);
|
||||
static const Color accent = Color(0xFFFF6F00);
|
||||
static const Color success = Color(0xFF2E7D32);
|
||||
static const Color danger = Color(0xFFC62828);
|
||||
static const Color background = Color(0xFFF5F5F5);
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// LEGACY API — field names preserved, values updated to 岩美 brand.
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
static const Color primary = Color(0xFF2563AC);
|
||||
static const Color primaryDark = Color(0xFF154072);
|
||||
static const Color primaryLight = Color(0xFF4F86C6);
|
||||
|
||||
/// Bordeaux accent — wine context cue. Was warm orange #FF6F00.
|
||||
static const Color accent = Color(0xFF8B2331);
|
||||
|
||||
static const Color success = Color(0xFF2E8B57);
|
||||
static const Color danger = Color(0xFFD14343);
|
||||
static const Color background = Color(0xFFF5F7FA);
|
||||
static const Color surface = Color(0xFFFFFFFF);
|
||||
static const Color border = Color(0xFFE0E0E0);
|
||||
static const Color textPrimary = Color(0xFF212121);
|
||||
static const Color textSecondary = Color(0xFF757575);
|
||||
static const Color border = Color(0xFFDCE2EB);
|
||||
static const Color textPrimary = Color(0xFF232934);
|
||||
static const Color textSecondary = Color(0xFF6E7888);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Full brand scale (50..900)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
static const Color brand50 = Color(0xFFEEF4FB);
|
||||
static const Color brand100 = Color(0xFFD6E5F5);
|
||||
static const Color brand200 = Color(0xFFADC9EA);
|
||||
static const Color brand300 = Color(0xFF7FA8DA);
|
||||
static const Color brand400 = Color(0xFF4F86C6);
|
||||
static const Color brand500 = Color(0xFF2563AC);
|
||||
static const Color brand600 = Color(0xFF1B4F8E);
|
||||
static const Color brand700 = Color(0xFF154072);
|
||||
static const Color brand800 = Color(0xFF0F3057);
|
||||
static const Color brand900 = Color(0xFF0A1F3B);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Full neutral (gray) scale
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
static const Color gray0 = Color(0xFFFFFFFF);
|
||||
static const Color gray25 = Color(0xFFFBFCFD);
|
||||
static const Color gray50 = Color(0xFFF5F7FA);
|
||||
static const Color gray100 = Color(0xFFECEFF4);
|
||||
static const Color gray200 = Color(0xFFDCE2EB);
|
||||
static const Color gray300 = Color(0xFFC2CAD6);
|
||||
static const Color gray400 = Color(0xFF99A3B3);
|
||||
static const Color gray500 = Color(0xFF6E7888);
|
||||
static const Color gray600 = Color(0xFF4F5867);
|
||||
static const Color gray700 = Color(0xFF353C48);
|
||||
static const Color gray800 = Color(0xFF232934);
|
||||
static const Color gray900 = Color(0xFF141821);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Accent (bordeaux) scale
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
static const Color accent50 = Color(0xFFFAEEF0);
|
||||
static const Color accent100 = Color(0xFFF1D2D7);
|
||||
static const Color accent500 = Color(0xFF8B2331);
|
||||
static const Color accent700 = Color(0xFF5F1621);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Semantic scale
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
static const Color success50 = Color(0xFFE8F5EE);
|
||||
static const Color success500 = Color(0xFF2E8B57);
|
||||
static const Color success700 = Color(0xFF1F6B41);
|
||||
|
||||
static const Color warning50 = Color(0xFFFFF4DB);
|
||||
static const Color warning500 = Color(0xFFE08E00);
|
||||
static const Color warning700 = Color(0xFFA66700);
|
||||
|
||||
static const Color danger50 = Color(0xFFFDECEC);
|
||||
static const Color danger500 = Color(0xFFD14343);
|
||||
static const Color danger700 = Color(0xFF9E2A2A);
|
||||
|
||||
static const Color info50 = Color(0xFFE5F1FB);
|
||||
static const Color info500 = Color(0xFF2F7BD0);
|
||||
static const Color info700 = Color(0xFF1F5C9F);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// State colors
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
static const Color rowHover = Color(0xFFEEF4FB);
|
||||
static const Color tableHeader = Color(0xFFF5F7FA);
|
||||
static const Color borderSubtle = Color(0xFFECEFF4);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Radii
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
static const double radiusSm = 4.0;
|
||||
static const double radiusMd = 6.0;
|
||||
static const double radiusLg = 10.0;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Spacing (4px base)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
static const double space1 = 4.0;
|
||||
static const double space2 = 8.0;
|
||||
static const double space3 = 12.0;
|
||||
static const double space4 = 16.0;
|
||||
static const double space5 = 20.0;
|
||||
static const double space6 = 24.0;
|
||||
static const double space8 = 32.0;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// ThemeData
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
static ThemeData light() {
|
||||
return ThemeData(
|
||||
@@ -21,78 +114,172 @@ class AppTheme {
|
||||
brightness: Brightness.light,
|
||||
).copyWith(
|
||||
primary: primary,
|
||||
surface: surface,
|
||||
onPrimary: Colors.white,
|
||||
secondary: brand400,
|
||||
surface: surface,
|
||||
onSurface: textPrimary,
|
||||
error: danger,
|
||||
outline: border,
|
||||
outlineVariant: borderSubtle,
|
||||
),
|
||||
scaffoldBackgroundColor: background,
|
||||
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: primary,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
centerTitle: false,
|
||||
toolbarHeight: 56,
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
color: surface,
|
||||
elevation: 1,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
side: const BorderSide(color: border, width: 0.5),
|
||||
titleTextStyle: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
letterSpacing: 0.4,
|
||||
),
|
||||
),
|
||||
|
||||
cardTheme: CardThemeData(
|
||||
color: surface,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(radiusLg),
|
||||
side: const BorderSide(color: border, width: 1),
|
||||
),
|
||||
margin: const EdgeInsets.all(0),
|
||||
),
|
||||
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: primary,
|
||||
foregroundColor: Colors.white,
|
||||
minimumSize: const Size(0, 36),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||
minimumSize: const Size(0, 40),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(radiusMd),
|
||||
),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
elevation: 0,
|
||||
).copyWith(
|
||||
backgroundColor: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.pressed)) return primaryDark;
|
||||
if (states.contains(WidgetState.hovered)) return brand600;
|
||||
return primary;
|
||||
}),
|
||||
overlayColor:
|
||||
WidgetStateProperty.all(Colors.white.withValues(alpha: 0.08)),
|
||||
),
|
||||
),
|
||||
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: primary,
|
||||
minimumSize: const Size(0, 36),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||
side: const BorderSide(color: primary),
|
||||
foregroundColor: brand700,
|
||||
minimumSize: const Size(0, 40),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(radiusMd),
|
||||
),
|
||||
side: const BorderSide(color: border, width: 1),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
).copyWith(
|
||||
backgroundColor: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.pressed)) return gray100;
|
||||
if (states.contains(WidgetState.hovered)) return gray50;
|
||||
return surface;
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: primary,
|
||||
minimumSize: const Size(0, 36),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(radiusSm),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderRadius: BorderRadius.circular(radiusMd),
|
||||
borderSide: const BorderSide(color: border),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderRadius: BorderRadius.circular(radiusMd),
|
||||
borderSide: const BorderSide(color: border),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderRadius: BorderRadius.circular(radiusMd),
|
||||
borderSide: const BorderSide(color: primary, width: 1.5),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(radiusMd),
|
||||
borderSide: const BorderSide(color: danger),
|
||||
),
|
||||
hoverColor: gray50,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: surface,
|
||||
labelStyle: const TextStyle(fontSize: 13, color: textSecondary),
|
||||
hintStyle: TextStyle(fontSize: 13, color: gray400),
|
||||
),
|
||||
dividerTheme: const DividerThemeData(color: border, thickness: 0.5),
|
||||
|
||||
dividerTheme: const DividerThemeData(
|
||||
color: borderSubtle,
|
||||
thickness: 1,
|
||||
space: 1,
|
||||
),
|
||||
|
||||
chipTheme: ChipThemeData(
|
||||
backgroundColor: gray100,
|
||||
labelStyle: const TextStyle(fontSize: 12, color: textPrimary),
|
||||
side: BorderSide.none,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 0),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
),
|
||||
|
||||
tooltipTheme: TooltipThemeData(
|
||||
decoration: BoxDecoration(
|
||||
color: gray900,
|
||||
borderRadius: BorderRadius.circular(radiusSm),
|
||||
),
|
||||
textStyle: const TextStyle(color: Colors.white, fontSize: 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
),
|
||||
|
||||
textTheme: const TextTheme(
|
||||
bodyLarge: TextStyle(fontSize: 14, color: textPrimary),
|
||||
bodyMedium: TextStyle(fontSize: 14, color: textPrimary),
|
||||
bodySmall: TextStyle(fontSize: 12, color: textSecondary),
|
||||
titleMedium: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textPrimary),
|
||||
labelMedium: TextStyle(fontSize: 12, color: textSecondary),
|
||||
displayLarge: TextStyle(fontSize: 36, fontWeight: FontWeight.w600, color: brand900, height: 1.2, letterSpacing: 0.4),
|
||||
displayMedium: TextStyle(fontSize: 28, fontWeight: FontWeight.w600, color: brand900, height: 1.2, letterSpacing: 0.4),
|
||||
headlineSmall: TextStyle(fontSize: 22, fontWeight: FontWeight.w600, color: gray900, height: 1.3),
|
||||
titleLarge: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: gray900, height: 1.35),
|
||||
titleMedium: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: textPrimary, height: 1.4),
|
||||
titleSmall: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: textPrimary, height: 1.4),
|
||||
bodyLarge: TextStyle(fontSize: 14, color: textPrimary, height: 1.55),
|
||||
bodyMedium: TextStyle(fontSize: 14, color: textPrimary, height: 1.55),
|
||||
bodySmall: TextStyle(fontSize: 12, color: textSecondary, height: 1.5),
|
||||
labelLarge: TextStyle(fontSize: 13, color: textPrimary, fontWeight: FontWeight.w500),
|
||||
labelMedium: TextStyle(fontSize: 12, color: textSecondary, letterSpacing: 0.4),
|
||||
labelSmall: TextStyle(fontSize: 11, color: textSecondary, letterSpacing: 0.4),
|
||||
),
|
||||
|
||||
pageTransitionsTheme: const PageTransitionsTheme(
|
||||
builders: {
|
||||
TargetPlatform.windows: FadeUpwardsPageTransitionsBuilder(),
|
||||
TargetPlatform.macOS: FadeUpwardsPageTransitionsBuilder(),
|
||||
TargetPlatform.linux: FadeUpwardsPageTransitionsBuilder(),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ class _JiuAppState extends ConsumerState<JiuApp> {
|
||||
Widget build(BuildContext context) {
|
||||
final router = ref.watch(appRouterProvider);
|
||||
return MaterialApp.router(
|
||||
title: '酒库管理系统',
|
||||
title: '岩美',
|
||||
theme: AppTheme.light(),
|
||||
routerConfig: router,
|
||||
debugShowCheckedModeBanner: false,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -591,10 +591,10 @@ class _ShopButton extends StatelessWidget {
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.wine_bar, color: Colors.white, size: 22),
|
||||
SizedBox(width: 8),
|
||||
_YanmeiMark(size: 28),
|
||||
SizedBox(width: 10),
|
||||
Text(
|
||||
'酒库管理系统',
|
||||
'岩美',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
@@ -608,6 +608,70 @@ class _ShopButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Brand mark widget — approximates the 岩美 logo SVG without flutter_svg.
|
||||
/// Dark blue rounded rect, white mountain/wave strokes, bordeaux dot.
|
||||
class _YanmeiMark extends StatelessWidget {
|
||||
final double size;
|
||||
const _YanmeiMark({this.size = 32});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0F3057),
|
||||
borderRadius: BorderRadius.circular(size * 0.19),
|
||||
),
|
||||
child: CustomPaint(
|
||||
painter: _YanmeiMarkPainter(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _YanmeiMarkPainter extends CustomPainter {
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final w = size.width;
|
||||
final h = size.height;
|
||||
final paint = Paint()
|
||||
..color = Colors.white
|
||||
..strokeWidth = w * 0.055
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeCap = StrokeCap.round
|
||||
..strokeJoin = StrokeJoin.round;
|
||||
|
||||
// Mountain/wave path: M14 38 L22 22 L32 32 L42 22 L50 38 (on 64px grid)
|
||||
final path = Path();
|
||||
path.moveTo(w * 0.219, h * 0.594);
|
||||
path.lineTo(w * 0.344, h * 0.344);
|
||||
path.lineTo(w * 0.500, h * 0.500);
|
||||
path.lineTo(w * 0.656, h * 0.344);
|
||||
path.lineTo(w * 0.781, h * 0.594);
|
||||
canvas.drawPath(path, paint);
|
||||
|
||||
// Horizontal baseline: M12 46 L52 46 (on 64px grid)
|
||||
canvas.drawLine(
|
||||
Offset(w * 0.1875, h * 0.719),
|
||||
Offset(w * 0.8125, h * 0.719),
|
||||
paint,
|
||||
);
|
||||
|
||||
// Bordeaux dot: circle cx=32 cy=52 r=2.4 (on 64px grid)
|
||||
canvas.drawCircle(
|
||||
Offset(w * 0.500, h * 0.859),
|
||||
w * 0.042,
|
||||
Paint()
|
||||
..color = const Color(0xFFC97B86)
|
||||
..style = PaintingStyle.fill,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
class _InfoRow extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
|
||||
@@ -18,18 +18,18 @@
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="A new Flutter project.">
|
||||
<meta name="description" content="岩美酒库管理系统 — 酒店饮品库存与采购管理平台">
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="jiu_client">
|
||||
<meta name="apple-mobile-web-app-title" content="岩美">
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
|
||||
<title>jiu_client</title>
|
||||
<title>岩美</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "jiu_client",
|
||||
"short_name": "jiu_client",
|
||||
"name": "岩美",
|
||||
"short_name": "岩美",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"background_color": "#0175C2",
|
||||
"theme_color": "#0175C2",
|
||||
"description": "A new Flutter project.",
|
||||
"background_color": "#2563AC",
|
||||
"theme_color": "#2563AC",
|
||||
"description": "岩美酒库管理系统 — 酒店饮品库存与采购管理平台",
|
||||
"orientation": "portrait-primary",
|
||||
"prefer_related_applications": false,
|
||||
"icons": [
|
||||
|
||||
Reference in New Issue
Block a user