init: progressively add entries with saved dates

fullscreen: debug page
This commit is contained in:
Thibault Deckers 2020-04-08 12:32:18 +09:00
parent 3328916c86
commit 2b2e7e31bd
14 changed files with 273 additions and 39 deletions

View file

@ -27,7 +27,7 @@ public class MediaStoreStreamHandler implements EventChannel.StreamHandler {
}
void fetchAll(Activity activity) {
Log.d(LOG_TAG, "fetchAll start");
// Log.d(LOG_TAG, "fetchAll start");
// Instant start = Instant.now();
Handler handler = new Handler(Looper.getMainLooper());
new MediaStoreImageProvider().fetchAll(activity, (entry) -> handler.post(() -> eventSink.success(entry))); // 350ms

View file

@ -150,6 +150,9 @@ public class MediaStoreImageProvider extends ImageProvider {
Log.w(LOG_TAG, "failed to get size for uri=" + itemUri + ", path=" + path + ", mimeType=" + mimeType);
} else {
newEntryHandler.handleEntry(entryMap);
if (entryCount % 30 == 0) {
Thread.sleep(10);
}
entryCount++;
}
}

View file

@ -27,16 +27,22 @@ class CollectionSource {
List<ImageEntry> entries,
}) : _rawEntries = entries ?? [];
final List<DateMetadata> savedDates = [];
Future<void> loadDates() async {
final stopwatch = Stopwatch()..start();
savedDates.addAll(await metadataDb.loadDates());
debugPrint('$runtimeType loadDates complete in ${stopwatch.elapsed.inMilliseconds}ms for ${savedDates.length} saved entries');
}
Future<void> loadCatalogMetadata() async {
final stopwatch = Stopwatch()..start();
final saved = await metadataDb.loadMetadataEntries();
_rawEntries.forEach((entry) {
final contentId = entry.contentId;
if (contentId != null) {
entry.catalogMetadata = saved.firstWhere((metadata) => metadata.contentId == contentId, orElse: () => null);
}
entry.catalogMetadata = saved.firstWhere((metadata) => metadata.contentId == contentId, orElse: () => null);
});
debugPrint('$runtimeType loadCatalogMetadata complete in ${stopwatch.elapsed.inMilliseconds}ms with ${saved.length} saved entries');
debugPrint('$runtimeType loadCatalogMetadata complete in ${stopwatch.elapsed.inMilliseconds}ms for ${saved.length} saved entries');
onCatalogMetadataChanged();
}
@ -45,11 +51,9 @@ class CollectionSource {
final saved = await metadataDb.loadAddresses();
_rawEntries.forEach((entry) {
final contentId = entry.contentId;
if (contentId != null) {
entry.addressDetails = saved.firstWhere((address) => address.contentId == contentId, orElse: () => null);
}
entry.addressDetails = saved.firstWhere((address) => address.contentId == contentId, orElse: () => null);
});
debugPrint('$runtimeType loadAddresses complete in ${stopwatch.elapsed.inMilliseconds}ms with ${saved.length} saved entries');
debugPrint('$runtimeType loadAddresses complete in ${stopwatch.elapsed.inMilliseconds}ms for ${saved.length} saved entries');
onAddressMetadataChanged();
}
@ -124,12 +128,11 @@ class CollectionSource {
sortedCities = lister((address) => address.city);
}
void add(ImageEntry entry) {
_rawEntries.add(entry);
eventBus.fire(EntryAddedEvent(entry));
}
void addAll(Iterable<ImageEntry> entries) {
entries.forEach((entry) {
final contentId = entry.contentId;
entry.catalogDateMillis = savedDates.firstWhere((metadata) => metadata.contentId == contentId, orElse: () => null)?.dateMillis;
});
_rawEntries.addAll(entries);
eventBus.fire(const EntryAddedEvent());
}

View file

