214 lines
7.8 KiB
Dart
214 lines
7.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:package_info_plus/package_info_plus.dart';
|
|
import 'package:share_plus/share_plus.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
|
|
import '../../../core/build_config.dart';
|
|
import '../../../core/design/tokens.dart';
|
|
import '../../../core/design/tripz_colors.dart';
|
|
import '../../../core/design/typography.dart';
|
|
import '../../../core/di.dart';
|
|
import '../../../core/l10n/l10n.dart';
|
|
import '../../../core/router.dart';
|
|
import '../../../core/session/session_cubit.dart';
|
|
import '../../../core/store_links.dart';
|
|
import '../../../core/ui/tripz_scaffold.dart';
|
|
import '../../profile/data/profile_repository.dart';
|
|
import '../cubit/settings_cubit.dart';
|
|
import '../cubit/settings_state.dart';
|
|
|
|
/// المظهر واللغة ومعلومات التطبيق — بأقسام كما في سيرو
|
|
/// (`setting_page.dart`: General · Preferences · Support & Info) بدل قائمة
|
|
/// مسطّحة بلا عناوين.
|
|
///
|
|
/// تبديل اللغة **يقلب الاتجاه كاملاً** بلا إعادة تشغيل — `MaterialApp`
|
|
/// يشتقّ RTL/LTR من الـlocale (docs/26 §4).
|
|
class SettingsPage extends StatelessWidget {
|
|
const SettingsPage({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = context.l10n;
|
|
return BlocBuilder<SettingsCubit, SettingsState>(
|
|
builder: (context, state) {
|
|
final cubit = context.read<SettingsCubit>();
|
|
return TripzScaffold(
|
|
title: l10n.settingsTitle,
|
|
child: ListView(
|
|
children: [
|
|
_SectionHeader(title: l10n.settingsSectionGeneral),
|
|
Text(l10n.settingsTheme, style: context.texts.titleSmall),
|
|
RadioGroup<ThemeMode>(
|
|
groupValue: state.themeMode,
|
|
onChanged: (v) => cubit.setThemeMode(v ?? ThemeMode.system),
|
|
child: Column(
|
|
children: [
|
|
RadioListTile(
|
|
value: ThemeMode.system,
|
|
contentPadding: EdgeInsets.zero,
|
|
title: Text(l10n.settingsThemeSystem),
|
|
),
|
|
RadioListTile(
|
|
value: ThemeMode.light,
|
|
contentPadding: EdgeInsets.zero,
|
|
title: Text(l10n.settingsThemeLight),
|
|
),
|
|
RadioListTile(
|
|
value: ThemeMode.dark,
|
|
contentPadding: EdgeInsets.zero,
|
|
title: Text(l10n.settingsThemeDark),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: Space.md),
|
|
Text(l10n.settingsLanguage, style: context.texts.titleSmall),
|
|
RadioGroup<String>(
|
|
groupValue: state.locale.languageCode,
|
|
// اللغة تُحفظ محلياً **وتُرسل للخادم**: الإشعارات تُصاغ على
|
|
// الخادم بلغة المستخدم المخزّنة عنده (`PATCH /users/me`
|
|
// يقبل `language` — docs/38 §3)، فلغةٌ محليّة وحدها تعني
|
|
// واجهةً عربية وإشعاراتٍ إنجليزية.
|
|
onChanged: (v) => _setLanguage(context, v ?? 'ar'),
|
|
child: Column(
|
|
children: [
|
|
RadioListTile(
|
|
value: 'ar',
|
|
contentPadding: EdgeInsets.zero,
|
|
title: Text(l10n.settingsLanguageArabic),
|
|
),
|
|
RadioListTile(
|
|
value: 'en',
|
|
contentPadding: EdgeInsets.zero,
|
|
title: Text(l10n.settingsLanguageEnglish),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
_SectionHeader(title: l10n.settingsSectionSupport),
|
|
_Tile(
|
|
icon: Icons.help_outline_rounded,
|
|
title: l10n.menuSupport,
|
|
subtitle: l10n.settingsSupportLead,
|
|
onTap: () => context.push(Routes.support),
|
|
),
|
|
_Tile(
|
|
icon: Icons.share_outlined,
|
|
title: l10n.menuShare,
|
|
subtitle: l10n.settingsShareLead,
|
|
onTap: () => SharePlus.instance.share(
|
|
ShareParams(text: l10n.shareMessagePlain(StoreLinks.thisApp)),
|
|
),
|
|
),
|
|
_Tile(
|
|
icon: Icons.shield_outlined,
|
|
title: l10n.menuPrivacy,
|
|
onTap: () async {
|
|
final uri = Uri.tryParse(BuildConfig.privacyUrl);
|
|
if (uri == null) return;
|
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
|
},
|
|
),
|
|
const _VersionTile(),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// فشل الخادم **لا يمنع** تبديل اللغة محلياً: الواجهة تستجيب فوراً،
|
|
/// والمزامنة تُعاد عند التبديل التالي. عكسه يعني زرّاً لا يستجيب بلا شبكة.
|
|
static Future<void> _setLanguage(BuildContext context, String code) async {
|
|
await context.read<SettingsCubit>().setLocale(Locale(code));
|
|
try {
|
|
final user = await sl<ProfileRepository>().update(language: code);
|
|
sl<SessionCubit>().onProfileCompleted(user);
|
|
} catch (_) {
|
|
// المزامنة أفضل-جهد — لا رسالة خطأ على تبديل نجح محلياً.
|
|
}
|
|
}
|
|
}
|
|
|
|
class _SectionHeader extends StatelessWidget {
|
|
const _SectionHeader({required this.title});
|
|
|
|
final String title;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(top: Space.lg, bottom: Space.xs),
|
|
child: Text(
|
|
title,
|
|
style: context.texts.labelMedium?.copyWith(
|
|
color: context.colors.primary,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _Tile extends StatelessWidget {
|
|
const _Tile({
|
|
required this.icon,
|
|
required this.title,
|
|
required this.onTap,
|
|
this.subtitle,
|
|
});
|
|
|
|
final IconData icon;
|
|
final String title;
|
|
final String? subtitle;
|
|
final VoidCallback onTap;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ListTile(
|
|
contentPadding: EdgeInsets.zero,
|
|
leading: Icon(icon),
|
|
title: Text(title),
|
|
subtitle: subtitle == null ? null : Text(subtitle!),
|
|
trailing: Icon(
|
|
Icons.chevron_left_rounded,
|
|
color: context.colors.onSurfaceVariant,
|
|
),
|
|
onTap: onTap,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// رقم الإصدار — أول ما يُسأل عنه في الدعم، وكان غائباً رغم وجود مفتاح
|
|
/// ترجمته أصلاً.
|
|
class _VersionTile extends StatelessWidget {
|
|
const _VersionTile();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = context.l10n;
|
|
return FutureBuilder<PackageInfo>(
|
|
future: PackageInfo.fromPlatform(),
|
|
builder: (context, snapshot) {
|
|
final info = snapshot.data;
|
|
return ListTile(
|
|
contentPadding: EdgeInsets.zero,
|
|
leading: const Icon(Icons.info_outline_rounded),
|
|
title: Text(l10n.aboutVersion),
|
|
trailing: Text(
|
|
// قبل وصول المعلومة يُترك المكان فارغاً لا «—»: القيمة تصل
|
|
// في إطار واحد، والشرطة تومض بلا داعٍ.
|
|
info == null ? '' : '${info.version} (${info.buildNumber})',
|
|
textDirection: TextDirection.ltr,
|
|
style: numericStyle(context.texts.bodySmall).copyWith(
|
|
color: context.colors.onSurfaceVariant,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|