diff --git a/README.md b/README.md index bce62b2..3cb70b2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,15 @@ A Flutter application for interfacing with the Friendica social network. -For Linux development be sure that libsecret-1-dev and libjsoncpp-dev are installed on the machine. For running only make sure the non-dev versions are... +For Linux development be sure that libsecret-1-dev and libjsoncpp-dev are installed on the machine. For running only +make sure the non-dev versions are... +## Development Notes + +Whenever a model is changed that is stored in ObjectBox it is necessary to execute the command: + +```bash +flutter pub run build_runner build +``` Licensed with the Mozilla Public License 2.0 copyleft license. diff --git a/lib/data/interfaces/hashtag_repo_intf.dart b/lib/data/interfaces/hashtag_repo_intf.dart new file mode 100644 index 0000000..ae9f17d --- /dev/null +++ b/lib/data/interfaces/hashtag_repo_intf.dart @@ -0,0 +1,11 @@ +import '../../models/hashtag.dart'; + +class IHashtagRepo { + void add(Hashtag tag) { + throw UnimplementedError(); + } + + List getMatchingHashTags(String text) { + throw UnimplementedError(); + } +} diff --git a/lib/data/objectbox/objectbox_hashtag_repo.dart b/lib/data/objectbox/objectbox_hashtag_repo.dart new file mode 100644 index 0000000..46c741b --- /dev/null +++ b/lib/data/objectbox/objectbox_hashtag_repo.dart @@ -0,0 +1,34 @@ +import '../../globals.dart'; +import '../../models/hashtag.dart'; +import '../../objectbox.g.dart'; +import '../interfaces/hashtag_repo_intf.dart'; +import 'objectbox_cache.dart'; + +class ObjectBoxHashtagRepo implements IHashtagRepo { + late final Box box; + + ObjectBoxHashtagRepo() { + box = getIt().store.box(); + } + + @override + void add(Hashtag tag) { + box.putAsync(tag); + } + + @override + List getMatchingHashTags(String text) { + return (box + .query( + text.length <= 2 + ? Hashtag_.tag.startsWith(text, caseSensitive: false) + : Hashtag_.tag.contains(text, caseSensitive: false), + ) + .order(Hashtag_.tag) + .build() + ..limit = 100) + .find() + .map((h) => h.tag) + .toList(); + } +} diff --git a/lib/main.dart b/lib/main.dart index 7d8343c..f9e016f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,9 +7,11 @@ import 'package:result_monad/result_monad.dart'; import 'data/interfaces/connections_repo_intf.dart'; import 'data/interfaces/groups_repo.intf.dart'; +import 'data/interfaces/hashtag_repo_intf.dart'; import 'data/memory/memory_groups_repo.dart'; import 'data/objectbox/objectbox_cache.dart'; import 'data/objectbox/objectbox_connections_repo.dart'; +import 'data/objectbox/objectbox_hashtag_repo.dart'; import 'globals.dart'; import 'models/TimelineIdentifiers.dart'; import 'routes.dart'; @@ -54,6 +56,7 @@ void main() async { final objectBoxCache = await ObjectBoxCache.create(); getIt.registerSingleton(objectBoxCache); getIt.registerSingleton(ObjectBoxConnectionsRepo()); + getIt.registerSingleton(ObjectBoxHashtagRepo()); getIt.registerSingleton(MemoryGroupsRepo()); getIt.registerLazySingleton(() => ConnectionsManager()); getIt.registerLazySingleton(() => HashtagService()); diff --git a/lib/models/hashtag.dart b/lib/models/hashtag.dart new file mode 100644 index 0000000..6d2afa5 --- /dev/null +++ b/lib/models/hashtag.dart @@ -0,0 +1,30 @@ +import 'package:objectbox/objectbox.dart'; + +@Entity() +class Hashtag { + @Id() + int id; + + @Unique(onConflict: ConflictStrategy.replace) + String tag; + + String url; + + @Property(type: PropertyType.date) + DateTime lastUpdateTime; + + Hashtag({ + this.id = 0, + required this.tag, + required this.url, + DateTime? updateTime, + }) : lastUpdateTime = updateTime ?? DateTime.now(); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Hashtag && runtimeType == other.runtimeType && tag == other.tag; + + @override + int get hashCode => tag.hashCode; +} diff --git a/lib/objectbox-model.json b/lib/objectbox-model.json index 1f5cef7..2597277 100644 --- a/lib/objectbox-model.json +++ b/lib/objectbox-model.json @@ -58,10 +58,41 @@ } ], "relations": [] + }, + { + "id": "2:8060242331335522964", + "lastPropertyId": "4:985152873657204249", + "name": "Hashtag", + "properties": [ + { + "id": "1:3633001791521338712", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:3468373950035339457", + "name": "tag", + "type": 9, + "flags": 34848, + "indexId": "2:6156017341759176249" + }, + { + "id": "3:5102584273729210526", + "name": "url", + "type": 9 + }, + { + "id": "4:985152873657204249", + "name": "lastUpdateTime", + "type": 10 + } + ], + "relations": [] } ], - "lastEntityId": "1:1213035855270739890", - "lastIndexId": "1:8342366639839511243", + "lastEntityId": "2:8060242331335522964", + "lastIndexId": "2:6156017341759176249", "lastRelationId": "0:0", "lastSequenceId": "0:0", "modelVersion": 5, diff --git a/lib/objectbox.g.dart b/lib/objectbox.g.dart index 486e8d3..82ec189 100644 --- a/lib/objectbox.g.dart +++ b/lib/objectbox.g.dart @@ -15,6 +15,7 @@ import 'package:objectbox/objectbox.dart'; import 'package:objectbox_flutter_libs/objectbox_flutter_libs.dart'; import 'models/connection.dart'; +import 'models/hashtag.dart'; export 'package:objectbox/objectbox.dart'; // so that callers only have to import this file @@ -73,6 +74,36 @@ final _entities = [ flags: 0) ], relations: [], + backlinks: []), + ModelEntity( + id: const IdUid(2, 8060242331335522964), + name: 'Hashtag', + lastPropertyId: const IdUid(4, 985152873657204249), + flags: 0, + properties: [ + ModelProperty( + id: const IdUid(1, 3633001791521338712), + name: 'id', + type: 6, + flags: 1), + ModelProperty( + id: const IdUid(2, 3468373950035339457), + name: 'tag', + type: 9, + flags: 34848, + indexId: const IdUid(2, 6156017341759176249)), + ModelProperty( + id: const IdUid(3, 5102584273729210526), + name: 'url', + type: 9, + flags: 0), + ModelProperty( + id: const IdUid(4, 985152873657204249), + name: 'lastUpdateTime', + type: 10, + flags: 0) + ], + relations: [], backlinks: []) ]; @@ -96,8 +127,8 @@ Future openStore( ModelDefinition getObjectBoxModel() { final model = ModelInfo( entities: _entities, - lastEntityId: const IdUid(1, 1213035855270739890), - lastIndexId: const IdUid(1, 8342366639839511243), + lastEntityId: const IdUid(2, 8060242331335522964), + lastIndexId: const IdUid(2, 6156017341759176249), lastRelationId: const IdUid(0, 0), lastSequenceId: const IdUid(0, 0), retiredEntityUids: const [], @@ -160,6 +191,40 @@ ModelDefinition getObjectBoxModel() { ..dbStatus = const fb.Int64Reader().vTableGet(buffer, rootOffset, 18, 0); + return object; + }), + Hashtag: EntityDefinition( + model: _entities[1], + toOneRelations: (Hashtag object) => [], + toManyRelations: (Hashtag object) => {}, + getId: (Hashtag object) => object.id, + setId: (Hashtag object, int id) { + object.id = id; + }, + objectToFB: (Hashtag object, fb.Builder fbb) { + final tagOffset = fbb.writeString(object.tag); + final urlOffset = fbb.writeString(object.url); + fbb.startTable(5); + fbb.addInt64(0, object.id); + fbb.addOffset(1, tagOffset); + fbb.addOffset(2, urlOffset); + fbb.addInt64(3, object.lastUpdateTime.millisecondsSinceEpoch); + fbb.finish(fbb.endTable()); + return object.id; + }, + objectFromFB: (Store store, ByteData fbData) { + final buffer = fb.BufferContext(fbData); + final rootOffset = buffer.derefObject(0); + + final object = Hashtag( + id: const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0), + tag: const fb.StringReader(asciiOptimization: true) + .vTableGet(buffer, rootOffset, 6, ''), + url: const fb.StringReader(asciiOptimization: true) + .vTableGet(buffer, rootOffset, 8, '')) + ..lastUpdateTime = DateTime.fromMillisecondsSinceEpoch( + const fb.Int64Reader().vTableGet(buffer, rootOffset, 10, 0)); + return object; }) }; @@ -204,3 +269,19 @@ class Connection_ { static final lastUpdateTime = QueryIntegerProperty(_entities[0].properties[8]); } + +/// [Hashtag] entity fields to define ObjectBox queries. +class Hashtag_ { + /// see [Hashtag.id] + static final id = QueryIntegerProperty(_entities[1].properties[0]); + + /// see [Hashtag.tag] + static final tag = QueryStringProperty(_entities[1].properties[1]); + + /// see [Hashtag.url] + static final url = QueryStringProperty(_entities[1].properties[2]); + + /// see [Hashtag.lastUpdateTime] + static final lastUpdateTime = + QueryIntegerProperty(_entities[1].properties[3]); +} diff --git a/lib/serializers/mastodon/hashtag_mastodon_extensions.dart b/lib/serializers/mastodon/hashtag_mastodon_extensions.dart new file mode 100644 index 0000000..b325584 --- /dev/null +++ b/lib/serializers/mastodon/hashtag_mastodon_extensions.dart @@ -0,0 +1,12 @@ +import '../../models/hashtag.dart'; + +extension HashtagMastodonExtensions on Hashtag { + static Hashtag fromJson(Map json) { + final tag = json['name']; + final url = json['url']; + return Hashtag( + tag: tag, + url: url, + ); + } +} diff --git a/lib/serializers/mastodon/timeline_entry_mastodon_extensions.dart b/lib/serializers/mastodon/timeline_entry_mastodon_extensions.dart index 31e09f6..2522da9 100644 --- a/lib/serializers/mastodon/timeline_entry_mastodon_extensions.dart +++ b/lib/serializers/mastodon/timeline_entry_mastodon_extensions.dart @@ -10,6 +10,7 @@ import '../../services/connections_manager.dart'; import '../../services/hashtag_service.dart'; import '../../utils/dateutils.dart'; import 'connection_mastodon_extensions.dart'; +import 'hashtag_mastodon_extensions.dart'; final _logger = Logger('TimelineEntryMastodonExtensions'); @@ -75,9 +76,9 @@ extension TimelineEntryMastodonExtensions on TimelineEntry { final List? tags = json['tags']; if (tags?.isNotEmpty ?? false) { final tagManager = getIt(); - for (final tag in tags!) { - final tagName = tag['name']; - tagManager.addHashtage(tagName); + for (final tagJson in tags!) { + final tag = HashtagMastodonExtensions.fromJson(tagJson); + tagManager.add(tag); } } diff --git a/lib/services/hashtag_service.dart b/lib/services/hashtag_service.dart index 70fec9a..a9c6ea9 100644 --- a/lib/services/hashtag_service.dart +++ b/lib/services/hashtag_service.dart @@ -1,21 +1,22 @@ import 'package:flutter/foundation.dart'; -class HashtagService extends ChangeNotifier { - final _hashTags = {}; +import '../data/interfaces/hashtag_repo_intf.dart'; +import '../globals.dart'; +import '../models/hashtag.dart'; - void clear() { - _hashTags.clear(); - notifyListeners(); +class HashtagService extends ChangeNotifier { + late final IHashtagRepo repo; + + HashtagService() { + repo = getIt(); } - void addHashtage(String hashtag) { - _hashTags.add(hashtag); + void add(Hashtag tag) { + repo.add(tag); notifyListeners(); } List getMatchingHashTags(String searchString) { - return _hashTags - .where((tag) => tag.toLowerCase().contains(searchString.toLowerCase())) - .toList(); + return repo.getMatchingHashTags(searchString); } }