diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..1853912
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+*.ipynb inguist-detectable=false
diff --git a/.gitignore b/.gitignore
index b5445c2..75d9de4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,6 +45,3 @@ app.*.map.json
# Exclude sensitive information
lib/data/sensitive.dart
-
-# Exclude tensorflow lite model files
-*.tflite
diff --git a/README.md b/README.md
index 09f0bd6..c63e332 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,17 @@
-# modern_art_app
+# Mobile app for the Cypriot National Gallery of Modern Art
-A Flutter application for the Cypriot National Gallery of Modern Art.
+The repository contains the source code for a cross-platform mobile app (built using Flutter) for
+the [Cypriot National Gallery of Modern Art](https://www.nicosia.org.cy/en-GB/discover/picture-galleries/state-gallery-of-contemporary-art/)
+in Nicosia, Cyprus.
+
+The app was built by the [BIO-SCENT](https://bioscent.cyens.org.cy/) research group at
+the [CYENS Centre of Excellence](https://www.cyens.org.cy), as part of the study _"A systematic
+approach for developing a robust artwork recognition framework using smartphone cameras"_ (currently
+under review).
+
+The app is able to automatically identify a number of artworks at the Gallery through the use of
+computer vision, and display details about them to the user. The artwork identification is performed
+on-device, using a Convolutional Neural Network (CNN) that trained using transfer learning on
+MobileNet V2.
+
+![App screenshots](assets/app_screenshots.png)
diff --git a/analysis_options.yaml b/analysis_options.yaml
new file mode 100644
index 0000000..13d7ec3
--- /dev/null
+++ b/analysis_options.yaml
@@ -0,0 +1,24 @@
+# This file configures the analyzer to use the lint rule set from `package:lint`
+
+# For apps, use the default set
+include: package:lint/analysis_options.yaml
+
+# Packages, that may be distributed (i.e. via pub.dev) should use the package
+# version, resulting in a better pub score.
+# include: package:lint/analysis_options_package.yaml
+
+# You might want to exclude auto-generated files from dart analysis
+analyzer:
+ exclude:
+ #- '**.freezed.dart'
+
+# A list of all rules can be found at https://dart-lang.github.io/linter/lints/options/options.html
+linter:
+ rules:
+ # avoid_classes_with_only_static_members: false
+
+ # Make constructors the first thing in every class
+ sort_constructors_first: true
+
+ # Choose wisely, but you don't have to
+ prefer_single_quotes: true
\ No newline at end of file
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index d419a06..1470d03 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -23,19 +23,6 @@
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
-
-
-
diff --git a/assets/CHANGELOG.md b/assets/CHANGELOG.md
index 5f9f6bc..482aee5 100644
--- a/assets/CHANGELOG.md
+++ b/assets/CHANGELOG.md
@@ -1,3 +1,7 @@
+## Version 1.0.0 (2021-12-21)
+- App migrated to null-safety
+- Many UI improvements and bug fixes
+
## Version 0.12.2 (2021-02-17)
- Minor UI improvements
- New tile in settings with link to the Gallery's website
diff --git a/assets/app_screenshots.png b/assets/app_screenshots.png
new file mode 100644
index 0000000..e5c81c1
Binary files /dev/null and b/assets/app_screenshots.png differ
diff --git a/assets/tflite/MobNetNoArt500Frames_4.tflite b/assets/tflite/MobNetNoArt500Frames_4.tflite
new file mode 100644
index 0000000..d81648b
Binary files /dev/null and b/assets/tflite/MobNetNoArt500Frames_4.tflite differ
diff --git a/lib/data/artists_dao.dart b/lib/data/artists_dao.dart
index ee6fe7a..2310a73 100644
--- a/lib/data/artists_dao.dart
+++ b/lib/data/artists_dao.dart
@@ -13,34 +13,42 @@ class ArtistsDao extends DatabaseAccessor with _$ArtistsDaoMixin {
into(artists).insertOnConflictUpdate(artist);
/// Gets a stream of all artists in the db.
- Stream> watchAllArtists({String languageCode = "en"}) =>
- (select(artists).join([
- leftOuterJoin(
- artistTranslations, artistTranslations.id.equalsExp(artists.id))
- ])
- ..where(artistTranslations.languageCode.equals(languageCode)))
- .map((e) {
- Artist artist = e.readTable(artists);
- return artist.copyWith(
+ Stream> watchAllArtists({String languageCode = 'en'}) =>
+ (select(artists).join(
+ [
+ leftOuterJoin(
+ artistTranslations,
+ artistTranslations.id.equalsExp(artists.id),
+ )
+ ],
+ )..where(artistTranslations.languageCode.equals(languageCode)))
+ .map(
+ (e) {
+ final Artist artist = e.readTable(artists);
+ return artist.copyWith(
name: e.readTable(artistTranslations).name,
- biography: e.readTable(artistTranslations).biography);
- }).watch();
+ biography: e.readTable(artistTranslations).biography,
+ );
+ },
+ ).watch();
/// Get artist by id.
Future getArtistById({
- @required String artistId,
- String languageCode = "en",
+ required String artistId,
+ String languageCode = 'en',
}) {
return ((select(artists)..where((tbl) => tbl.id.equals(artistId))).join(
[
leftOuterJoin(
- artistTranslations, artistTranslations.id.equalsExp(artists.id))
+ artistTranslations,
+ artistTranslations.id.equalsExp(artists.id),
+ )
],
)..where(artistTranslations.languageCode.equals(languageCode)))
.map(
(e) {
- Artist artist = e.readTable(artists);
- ArtistTranslation translation = e.readTable(artistTranslations);
+ final Artist artist = e.readTable(artists);
+ final ArtistTranslation translation = e.readTable(artistTranslations);
return artist.copyWith(
name: translation.name,
biography: translation.biography,
diff --git a/lib/data/artworks_dao.dart b/lib/data/artworks_dao.dart
index 331a602..fce2524 100644
--- a/lib/data/artworks_dao.dart
+++ b/lib/data/artworks_dao.dart
@@ -14,58 +14,87 @@ class ArtworksDao extends DatabaseAccessor
/// Gets a live stream of all artworks in the db (automatically emits new items
/// whenever the underlying data changes).
- Stream> watchAllArtworks({String languageCode = "en"}) =>
+ Stream> watchAllArtworks({String languageCode = 'en'}) =>
(select(artworks).join([
leftOuterJoin(
- artworkTranslations, artworkTranslations.id.equalsExp(artworks.id)),
- leftOuterJoin(artistTranslations,
- artistTranslations.id.equalsExp(artworks.artistId))
+ artworkTranslations,
+ artworkTranslations.id.equalsExp(artworks.id),
+ ),
+ leftOuterJoin(
+ artistTranslations,
+ artistTranslations.id.equalsExp(artworks.artistId),
+ ),
])
- ..where(artworkTranslations.languageCode.equals(languageCode) &
- artistTranslations.languageCode.equals(languageCode)))
- .map((e) => composeTranslatedArtwork(
+ ..where(
+ artworkTranslations.languageCode.equals(languageCode) &
+ artistTranslations.languageCode.equals(languageCode),
+ ))
+ .map(
+ (e) => composeTranslatedArtwork(
artwork: e.readTable(artworks),
artworkTranslation: e.readTable(artworkTranslations),
- artistTranslation: e.readTable(artistTranslations)))
+ artistTranslation: e.readTable(artistTranslations),
+ ),
+ )
.watch();
/// Gets a list of all [Artwork]s for a given [Artist].
Stream> watchArtworksByArtist({
- @required String artistId,
- String languageCode = "en",
+ required String artistId,
+ String languageCode = 'en',
}) =>
((select(artworks)..where((artwork) => artwork.artistId.equals(artistId)))
- .join([
- leftOuterJoin(
- artworkTranslations, artworkTranslations.id.equalsExp(artworks.id)),
- leftOuterJoin(artistTranslations,
- artistTranslations.id.equalsExp(artworks.artistId))
- ])
- ..where(artworkTranslations.languageCode.equals(languageCode) &
- artistTranslations.languageCode.equals(languageCode)))
- .map((e) => composeTranslatedArtwork(
+ .join(
+ [
+ leftOuterJoin(
+ artworkTranslations,
+ artworkTranslations.id.equalsExp(artworks.id),
+ ),
+ leftOuterJoin(
+ artistTranslations,
+ artistTranslations.id.equalsExp(artworks.artistId),
+ ),
+ ],
+ )..where(
+ artworkTranslations.languageCode.equals(languageCode) &
+ artistTranslations.languageCode.equals(languageCode),
+ ))
+ .map(
+ (e) => composeTranslatedArtwork(
artwork: e.readTable(artworks),
artworkTranslation: e.readTable(artworkTranslations),
- artistTranslation: e.readTable(artistTranslations)))
+ artistTranslation: e.readTable(artistTranslations),
+ ),
+ )
.watch();
/// Get artwork by id.
Future getArtworkById({
- @required String artworkId,
- String languageCode = "en",
+ required String artworkId,
+ String languageCode = 'en',
}) =>
- ((select(artworks)..where((tbl) => tbl.id.equals(artworkId))).join([
- leftOuterJoin(
- artworkTranslations, artworkTranslations.id.equalsExp(artworks.id)),
- leftOuterJoin(artistTranslations,
- artistTranslations.id.equalsExp(artworks.artistId))
- ])
- ..where(artworkTranslations.languageCode.equals(languageCode) &
- artistTranslations.languageCode.equals(languageCode)))
- .map((e) => composeTranslatedArtwork(
+ ((select(artworks)..where((tbl) => tbl.id.equals(artworkId))).join(
+ [
+ leftOuterJoin(
+ artworkTranslations,
+ artworkTranslations.id.equalsExp(artworks.id),
+ ),
+ leftOuterJoin(
+ artistTranslations,
+ artistTranslations.id.equalsExp(artworks.artistId),
+ ),
+ ],
+ )..where(
+ artworkTranslations.languageCode.equals(languageCode) &
+ artistTranslations.languageCode.equals(languageCode),
+ ))
+ .map(
+ (e) => composeTranslatedArtwork(
artwork: e.readTable(artworks),
artworkTranslation: e.readTable(artworkTranslations),
- artistTranslation: e.readTable(artistTranslations)))
+ artistTranslation: e.readTable(artistTranslations),
+ ),
+ )
.getSingle();
/// Safely insert an [Artwork] into db, with the use of an [ArtworksCompanion].
@@ -80,11 +109,12 @@ class ArtworksDao extends DatabaseAccessor
}
Artwork composeTranslatedArtwork({
- Artwork artwork,
- ArtworkTranslation artworkTranslation,
- ArtistTranslation artistTranslation,
+ required Artwork artwork,
+ required ArtworkTranslation artworkTranslation,
+ required ArtistTranslation artistTranslation,
}) =>
artwork.copyWith(
- name: artworkTranslation.name,
- description: artworkTranslation.description,
- artist: artistTranslation.name);
+ name: artworkTranslation.name,
+ description: artworkTranslation.description,
+ artist: artistTranslation.name,
+ );
diff --git a/lib/data/data_processing.dart b/lib/data/data_processing.dart
index 145d8ed..4896a83 100644
--- a/lib/data/data_processing.dart
+++ b/lib/data/data_processing.dart
@@ -1,6 +1,7 @@
import 'dart:convert';
import 'dart:io';
+import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;
import 'package:modern_art_app/data/artists_dao.dart';
@@ -11,10 +12,10 @@ import 'package:modern_art_app/data/url_data_sources.dart';
/// Returns a list with the paths of all asset files; the [assetType] argument
/// can optionally be used to get assets only from specific subdirectories in
/// assets.
-Future> getAllAssets({String assetType = "assets"}) async {
- final assetManifest = await rootBundle.loadString("AssetManifest.json");
- final Map assetMap = json.decode(assetManifest);
-// await Future.delayed(Duration(seconds: 1));
+Future> getAllAssets({String assetType = 'assets'}) async {
+ final assetManifest = await rootBundle.loadString('AssetManifest.json');
+ final Map assetMap =
+ json.decode(assetManifest) as Map;
return assetMap.keys
.where((String key) => key.contains(assetType.toLowerCase()))
.toList();
@@ -23,67 +24,72 @@ Future> getAllAssets({String assetType = "assets"}) async {
/// Reads the provided json file at [assetsPath] and returns a future list of map
/// item entries (the entries must be processed with [parseItemMap] first to be
/// valid db entities).
-Future> getLocalJsonItemList(String assetsPath) async {
- return rootBundle
- .loadString(assetsPath)
- .then((jsonStr) => List