Skip to content

Commit

Permalink
#260 update servers (#535)
Browse files Browse the repository at this point in the history
* #260 update servers:
- use sqflite as cache database
- store all item actions in cache
- use virtual markers to distinct between saved and intermediate state
- images are stored on device data instead of stream to reduce mem_usage
- auto-discover when add + delete are done in cache (removes item from cache and never stores it)

- apply door to use cache
- apply flyer to use cache

* 530: let refresh button rotate while flushing
  • Loading branch information
Stift authored Feb 3, 2025
1 parent 6ab106a commit 98e1398
Show file tree
Hide file tree
Showing 60 changed files with 2,011 additions and 316 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
"circleci.filters.branchFilter": "allBranches",
"circleci.persistedProjectSelection": [
"gh/verdigado/gruene_app"
]
],
"cSpell.enabled": false
}
5 changes: 4 additions & 1 deletion analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
include: package:flutter_lints/flutter.yaml

analyzer:
exclude: [build/**, lib/swagger_generated_code/**]
exclude:
- build/**
- lib/swagger_generated_code/**
- lib/**.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
138 changes: 138 additions & 0 deletions lib/app/services/campaign_action_database.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import 'package:gruene_app/features/campaigns/helper/campaign_action.dart';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';

class CampaignActionDatabase {
static final CampaignActionDatabase instance = CampaignActionDatabase._internal();

static Database? _database;

CampaignActionDatabase._internal();

Future<Database> get database async {
return _database ??= await _initDatabase();
}

Future<Database> _initDatabase() async {
final databasePath = await getDatabasesPath();
final path = join(databasePath, 'campaign_action_db.db');
return await openDatabase(
path,
version: 1,
onCreate: _createDatabase,
);
}

Future<void> _createDatabase(Database db, _) async {
return await db.execute('''
CREATE TABLE ${CampaignActionFields.tableName} (
${CampaignActionFields.id} ${CampaignActionFields.idType},
${CampaignActionFields.poiId} ${CampaignActionFields.intTypeNullable},
${CampaignActionFields.poiTempId} ${CampaignActionFields.intType},
${CampaignActionFields.actionType} ${CampaignActionFields.intType},
${CampaignActionFields.serialized} ${CampaignActionFields.textTypeNullable}
)
''');
}

Future<CampaignAction> create(CampaignAction campaignAction) async {
final db = await instance.database;
final id = await db.insert(CampaignActionFields.tableName, campaignAction.toMap());
return campaignAction.copyWith(id: id);
}

Future<List<CampaignAction>> readAll() async {
final db = await instance.database;
const orderBy = '${CampaignActionFields.poiTempId} ASC, ${CampaignActionFields.id} ASC';
final result = await db.query(CampaignActionFields.tableName, orderBy: orderBy);
return result.map((json) => CampaignAction.fromMap(json)).toList();
}

Future<List<CampaignAction>> readAllByActionType(List<int> posterActions) async {
final db = await instance.database;
const orderBy = '${CampaignActionFields.poiTempId} ASC, ${CampaignActionFields.id} ASC';
final result = await db.query(
CampaignActionFields.tableName,
orderBy: orderBy,
where: '${CampaignActionFields.actionType} IN (${List.filled(posterActions.length, '?').join(',')})',
whereArgs: posterActions,
);
return result.map((json) => CampaignAction.fromMap(json)).toList();
}

Future<List<CampaignAction>> getActionsWithPoiId(String poiId) async {
final db = await instance.database;
const orderBy = '${CampaignActionFields.poiTempId} ASC, ${CampaignActionFields.id} ASC';
final result = await db.query(
CampaignActionFields.tableName,
orderBy: orderBy,
where: '${CampaignActionFields.poiId} = ? OR ${CampaignActionFields.poiTempId} = ?',
whereArgs: [poiId, poiId],
);
return result.map((json) => CampaignAction.fromMap(json)).toList();
}

Future<void> update(CampaignAction campaignAction) async {
final db = await instance.database;
db.update(
CampaignActionFields.tableName,
campaignAction.toMap(),
where: '${CampaignActionFields.id} = ?',
whereArgs: [campaignAction.id],
);
}

Future<void> updatePoiId(int oldId, int newId) async {
final db = await instance.database;
await db.update(
CampaignActionFields.tableName,
{CampaignActionFields.poiId: newId},
where: '${CampaignActionFields.poiId} = ?',
whereArgs: [oldId],
);
}

Future<int> delete(int id) async {
final db = await instance.database;
return await db.delete(
CampaignActionFields.tableName,
where: '${CampaignActionFields.id} = ?',
whereArgs: [id],
);
}

Future<void> close() async {
final db = await instance.database;
db.close();
}

Future<int> getCount() async {
final db = await instance.database;
int count = Sqflite.firstIntValue(await db.rawQuery('SELECT COUNT(*) FROM ${CampaignActionFields.tableName}')) ?? 0;
return count;
}

Future<bool> actionsWithPoiIdExists(String poiId) async {
final db = await instance.database;
final result = await db.query(
CampaignActionFields.tableName,
columns: ['COUNT(*)'],
where: '${CampaignActionFields.poiId} = ? OR ${CampaignActionFields.poiTempId} = ?',
whereArgs: [poiId, poiId],
);
return (Sqflite.firstIntValue(result) ?? 0) > 0;
}
}

class CampaignActionFields {
static const String tableName = 'campaign_action';
static const String idType = 'INTEGER PRIMARY KEY AUTOINCREMENT';
static const String textTypeNullable = 'TEXT';
static const String intType = 'INTEGER NOT NULL';
static const String intTypeNullable = 'INTEGER';
static const String id = '_id';
static const String poiId = 'poiId';
static const String poiTempId = 'poiTempId';
static const String actionType = 'actionType';
static const String serialized = 'serialized';
}
33 changes: 25 additions & 8 deletions lib/app/services/converters.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import 'dart:convert';

import 'package:gruene_app/app/geocode/nominatim.dart';
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_create_model.dart';
import 'package:gruene_app/features/campaigns/models/doors/door_detail_model.dart';
import 'package:gruene_app/features/campaigns/models/doors/door_update_model.dart';
import 'package:gruene_app/features/campaigns/models/flyer/flyer_create_model.dart';
import 'package:gruene_app/features/campaigns/models/flyer/flyer_detail_model.dart';
import 'package:gruene_app/features/campaigns/models/flyer/flyer_update_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 @@ -15,17 +24,25 @@ import 'package:intl/intl.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
import 'package:turf/turf.dart' as turf;

part 'converters/poi_type_parsing.dart';
part 'converters/address_model_parsing.dart';
part 'converters/campaign_action_parsing.dart';
part 'converters/date_time_parsing.dart';
part 'converters/focus_area_parsing.dart';
part 'converters/lat_lng_parsing.dart';
part 'converters/lat_lng_parsing_extended.dart';
part 'converters/poi_service_type_parsing.dart';
part 'converters/poi_poster_status_parsing.dart';
part 'converters/poster_status_parsing.dart';
part 'converters/address_model_parsing.dart';
part 'converters/map_string_dynamic_converter.dart';
part 'converters/place_parser.dart';
part 'converters/poi_address_parsing.dart';
part 'converters/focus_area_parsing.dart';
part 'converters/poi_parsing.dart';
part 'converters/poi_poster_status_parsing.dart';
part 'converters/poi_service_type_parsing.dart';
part 'converters/poi_type_parsing.dart';
part 'converters/poster_create_model_parsing.dart';
part 'converters/poster_status_parsing.dart';
part 'converters/poster_update_model_parsing.dart';
part 'converters/slider_range_parsing.dart';
part 'converters/place_parser.dart';
part 'converters/string_extension.dart';
part 'converters/date_time_parsing.dart';
part 'converters/door_update_model_parsing.dart';
part 'converters/door_create_model_parsing.dart';
part 'converters/flyer_create_model_parsing.dart';
part 'converters/flyer_update_model_parsing.dart';
74 changes: 74 additions & 0 deletions lib/app/services/converters/campaign_action_parsing.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
part of '../converters.dart';

extension CampaignActionParsing on CampaignAction {
PosterCreateModel getAsPosterCreate() {
var data = jsonDecode(serialized!) as Map<String, dynamic>;
var model = PosterCreateModel.fromJson(data.convertLatLongField());
return model;
}

PosterUpdateModel getAsPosterUpdate() {
var data = jsonDecode(serialized!) as Map<String, dynamic>;
var model = PosterUpdateModel.fromJson(data.updateIdField(poiId!).convertLatLongField());

return model;
}

DoorCreateModel getAsDoorCreate() {
var data = jsonDecode(serialized!) as Map<String, dynamic>;
var model = DoorCreateModel.fromJson(data.convertLatLongField());

return model;
}

DoorUpdateModel getAsDoorUpdate() {
var data = jsonDecode(serialized!) as Map<String, dynamic>;
var model = DoorUpdateModel.fromJson(data.updateIdField(poiId!).convertLatLongField());

return model;
}

FlyerCreateModel getAsFlyerCreate() {
var data = jsonDecode(serialized!) as Map<String, dynamic>;
var model = FlyerCreateModel.fromJson(data.convertLatLongField());

return model;
}

FlyerUpdateModel getAsFlyerUpdate() {
var data = jsonDecode(serialized!) as Map<String, dynamic>;
var model = FlyerUpdateModel.fromJson(data.updateIdField(poiId!).convertLatLongField());

return model;
}

PosterListItemModel getPosterUpdateAsPosterListItem(DateTime originalCreatedAt) {
var updateModel = getAsPosterUpdate().transformToPosterDetailModel();
return PosterListItemModel(
id: updateModel.id,
thumbnailUrl: updateModel.thumbnailUrl,
imageUrl: updateModel.imageUrl,
address: updateModel.address,
status: updateModel.status.translatePosterStatus(),
lastChangeStatus: t.campaigns.poster.updated,
lastChangeDateTime: '${DateTime.fromMillisecondsSinceEpoch(poiTempId).getAsLocalDateTimeString()}*',
createdAt: originalCreatedAt,
isCached: true,
);
}

PosterListItemModel getPosterCreateAsPosterListItem() {
var createModel = getAsPosterCreate().transformToPosterDetailModel(poiTempId.toString());
return PosterListItemModel(
id: createModel.id,
thumbnailUrl: createModel.thumbnailUrl,
imageUrl: createModel.imageUrl,
address: createModel.address,
status: createModel.status.translatePosterStatus(),
lastChangeStatus: t.campaigns.poster.updated,
lastChangeDateTime: '${DateTime.fromMillisecondsSinceEpoch(poiTempId).getAsLocalDateTimeString()}*',
createdAt: DateTime.fromMillisecondsSinceEpoch(poiTempId),
isCached: true,
);
}
}
23 changes: 23 additions & 0 deletions lib/app/services/converters/door_create_model_parsing.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
part of '../converters.dart';

extension DoorCreateModelParsing on DoorCreateModel {
DoorDetailModel transformToDoorDetailModel(String temporaryId) {
return DoorDetailModel(
id: temporaryId,
address: address,
closedDoors: closedDoors,
openedDoors: openedDoors,
location: location,
createdAt: '${DateTime.now().getAsLocalDateTimeString()}*', // should mark this as preliminary
isCached: true,
);
}

MarkerItemModel transformToVirtualMarkerItem(int temporaryId) {
return MarkerItemModel.virtual(
id: temporaryId,
status: PoiServiceType.door.name,
location: location,
);
}
}
21 changes: 21 additions & 0 deletions lib/app/services/converters/door_update_model_parsing.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
part of '../converters.dart';

extension DoorUpdateModelParsing on DoorUpdateModel {
DoorDetailModel transformToDoorDetailModel() {
var newDoorDetail = oldDoorDetail.copyWith(
address: address,
closedDoors: closedDoors,
openedDoors: openedDoors,
isCached: true,
);
return newDoorDetail;
}

MarkerItemModel transformToVirtualMarkerItem() {
return MarkerItemModel.virtual(
id: int.parse(id),
status: PoiServiceType.door.name,
location: location,
);
}
}
22 changes: 22 additions & 0 deletions lib/app/services/converters/flyer_create_model_parsing.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
part of '../converters.dart';

extension FlyerCreateModelParsing on FlyerCreateModel {
FlyerDetailModel transformToFlyerDetailModel(String temporaryId) {
return FlyerDetailModel(
id: temporaryId,
address: address,
flyerCount: flyerCount,
location: location,
createdAt: '${DateTime.now().getAsLocalDateTimeString()}*', // should mark this as preliminary
isCached: true,
);
}

MarkerItemModel transformToVirtualMarkerItem(int temporaryId) {
return MarkerItemModel.virtual(
id: temporaryId,
status: PoiServiceType.flyer.name,
location: location,
);
}
}
20 changes: 20 additions & 0 deletions lib/app/services/converters/flyer_update_model_parsing.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
part of '../converters.dart';

extension FlyerUpdateModelParsing on FlyerUpdateModel {
FlyerDetailModel transformToFlyerDetailModel() {
var newFlyerDetail = oldFlyerDetail.copyWith(
address: address,
flyerCount: flyerCount,
isCached: true,
);
return newFlyerDetail;
}

MarkerItemModel transformToVirtualMarkerItem() {
return MarkerItemModel.virtual(
id: int.parse(id),
status: PoiServiceType.flyer.name,
location: location,
);
}
}
Loading

0 comments on commit 98e1398

Please sign in to comment.