кастомизация темы — режим, AMOLED, расписание переключения
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class AppAmoled {
|
||||
static const prefKey = 'app_amoled';
|
||||
static final ValueNotifier<bool> current = ValueNotifier(false);
|
||||
|
||||
static Future<bool> load() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(prefKey) ?? false;
|
||||
}
|
||||
|
||||
static Future<void> save(bool value) async {
|
||||
current.value = value;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(prefKey, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
enum AppThemeMode { system, light, dark, schedule }
|
||||
|
||||
class AppThemeModeConfig {
|
||||
static const prefKey = 'app_theme_mode';
|
||||
static final ValueNotifier<AppThemeMode> current = ValueNotifier(
|
||||
AppThemeMode.system,
|
||||
);
|
||||
|
||||
static Future<AppThemeMode> load() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return _parse(prefs.getString(prefKey));
|
||||
}
|
||||
|
||||
static Future<void> save(AppThemeMode mode) async {
|
||||
current.value = mode;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(prefKey, mode.name);
|
||||
}
|
||||
|
||||
static AppThemeMode _parse(String? val) {
|
||||
switch (val) {
|
||||
case 'light':
|
||||
return AppThemeMode.light;
|
||||
case 'dark':
|
||||
return AppThemeMode.dark;
|
||||
case 'schedule':
|
||||
return AppThemeMode.schedule;
|
||||
default:
|
||||
return AppThemeMode.system;
|
||||
}
|
||||
}
|
||||
|
||||
static String label(AppThemeMode mode) {
|
||||
switch (mode) {
|
||||
case AppThemeMode.system:
|
||||
return 'Системная';
|
||||
case AppThemeMode.light:
|
||||
return 'Светлая';
|
||||
case AppThemeMode.dark:
|
||||
return 'Тёмная';
|
||||
case AppThemeMode.schedule:
|
||||
return 'По расписанию';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class ThemeSchedule {
|
||||
final TimeOfDay darkStart;
|
||||
final TimeOfDay darkEnd;
|
||||
|
||||
const ThemeSchedule({required this.darkStart, required this.darkEnd});
|
||||
|
||||
bool isDarkAt(DateTime now) {
|
||||
final nowMin = now.hour * 60 + now.minute;
|
||||
final startMin = darkStart.hour * 60 + darkStart.minute;
|
||||
final endMin = darkEnd.hour * 60 + darkEnd.minute;
|
||||
if (startMin == endMin) return false;
|
||||
if (startMin < endMin) {
|
||||
return nowMin >= startMin && nowMin < endMin;
|
||||
}
|
||||
return nowMin >= startMin || nowMin < endMin;
|
||||
}
|
||||
|
||||
Duration durationUntilNextSwitch(DateTime now) {
|
||||
final nowMin = now.hour * 60 + now.minute;
|
||||
final startMin = darkStart.hour * 60 + darkStart.minute;
|
||||
final endMin = darkEnd.hour * 60 + darkEnd.minute;
|
||||
int? nextMin;
|
||||
for (final m in [startMin, endMin]) {
|
||||
final delta = (m - nowMin + 1440) % 1440;
|
||||
final candidate = delta == 0 ? 1440 : delta;
|
||||
if (nextMin == null || candidate < nextMin) nextMin = candidate;
|
||||
}
|
||||
final secondsLeft = (nextMin ?? 1) * 60 - now.second;
|
||||
return Duration(seconds: secondsLeft.clamp(1, 24 * 60 * 60));
|
||||
}
|
||||
}
|
||||
|
||||
class AppThemeSchedule {
|
||||
static const prefKey = 'app_theme_schedule';
|
||||
static const _defaultStart = TimeOfDay(hour: 22, minute: 0);
|
||||
static const _defaultEnd = TimeOfDay(hour: 7, minute: 0);
|
||||
|
||||
static final ValueNotifier<ThemeSchedule> current = ValueNotifier(
|
||||
const ThemeSchedule(darkStart: _defaultStart, darkEnd: _defaultEnd),
|
||||
);
|
||||
|
||||
static Future<ThemeSchedule> load() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return _parse(prefs.getString(prefKey));
|
||||
}
|
||||
|
||||
static Future<void> save(ThemeSchedule schedule) async {
|
||||
current.value = schedule;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(
|
||||
prefKey,
|
||||
'${_fmt(schedule.darkStart)}-${_fmt(schedule.darkEnd)}',
|
||||
);
|
||||
}
|
||||
|
||||
static String _fmt(TimeOfDay t) =>
|
||||
'${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}';
|
||||
|
||||
static ThemeSchedule _parse(String? val) {
|
||||
if (val == null) {
|
||||
return const ThemeSchedule(
|
||||
darkStart: _defaultStart,
|
||||
darkEnd: _defaultEnd,
|
||||
);
|
||||
}
|
||||
final parts = val.split('-');
|
||||
if (parts.length != 2) {
|
||||
return const ThemeSchedule(
|
||||
darkStart: _defaultStart,
|
||||
darkEnd: _defaultEnd,
|
||||
);
|
||||
}
|
||||
final start = _parseTime(parts[0]) ?? _defaultStart;
|
||||
final end = _parseTime(parts[1]) ?? _defaultEnd;
|
||||
return ThemeSchedule(darkStart: start, darkEnd: end);
|
||||
}
|
||||
|
||||
static TimeOfDay? _parseTime(String val) {
|
||||
final parts = val.split(':');
|
||||
if (parts.length != 2) return null;
|
||||
final h = int.tryParse(parts[0]);
|
||||
final m = int.tryParse(parts[1]);
|
||||
if (h == null || m == null) return null;
|
||||
if (h < 0 || h > 23 || m < 0 || m > 59) return null;
|
||||
return TimeOfDay(hour: h, minute: m);
|
||||
}
|
||||
|
||||
static String format(TimeOfDay t) => _fmt(t);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import 'package:material_symbols_icons/symbols.dart';
|
||||
import '../../../core/utils/haptics.dart';
|
||||
import 'appearance_screen.dart';
|
||||
import 'font_settings_screen.dart';
|
||||
import 'theme_settings_screen.dart';
|
||||
|
||||
class _CustomizationCategory {
|
||||
final IconData icon;
|
||||
@@ -24,6 +25,12 @@ class CustomizationScreen extends StatelessWidget {
|
||||
const CustomizationScreen({super.key});
|
||||
|
||||
static const List<_CustomizationCategory> _categories = [
|
||||
_CustomizationCategory(
|
||||
icon: Symbols.dark_mode,
|
||||
title: 'Тема',
|
||||
subtitle: 'Светлая, тёмная, AMOLED, расписание',
|
||||
builder: _buildThemeSettings,
|
||||
),
|
||||
_CustomizationCategory(
|
||||
icon: Symbols.palette,
|
||||
title: 'Внешний вид',
|
||||
@@ -44,6 +51,9 @@ class CustomizationScreen extends StatelessWidget {
|
||||
static Widget _buildFontSettings(BuildContext context) =>
|
||||
const FontSettingsScreen();
|
||||
|
||||
static Widget _buildThemeSettings(BuildContext context) =>
|
||||
const ThemeSettingsScreen();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:m3e_collection/m3e_collection.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../../../core/config/app_amoled.dart';
|
||||
import '../../../core/config/app_theme_mode.dart';
|
||||
import '../../../core/config/app_theme_schedule.dart';
|
||||
import '../../../core/utils/haptics.dart';
|
||||
import '../../../main.dart';
|
||||
|
||||
class ThemeSettingsScreen extends StatelessWidget {
|
||||
const ThemeSettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: cs.surface,
|
||||
appBar: AppBarM3E(titleText: 'Тема', backgroundColor: cs.surface),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: ListView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 120),
|
||||
children: const [
|
||||
_ThemeModeCard(),
|
||||
SizedBox(height: 12),
|
||||
_AmoledCard(),
|
||||
SizedBox(height: 12),
|
||||
_ScheduleCard(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ThemeModeCard extends StatelessWidget {
|
||||
const _ThemeModeCard();
|
||||
|
||||
static const _items = [
|
||||
(mode: AppThemeMode.system, icon: Symbols.brightness_auto, label: 'Системная'),
|
||||
(mode: AppThemeMode.light, icon: Symbols.light_mode, label: 'Светлая'),
|
||||
(mode: AppThemeMode.dark, icon: Symbols.dark_mode, label: 'Тёмная'),
|
||||
(mode: AppThemeMode.schedule, icon: Symbols.schedule, label: 'По расписанию'),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Material(
|
||||
color: cs.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 18, 20, 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Режим темы',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Светлая, тёмная или авто-переключение',
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ValueListenableBuilder<AppThemeMode>(
|
||||
valueListenable: AppThemeModeConfig.current,
|
||||
builder: (context, current, _) {
|
||||
return Column(
|
||||
children: [
|
||||
for (final item in _items)
|
||||
_ModeTile(
|
||||
icon: item.icon,
|
||||
label: item.label,
|
||||
selected: current == item.mode,
|
||||
onTap: () {
|
||||
if (current == item.mode) return;
|
||||
Haptics.selection();
|
||||
KometApp.stateOf(context)?.applyThemeMode(item.mode);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ModeTile extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final bool selected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _ModeTile({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.selected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: cs.onSurface, size: 22, weight: 500),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
selected ? Symbols.radio_button_checked : Symbols.radio_button_unchecked,
|
||||
color: selected ? cs.primary : cs.outline,
|
||||
size: 22,
|
||||
fill: selected ? 1 : 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AmoledCard extends StatelessWidget {
|
||||
const _AmoledCard();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Material(
|
||||
color: cs.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 14, 12, 14),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Symbols.contrast, color: cs.onSurface, size: 24, weight: 500),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'AMOLED-чёрный',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'Чистый чёрный фон для OLED-экранов',
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable: AppAmoled.current,
|
||||
builder: (context, value, _) {
|
||||
return Switch(
|
||||
value: value,
|
||||
onChanged: (v) {
|
||||
Haptics.selection();
|
||||
KometApp.stateOf(context)?.applyAmoled(v);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ScheduleCard extends StatelessWidget {
|
||||
const _ScheduleCard();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return ValueListenableBuilder<AppThemeMode>(
|
||||
valueListenable: AppThemeModeConfig.current,
|
||||
builder: (context, mode, _) {
|
||||
final enabled = mode == AppThemeMode.schedule;
|
||||
return AnimatedOpacity(
|
||||
opacity: enabled ? 1 : 0.5,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Material(
|
||||
color: cs.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 18, 20, 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Расписание',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
enabled
|
||||
? 'Когда автоматически включается тёмная тема'
|
||||
: 'Доступно в режиме «По расписанию»',
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ValueListenableBuilder<ThemeSchedule>(
|
||||
valueListenable: AppThemeSchedule.current,
|
||||
builder: (context, schedule, _) {
|
||||
return Column(
|
||||
children: [
|
||||
_TimeRow(
|
||||
icon: Symbols.bedtime,
|
||||
label: 'Тёмная с',
|
||||
time: schedule.darkStart,
|
||||
enabled: enabled,
|
||||
onPick: (picked) {
|
||||
KometApp.stateOf(context)?.applyThemeSchedule(
|
||||
ThemeSchedule(
|
||||
darkStart: picked,
|
||||
darkEnd: schedule.darkEnd,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_TimeRow(
|
||||
icon: Symbols.wb_sunny,
|
||||
label: 'Светлая с',
|
||||
time: schedule.darkEnd,
|
||||
enabled: enabled,
|
||||
onPick: (picked) {
|
||||
KometApp.stateOf(context)?.applyThemeSchedule(
|
||||
ThemeSchedule(
|
||||
darkStart: schedule.darkStart,
|
||||
darkEnd: picked,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TimeRow extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final TimeOfDay time;
|
||||
final bool enabled;
|
||||
final ValueChanged<TimeOfDay> onPick;
|
||||
|
||||
const _TimeRow({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.time,
|
||||
required this.enabled,
|
||||
required this.onPick,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Material(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: enabled ? () => _pick(context) : null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: cs.onSurface, size: 22, weight: 500),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
AppThemeSchedule.format(time),
|
||||
style: TextStyle(
|
||||
color: cs.primary,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _pick(BuildContext context) async {
|
||||
Haptics.tap();
|
||||
final picked = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: time,
|
||||
builder: (ctx, child) => MediaQuery(
|
||||
data: MediaQuery.of(ctx).copyWith(alwaysUse24HourFormat: true),
|
||||
child: child ?? const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
if (picked != null) onPick(picked);
|
||||
}
|
||||
}
|
||||
+104
-2
@@ -8,10 +8,13 @@ import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'backend/api.dart';
|
||||
import 'core/config/app_accent.dart';
|
||||
import 'core/config/app_amoled.dart';
|
||||
import 'core/config/app_bubble_behavior.dart';
|
||||
import 'core/config/app_bubble_shape.dart';
|
||||
import 'core/config/app_cache_extent.dart';
|
||||
import 'core/config/app_fonts.dart';
|
||||
import 'core/config/app_theme_mode.dart';
|
||||
import 'core/config/app_theme_schedule.dart';
|
||||
import 'backend/modules/account.dart';
|
||||
import 'backend/modules/chats.dart';
|
||||
import 'backend/modules/contacts.dart';
|
||||
@@ -80,6 +83,9 @@ void main() async {
|
||||
AppBubbleShape.current.value = await AppBubbleShape.load();
|
||||
AppBubbleBehavior.current.value = await AppBubbleBehavior.load();
|
||||
AppCacheExtent.current.value = await AppCacheExtent.load();
|
||||
AppThemeModeConfig.current.value = await AppThemeModeConfig.load();
|
||||
AppAmoled.current.value = await AppAmoled.load();
|
||||
AppThemeSchedule.current.value = await AppThemeSchedule.load();
|
||||
runApp(
|
||||
KometApp(
|
||||
initialLocale: initialLocale,
|
||||
@@ -122,7 +128,7 @@ class KometApp extends StatefulWidget {
|
||||
State<KometApp> createState() => KometAppState();
|
||||
}
|
||||
|
||||
class KometAppState extends State<KometApp> {
|
||||
class KometAppState extends State<KometApp> with WidgetsBindingObserver {
|
||||
static const _fallbackSeed = Color(0xFFC1C4FF);
|
||||
|
||||
late Locale _locale;
|
||||
@@ -134,6 +140,7 @@ class KometAppState extends State<KometApp> {
|
||||
StreamSubscription<SessionExpiredException>? _sessionExpiredSub;
|
||||
StreamSubscription<LoginStatus>? _loginStatusSub;
|
||||
StreamSubscription<VpnBypassResult>? _vpnBypassSub;
|
||||
Timer? _scheduleTimer;
|
||||
String? _lastVpnNotice;
|
||||
DateTime _lastVpnNoticeAt = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
late final ValueNotifier<bool> fpsOverlayEnabled = ValueNotifier(
|
||||
@@ -157,6 +164,13 @@ class KometAppState extends State<KometApp> {
|
||||
_locale = widget.initialLocale;
|
||||
_fontId = widget.initialFontId;
|
||||
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
AppThemeModeConfig.current.addListener(_onThemeModeChanged);
|
||||
AppAmoled.current.addListener(_onAmoledChanged);
|
||||
AppThemeSchedule.current.addListener(_onScheduleChanged);
|
||||
_lastAppliedThemeMode = _effectiveThemeMode;
|
||||
_rescheduleSwitch();
|
||||
|
||||
api.setReconnectCallback(() async {
|
||||
try {
|
||||
final accountId = await TokenStorage.getActiveAccountId();
|
||||
@@ -228,6 +242,11 @@ class KometAppState extends State<KometApp> {
|
||||
_sessionExpiredSub?.cancel();
|
||||
_loginStatusSub?.cancel();
|
||||
_vpnBypassSub?.cancel();
|
||||
_scheduleTimer?.cancel();
|
||||
AppThemeModeConfig.current.removeListener(_onThemeModeChanged);
|
||||
AppAmoled.current.removeListener(_onAmoledChanged);
|
||||
AppThemeSchedule.current.removeListener(_onScheduleChanged);
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_profileUpdateController.close();
|
||||
fpsOverlayEnabled.dispose();
|
||||
vpnBypassEnabled.dispose();
|
||||
@@ -237,6 +256,79 @@ class KometAppState extends State<KometApp> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state != AppLifecycleState.resumed) return;
|
||||
if (AppThemeModeConfig.current.value != AppThemeMode.schedule) return;
|
||||
_rescheduleSwitch();
|
||||
final next = _effectiveThemeMode;
|
||||
if (next == _lastAppliedThemeMode) return;
|
||||
_lastAppliedThemeMode = next;
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
void _onThemeModeChanged() {
|
||||
_rescheduleSwitch();
|
||||
_lastAppliedThemeMode = _effectiveThemeMode;
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
void _onAmoledChanged() {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
void _onScheduleChanged() {
|
||||
if (AppThemeModeConfig.current.value != AppThemeMode.schedule) return;
|
||||
_rescheduleSwitch();
|
||||
final next = _effectiveThemeMode;
|
||||
if (next == _lastAppliedThemeMode) return;
|
||||
_lastAppliedThemeMode = next;
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
ThemeMode _lastAppliedThemeMode = ThemeMode.system;
|
||||
|
||||
void _rescheduleSwitch() {
|
||||
_scheduleTimer?.cancel();
|
||||
_scheduleTimer = null;
|
||||
if (AppThemeModeConfig.current.value != AppThemeMode.schedule) return;
|
||||
final until = AppThemeSchedule.current.value.durationUntilNextSwitch(
|
||||
DateTime.now(),
|
||||
);
|
||||
_scheduleTimer = Timer(until, () {
|
||||
if (!mounted) return;
|
||||
_lastAppliedThemeMode = _effectiveThemeMode;
|
||||
setState(() {});
|
||||
_rescheduleSwitch();
|
||||
});
|
||||
}
|
||||
|
||||
ThemeMode get _effectiveThemeMode {
|
||||
switch (AppThemeModeConfig.current.value) {
|
||||
case AppThemeMode.system:
|
||||
return ThemeMode.system;
|
||||
case AppThemeMode.light:
|
||||
return ThemeMode.light;
|
||||
case AppThemeMode.dark:
|
||||
return ThemeMode.dark;
|
||||
case AppThemeMode.schedule:
|
||||
final isDark = AppThemeSchedule.current.value.isDarkAt(DateTime.now());
|
||||
return isDark ? ThemeMode.dark : ThemeMode.light;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> applyThemeMode(AppThemeMode mode) async {
|
||||
await AppThemeModeConfig.save(mode);
|
||||
}
|
||||
|
||||
Future<void> applyAmoled(bool value) async {
|
||||
await AppAmoled.save(value);
|
||||
}
|
||||
|
||||
Future<void> applyThemeSchedule(ThemeSchedule schedule) async {
|
||||
await AppThemeSchedule.save(schedule);
|
||||
}
|
||||
|
||||
Future<void> setFpsOverlayEnabled(bool value) async {
|
||||
if (fpsOverlayEnabled.value == value) return;
|
||||
fpsOverlayEnabled.value = value;
|
||||
@@ -359,6 +451,16 @@ class KometAppState extends State<KometApp> {
|
||||
}
|
||||
|
||||
ColorScheme _adjustDarkScheme(ColorScheme base) {
|
||||
if (AppAmoled.current.value) {
|
||||
return base.copyWith(
|
||||
surface: Colors.black,
|
||||
surfaceContainerLowest: Colors.black,
|
||||
surfaceContainerLow: const Color(0xFF080808),
|
||||
surfaceContainer: const Color(0xFF101010),
|
||||
surfaceContainerHigh: const Color(0xFF161616),
|
||||
surfaceContainerHighest: const Color(0xFF1C1C1C),
|
||||
);
|
||||
}
|
||||
return base.copyWith(
|
||||
surface: Color.alphaBlend(
|
||||
base.primary.withValues(alpha: 0.05),
|
||||
@@ -423,7 +525,7 @@ class KometAppState extends State<KometApp> {
|
||||
title: 'Komet',
|
||||
debugShowCheckedModeBanner: false,
|
||||
locale: _locale,
|
||||
themeMode: ThemeMode.system,
|
||||
themeMode: _effectiveThemeMode,
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
theme: _lightTheme,
|
||||
|
||||
Reference in New Issue
Block a user