Skip to content

Commit 8766545

Browse files
committed
WorldClockTab: Add time in other cities
This adds a button that pops up a window where you can select a city. After chosing the city the city is added to the WorldClockTab and the time in that spesific city is displayed.
1 parent 669c92f commit 8766545

File tree

8 files changed

+604
-36
lines changed

8 files changed

+604
-36
lines changed

lib/main.dart

Lines changed: 5 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,17 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import 'dart:async';
18-
import 'dart:ui';
19-
2017
import 'package:flutter/material.dart';
2118

2219
import './timer/tab.dart';
2320
import './keyboard.dart';
21+
import './worldClock/tab.dart';
22+
import './worldClock/timezones.dart';
23+
import './state.dart';
2424

2525
void main() {
26+
setupTimezoneInfo();
27+
appState = AppState.tryFromDisk();
2628
runApp(new Clock());
2729
}
2830

@@ -141,38 +143,6 @@ class _ClockApp extends State<ClockApp> with TickerProviderStateMixin {
141143
}
142144
}
143145

144-
class WorldClockTab extends StatefulWidget {
145-
@override
146-
_WorldClockTabState createState() => _WorldClockTabState();
147-
}
148-
149-
class _WorldClockTabState extends State<WorldClockTab> {
150-
DateTime _datetime = DateTime.now();
151-
Timer? _ctimer;
152-
153-
@override
154-
void deactivate() {
155-
_ctimer?.cancel();
156-
super.deactivate();
157-
}
158-
159-
@override
160-
Widget build(BuildContext context) {
161-
if (_ctimer == null)
162-
_ctimer = Timer.periodic(Duration(seconds: 1), (me) {
163-
_datetime = DateTime.now();
164-
setState(() {});
165-
});
166-
return Material(
167-
child: Center(
168-
child: Text(
169-
"${_datetime.hour}:${_datetime.minute < 10 ? "0" + _datetime.minute.toString() : _datetime.minute}:${_datetime.second < 10 ? "0" + _datetime.second.toString() : _datetime.second}",
170-
style: TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
171-
)),
172-
);
173-
}
174-
}
175-
176146
class AlarmsTab extends StatelessWidget {
177147
@override
178148
Widget build(BuildContext context) {

lib/state.dart

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
Copyright 2022 The dahliaOS Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import 'dart:io';
18+
import 'dart:convert';
19+
import 'package:flutter/foundation.dart';
20+
21+
import './worldClock/timezones.dart';
22+
23+
// Config file contents:
24+
/*
25+
{
26+
"timezones": ["Europe/Amsterdam"]
27+
}
28+
*/
29+
30+
var appState = AppState.empty();
31+
32+
enum _UnableToLoadReason {
33+
IsWeb,
34+
LocationUnknown,
35+
FileDoesNotExist,
36+
JsonDecode,
37+
}
38+
39+
class AppState {
40+
// Empty yields an empty instance of AppState
41+
AppState.empty();
42+
43+
// Tries to load the appstate from the disk
44+
AppState.tryFromDisk() {
45+
if (kIsWeb) {
46+
// We are on the web so we can't load the appstate from disk, return an empty AppState
47+
_loadError = _UnableToLoadReason.IsWeb;
48+
return;
49+
}
50+
51+
if (_appConfigLocation == null) {
52+
// The app config location is unknown, return an empty AppState
53+
_loadError = _UnableToLoadReason.LocationUnknown;
54+
return;
55+
}
56+
57+
final File configFile = File(_appConfigLocation!);
58+
if (!configFile.existsSync()) {
59+
// Config file does not yet exist, return an empty AppState
60+
_loadError = _UnableToLoadReason.FileDoesNotExist;
61+
return;
62+
}
63+
64+
try {
65+
final String configContents = configFile.readAsStringSync();
66+
dynamic config = jsonDecode(configContents);
67+
if (!(config is Map)) {
68+
throw 'config is not an object';
69+
}
70+
71+
dynamic timezones = config['timezones'];
72+
if (timezones is List) {
73+
// Check if there are overlapping timezones from the config file and the timezones we know of
74+
for (var cityTimezone in timezonesOfCities) {
75+
for (var timezone in timezones) {
76+
if (timezone is String && cityTimezone.key == timezone) {
77+
_cityTimezones[timezone] = cityTimezone;
78+
}
79+
}
80+
}
81+
}
82+
} catch (e) {
83+
print("app state error: $e");
84+
_loadError = _UnableToLoadReason.JsonDecode;
85+
}
86+
}
87+
88+
_UnableToLoadReason? _loadError;
89+
Map<String, CityTimeZone> _cityTimezones = {};
90+
91+
_writeConfigToDisk() async {
92+
// Check for errors that will prevent writing to disk
93+
switch (_loadError) {
94+
case _UnableToLoadReason.IsWeb:
95+
case _UnableToLoadReason.LocationUnknown:
96+
// Do not write the config with these errors
97+
return;
98+
default:
99+
// Lets continue
100+
break;
101+
}
102+
103+
final File configFile = File(_appConfigLocation!);
104+
try {
105+
if (!await configFile.exists()) {
106+
// The config file does not yet exist, lets create it
107+
await configFile.create();
108+
}
109+
String configFileContents = jsonEncode({
110+
'timezones': _cityTimezones.keys.toList(),
111+
});
112+
await configFile.writeAsString(configFileContents);
113+
} catch (e) {
114+
print("updating app config error: $e");
115+
}
116+
}
117+
118+
List<CityTimeZone> get cityTimezones => _cityTimezones.values.toList();
119+
120+
addCityTimezone(CityTimeZone entry) {
121+
_cityTimezones[entry.key] = entry;
122+
_writeConfigToDisk();
123+
}
124+
125+
removeCityTimezone(CityTimeZone entry) {
126+
_cityTimezones.remove(entry.key);
127+
_writeConfigToDisk();
128+
}
129+
}
130+
131+
String? get _appConfigLocation {
132+
String? homeDir =
133+
Platform.environment[Platform.isWindows ? 'UserProfile' : 'HOME'];
134+
return homeDir != null ? homeDir + "/.config/clock.json" : null;
135+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
Copyright 2022 The dahliaOS Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import 'package:flutter/material.dart';
18+
19+
import './timezones.dart';
20+
21+
class AddTimezoneScreen extends StatefulWidget {
22+
const AddTimezoneScreen({required this.onSelect});
23+
24+
final void Function(CityTimeZone cityTZ) onSelect;
25+
26+
@override
27+
State<AddTimezoneScreen> createState() => _AddTimezoneScreenState();
28+
}
29+
30+
class _AddTimezoneScreenState extends State<AddTimezoneScreen> {
31+
List<CityTimeZone> options = timezonesOfCities;
32+
33+
onInput(String query) {
34+
String normalizedQuery = query.toLowerCase();
35+
options = timezonesOfCities
36+
.where((e) => e.city.toLowerCase().contains(normalizedQuery))
37+
.toList();
38+
setState(() {});
39+
}
40+
41+
onSearchSubmit() {
42+
if (options.isNotEmpty) onSelect(options.first);
43+
}
44+
45+
onSelect(CityTimeZone option) {
46+
widget.onSelect(option);
47+
Navigator.of(context).pop();
48+
}
49+
50+
@override
51+
Widget build(BuildContext context) {
52+
return Scaffold(
53+
appBar: AppBar(
54+
centerTitle: true,
55+
elevation: 0,
56+
toolbarHeight: 75,
57+
title: Text('Choose a city'),
58+
),
59+
body: Column(
60+
children: [
61+
_SearchField(
62+
onInput: onInput,
63+
onSubmit: onSearchSubmit,
64+
),
65+
_Options(options: options, onSelect: onSelect),
66+
],
67+
),
68+
);
69+
}
70+
}
71+
72+
class _Options extends StatelessWidget {
73+
const _Options({required this.options, this.onSelect});
74+
75+
final List<CityTimeZone> options;
76+
final void Function(CityTimeZone)? onSelect;
77+
78+
@override
79+
Widget build(BuildContext context) {
80+
return Expanded(
81+
child: ListView.builder(
82+
itemCount: options.length,
83+
itemBuilder: (BuildContext context, int index) {
84+
final CityTimeZone option = options[index];
85+
86+
return TextButton(
87+
key: Key(index.toString()),
88+
style: TextButton.styleFrom(
89+
alignment: Alignment.centerLeft,
90+
padding: const EdgeInsets.all(20),
91+
),
92+
child: Column(
93+
crossAxisAlignment: CrossAxisAlignment.start,
94+
children: [
95+
Text(
96+
option.city,
97+
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
98+
),
99+
Text(
100+
option.prettyUtcOffset,
101+
style: TextStyle(fontSize: 12),
102+
),
103+
],
104+
),
105+
onPressed: onSelect != null ? () => onSelect!(option) : null,
106+
);
107+
},
108+
),
109+
);
110+
}
111+
}
112+
113+
class _SearchField extends StatelessWidget {
114+
const _SearchField({required this.onInput, this.onSubmit});
115+
116+
final void Function(String s) onInput;
117+
final void Function()? onSubmit;
118+
119+
@override
120+
Widget build(BuildContext context) {
121+
return Padding(
122+
padding: const EdgeInsets.all(24),
123+
child: TextField(
124+
autofocus: true,
125+
decoration: InputDecoration(labelText: "City"),
126+
onChanged: onInput,
127+
onSubmitted: onSubmit != null ? (String s) => onSubmit!() : null,
128+
),
129+
);
130+
}
131+
}

0 commit comments

Comments
 (0)