Skip to content

Commit 82fcdc2

Browse files
committed
✨ Add default Light theme and theme-aware map tiles
📝 Summary: Changes default theme from "System" to "Light Mode" for first-time users. Implements theme-aware map tiles that automatically switch appearance based on the current app theme using CartoDB Voyager with color inversion for dark mode. 🔧 Changes: - 🌞 Set default theme to Light Mode (was System) - 🗺️ Add theme-aware map tiles with inverted Voyager for dark mode - 🎨 Implement ColorFiltered widget to invert light tiles in dark theme - 📚 Add comprehensive CartoDB basemap research documentation - ✅ Update tests for new default theme setting 📁 Files Changed: - lib/main.dart (default theme fallback) - lib/src/services/settings_service.dart (default theme value) - lib/src/services/offline/offline_map_service.dart (inverted Voyager tiles) - lib/src/presentation/widgets/map_selection_widget.dart (dark mode filter) - lib/src/presentation/screens/map_picker_screen.dart (dark mode filter) - test/services/settings_service_test.dart (test expectations) - docs/research/cartodb_basemap_dark_mode_visibility.md (new research doc) ✅ Verification: All 289 tests pass, no analyzer issues
1 parent 5a28d71 commit 82fcdc2

7 files changed

Lines changed: 531 additions & 18 deletions

File tree

docs/research/cartodb_basemap_dark_mode_visibility.md

Lines changed: 414 additions & 0 deletions
Large diffs are not rendered by default.

lib/main.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Future<void> main() async {
2020
// Pre-initialize static notifiers from SharedPreferences to avoid race condition
2121
// This ensures ValueListenableBuilders have correct values when the widget tree is built
2222
final prefs = await SharedPreferences.getInstance();
23-
final themeMode = prefs.getString('themeMode') ?? 'system';
23+
final themeMode = prefs.getString('themeMode') ?? 'light';
2424
final languageCode = prefs.getString('locale') ?? 'en';
2525

2626
SettingsService.themeModeNotifier.value = themeMode;

lib/src/presentation/screens/map_picker_screen.dart

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -249,17 +249,29 @@ class _MapPickerScreenState extends State<MapPickerScreen>
249249
}
250250

251251
/// Builds the tile layer, using cached tiles when available.
252+
/// The tile style automatically adjusts based on the current theme (light/dark).
253+
/// For dark mode, CartoDB Voyager tiles are inverted using ColorFiltered.
252254
Widget _buildTileLayer() {
255+
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
256+
257+
Widget tileLayer;
258+
253259
try {
254260
final offlineMapService = getIt<OfflineMapService>();
255-
return offlineMapService.getCachedTileLayer();
261+
tileLayer = offlineMapService.getThemedCachedTileLayer(
262+
isDarkMode: isDarkMode,
263+
);
256264
} catch (_) {
257265
// Fall back to network tiles if service not initialized
258-
return TileLayer(
259-
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
260-
userAgentPackageName: 'com.ph_fare_calculator',
261-
);
266+
tileLayer = OfflineMapService.getNetworkTileLayer(isDarkMode: isDarkMode);
262267
}
268+
269+
// Apply color inversion for dark mode to create dark appearance from Voyager tiles
270+
if (isDarkMode) {
271+
return OfflineMapService.wrapWithDarkModeFilter(tileLayer);
272+
}
273+
274+
return tileLayer;
263275
}
264276

265277
Widget _buildAnimatedCenterPin(ColorScheme colorScheme) {

lib/src/presentation/widgets/map_selection_widget.dart

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -258,20 +258,35 @@ class _MapSelectionWidgetState extends State<MapSelectionWidget>
258258
}
259259

260260
/// Builds the tile layer, using cached tiles when available.
261+
/// The tile style automatically adjusts based on the current theme (light/dark).
262+
/// For dark mode, CartoDB Voyager tiles are inverted using ColorFiltered.
261263
Widget _buildTileLayer() {
264+
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
265+
266+
Widget tileLayer;
267+
262268
if (widget.useCachedTiles) {
263269
try {
264270
final offlineMapService = getIt<OfflineMapService>();
265-
return offlineMapService.getCachedTileLayer();
271+
tileLayer = offlineMapService.getThemedCachedTileLayer(
272+
isDarkMode: isDarkMode,
273+
);
266274
} catch (_) {
267275
// Fall back to network tiles if service not initialized
276+
tileLayer = OfflineMapService.getNetworkTileLayer(
277+
isDarkMode: isDarkMode,
278+
);
268279
}
280+
} else {
281+
tileLayer = OfflineMapService.getNetworkTileLayer(isDarkMode: isDarkMode);
269282
}
270283

271-
return TileLayer(
272-
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
273-
userAgentPackageName: 'com.ph_fare_calculator',
274-
);
284+
// Apply color inversion for dark mode to create dark appearance from Voyager tiles
285+
if (isDarkMode) {
286+
return OfflineMapService.wrapWithDarkModeFilter(tileLayer);
287+
}
288+
289+
return tileLayer;
275290
}
276291

