Skip to content

Commit e50aed4

Browse files
committed
Dynamic apikey
1 parent 225d0df commit e50aed4

File tree

12 files changed

+208
-50
lines changed

12 files changed

+208
-50
lines changed

assets/apikey.json

Lines changed: 0 additions & 3 deletions
This file was deleted.

lib/constants.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const kAppStateFileName = 'appstate.json';
88
const kSettingsFileName = 'settings.json';
99
const kFavLocationsFileName = 'favlocations.json';
1010
const kLastLocation = 'lastLocation';
11+
const kApiKey = 'apiKey';
1112

1213
const kPaneWidth = 240.0;
1314
const kBreakPoint = 800.0;

lib/main.dart

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1-
import 'dart:convert';
2-
31
import 'package:connectivity_plus/connectivity_plus.dart';
42
import 'package:flutter/material.dart';
5-
import 'package:flutter/services.dart';
63
import 'package:open_weather_client/open_weather.dart';
74
import 'package:watch_it/watch_it.dart';
85
import 'package:yaru/yaru.dart';
@@ -15,9 +12,6 @@ import 'src/weather/weather_model.dart';
1512
Future<void> main() async {
1613
await YaruWindowTitleBar.ensureInitialized();
1714

18-
final apiKey = await loadApiKey();
19-
di.registerSingleton<OpenWeather>(OpenWeather(apiKey: apiKey ?? ''));
20-
2115
final locationsService = LocationsService();
2216
await locationsService.init();
2317
di.registerSingleton<LocationsService>(
@@ -30,17 +24,11 @@ Future<void> main() async {
3024

3125
di.registerLazySingleton(
3226
() => WeatherModel(
33-
locationsService: di<LocationsService>(),
34-
openWeather: di<OpenWeather>(),
27+
locationsService: locationsService,
28+
openWeather: OpenWeather(apiKey: locationsService.apiKey ?? ''),
3529
),
3630
dispose: (s) => s.dispose(),
3731
);
3832

3933
runApp(const App());
4034
}
41-
42-
Future<String?> loadApiKey() async {
43-
final source = await rootBundle.loadString('assets/apikey.json');
44-
final json = jsonDecode(source);
45-
return json['apiKey'] as String;
46-
}

lib/src/locations/locations_service.dart

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,26 @@ import '../../constants.dart';
44
import '../../utils.dart';
55

66
class LocationsService {
7+
String? _apiKey;
8+
String? get apiKey => _apiKey;
9+
Future<void> setApiKey(String? value) {
10+
if (value == _apiKey) return Future.value();
11+
return writeSetting(kApiKey, value).then((_) {
12+
_apiKey = value;
13+
_apiKeyController.add(true);
14+
});
15+
}
16+
17+
final _apiKeyController = StreamController<bool>.broadcast();
18+
Stream<bool> get apiKeyChanged => _apiKeyController.stream;
19+
720
String? _lastLocation;
821
String? get lastLocation => _lastLocation;
922
void setLastLocation(String? value) {
1023
if (value == _lastLocation) return;
1124
writeAppState(kLastLocation, value).then((_) {
12-
_lastLocationController.add(true);
1325
_lastLocation = value;
26+
_lastLocationController.add(true);
1427
});
1528
}
1629

@@ -42,6 +55,7 @@ class LocationsService {
4255
}
4356

4457
Future<void> init() async {
58+
_apiKey = (await readSetting(kApiKey)) as String?;
4559
_lastLocation = (await readAppState(kLastLocation)) as String?;
4660
_favLocations = Set.from(
4761
(await readStringIterable(filename: kFavLocationsFileName) ?? <String>{}),

lib/src/weather/view/city_search_field.dart

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@ import 'package:flutter/material.dart';
22
import 'package:watch_it/watch_it.dart';
33
import 'package:yaru/yaru.dart';
44

5+
import '../../../string_x.dart';
56
import '../../build_context_x.dart';
67
import '../weather_model.dart';
78

8-
class CitySearchField extends StatefulWidget {
9+
class CitySearchField extends StatefulWidget with WatchItStatefulWidgetMixin {
910
const CitySearchField({
1011
super.key,
12+
this.watchError = false,
1113
});
1214

15+
final bool watchError;
16+
1317
@override
1418
State<CitySearchField> createState() => _CitySearchFieldState();
1519
}
@@ -32,6 +36,7 @@ class _CitySearchFieldState extends State<CitySearchField> {
3236
@override
3337
Widget build(BuildContext context) {
3438
final model = di<WeatherModel>();
39+
final error = watchPropertyValue((WeatherModel m) => m.error);
3540
final theme = context.theme;
3641
var textField = TextField(
3742
autofocus: true,
@@ -49,17 +54,26 @@ class _CitySearchFieldState extends State<CitySearchField> {
4954
?.copyWith(fontWeight: FontWeight.w500),
5055
decoration: InputDecoration(
5156
fillColor: theme.colorScheme.onSurface.withOpacity(0.2),
52-
prefixIcon: const Icon(
57+
prefixIcon: Icon(
5358
YaruIcons.search,
5459
size: 15,
60+
color: theme.colorScheme.onSurface,
5561
),
5662
border: const OutlineInputBorder(borderSide: BorderSide.none),
5763
enabledBorder: const OutlineInputBorder(borderSide: BorderSide.none),
5864
focusedBorder: const OutlineInputBorder(borderSide: BorderSide.none),
5965
prefixIconConstraints:
6066
const BoxConstraints(minWidth: 35, minHeight: 30),
6167
filled: true,
62-
hintText: 'Cityname',
68+
hintText: 'City name',
69+
errorText: widget.watchError
70+
? (error?.cityNotFound == true
71+
? 'City not found'
72+
: error?.emptyCity == true
73+
? 'Please enter a city name'
74+
: error)
75+
: null,
76+
errorMaxLines: 10,
6377
suffixIconConstraints: const BoxConstraints(
6478
maxHeight: kYaruTitleBarItemHeight,
6579
minHeight: kYaruTitleBarItemHeight,
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import 'package:animated_emoji/animated_emoji.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:watch_it/watch_it.dart';
4+
import 'package:yaru/yaru.dart';
5+
6+
import '../../../string_x.dart';
7+
import '../../build_context_x.dart';
8+
import '../weather_model.dart';
9+
import 'city_search_field.dart';
10+
11+
class ErrorView extends StatefulWidget {
12+
const ErrorView({super.key, required this.error});
13+
14+
final String error;
15+
16+
@override
17+
State<ErrorView> createState() => _ErrorViewState();
18+
}
19+
20+
class _ErrorViewState extends State<ErrorView> {
21+
late TextEditingController _controller;
22+
23+
@override
24+
void initState() {
25+
super.initState();
26+
_controller = TextEditingController();
27+
}
28+
29+
@override
30+
void dispose() {
31+
_controller.dispose();
32+
super.dispose();
33+
}
34+
35+
@override
36+
Widget build(BuildContext context) {
37+
final model = di<WeatherModel>();
38+
39+
return Center(
40+
child: Padding(
41+
padding: const EdgeInsets.all(kYaruPagePadding),
42+
child: Column(
43+
mainAxisSize: MainAxisSize.min,
44+
children: [
45+
AnimatedEmoji(
46+
widget.error.emptyCity
47+
? AnimatedEmojis.sunWithFace
48+
: widget.error.cityNotFound
49+
? AnimatedEmojis.thinkingFace
50+
: widget.error.invalidKey
51+
? AnimatedEmojis.disguise
52+
: AnimatedEmojis.xEyes,
53+
size: 40,
54+
),
55+
const SizedBox(
56+
height: kYaruPagePadding,
57+
),
58+
if (widget.error.invalidKey)
59+
Flexible(
60+
child: Row(
61+
mainAxisSize: MainAxisSize.min,
62+
children: [
63+
SizedBox(
64+
width: 300,
65+
child: TextField(
66+
onChanged: (v) => _controller.text = v,
67+
decoration: InputDecoration(
68+
suffixIconConstraints: const BoxConstraints(
69+
maxHeight: kYaruTitleBarItemHeight,
70+
minHeight: kYaruTitleBarItemHeight,
71+
minWidth: 45,
72+
),
73+
suffixIcon: Tooltip(
74+
message: 'Save',
75+
child: ClipRRect(
76+
borderRadius: const BorderRadius.only(
77+
topRight: Radius.circular(kYaruButtonRadius),
78+
bottomRight: Radius.circular(kYaruButtonRadius),
79+
),
80+
child: Material(
81+
color: Colors.transparent,
82+
child: InkWell(
83+
onTap: () => model.setApiKeyAndLoadWeather(
84+
_controller.text.toString(),
85+
),
86+
child: const Center(
87+
widthFactor: 0,
88+
child: Icon(YaruIcons.save),
89+
),
90+
),
91+
),
92+
),
93+
),
94+
errorMaxLines: 5,
95+
errorText: widget.error.invalidKey
96+
? 'Please enter a valid API key'
97+
: widget.error,
98+
border: const OutlineInputBorder(
99+
borderSide: BorderSide.none,
100+
),
101+
enabledBorder: const OutlineInputBorder(
102+
borderSide: BorderSide.none,
103+
),
104+
fillColor: context.theme.colorScheme.surface
105+
.withOpacity(0.3),
106+
label: const Text('OpenWeather API key'),
107+
),
108+
),
109+
),
110+
],
111+
),
112+
)
113+
else if (widget.error.emptyCity || widget.error.cityNotFound)
114+
const SizedBox(
115+
width: 300,
116+
child: CitySearchField(
117+
watchError: true,
118+
),
119+
)
120+
else
121+
Text(
122+
widget.error,
123+
textAlign: TextAlign.center,
124+
),
125+
],
126+
),
127+
),
128+
);
129+
}
130+
}

lib/src/weather/view/forecast_chart.dart

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import '../../../constants.dart';
1111
import '../../build_context_x.dart';
1212
import '../weather_data_x.dart';
1313
import '../weather_model.dart';
14+
import 'error_view.dart';
1415

1516
class ForeCastChart extends StatelessWidget with WatchItMixin {
1617
const ForeCastChart({super.key});
@@ -31,15 +32,7 @@ class ForeCastChart extends StatelessWidget with WatchItMixin {
3132
),
3233
width: context.mq.size.width,
3334
child: (error != null)
34-
? Center(
35-
child: Padding(
36-
padding: const EdgeInsets.all(kYaruPagePadding),
37-
child: Text(
38-
error,
39-
textAlign: TextAlign.center,
40-
),
41-
),
42-
)
35+
? ErrorView(error: error)
4336
: (notTodayForecastDaily == null)
4437
? Center(
4538
child: YaruCircularProgressIndicator(

lib/src/weather/view/today_chart.dart

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import '../../../constants.dart';
88
import '../../build_context_x.dart';
99
import '../weather_data_x.dart';
1010
import '../weather_model.dart';
11+
import 'error_view.dart';
1112
import 'today_tile.dart';
1213

1314
class TodayChart extends StatefulWidget with WatchItStatefulWidgetMixin {
@@ -57,15 +58,7 @@ class _TodayChartState extends State<TodayChart> {
5758
color: context.theme.colorScheme.surface.withOpacity(0.3),
5859
),
5960
child: error != null
60-
? Center(
61-
child: Padding(
62-
padding: const EdgeInsets.all(kYaruPagePadding),
63-
child: Text(
64-
error,
65-
textAlign: TextAlign.center,
66-
),
67-
),
68-
)
61+
? ErrorView(error: error)
6962
: forecast == null || data == null
7063
? Center(
7164
child: YaruCircularProgressIndicator(

lib/src/weather/weather_model.dart

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,16 @@ class WeatherModel extends SafeChangeNotifier {
1919
}) : _openWeather = openWeather,
2020
_locationsService = locationsService;
2121

22-
final OpenWeather _openWeather;
22+
OpenWeather _openWeather;
23+
void setApiKeyAndLoadWeather(String apiKey) {
24+
_locationsService.setApiKey(apiKey).then((_) {
25+
_openWeather = OpenWeather(apiKey: apiKey);
26+
loadWeather();
27+
});
28+
}
29+
2330
final LocationsService _locationsService;
31+
StreamSubscription<bool>? _apiKeyChangedSub;
2432
StreamSubscription<bool>? _lastLocationChangedSub;
2533
StreamSubscription<bool>? _favLocationsChangedSub;
2634

@@ -48,21 +56,21 @@ class WeatherModel extends SafeChangeNotifier {
4856
_error = null;
4957
notifyListeners();
5058

51-
cityName ??= lastLocation;
59+
cityName ??= lastLocation ?? '';
5260

61+
_apiKeyChangedSub =
62+
_locationsService.apiKeyChanged.listen((_) => notifyListeners());
5363
_lastLocationChangedSub ??=
5464
_locationsService.lastLocationChanged.listen((_) => notifyListeners());
5565
_favLocationsChangedSub ??=
5666
_locationsService.favLocationsChanged.listen((_) => notifyListeners());
5767

58-
if (cityName != null) {
59-
_weatherData = await loadWeatherFromCityName(cityName);
68+
_weatherData = await loadWeatherFromCityName(cityName);
69+
_locationsService.setLastLocation(cityName);
70+
_fiveDaysForCast = await loadForeCastByCityName(cityName: cityName);
71+
if (_weatherData != null) {
6072
_locationsService.setLastLocation(cityName);
61-
_fiveDaysForCast = await loadForeCastByCityName(cityName: cityName);
62-
if (_weatherData != null) {
63-
_locationsService.setLastLocation(cityName);
64-
_locationsService.addFavLocation(cityName);
65-
}
73+
_locationsService.addFavLocation(cityName);
6674
}
6775

6876
if (_weatherData?.weatherType != null) {
@@ -75,6 +83,7 @@ class WeatherModel extends SafeChangeNotifier {
7583
@override
7684
Future<void> dispose() async {
7785
super.dispose();
86+
await _apiKeyChangedSub?.cancel();
7887
await _lastLocationChangedSub?.cancel();
7988
await _favLocationsChangedSub?.cancel();
8089
}

lib/string_x.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,8 @@ extension StringExtension on String {
99
(Match m) => m[1] == null ? ' ${m[0]}' : m[1]!.toUpperCase(),
1010
);
1111
}
12+
13+
bool get emptyCity => contains('Nothing to geocode');
14+
bool get cityNotFound => contains('city not found');
15+
bool get invalidKey => contains('Invalid API key');
1216
}

0 commit comments

Comments
 (0)