Skip to content

Commit

Permalink
#260 update servers:
Browse files Browse the repository at this point in the history
- 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)
  • Loading branch information
Stift committed Jan 17, 2025
1 parent c47ff5c commit 8646b67
Show file tree
Hide file tree
Showing 44 changed files with 1,177 additions and 151 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
"gruene",
"housenumber"
],
"files.eol": "\n"
"files.eol": "\n",
"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
130 changes: 130 additions & 0 deletions lib/app/services/campaign_action_database.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import 'dart:io';

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');
await File(path).delete();
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;
}
}

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';
}
11 changes: 11 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,9 @@ 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';
part 'converters/map_string_dynamic_converter.dart';
part 'converters/string_extension.dart';
16 changes: 16 additions & 0 deletions lib/app/services/converters/campaign_action_parsing.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
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;
}
}
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);
}
}
13 changes: 13 additions & 0 deletions lib/app/services/converters/map_string_dynamic_converter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
part of '../converters.dart';

extension MapStringDynamicConverter on Map<String, dynamic> {
Map<String, dynamic> convertLatLongField({String fieldName = 'location'}) {
this[fieldName] = (this[fieldName] as List<dynamic>).cast<double>();
return this;
}

Map<String, dynamic> updateIdField(int id) {
this['id'] = id.toString();
return this;
}
}
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: imageFileLocation,
imageUrl: imageFileLocation,
location: location,
comment: '',
createdAt: '${DateTime.now().getAsLocalDateTimeString()}*', // should mark this as preliminary
isCached: true,
);
}
}
31 changes: 31 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,31 @@
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) {
var newPosterDetail = oldPosterDetail.copyWith(
status: status,
address: address,
thumbnailUrl: newImageFileLocation ?? (!removePreviousPhotos ? oldPosterDetail.thumbnailUrl : null),
imageUrl: newImageFileLocation ?? (!removePreviousPhotos ? oldPosterDetail.imageUrl : null),
comment: comment,
isCached: true,
);
return newPosterDetail;
}

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

return newPosterUpdate.copyWith(
removePreviousPhotos: newPosterUpdate.removePreviousPhotos || oldPosterUdpate.removePreviousPhotos,
);
}
}
5 changes: 5 additions & 0 deletions lib/app/services/converters/string_extension.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
part of '../converters.dart';

extension StringExtension on String {
bool isNetworkImageUrl() => Uri.parse(this).hasScheme;
}
Loading

0 comments on commit 8646b67

Please sign in to comment.