Skip to content

Commit

Permalink
#260 update servers:
Browse files Browse the repository at this point in the history
- use object box as cache provider
- store all item actions in cache
- use virtual markers to distinct between saved and intermediate state
  • Loading branch information
Stift committed Jan 14, 2025
1 parent c47ff5c commit 92f498d
Show file tree
Hide file tree
Showing 33 changed files with 714 additions and 60 deletions.
2 changes: 1 addition & 1 deletion analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
include: package:flutter_lints/flutter.yaml

analyzer:
exclude: [build/**, lib/swagger_generated_code/**]
exclude: [build/**, lib/swagger_generated_code/**, lib/objectbox_generated_code/*.g.dart]
errors:
always_use_package_imports: error
directives_ordering: error
Expand Down
4 changes: 4 additions & 0 deletions build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ targets:
$default:
sources:
- swaggers/**
- lib/$lib$
# - $package$
- lib/**
- pubspec.yaml
builders:
chopper_generator:
options:
Expand Down
9 changes: 9 additions & 0 deletions lib/app/services/converters.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import 'dart:convert';

import 'package:gruene_app/app/services/enums.dart';
import 'package:gruene_app/app/services/nominatim_service.dart';
import 'package:gruene_app/features/campaigns/helper/campaign_action.dart';
import 'package:gruene_app/features/campaigns/models/doors/door_detail_model.dart';
import 'package:gruene_app/features/campaigns/models/flyer/flyer_detail_model.dart';
import 'package:gruene_app/features/campaigns/models/map_layer_model.dart';
import 'package:gruene_app/features/campaigns/models/marker_item_model.dart';
import 'package:gruene_app/features/campaigns/models/posters/poster_create_model.dart';
import 'package:gruene_app/features/campaigns/models/posters/poster_detail_model.dart';
import 'package:gruene_app/features/campaigns/models/posters/poster_list_item_model.dart';
import 'package:gruene_app/features/campaigns/models/posters/poster_update_model.dart';
import 'package:gruene_app/features/campaigns/widgets/enhanced_wheel_slider.dart';
import 'package:gruene_app/features/campaigns/widgets/text_input_field.dart';
import 'package:gruene_app/i18n/translations.g.dart';
Expand All @@ -25,3 +30,7 @@ part 'converters/poi_address_parsing.dart';
part 'converters/focus_area_parsing.dart';
part 'converters/poi_parsing.dart';
part 'converters/slider_range_parsing.dart';
part 'converters/date_time_parsing.dart';
part 'converters/poster_create_model_parsing.dart';
part 'converters/poster_update_model_parsing.dart';
part 'converters/campaign_action_parsing.dart';
25 changes: 25 additions & 0 deletions lib/app/services/converters/campaign_action_parsing.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
part of '../converters.dart';

extension CampaignActionParsing on CampaignAction {
PosterCreateModel getSerializedAsPosterCreate() {
var data = jsonDecode(serialized!) as Map<String, dynamic>;
if (data['photo'] != null) {
data['photo'] = (data['photo'] as List<dynamic>).cast<int>();
}
data['location'] = (data['location'] as List<dynamic>).cast<double>();

var model = PosterCreateModel.fromJson(data);
return model;
}

PosterUpdateModel getSerializedAsPosterUpdate() {
var data = jsonDecode(serialized!) as Map<String, dynamic>;
if (data['photo'] != null) {
data['photo'] = (data['photo'] as List<dynamic>).cast<int>();
}
data['location'] = (data['location'] as List<dynamic>).cast<double>();

var model = PosterUpdateModel.fromJson(data);
return model;
}
}
13 changes: 13 additions & 0 deletions lib/app/services/converters/date_time_parsing.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
part of '../converters.dart';

extension DateTimeParsing on DateTime {
String getAsLocalDateTimeString() {
DateTime utcDateTime = this;
DateTime localDateTime = utcDateTime.toLocal();
final dateString = DateFormat(t.campaigns.poster.date_format).format(localDateTime);
final timeString = DateFormat(t.campaigns.poster.time_format).format(localDateTime);
return t.campaigns.poster.datetime_display_template
.replaceAll('{date}', dateString)
.replaceAll('{time}', timeString);
}
}
23 changes: 5 additions & 18 deletions lib/app/services/converters/poi_parsing.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ extension PoiParsing on Poi {
address: poi.address.transformToAddressModel(),
openedDoors: poi.house!.countOpenedDoors.toInt(),
closedDoors: poi.house!.countClosedDoors.toInt(),
createdAt: _getUtcDateTimeAsLocalDateTimeString(poi.createdAt),
createdAt: poi.createdAt.getAsLocalDateTimeString(),
);
}

Expand All @@ -37,8 +37,9 @@ extension PoiParsing on Poi {
imageUrl: _getImageUrl(poi),
address: poi.address.transformToAddressModel(),
status: poi.poster!.status.transformToModelPosterStatus(),
location: coords.transformToLatLng(),
comment: poi.poster!.comment ?? '',
createdAt: _getUtcDateTimeAsLocalDateTimeString(poi.createdAt),
createdAt: poi.createdAt.getAsLocalDateTimeString(),
);
}

Expand All @@ -51,7 +52,7 @@ extension PoiParsing on Poi {
id: poi.id,
address: poi.address.transformToAddressModel(),
flyerCount: poi.flyerSpot!.flyerCount.toInt(),
createdAt: _getUtcDateTimeAsLocalDateTimeString(poi.createdAt),
createdAt: poi.createdAt.getAsLocalDateTimeString(),
);
}

Expand All @@ -67,25 +68,11 @@ extension PoiParsing on Poi {
address: poi.address.transformToAddressModel(),
status: poi.poster!.status.translatePosterStatus(),
lastChangeStatus: poi._getLastChangeStatus(),
lastChangeDateTime: poi._getLastChangeDateTimeInfo(),
lastChangeDateTime: poi.updatedAt.getAsLocalDateTimeString(),
createdAt: poi.createdAt,
);
}

String _getLastChangeDateTimeInfo() {
return _getUtcDateTimeAsLocalDateTimeString(updatedAt);
}

String _getUtcDateTimeAsLocalDateTimeString(DateTime utcTime) {
final localTime = utcTime.toLocal();

final lastChangeDate = DateFormat(t.campaigns.poster.date_format).format(localTime);
final lastChangeTime = DateFormat(t.campaigns.poster.time_format).format(localTime);
return t.campaigns.poster.datetime_display_template
.replaceAll('{date}', lastChangeDate)
.replaceAll('{time}', lastChangeTime);
}

String _getLastChangeStatus() {
return createdAt == updatedAt ? t.campaigns.poster.created : t.campaigns.poster.updated;
}
Expand Down
13 changes: 13 additions & 0 deletions lib/app/services/converters/poi_service_type_parsing.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,17 @@ extension PoiServiceTypeParsing on PoiServiceType {
return CreatePoiType.flyerSpot;
}
}

String getAsMarkerItemStatus(PosterStatus? posterStatus) {
var typeName = name;
switch (this) {
case PoiServiceType.poster:
String statusSuffix = '';
if (posterStatus != null) statusSuffix = '_${posterStatus.name}';
return '$typeName$statusSuffix';
case PoiServiceType.door:
case PoiServiceType.flyer:
return typeName;
}
}
}
25 changes: 25 additions & 0 deletions lib/app/services/converters/poster_create_model_parsing.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
part of '../converters.dart';

extension PosterCreateModelParsing on PosterCreateModel {
MarkerItemModel transformToVirtualMarkerItem(int temporaryId) {
return MarkerItemModel.virtual(
id: temporaryId,
status: PoiServiceType.poster.getAsMarkerItemStatus(PosterStatus.ok),
location: location,
);
}

PosterDetailModel transformToPosterDetailModel(int temporaryId) {
return PosterDetailModel(
id: temporaryId.toString(),
status: PosterStatus.ok,
address: address,
thumbnailUrl: null,
imageUrl: null,
location: location,
comment: '',
createdAt: '${DateTime.now().getAsLocalDateTimeString()}*', // should mark this as preliminary
isCached: true,
);
}
}
33 changes: 33 additions & 0 deletions lib/app/services/converters/poster_update_model_parsing.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
part of '../converters.dart';

extension PosterUpdateModelParsing on PosterUpdateModel {
MarkerItemModel transformToVirtualMarkerItem() {
return MarkerItemModel.virtual(
id: int.parse(id),
status: PoiServiceType.poster.getAsMarkerItemStatus(status),
location: location,
);
}

PosterDetailModel transformToPosterDetailModel(int temporaryId) {
return PosterDetailModel(
id: temporaryId.toString(),
status: status,
address: address,
thumbnailUrl: null,
imageUrl: null,
location: location,
comment: comment,
createdAt: '${DateTime.now().getAsLocalDateTimeString()}*', // should mark this as preliminary
isCached: true,
);
}

PosterUpdateModel mergeWith(PosterUpdateModel newPosterUpdate) {
var oldPosterUdpate = this;

return newPosterUpdate.copyWith(
removePreviousPhotos: newPosterUpdate.removePreviousPhotos || oldPosterUdpate.removePreviousPhotos,
);
}
}
13 changes: 12 additions & 1 deletion lib/app/services/nominatim_service.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import 'package:gruene_app/app/geocode/nominatim.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:logger/logger.dart';
import 'package:maplibre_gl/maplibre_gl.dart';

part 'nominatim_service.g.dart';

class NominatimService {
final Logger _logger = Logger();

Expand All @@ -22,13 +25,14 @@ class NominatimService {
}
}

@JsonSerializable()
class AddressModel {
final String street;
final String city;
final String zipCode;
final String houseNumber;

const AddressModel({this.street = '', this.houseNumber = '', this.zipCode = '', this.city = ''});
AddressModel({this.street = '', this.houseNumber = '', this.zipCode = '', this.city = ''});

/*
* More details on how to find and categorize "places" can be found in the OSM Wiki:
Expand All @@ -48,4 +52,11 @@ class AddressModel {
place.address?['town']?.toString() ??
place.address?['village']?.toString() ??
'';

/// Connect the generated [_$AddressModelFromJson] function to the `fromJson`
/// factory.
factory AddressModel.fromJson(Map<String, dynamic> json) => _$AddressModelFromJson(json);

/// Connect the generated [_$AddressModelToJson] function to the `toJson` method.
Map<String, dynamic> toJson() => _$AddressModelToJson(this);
}
27 changes: 27 additions & 0 deletions lib/app/services/object_box.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import 'dart:async';

import 'package:gruene_app/objectbox_generated_code/objectbox.g.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';

class ObjectBox {
static ObjectBox? _instance;

// ObjectBox._();

factory ObjectBox() => _instance!;

/// The Store of this app.
late final Store store;

ObjectBox._create(this.store) {
// Add any additional setup code, e.g. build queries.
}

/// Create an instance of ObjectBox to use throughout the app.
static Future<void> init() async {
final docsDir = await getApplicationDocumentsDirectory();
final store = await openStore(directory: p.join(docsDir.path, 'wk-cache'));
_instance = ObjectBox._create(store);
}
}
58 changes: 50 additions & 8 deletions lib/app/widgets/app_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:gruene_app/app/auth/bloc/auth_bloc.dart';
import 'package:gruene_app/app/constants/routes.dart';
import 'package:gruene_app/app/theme/theme.dart';
import 'package:gruene_app/app/widgets/icon.dart';
import 'package:gruene_app/features/campaigns/helper/campaign_action_cache.dart';

class MainAppBar extends StatelessWidget implements PreferredSizeWidget {
const MainAppBar({super.key});
Expand All @@ -24,14 +25,7 @@ class MainAppBar extends StatelessWidget implements PreferredSizeWidget {
backgroundColor: theme.primaryColor,
centerTitle: true,
actions: [
if (currentRoute.path == Routes.campaigns.path)
IconButton(
icon: CustomIcon(
path: 'assets/icons/refresh.svg',
color: ThemeColors.background,
),
onPressed: null,
),
if (currentRoute.path == Routes.campaigns.path) RefreshButton(),
if (currentRoute.path != Routes.settings.path && isLoggedIn)
IconButton(
icon: CustomIcon(
Expand All @@ -47,3 +41,51 @@ class MainAppBar extends StatelessWidget implements PreferredSizeWidget {
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}

class RefreshButton extends StatefulWidget {
const RefreshButton({
super.key,
});

@override
State<RefreshButton> createState() => _RefreshButtonState();
}

class _RefreshButtonState extends State<RefreshButton> {
int _currentCount = 0;

@override
void initState() {
var campaignActionCache = CampaignActionCache();
_currentCount = campaignActionCache.getCachedActionCount();

campaignActionCache.addListener(_setCurrentCounter);
super.initState();
}

@override
Widget build(BuildContext context) {
const maxLabelCount = 99;
var labelText = _currentCount > maxLabelCount ? '$maxLabelCount+' : _currentCount.toString();
return IconButton(
icon: Badge(
label: Text(labelText),
isLabelVisible: _currentCount != 0,
child: CustomIcon(
path: 'assets/icons/refresh.svg',
color: ThemeColors.background,
),
),
onPressed: null,
);
}

void _setCurrentCounter() {
var campaignActionCache = CampaignActionCache();
final newCount = campaignActionCache.getCachedActionCount();
if (!mounted) return;
setState(() {
_currentCount = newCount;
});
}
}
Loading

0 comments on commit 92f498d

Please sign in to comment.