@ -27,6 +27,7 @@ class ImageEntry {
final int sourceDateTakenMillis;
final String bucketDisplayName;
final int durationMillis;
int _catalogDateMillis;
CatalogMetadata _catalogMetadata;
AddressDetails _addressDetails;
@ -134,11 +135,11 @@ class ImageEntry {
DateTime get bestDate {
if (_bestDate == null) {
if ((_catalogMetadata?.dateMillis ?? 0) > 0) {
_bestDate = DateTime.fromMillisecondsSinceEpoch(_catalogMetadata.dateMillis);
} else if (sourceDateTakenMillis != null && sourceDateTakenMillis > 0) {
if ((_catalogDateMillis ?? 0) > 0) {
_bestDate = DateTime.fromMillisecondsSinceEpoch(_catalogDateMillis);
} else if ((sourceDateTakenMillis ?? 0) > 0) {
_bestDate = DateTime.fromMillisecondsSinceEpoch(sourceDateTakenMillis);
} else if (dateModifiedSecs != null && dateModifiedSecs > 0) {
} else if ((dateModifiedSecs ?? 0) > 0) {
_bestDate = DateTime.fromMillisecondsSinceEpoch(dateModifiedSecs * 1000);
}
}
@ -181,13 +182,17 @@ class ImageEntry {
CatalogMetadata get catalogMetadata => _catalogMetadata;
set catalogMetadata(CatalogMetadata newMetadata) {
_catalogMetadata = newMetadata;
set catalogDateMillis(int dateMillis) {
_catalogDateMillis = dateMillis;
_bestDate = null;
}
set catalogMetadata(CatalogMetadata newMetadata) {
if (newMetadata == null) return;
catalogDateMillis = newMetadata.dateMillis;
_catalogMetadata = newMetadata;
_bestTitle = null;
if (_catalogMetadata != null) {
metadataChangeNotifier.notifyListeners();
}
metadataChangeNotifier.notifyListeners();
}
Future<void> catalog() async {

View file

@ -1,6 +1,32 @@
import 'package:flutter/widgets.dart';
import 'package:geocoder/model.dart';
class DateMetadata {
final int contentId, dateMillis;
DateMetadata({
this.contentId,
this.dateMillis,
});
factory DateMetadata.fromMap(Map map) {
return DateMetadata(
contentId: map['contentId'],
dateMillis: map['dateMillis'] ?? 0,
);
}
Map<String, dynamic> toMap() => {
'contentId': contentId,
'dateMillis': dateMillis,
};
@override
String toString() {
return 'DateMetadata{contentId=$contentId, dateMillis=$dateMillis}';
}
}
class CatalogMetadata {
final int contentId, dateMillis, videoRotation;
final String xmpSubjects, xmpTitleDescription;

View file

@ -12,6 +12,7 @@ class MetadataDb {
Future<String> get path async => join(await getDatabasesPath(), 'metadata.db');
static const dateTakenTable = 'dateTaken';
static const metadataTable = 'metadata';
static const addressTable = 'address';
static const favouriteTable = 'favourites';
@ -23,6 +24,7 @@ class MetadataDb {
_database = openDatabase(
await path,
onCreate: (db, version) async {
await db.execute('CREATE TABLE $dateTakenTable(contentId INTEGER PRIMARY KEY, dateMillis INTEGER)');
await db.execute('CREATE TABLE $metadataTable(contentId INTEGER PRIMARY KEY, dateMillis INTEGER, videoRotation INTEGER, xmpSubjects TEXT, xmpTitleDescription TEXT, latitude REAL, longitude REAL)');
await db.execute('CREATE TABLE $addressTable(contentId INTEGER PRIMARY KEY, addressLine TEXT, countryName TEXT, adminArea TEXT, locality TEXT)');
await db.execute('CREATE TABLE $favouriteTable(contentId INTEGER PRIMARY KEY, path TEXT)');
@ -43,6 +45,25 @@ class MetadataDb {
await init();
}
// date taken
Future<void> clearDates() async {
final db = await _database;
final count = await db.delete(dateTakenTable, where: '1');
debugPrint('$runtimeType clearDates deleted $count entries');
}
Future<List<DateMetadata>> loadDates() async {
// final stopwatch = Stopwatch()..start();
final db = await _database;
final maps = await db.query(dateTakenTable);
final metadataEntries = maps.map((map) => DateMetadata.fromMap(map)).toList();
// debugPrint('$runtimeType loadDates complete in ${stopwatch.elapsed.inMilliseconds}ms for ${metadataEntries.length} entries');
return metadataEntries;
}
// catalog metadata
Future<void> clearMetadataEntries() async {
final db = await _database;
final count = await db.delete(metadataTable, where: '1');
@ -54,7 +75,7 @@ class MetadataDb {
final db = await _database;
final maps = await db.query(metadataTable);
final metadataEntries = maps.map((map) => CatalogMetadata.fromMap(map)).toList();
// debugPrint('$runtimeType loadMetadataEntries complete in ${stopwatch.elapsed.inMilliseconds}ms with ${metadataEntries.length} entries');
// debugPrint('$runtimeType loadMetadataEntries complete in ${stopwatch.elapsed.inMilliseconds}ms for ${metadataEntries.length} entries');
return metadataEntries;
}
@ -63,15 +84,26 @@ class MetadataDb {
final stopwatch = Stopwatch()..start();
final db = await _database;
final batch = db.batch();
metadataEntries.where((metadata) => metadata != null).forEach((metadata) => batch.insert(
metadataTable,
metadata.toMap(),
metadataEntries.where((metadata) => metadata != null).forEach((metadata) {
if (metadata.dateMillis != 0) {
batch.insert(
dateTakenTable,
DateMetadata(contentId: metadata.contentId, dateMillis: metadata.dateMillis).toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
));
);
}
batch.insert(
metadataTable,
metadata.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
});
await batch.commit(noResult: true);
debugPrint('$runtimeType saveMetadata complete in ${stopwatch.elapsed.inMilliseconds}ms with ${metadataEntries.length} entries');
debugPrint('$runtimeType saveMetadata complete in ${stopwatch.elapsed.inMilliseconds}ms for ${metadataEntries.length} entries');
}
// address
Future<void> clearAddresses() async {
final db = await _database;
final count = await db.delete(addressTable, where: '1');
@ -83,7 +115,7 @@ class MetadataDb {
final db = await _database;
final maps = await db.query(addressTable);
final addresses = maps.map((map) => AddressDetails.fromMap(map)).toList();
// debugPrint('$runtimeType loadAddresses complete in ${stopwatch.elapsed.inMilliseconds}ms with ${addresses.length} entries');
// debugPrint('$runtimeType loadAddresses complete in ${stopwatch.elapsed.inMilliseconds}ms for ${addresses.length} entries');
return addresses;
}
@ -98,7 +130,7 @@ class MetadataDb {
conflictAlgorithm: ConflictAlgorithm.replace,
));
await batch.commit(noResult: true);
debugPrint('$runtimeType saveAddresses complete in ${stopwatch.elapsed.inMilliseconds}ms with ${addresses.length} entries');
debugPrint('$runtimeType saveAddresses complete in ${stopwatch.elapsed.inMilliseconds}ms for ${addresses.length} entries');
}
// favourites
@ -114,7 +146,7 @@ class MetadataDb {
final db = await _database;
final maps = await db.query(favouriteTable);
final favouriteRows = maps.map((map) => FavouriteRow.fromMap(map)).toList();
// debugPrint('$runtimeType loadFavourites complete in ${stopwatch.elapsed.inMilliseconds}ms with ${favouriteRows.length} entries');
// debugPrint('$runtimeType loadFavourites complete in ${stopwatch.elapsed.inMilliseconds}ms for ${favouriteRows.length} entries');
return favouriteRows;
}
@ -129,7 +161,7 @@ class MetadataDb {
conflictAlgorithm: ConflictAlgorithm.replace,
));
await batch.commit(noResult: true);
// debugPrint('$runtimeType addFavourites complete in ${stopwatch.elapsed.inMilliseconds}ms with ${favouriteRows.length} entries');
// debugPrint('$runtimeType addFavourites complete in ${stopwatch.elapsed.inMilliseconds}ms for ${favouriteRows.length} entries');
}
Future<void> removeFavourites(Iterable<FavouriteRow> favouriteRows) async {
@ -147,6 +179,6 @@ class MetadataDb {
whereArgs: [id],
));
await batch.commit(noResult: true);
// debugPrint('$runtimeType removeFavourites complete in ${stopwatch.elapsed.inMilliseconds}ms with ${favouriteRows.length} entries');
// debugPrint('$runtimeType removeFavourites complete in ${stopwatch.elapsed.inMilliseconds}ms for ${favouriteRows.length} entries');
}
}

View file

@ -80,7 +80,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
return ValueListenableBuilder<PageState>(
valueListenable: stateNotifier,
builder: (context, state, child) {
debugPrint('$runtimeType builder state=$state');
return AnimatedBuilder(
animation: collection.filterChangeNotifier,
builder: (context, child) => SliverAppBar(

View file

@ -33,7 +33,7 @@ class ThumbnailCollection extends StatelessWidget {
builder: (c, mqViewInsetsBottom, child) {
return Consumer<CollectionLens>(
builder: (context, collection, child) {
debugPrint('$runtimeType collection builder entries=${collection.entryCount}');
// debugPrint('$runtimeType collection builder entries=${collection.entryCount}');
final sectionKeys = collection.sections.keys.toList();
final showHeaders = collection.showHeaders;
return GridScaleGestureDetector(

View file

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:aves/model/collection_lens.dart';
import 'package:aves/model/collection_source.dart';
import 'package:aves/model/favourite_repo.dart';
@ -35,15 +37,20 @@ class MediaStoreSource {
if (currentTimeZone != catalogTimeZone) {
// clear catalog metadata to get correct date/times when moving to a different time zone
debugPrint('$runtimeType clear catalog metadata to get correct date/times');
await metadataDb.clearDates();
await metadataDb.clearMetadataEntries();
settings.catalogTimeZone = currentTimeZone;
}
await _source.loadDates(); // 100ms for 5400 entries
var refreshCount = 10;
const refreshCountMax = 1000;
final allEntries = <ImageEntry>[];
_eventChannel.receiveBroadcastStream().cast<Map>().listen(
(entryMap) {
allEntries.add(ImageEntry.fromMap(entryMap));
if (allEntries.length >= 100) {
if (allEntries.length >= refreshCount) {
refreshCount = min(refreshCount * 10, refreshCountMax);
_source.addAll(allEntries);
allEntries.clear();
// debugPrint('$runtimeType streamed ${_source.entries.length} entries at ${stopwatch.elapsed.inMilliseconds}ms');
@ -54,7 +61,7 @@ class MediaStoreSource {
_source.addAll(allEntries);
// TODO reduce setup time until here
_source.updateAlbums(); // <50ms
await _source.loadCatalogMetadata(); // 650ms
await _source.loadCatalogMetadata(); // 400ms for 5400 entries
await _source.catalogEntries(); // <50ms
await _source.loadAddresses(); // 350ms
await _source.locateEntries(); // <50ms

View file

@ -21,6 +21,7 @@ class DebugPage extends StatefulWidget {
class DebugPageState extends State<DebugPage> {
Future<int> _dbFileSizeLoader;
Future<List<DateMetadata>> _dbDateLoader;
Future<List<CatalogMetadata>> _dbMetadataLoader;
Future<List<AddressDetails>> _dbAddressLoader;
Future<List<FavouriteRow>> _dbFavouritesLoader;
@ -78,6 +79,23 @@ class DebugPageState extends State<DebugPage> {
);
},
),
FutureBuilder(
future: _dbDateLoader,
builder: (context, AsyncSnapshot<List<DateMetadata>> snapshot) {
if (snapshot.hasError) return Text(snapshot.error.toString());
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
return Row(
children: [
Text('DB date rows: ${snapshot.data.length}'),
const Spacer(),
RaisedButton(
onPressed: () => metadataDb.clearDates().then((_) => _startDbReport()),
child: const Text('Clear'),
),
],
);
},
),
FutureBuilder(
future: _dbMetadataLoader,
builder: (context, AsyncSnapshot<List<CatalogMetadata>> snapshot) {
@ -175,6 +193,7 @@ class DebugPageState extends State<DebugPage> {
void _startDbReport() {
_dbFileSizeLoader = metadataDb.dbFileSize();
_dbDateLoader = metadataDb.loadDates();
_dbMetadataLoader = metadataDb.loadMetadataEntries();
_dbAddressLoader = metadataDb.loadAddresses();
_dbFavouritesLoader = metadataDb.loadFavourites();

View file

@ -0,0 +1,115 @@
import 'package:aves/model/image_entry.dart';
import 'package:aves/model/image_metadata.dart';
import 'package:aves/model/metadata_db.dart';
import 'package:aves/widgets/fullscreen/info/info_page.dart';
import 'package:flutter/material.dart';
class FullscreenDebugPage extends StatefulWidget {
final ImageEntry entry;
const FullscreenDebugPage({@required this.entry});
@override
_FullscreenDebugPageState createState() => _FullscreenDebugPageState();
}
class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
Future<DateMetadata> _dbDateLoader;
Future<CatalogMetadata> _dbMetadataLoader;
Future<AddressDetails> _dbAddressLoader;
int get contentId => widget.entry.contentId;
@override
void initState() {
super.initState();
_startDbReport();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Debug'),
),
body: SafeArea(
child: ListView(
padding: const EdgeInsets.all(16),
children: [
FutureBuilder(
future: _dbDateLoader,
builder: (context, AsyncSnapshot<DateMetadata> snapshot) {
if (snapshot.hasError) return Text(snapshot.error.toString());
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
final data = snapshot.data;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('DB date:${data == null ? ' no row' : ''}'),
if (data != null)
InfoRowGroup({
'dateMillis': '${data.dateMillis}',
}),
],
);
},
),
const SizedBox(height: 16),
FutureBuilder(
future: _dbMetadataLoader,
builder: (context, AsyncSnapshot<CatalogMetadata> snapshot) {
if (snapshot.hasError) return Text(snapshot.error.toString());
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
final data = snapshot.data;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('DB metadata:${data == null ? ' no row' : ''}'),
if (data != null)
InfoRowGroup({
'dateMillis': '${data.dateMillis}',
'videoRotation': '${data.videoRotation}',
'latitude': '${data.latitude}',
'longitude': '${data.longitude}',
'xmpSubjects': '${data.xmpSubjects}',
'xmpTitleDescription': '${data.xmpTitleDescription}',
}),
],
);
},
),
const SizedBox(height: 16),
FutureBuilder(
future: _dbAddressLoader,
builder: (context, AsyncSnapshot<AddressDetails> snapshot) {
if (snapshot.hasError) return Text(snapshot.error.toString());
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
final data = snapshot.data;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('DB address:${data == null ? ' no row' : ''}'),
if (data != null)
InfoRowGroup({
'dateMillis': '${data.addressLine}',
'countryName': '${data.countryName}',
'adminArea': '${data.adminArea}',
'locality': '${data.locality}',
}),
],
);
},
),
],
),
),
);
}
void _startDbReport() {
_dbDateLoader = metadataDb.loadDates().then((values) => values.firstWhere((row) => row.contentId == contentId, orElse: () => null));
_dbMetadataLoader = metadataDb.loadMetadataEntries().then((values) => values.firstWhere((row) => row.contentId == contentId, orElse: () => null));
_dbAddressLoader = metadataDb.loadAddresses().then((values) => values.firstWhere((row) => row.contentId == contentId, orElse: () => null));
setState(() {});
}
}

View file

@ -5,6 +5,7 @@ import 'package:aves/model/image_entry.dart';
import 'package:aves/model/image_file_service.dart';
import 'package:aves/utils/android_app_service.dart';
import 'package:aves/widgets/common/image_providers/uri_image_provider.dart';
import 'package:aves/widgets/fullscreen/debug.dart';
import 'package:aves/widgets/fullscreen/fullscreen_actions.dart';
import 'package:flushbar/flushbar.dart';
import 'package:flutter/material.dart';
@ -63,6 +64,9 @@ class FullscreenActionDelegate {
case FullscreenAction.share:
AndroidAppService.share(entry.uri, entry.mimeType);
break;
case FullscreenAction.debug:
_goToDebug(context, entry);
break;
}
}
@ -177,4 +181,13 @@ class FullscreenActionDelegate {
if (newName == null || newName.isEmpty) return;
_showFeedback(context, await entry.rename(newName) ? 'Done!' : 'Failed');
}
void _goToDebug(BuildContext context, ImageEntry entry) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FullscreenDebugPage(entry: entry),
),
);
}
}

View file

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:outline_material_icons/outline_material_icons.dart';
enum FullscreenAction { delete, edit, info, open, openMap, print, rename, rotateCCW, rotateCW, setAs, share, toggleFavourite }
enum FullscreenAction { delete, edit, info, open, openMap, print, rename, rotateCCW, rotateCW, setAs, share, toggleFavourite, debug }
class FullscreenActions {
static const inApp = [
@ -53,6 +53,8 @@ extension ExtraFullscreenAction on FullscreenAction {
return 'Set as…';
case FullscreenAction.openMap:
return 'Show on map…';
case FullscreenAction.debug:
return 'Debug';
}
return null;
}
@ -83,6 +85,8 @@ extension ExtraFullscreenAction on FullscreenAction {
case FullscreenAction.setAs:
case FullscreenAction.openMap:
return null;
case FullscreenAction.debug:
return OMIcons.whatshot;
}
return null;
}

View file

@ -75,6 +75,10 @@ class FullscreenTopOverlay extends StatelessWidget {
...inAppActions.map(_buildPopupMenuItem),
const PopupMenuDivider(),
...externalAppActions.map(_buildPopupMenuItem),
if (kDebugMode) ...[
const PopupMenuDivider(),
_buildPopupMenuItem(FullscreenAction.debug),
]
],
onSelected: onActionSelected,
),
@ -109,6 +113,8 @@ class FullscreenTopOverlay extends StatelessWidget {
case FullscreenAction.edit:
case FullscreenAction.setAs:
return true;
case FullscreenAction.debug:
return kDebugMode;
}
return false;
}
@ -153,6 +159,7 @@ class FullscreenTopOverlay extends StatelessWidget {
case FullscreenAction.open:
case FullscreenAction.edit:
case FullscreenAction.setAs:
case FullscreenAction.debug:
break;
}
return child != null
@ -188,6 +195,7 @@ class FullscreenTopOverlay extends StatelessWidget {
case FullscreenAction.rotateCCW:
case FullscreenAction.rotateCW:
case FullscreenAction.print:
case FullscreenAction.debug:
child = MenuRow(text: action.getText(), icon: action.getIcon());
break;
// external app actions