diff --git a/lib/domain/interfaces/config_provider.dart b/lib/domain/interfaces/config_provider.dart index 6bf8051..1ef219b 100644 --- a/lib/domain/interfaces/config_provider.dart +++ b/lib/domain/interfaces/config_provider.dart @@ -59,7 +59,17 @@ abstract class ConfigProvider extends ChangeNotifier { int get nightStartMinute; // Minute when night mode starts (0-59), default 0 set nightStartMinute(int value); - + + // black Screen in Darkness + bool get darkScreenEnabled; // Enable dark screen in darkness + set darkScreenEnabled(bool value); + + int get darkScreenThreshold; // Threshold for dark screen + set darkScreenThreshold(int value); + + int get darkScreenOffset; // Offset for hysteresis + set darkScreenOffset(int value); + bool get useNativeScreenOff; // Use Device Admin lockNow() for true screen off (Android) set useNativeScreenOff(bool value); diff --git a/lib/infrastructure/services/json_config_service.dart b/lib/infrastructure/services/json_config_service.dart index b798b1f..a90005a 100644 --- a/lib/infrastructure/services/json_config_service.dart +++ b/lib/infrastructure/services/json_config_service.dart @@ -232,6 +232,31 @@ class JsonConfigService extends ConfigProvider { set useNativeScreenOff(bool value) { _config['use_native_screen_off'] = value; } + + // Dark screen in darkeness settings + @override + bool get darkScreenEnabled => _config['dark_screen_enabled'] ?? false; + + @override + set darkScreenEnabled(bool value) { + _config['dark_screen_enabled'] = value; + } + + @override + int get darkScreenThreshold => _config['dark_screen_threshold'] ?? 10; + + @override + set darkScreenThreshold(int value) { + _config['dark_screen_threshold'] = value; + } + + @override + int get darkScreenOffset => _config['dark_screen_offset'] ?? 5; + + @override + set darkScreenOffset(int value) { + _config['dark_screen_offset'] = value; + } // Custom photo directory (for "local folder" mode) @override diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 9c09f73..9663f9f 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -122,7 +122,13 @@ "grantDeviceAdmin": "Geräte-Admin gewähren", "deviceAdminEnabled": "Geräte-Admin aktiviert - Bildschirm wird komplett ausgeschaltet", "screenLockWarning": "Wichtig: Die Bildschirmsperre (PIN/Muster/Passwort) muss deaktiviert sein, damit das automatische Aufwachen funktioniert. Gehe zu Einstellungen → Sicherheit → Bildschirmsperre → Keine.", - + + "darkScreenEnabled": "Bildschirm bei Dunkelheit dunkel schalten", + "darkScreenEnabledSubtitle": "Keine Anzeige in der Dunkelheit", + "darkScreenThreshold": "Schwellenwert für dunklen Bildschirm", + "darkScreenOffset": "Einschaltverzögerung", + + "deviceAdminActive": "Geräte-Admin aktiv", "deviceAdminUninstallWarning": "Um diese App zu deinstallieren, muss zuerst die Geräte-Admin-Berechtigung in den Android-Einstellungen deaktiviert werden.", "openDeviceAdminSettings": "Geräte-Admin-Einstellungen öffnen", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 300b167..a8f800c 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -122,7 +122,12 @@ "grantDeviceAdmin": "Grant Device Admin", "deviceAdminEnabled": "Device Admin enabled - screen will turn off completely", "screenLockWarning": "Important: Screen lock (PIN/Pattern/Password) must be disabled for automatic wake-up to work. Go to Settings → Security → Screen lock → None.", - + + "darkScreenEnabled": "Dark screen in darkness", + "darkScreenEnabledSubtitle": "No picture displayed in darkness", + "darkScreenThreshold": "Threshold for dark screen", + "darkScreenOffset": "Offset for screen on", + "deviceAdminActive": "Device Admin Active", "deviceAdminUninstallWarning": "To uninstall this app, you must first disable Device Admin permission in Android settings.", "openDeviceAdminSettings": "Open Device Admin Settings", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 2982ac9..8fbfd77 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -530,6 +530,30 @@ abstract class AppLocalizations { /// **'Important: Screen lock (PIN/Pattern/Password) must be disabled for automatic wake-up to work. Go to Settings → Security → Screen lock → None.'** String get screenLockWarning; + /// No description provided for @darkScreenEnabled. + /// + /// In en, this message translates to: + /// **'Dark screen in darkness'** + String get darkScreenEnabled; + + /// No description provided for @darkScreenEnabledSubtitle. + /// + /// In en, this message translates to: + /// **'No picture displayed in darkness'** + String get darkScreenEnabledSubtitle; + + /// No description provided for @darkScreenThreshold. + /// + /// In en, this message translates to: + /// **'Threshold for dark screen'** + String get darkScreenThreshold; + + /// No description provided for @darkScreenOffset. + /// + /// In en, this message translates to: + /// **'Offset for screen on'** + String get darkScreenOffset; + /// No description provided for @deviceAdminActive. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 875fc4c..fea62f9 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -248,6 +248,18 @@ class AppLocalizationsDe extends AppLocalizations { String get screenLockWarning => 'Wichtig: Die Bildschirmsperre (PIN/Muster/Passwort) muss deaktiviert sein, damit das automatische Aufwachen funktioniert. Gehe zu Einstellungen → Sicherheit → Bildschirmsperre → Keine.'; + @override + String get darkScreenEnabled => 'Bildschirm bei Dunkelheit dunkel schalten'; + + @override + String get darkScreenEnabledSubtitle => 'Keine Anzeige in der Dunkelheit'; + + @override + String get darkScreenThreshold => 'Schwellenwert für dunklen Bildschirm'; + + @override + String get darkScreenOffset => 'Einschaltverzögerung'; + @override String get deviceAdminActive => 'Geräte-Admin aktiv'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index a1ce11a..c430328 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -246,6 +246,18 @@ class AppLocalizationsEn extends AppLocalizations { String get screenLockWarning => 'Important: Screen lock (PIN/Pattern/Password) must be disabled for automatic wake-up to work. Go to Settings → Security → Screen lock → None.'; + @override + String get darkScreenEnabled => 'Dark screen in darkness'; + + @override + String get darkScreenEnabledSubtitle => 'No picture displayed in darkness'; + + @override + String get darkScreenThreshold => 'Threshold for dark screen'; + + @override + String get darkScreenOffset => 'Offset for screen on'; + @override String get deviceAdminActive => 'Device Admin Active'; diff --git a/lib/ui/screens/settings_screen.dart b/lib/ui/screens/settings_screen.dart index c133a35..5ac9ac5 100644 --- a/lib/ui/screens/settings_screen.dart +++ b/lib/ui/screens/settings_screen.dart @@ -1,8 +1,10 @@ import 'dart:io'; +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:light_sensor/light_sensor.dart'; import '../../l10n/app_localizations.dart'; import 'package:path_provider/path_provider.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -52,6 +54,13 @@ class _SettingsScreenState extends State with WidgetsBindingObse late TimeOfDay _nightStartTime; late bool _useNativeScreenOff; bool _deviceAdminEnabled = false; + + // Auto dark screen settings + late bool _darkScreenEnabled; + late int _darkScreenThreshold; + late int _darkScreenOffset; + int? _currentLux; + StreamSubscription? _luxSubscription; // Screen orientation setting late String _screenOrientation; @@ -118,7 +127,20 @@ class _SettingsScreenState extends State with WidgetsBindingObse _dayStartTime = TimeOfDay(hour: config.dayStartHour, minute: config.dayStartMinute); _nightStartTime = TimeOfDay(hour: config.nightStartHour, minute: config.nightStartMinute); _useNativeScreenOff = config.useNativeScreenOff; - + + //Dark screen setting + _darkScreenEnabled = config.darkScreenEnabled; + _darkScreenThreshold = config.darkScreenThreshold; + _darkScreenOffset = config.darkScreenOffset; + + LightSensor.hasSensor().then((has) { + if (has) { + _luxSubscription = LightSensor.luxStream().listen((lux) { + if (mounted) setState(() => _currentLux = lux); + }); + } + }); + // Screen orientation _screenOrientation = config.screenOrientation; @@ -180,6 +202,8 @@ class _SettingsScreenState extends State with WidgetsBindingObse void dispose() { WidgetsBinding.instance.removeObserver(this); _nextcloudUrlController.dispose(); + //_luxSubscription?.cancel(); + _luxSubscription = null; super.dispose(); } @@ -236,7 +260,13 @@ class _SettingsScreenState extends State with WidgetsBindingObse config.nightStartHour = _nightStartTime.hour; config.nightStartMinute = _nightStartTime.minute; config.useNativeScreenOff = _useNativeScreenOff; - + + // Display off in darkness settings + config.darkScreenEnabled = _darkScreenEnabled; + config.darkScreenThreshold = _darkScreenThreshold; + config.darkScreenOffset = _darkScreenOffset; + + // Screen orientation config.screenOrientation = _screenOrientation; @@ -463,9 +493,28 @@ class _SettingsScreenState extends State with WidgetsBindingObse const SizedBox(height: 24), const Divider(), const SizedBox(height: 16), - + // === ANDROID SETTINGS (only on Android) === if (Platform.isAndroid) ...[ + + const SizedBox(height: 8), + + SwitchListTile( + title: Text(AppLocalizations.of(context)!.darkScreenEnabled), + subtitle: Text(AppLocalizations.of(context)!.darkScreenEnabledSubtitle), + secondary: const Icon(Icons.nightlight_round), + value: _darkScreenEnabled, + onChanged: (value) { + setState(() => _darkScreenEnabled = value); + }, + ), + + if (_darkScreenEnabled) _buildDarkScreenSettings(), + + const SizedBox(height: 24), + const Divider(), + const SizedBox(height: 16), + _buildSectionHeader(AppLocalizations.of(context)!.sectionAndroid), const SizedBox(height: 8), @@ -511,7 +560,9 @@ class _SettingsScreenState extends State with WidgetsBindingObse setState(() => _keepAliveEnabled = value); }, ), - + + + const SizedBox(height: 24), const Divider(), const SizedBox(height: 16), @@ -1514,6 +1565,71 @@ class _SettingsScreenState extends State with WidgetsBindingObse ], ]; } + + Widget _buildDarkScreenSettings() { + final displayValue = '${(_darkScreenThreshold)} lx'; + return Padding( + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.mode_night_outlined, size: 20), + const SizedBox(width: 12), + Expanded(child: Text(AppLocalizations.of(context)!.darkScreenThreshold)), + if (_currentLux != null) ...[ + Icon(Icons.wb_sunny_outlined, size: 16, color: Colors.grey), + const SizedBox(width: 4), + Text( + '${_currentLux!.round()} lx', + style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey), + ), + const SizedBox(width: 12), + ], + Text( + displayValue, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + Slider( + value: _darkScreenThreshold.toDouble(), + min: 0, + max: 100, + divisions: 100, + onChanged: (value) { + setState(() => _darkScreenThreshold = value.round()); + }, + ), + Row( + children: [ + const Icon(Icons.brightness_6, size: 20), + const SizedBox(width: 12), + Expanded(child: Text(AppLocalizations.of(context)!.darkScreenOffset)), + Text( + '${(_darkScreenOffset)} lx', + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + Slider( + value: _darkScreenOffset.toDouble(), + min: 1, + max: 20, + divisions: 20, + onChanged: (value) { + setState(() => _darkScreenOffset = value.round()); + }, + ), + ], + ) + ); + } Future _selectTime({required bool isDay}) async { final initialTime = isDay ? _dayStartTime : _nightStartTime; diff --git a/lib/ui/screens/slideshow_screen.dart b/lib/ui/screens/slideshow_screen.dart index 81b0941..18c1f82 100644 --- a/lib/ui/screens/slideshow_screen.dart +++ b/lib/ui/screens/slideshow_screen.dart @@ -5,6 +5,7 @@ import 'package:flutter/services.dart'; import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; +import 'package:light_sensor/light_sensor.dart'; import '../../l10n/app_localizations.dart'; import '../../domain/interfaces/config_provider.dart'; import '../../domain/interfaces/display_controller.dart'; @@ -68,8 +69,15 @@ class _SlideshowScreenState extends State with TickerProviderSt StreamSubscription? _scheduleSubscription; Timer? _scheduleTimer; bool _scheduleWasEnabled = false; // Track previous state for detecting changes - + // Display off state for black overlay + bool _scheduleWantsDark = false; + bool _sensorWantsDark = false; + int _sensorValue = 1000; + StreamSubscription? _lightSubscription; + bool _darkScreenEnabled = false; + late int _darkThreshold; + late int _lightThreshold; bool _isDisplayOff = false; // Current photo location name (from geocoding) @@ -92,6 +100,7 @@ class _SlideshowScreenState extends State with TickerProviderSt WidgetsBinding.instance.addPostFrameCallback((_) { _initService(); _initKeepAliveService(); + _initLightSensor(); // Schedule init is now handled reactively in build() via _updateDisplaySchedule() }); } @@ -252,21 +261,18 @@ class _SlideshowScreenState extends State with TickerProviderSt ? displayController : null; + _scheduleWantsDark = isNight; + if (isNight && !_isDisplayOff) { - // Switch to night mode (screen off) print('📺 Switching to NIGHT mode (screen off), wake at $nextTransition'); - if (config.useNativeScreenOff && nativeController != null) { await nativeController.sleepUntil(nextTransition); } else { await displayController.setMode(DisplayMode.off); } if (mounted) setState(() => _isDisplayOff = true); - - } else if (!isNight && _isDisplayOff) { - // Switch to day mode (screen on) + } else if (!isNight && _isDisplayOff && !_sensorWantsDark) { print('📺 Switching to DAY mode (screen on)'); - if (nativeController != null) { await nativeController.wakeNow(); } else { @@ -365,20 +371,16 @@ class _SlideshowScreenState extends State with TickerProviderSt } void _openSettings() { - _timer?.cancel(); // Stop auto-advance while in settings - + _timer?.cancel(); + Navigator.of(context).push( MaterialPageRoute(builder: (context) => const SettingsScreen()), ).then((_) { - // Restore immersive mode after returning from settings SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); - - // Re-apply configured screen orientation final config = context.read(); SystemChrome.setPreferredOrientations(_getDeviceOrientations(config.screenOrientation)); - - // Restart timer when returning from settings _startTimer(); + _initLightSensor(); }); } @@ -551,6 +553,7 @@ class _SlideshowScreenState extends State with TickerProviderSt _timer?.cancel(); _photosSubscription?.cancel(); _scheduleSubscription?.cancel(); + _lightSubscription?.cancel(); _scheduleTimer?.cancel(); for (var slide in _slides) { slide.controller.dispose(); @@ -561,6 +564,7 @@ class _SlideshowScreenState extends State with TickerProviderSt @override Widget build(BuildContext context) { + _log.info("build"); // Cache screen size for optimized image loading // IMPORTANT: Use physical pixels (multiply by devicePixelRatio) final mediaQuerySize = MediaQuery.of(context).size; @@ -576,10 +580,16 @@ class _SlideshowScreenState extends State with TickerProviderSt _screenSize = physicalSize; } final config = context.watch(); - + // React to schedule config changes _updateDisplaySchedule(config); - + + _darkScreenEnabled = config.darkScreenEnabled; + _darkThreshold = config.darkScreenThreshold; + _lightThreshold = _darkThreshold + config.darkScreenOffset; + + WidgetsBinding.instance.addPostFrameCallback((_) => _applyLightValue()); + if (_isLoading) { return const Scaffold( body: Center(child: CircularProgressIndicator()), @@ -729,6 +739,49 @@ class _SlideshowScreenState extends State with TickerProviderSt config.addListener(_onConfigChanged); } + Future _initLightSensor() async { + final hasSensor = await LightSensor.hasSensor(); + if (!hasSensor) return; + _lightSubscription = LightSensor.luxStream().listen((lux) { + if (mounted) { + setState(() { + _sensorValue = lux; + _applyLightValue(); + }); + } + }); + } + + void _applyLightValue(){ + bool newValue; + if (_sensorValue < _darkThreshold) { + newValue = true; + } else if (_sensorValue > _lightThreshold) { + newValue = false; + } else { + return; // Hysterese-Zone – nichts tun + } + + if (newValue != _sensorWantsDark) { + _sensorWantsDark = newValue; + _applyDisplayState(); + } + } + + void _applyDisplayState() { + final shouldBeOff = _scheduleWantsDark || (_sensorWantsDark && _darkScreenEnabled); + + if (shouldBeOff == _isDisplayOff || !mounted) return; + + if (shouldBeOff) { + final displayController = context.read(); + displayController.setMode(DisplayMode.off); + setState(() => _isDisplayOff = true); + } else { + _restoreDisplay(); + } + } + /// Handle config changes for Keep Alive service void _onConfigChanged() { final config = context.read(); diff --git a/pubspec.yaml b/pubspec.yaml index 0a2de9e..58020f3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,6 +50,7 @@ dependencies: exif_reader: ^4.0.2 http: ^1.2.0 intl: ^0.20.2 + light_sensor: ^3.0.2 dev_dependencies: flutter_test: