refactor: extract timezone data, remove duplicate i18n keys
This commit is contained in:
@@ -1457,6 +1457,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.11"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timezone
|
||||
sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.4"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:legacy_shared/legacy_shared.dart';
|
||||
|
||||
import 'timezone_view_state.dart';
|
||||
|
||||
final timezoneViewModelProvider =
|
||||
NotifierProvider.autoDispose<TimezoneViewModel, TimezoneViewState>(
|
||||
TimezoneViewModel.new,
|
||||
);
|
||||
|
||||
class TimezoneViewModel extends Notifier<TimezoneViewState> {
|
||||
late final DeviceSettingsUpdateDatasource _datasource;
|
||||
|
||||
@override
|
||||
TimezoneViewState build() {
|
||||
_datasource = ref.read(deviceSettingsUpdateProvider);
|
||||
Future.microtask(_load);
|
||||
return const TimezoneViewState();
|
||||
}
|
||||
|
||||
void _load() {
|
||||
final device = ref.read(selectedDeviceProvider);
|
||||
if (device == null) return;
|
||||
|
||||
state = state.copyWith(
|
||||
timezone: device.settings.timezone,
|
||||
isLoading: false,
|
||||
);
|
||||
}
|
||||
|
||||
void selectTimezone(int value) {
|
||||
state = state.copyWith(timezone: value);
|
||||
}
|
||||
|
||||
Future<void> save() async {
|
||||
final device = ref.read(selectedDeviceProvider);
|
||||
if (device == null) return;
|
||||
|
||||
if (state.timezone == device.settings.timezone) {
|
||||
state = state.copyWith(saveSuccess: true);
|
||||
return;
|
||||
}
|
||||
|
||||
state = state.copyWith(isSaving: true, errorEvent: null, saveSuccess: false);
|
||||
|
||||
try {
|
||||
final updatedSettings = device.settings.copyWith(
|
||||
timezone: state.timezone,
|
||||
);
|
||||
await _datasource.updateDeviceSettings(
|
||||
device: device,
|
||||
updatedSettings: updatedSettings,
|
||||
);
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(isSaving: false, saveSuccess: true);
|
||||
} catch (e) {
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(
|
||||
isSaving: false,
|
||||
errorEvent: TimezoneErrorEvent.update,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'timezone_view_state.freezed.dart';
|
||||
|
||||
enum TimezoneErrorEvent { update }
|
||||
|
||||
@freezed
|
||||
abstract class TimezoneViewState with _$TimezoneViewState {
|
||||
const factory TimezoneViewState({
|
||||
@Default(true) bool isLoading,
|
||||
@Default(false) bool isSaving,
|
||||
@Default(0) int timezone,
|
||||
TimezoneErrorEvent? errorEvent,
|
||||
@Default(false) bool saveSuccess,
|
||||
}) = _TimezoneViewState;
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'timezone_view_state.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$TimezoneViewState {
|
||||
|
||||
bool get isLoading; bool get isSaving; int get timezone; TimezoneErrorEvent? get errorEvent; bool get saveSuccess;
|
||||
/// Create a copy of TimezoneViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$TimezoneViewStateCopyWith<TimezoneViewState> get copyWith => _$TimezoneViewStateCopyWithImpl<TimezoneViewState>(this as TimezoneViewState, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is TimezoneViewState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isSaving, isSaving) || other.isSaving == isSaving)&&(identical(other.timezone, timezone) || other.timezone == timezone)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent)&&(identical(other.saveSuccess, saveSuccess) || other.saveSuccess == saveSuccess));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,isLoading,isSaving,timezone,errorEvent,saveSuccess);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TimezoneViewState(isLoading: $isLoading, isSaving: $isSaving, timezone: $timezone, errorEvent: $errorEvent, saveSuccess: $saveSuccess)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $TimezoneViewStateCopyWith<$Res> {
|
||||
factory $TimezoneViewStateCopyWith(TimezoneViewState value, $Res Function(TimezoneViewState) _then) = _$TimezoneViewStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
bool isLoading, bool isSaving, int timezone, TimezoneErrorEvent? errorEvent, bool saveSuccess
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$TimezoneViewStateCopyWithImpl<$Res>
|
||||
implements $TimezoneViewStateCopyWith<$Res> {
|
||||
_$TimezoneViewStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final TimezoneViewState _self;
|
||||
final $Res Function(TimezoneViewState) _then;
|
||||
|
||||
/// Create a copy of TimezoneViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? isSaving = null,Object? timezone = null,Object? errorEvent = freezed,Object? saveSuccess = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isSaving: null == isSaving ? _self.isSaving : isSaving // ignore: cast_nullable_to_non_nullable
|
||||
as bool,timezone: null == timezone ? _self.timezone : timezone // ignore: cast_nullable_to_non_nullable
|
||||
as int,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable
|
||||
as TimezoneErrorEvent?,saveSuccess: null == saveSuccess ? _self.saveSuccess : saveSuccess // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [TimezoneViewState].
|
||||
extension TimezoneViewStatePatterns on TimezoneViewState {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _TimezoneViewState value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _TimezoneViewState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _TimezoneViewState value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _TimezoneViewState():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _TimezoneViewState value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _TimezoneViewState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isLoading, bool isSaving, int timezone, TimezoneErrorEvent? errorEvent, bool saveSuccess)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _TimezoneViewState() when $default != null:
|
||||
return $default(_that.isLoading,_that.isSaving,_that.timezone,_that.errorEvent,_that.saveSuccess);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isLoading, bool isSaving, int timezone, TimezoneErrorEvent? errorEvent, bool saveSuccess) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _TimezoneViewState():
|
||||
return $default(_that.isLoading,_that.isSaving,_that.timezone,_that.errorEvent,_that.saveSuccess);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isLoading, bool isSaving, int timezone, TimezoneErrorEvent? errorEvent, bool saveSuccess)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _TimezoneViewState() when $default != null:
|
||||
return $default(_that.isLoading,_that.isSaving,_that.timezone,_that.errorEvent,_that.saveSuccess);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _TimezoneViewState implements TimezoneViewState {
|
||||
const _TimezoneViewState({this.isLoading = true, this.isSaving = false, this.timezone = 0, this.errorEvent, this.saveSuccess = false});
|
||||
|
||||
|
||||
@override@JsonKey() final bool isLoading;
|
||||
@override@JsonKey() final bool isSaving;
|
||||
@override@JsonKey() final int timezone;
|
||||
@override final TimezoneErrorEvent? errorEvent;
|
||||
@override@JsonKey() final bool saveSuccess;
|
||||
|
||||
/// Create a copy of TimezoneViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$TimezoneViewStateCopyWith<_TimezoneViewState> get copyWith => __$TimezoneViewStateCopyWithImpl<_TimezoneViewState>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _TimezoneViewState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isSaving, isSaving) || other.isSaving == isSaving)&&(identical(other.timezone, timezone) || other.timezone == timezone)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent)&&(identical(other.saveSuccess, saveSuccess) || other.saveSuccess == saveSuccess));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,isLoading,isSaving,timezone,errorEvent,saveSuccess);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TimezoneViewState(isLoading: $isLoading, isSaving: $isSaving, timezone: $timezone, errorEvent: $errorEvent, saveSuccess: $saveSuccess)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$TimezoneViewStateCopyWith<$Res> implements $TimezoneViewStateCopyWith<$Res> {
|
||||
factory _$TimezoneViewStateCopyWith(_TimezoneViewState value, $Res Function(_TimezoneViewState) _then) = __$TimezoneViewStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
bool isLoading, bool isSaving, int timezone, TimezoneErrorEvent? errorEvent, bool saveSuccess
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$TimezoneViewStateCopyWithImpl<$Res>
|
||||
implements _$TimezoneViewStateCopyWith<$Res> {
|
||||
__$TimezoneViewStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _TimezoneViewState _self;
|
||||
final $Res Function(_TimezoneViewState) _then;
|
||||
|
||||
/// Create a copy of TimezoneViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,Object? isSaving = null,Object? timezone = null,Object? errorEvent = freezed,Object? saveSuccess = null,}) {
|
||||
return _then(_TimezoneViewState(
|
||||
isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isSaving: null == isSaving ? _self.isSaving : isSaving // ignore: cast_nullable_to_non_nullable
|
||||
as bool,timezone: null == timezone ? _self.timezone : timezone // ignore: cast_nullable_to_non_nullable
|
||||
as int,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable
|
||||
as TimezoneErrorEvent?,saveSuccess: null == saveSuccess ? _self.saveSuccess : saveSuccess // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
@@ -4,18 +4,233 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:legacy_shared/legacy_shared.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
|
||||
import '../timezone_data.dart';
|
||||
import 'state/timezone_view_model.dart';
|
||||
|
||||
class TimezoneScreen extends ConsumerWidget {
|
||||
const TimezoneScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = ref.watch(themePortProvider);
|
||||
final primaryColor = theme.getColorFor(ThemeCode.legacyPrimary);
|
||||
final state = ref.watch(timezoneViewModelProvider);
|
||||
final vm = ref.read(timezoneViewModelProvider.notifier);
|
||||
|
||||
ref.listen(
|
||||
timezoneViewModelProvider.select((s) => s.errorEvent),
|
||||
(previous, next) {
|
||||
if (next != null) {
|
||||
showTopSnackbar(
|
||||
context,
|
||||
message: context.translate(I18n.errorTimezone),
|
||||
type: MessageType.error,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
ref.listen(
|
||||
timezoneViewModelProvider.select((s) => s.saveSuccess),
|
||||
(previous, next) {
|
||||
if (next && !(previous ?? false)) {
|
||||
showTopSnackbar(
|
||||
context,
|
||||
message: context.translate(I18n.timezoneUpdated),
|
||||
type: MessageType.success,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
final selected = timezoneEntries.where((t) => t.$1 == state.timezone).firstOrNull;
|
||||
final others = timezoneEntries.where((t) => t.$1 != state.timezone).toList();
|
||||
|
||||
return LegacyPageLayout(
|
||||
theme: theme,
|
||||
title: context.translate(I18n.timezone),
|
||||
body: const Center(
|
||||
child: Text('Coming soon'),
|
||||
body: state.isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
children: [
|
||||
if (selected != null) ...[
|
||||
_SelectedTimezoneCard(
|
||||
offset: selected.$1,
|
||||
city: selected.$2,
|
||||
continent: selected.$3,
|
||||
label: formatUtcOffset(selected.$1),
|
||||
primaryColor: primaryColor,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4, bottom: 8),
|
||||
child: Text(
|
||||
context.translate(I18n.timezoneOther),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
...others.map(
|
||||
(tz) => _TimezoneItem(
|
||||
offset: tz.$1,
|
||||
city: tz.$2,
|
||||
continent: tz.$3,
|
||||
label: formatUtcOffset(tz.$1),
|
||||
primaryColor: primaryColor,
|
||||
onTap: () => vm.selectTimezone(tz.$1),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
footer: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
|
||||
child: PrimaryButton(
|
||||
onPressed: state.isSaving ? null : () => vm.save(),
|
||||
text: state.isSaving ? '...' : context.translate(I18n.save),
|
||||
color: primaryColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SelectedTimezoneCard extends StatelessWidget {
|
||||
final int offset;
|
||||
final String city;
|
||||
final String continent;
|
||||
final String label;
|
||||
final Color primaryColor;
|
||||
|
||||
const _SelectedTimezoneCard({
|
||||
required this.offset,
|
||||
required this.city,
|
||||
required this.continent,
|
||||
required this.label,
|
||||
required this.primaryColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: primaryColor.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: primaryColor.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 52,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
color: primaryColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(Icons.access_time, color: Colors.white, size: 28),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'$city · $continent',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: primaryColor.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.check_circle, color: primaryColor, size: 28),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TimezoneItem extends StatelessWidget {
|
||||
final int offset;
|
||||
final String city;
|
||||
final String continent;
|
||||
final String label;
|
||||
final Color primaryColor;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _TimezoneItem({
|
||||
required this.offset,
|
||||
required this.city,
|
||||
required this.continent,
|
||||
required this.label,
|
||||
required this.primaryColor,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.public,
|
||||
color: Colors.grey.shade500,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'$label · $city',
|
||||
style: const TextStyle(fontSize: 15),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
continent,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
const timezoneEntries = [
|
||||
(-12, 'Baker Island', 'Pacific'),
|
||||
(-11, 'Pago Pago', 'Pacific'),
|
||||
(-10, 'Honolulu', 'Pacific'),
|
||||
(-9, 'Anchorage', 'America'),
|
||||
(-8, 'Los Angeles', 'America'),
|
||||
(-7, 'Denver', 'America'),
|
||||
(-6, 'Mexico City', 'America'),
|
||||
(-5, 'New York', 'America'),
|
||||
(-4, 'Santiago', 'America'),
|
||||
(-3, 'Buenos Aires', 'America'),
|
||||
(-2, 'South Georgia', 'Atlantic'),
|
||||
(-1, 'Azores', 'Atlantic'),
|
||||
(0, 'London', 'Europe'),
|
||||
(1, 'Madrid', 'Europe'),
|
||||
(2, 'Cairo', 'Africa'),
|
||||
(3, 'Moscow', 'Europe'),
|
||||
(4, 'Dubai', 'Asia'),
|
||||
(5, 'Karachi', 'Asia'),
|
||||
(6, 'Dhaka', 'Asia'),
|
||||
(7, 'Bangkok', 'Asia'),
|
||||
(8, 'Shanghai', 'Asia'),
|
||||
(9, 'Tokyo', 'Asia'),
|
||||
(10, 'Sydney', 'Australia'),
|
||||
(11, 'Noumea', 'Pacific'),
|
||||
(12, 'Auckland', 'Pacific'),
|
||||
];
|
||||
|
||||
String formatUtcOffset(int offset) {
|
||||
final sign = offset >= 0 ? '+' : '';
|
||||
return 'UTC$sign$offset';
|
||||
}
|
||||
@@ -1119,6 +1119,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.11"
|
||||
timezone:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: timezone
|
||||
sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.4"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -54,6 +54,7 @@ dependencies:
|
||||
json_serializable: ^6.11.2
|
||||
url_launcher: ^6.3.2
|
||||
flutter_contacts: ^1.1.9+2
|
||||
timezone: ^0.9.4
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
|
||||
@@ -629,6 +629,8 @@
|
||||
"measure": "Messen",
|
||||
"callHistory": "Anrufverlauf",
|
||||
"callHistoryEmpty": "Keine Anrufe aufgezeichnet",
|
||||
"timezoneSearch": "Stadt suchen...",
|
||||
"timezoneNoResults": "Keine Städte gefunden",
|
||||
"callIncoming": "Eingehend",
|
||||
"callOutgoing": "Ausgehend",
|
||||
"callMissed": "Verpasst",
|
||||
@@ -697,5 +699,8 @@
|
||||
"batteryNightModeTitle": "Nächtlicher Energiesparmodus",
|
||||
"batteryNightModeDescription": "Die Uhr spart Batterie und trennt sich zwischen 22:00 und 06:00 Uhr von allen Netzwerken.",
|
||||
"batteryNightModeUpdated": "Nachtmodus aktualisiert",
|
||||
"errorBatteryNightMode": "Der Nachtmodus konnte nicht aktualisiert werden"
|
||||
"errorBatteryNightMode": "Der Nachtmodus konnte nicht aktualisiert werden",
|
||||
"timezoneUpdated": "Zeitzone aktualisiert",
|
||||
"errorTimezone": "Die Zeitzone konnte nicht aktualisiert werden",
|
||||
"timezoneOther": "Andere Zeitzonen"
|
||||
}
|
||||
|
||||
@@ -761,6 +761,8 @@
|
||||
"measure": "Measure",
|
||||
"callHistory": "Call history",
|
||||
"callHistoryEmpty": "No calls recorded",
|
||||
"timezoneSearch": "Search city...",
|
||||
"timezoneNoResults": "No cities found",
|
||||
"callIncoming": "Incoming",
|
||||
"callOutgoing": "Outgoing",
|
||||
"callMissed": "Missed",
|
||||
@@ -828,5 +830,8 @@
|
||||
"batteryNightModeTitle": "Night energy saving mode",
|
||||
"batteryNightModeDescription": "The watch will save battery and disconnect from all networks between 22:00 and 06:00.",
|
||||
"batteryNightModeUpdated": "Night mode updated",
|
||||
"errorBatteryNightMode": "Could not update night mode"
|
||||
"errorBatteryNightMode": "Could not update night mode",
|
||||
"timezoneUpdated": "Timezone updated",
|
||||
"errorTimezone": "Could not update timezone",
|
||||
"timezoneOther": "Other timezones"
|
||||
}
|
||||
|
||||
@@ -759,6 +759,8 @@
|
||||
"measure": "Medir",
|
||||
"callHistory": "Historial de llamadas",
|
||||
"callHistoryEmpty": "No hay llamadas registradas",
|
||||
"timezoneSearch": "Buscar ciudad...",
|
||||
"timezoneNoResults": "No se encontraron ciudades",
|
||||
"callIncoming": "Entrantes",
|
||||
"callOutgoing": "Salientes",
|
||||
"callMissed": "Perdidas",
|
||||
@@ -826,5 +828,8 @@
|
||||
"batteryNightModeTitle": "Modo de ahorro de energía nocturno",
|
||||
"batteryNightModeDescription": "El reloj ahorrará batería y se desconectará de todas las redes entre las 22:00 y las 06:00.",
|
||||
"batteryNightModeUpdated": "Modo nocturno actualizado",
|
||||
"errorBatteryNightMode": "No se pudo actualizar el modo nocturno"
|
||||
"errorBatteryNightMode": "No se pudo actualizar el modo nocturno",
|
||||
"timezoneUpdated": "Zona horaria actualizada",
|
||||
"errorTimezone": "No se pudo actualizar la zona horaria",
|
||||
"timezoneOther": "Otras zonas horarias"
|
||||
}
|
||||
|
||||
@@ -629,6 +629,8 @@
|
||||
"measure": "Mesurer",
|
||||
"callHistory": "Historique des appels",
|
||||
"callHistoryEmpty": "Aucun appel enregistré",
|
||||
"timezoneSearch": "Rechercher une ville...",
|
||||
"timezoneNoResults": "Aucune ville trouvée",
|
||||
"callIncoming": "Entrants",
|
||||
"callOutgoing": "Sortants",
|
||||
"callMissed": "Manqués",
|
||||
@@ -697,5 +699,8 @@
|
||||
"batteryNightModeTitle": "Mode d'économie d'énergie nocturne",
|
||||
"batteryNightModeDescription": "La montre économisera la batterie et se déconnectera de tous les réseaux entre 22h00 et 06h00.",
|
||||
"batteryNightModeUpdated": "Mode nuit mis à jour",
|
||||
"errorBatteryNightMode": "Impossible de mettre à jour le mode nuit"
|
||||
"errorBatteryNightMode": "Impossible de mettre à jour le mode nuit",
|
||||
"timezoneUpdated": "Fuseau horaire mis à jour",
|
||||
"errorTimezone": "Impossible de mettre à jour le fuseau horaire",
|
||||
"timezoneOther": "Autres fuseaux horaires"
|
||||
}
|
||||
|
||||
@@ -629,6 +629,8 @@
|
||||
"measure": "Misurare",
|
||||
"callHistory": "Cronologia chiamate",
|
||||
"callHistoryEmpty": "Nessuna chiamata registrata",
|
||||
"timezoneSearch": "Cerca città...",
|
||||
"timezoneNoResults": "Nessuna città trovata",
|
||||
"callIncoming": "In arrivo",
|
||||
"callOutgoing": "In uscita",
|
||||
"callMissed": "Perse",
|
||||
@@ -697,5 +699,8 @@
|
||||
"batteryNightModeTitle": "Modalità risparmio energetico notturno",
|
||||
"batteryNightModeDescription": "L'orologio risparmierà batteria e si disconnetterà da tutte le reti tra le 22:00 e le 06:00.",
|
||||
"batteryNightModeUpdated": "Modalità notturna aggiornata",
|
||||
"errorBatteryNightMode": "Impossibile aggiornare la modalità notturna"
|
||||
"errorBatteryNightMode": "Impossibile aggiornare la modalità notturna",
|
||||
"timezoneUpdated": "Fuso orario aggiornato",
|
||||
"errorTimezone": "Impossibile aggiornare il fuso orario",
|
||||
"timezoneOther": "Altri fusi orari"
|
||||
}
|
||||
|
||||
@@ -629,6 +629,8 @@
|
||||
"measure": "Medir",
|
||||
"callHistory": "Histórico de chamadas",
|
||||
"callHistoryEmpty": "Nenhuma chamada registrada",
|
||||
"timezoneSearch": "Buscar cidade...",
|
||||
"timezoneNoResults": "Nenhuma cidade encontrada",
|
||||
"callIncoming": "Recebidas",
|
||||
"callOutgoing": "Efetuadas",
|
||||
"callMissed": "Perdidas",
|
||||
@@ -697,5 +699,8 @@
|
||||
"batteryNightModeTitle": "Modo de economia de energia noturno",
|
||||
"batteryNightModeDescription": "O relógio poupará bateria e desconectar-se-á de todas as redes entre as 22:00 e as 06:00.",
|
||||
"batteryNightModeUpdated": "Modo noturno atualizado",
|
||||
"errorBatteryNightMode": "Não foi possível atualizar o modo noturno"
|
||||
"errorBatteryNightMode": "Não foi possível atualizar o modo noturno",
|
||||
"timezoneUpdated": "Fuso horário atualizado",
|
||||
"errorTimezone": "Não foi possível atualizar o fuso horário",
|
||||
"timezoneOther": "Outros fusos horários"
|
||||
}
|
||||
|
||||
@@ -833,4 +833,7 @@ class I18n {
|
||||
static const String batteryNightModeDescription = 'batteryNightModeDescription';
|
||||
static const String batteryNightModeUpdated = 'batteryNightModeUpdated';
|
||||
static const String errorBatteryNightMode = 'errorBatteryNightMode';
|
||||
static const String timezoneUpdated = 'timezoneUpdated';
|
||||
static const String errorTimezone = 'errorTimezone';
|
||||
static const String timezoneOther = 'timezoneOther';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user