277292
List<Marker> _buildMarkers(BuildContext context) {

lib/src/services/offline/offline_map_service.dart

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:async';
22
import 'dart:math' as math;
33

4+
import 'package:flutter/material.dart';
45
import 'package:flutter_map/flutter_map.dart';
56
import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart' as fmtc;
67
import 'package:hive/hive.dart';
@@ -532,21 +533,92 @@ class OfflineMapService {
532533
await _regionsBox?.clear();
533534
}
534535

536+
/// CartoDB Voyager tile URL - used for both light and dark mode
537+
/// Voyager has excellent road visibility and supports zoom levels 0-20
538+
/// For dark mode, we apply a color inversion filter at the widget level
539+
static const String _voyagerTileUrl =
540+
'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png';
541+
542+
/// Subdomains for CartoDB tile servers (load balancing)
543+
static const List<String> _cartoSubdomains = ['a', 'b', 'c', 'd'];
544+
545+
/// Color inversion matrix for dark mode
546+
/// This inverts the light Voyager tiles to create a dark appearance with visible roads
547+
static const ColorFilter darkModeInvertFilter = ColorFilter.matrix(<double>[
548+
-1,
549+
0,
550+
0,
551+
0,
552+
255,
553+
0,
554+
-1,
555+
0,
556+
0,
557+
255,
558+
0,
559+
0,
560+
-1,
561+
0,
562+
255,
563+
0,
564+
0,
565+
0,
566+
1,
567+
0,
568+
]);
569+
570+
/// Wraps a widget with the dark mode color inversion filter
571+
///
572+
/// Use this to wrap TileLayer widgets when displaying maps in dark mode.
573+
/// The filter inverts the light Voyager tiles to create a dark appearance.
574+
static Widget wrapWithDarkModeFilter(Widget child) {
575+
return ColorFiltered(colorFilter: darkModeInvertFilter, child: child);
576+
}
577+
535578
/// Gets a tile layer that uses the FMTC cache.
536579
///
537580
/// Falls back to network tiles when cache misses occur.
581+
/// Uses light mode tiles by default.
538582
TileLayer getCachedTileLayer() {
583+
return getThemedCachedTileLayer(isDarkMode: false);
584+
}
585+
586+
/// Gets a theme-aware tile layer that uses the FMTC cache.
587+
///
588+
/// Uses CartoDB Voyager tiles for both light and dark mode.
589+
/// For dark mode, the calling widget should wrap this with [wrapWithDarkModeFilter].
590+
/// Falls back to network tiles when cache misses occur.
591+
TileLayer getThemedCachedTileLayer({required bool isDarkMode}) {
539592
_ensureInitialized();
540593

594+
// Both light and dark mode use Voyager tiles
595+
// Dark mode applies color inversion at the widget level
541596
return TileLayer(
542-
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
597+
urlTemplate: _voyagerTileUrl,
598+
subdomains: _cartoSubdomains,
543599
userAgentPackageName: 'com.ph_fare_calculator',
600+
maxZoom: 20, // Voyager supports up to zoom 20
544601
tileProvider: fmtc.FMTCTileProvider(
545602
stores: {_storeName: fmtc.BrowseStoreStrategy.readUpdateCreate},
546603
),
547604
);
548605
}
549606

607+
/// Gets a tile layer without FMTC caching (for fallback scenarios).
608+
///
609+
/// Uses CartoDB Voyager tiles for both light and dark mode.
610+
/// For dark mode, the calling widget should wrap this with [wrapWithDarkModeFilter].
611+
static TileLayer getNetworkTileLayer({required bool isDarkMode}) {
612+
// Both light and dark mode use Voyager tiles
613+
// Dark mode applies color inversion at the widget level
614+
return TileLayer(
615+
urlTemplate: _voyagerTileUrl,
616+
subdomains: _cartoSubdomains,
617+
userAgentPackageName: 'com.ph_fare_calculator',
618+
maxZoom: 20, // Voyager supports up to zoom 20
619+
);
620+
}
621+
550622
/// Checks if a point is within any downloaded region.
551623
bool isPointCached(LatLng point) {
552624
for (final region in _allRegions) {

lib/src/services/settings_service.dart

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,8 @@ class SettingsService {
2020
static const String _keyLastLocationName = 'last_known_location_name';
2121

2222
/// Notifier for theme mode changes. Values: 'system', 'light', 'dark'
23-
static final ValueNotifier<String> themeModeNotifier = ValueNotifier(
24-
'system',
25-
);
23+
/// Default is 'light' for first-time users.
24+
static final ValueNotifier<String> themeModeNotifier = ValueNotifier('light');
2625
static final ValueNotifier<Locale> localeNotifier = ValueNotifier(
2726
const Locale('en'),
2827
);
@@ -64,10 +63,10 @@ class SettingsService {
6463
}
6564

6665
/// Get the theme mode preference. Returns 'system', 'light', or 'dark'.
67-
/// Default is 'system' (follows device settings).
66+
/// Default is 'light' for first-time users.
6867
Future<String> getThemeMode() async {
6968
final prefs = await SharedPreferences.getInstance();
70-
final value = prefs.getString(_keyThemeMode) ?? 'system';
69+
final value = prefs.getString(_keyThemeMode) ?? 'light';
7170
// Validate the value
7271
if (value != 'system' && value != 'light' && value != 'dark') {
7372
return 'system';

test/services/settings_service_test.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ void main() {
1414
test('Default values are correct', () async {
1515
expect(await settingsService.getProvincialMode(), false);
1616
expect(await settingsService.getTrafficFactor(), TrafficFactor.medium);
17-
expect(await settingsService.getThemeMode(), 'system');
17+
// Default theme is 'light' for first-time users (changed from 'system')
18+
expect(await settingsService.getThemeMode(), 'light');
1819
});
1920

2021
test('Provincial mode is saved and retrieved', () async {

0 commit comments

Comments
 (0)