misc fixes
This commit is contained in:
parent
bc38edfea1
commit
48a62e85c5
11 changed files with 172 additions and 173 deletions
|
@ -1,4 +1,3 @@
|
|||
import 'package:aves/model/collection_lens.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/model/image_file_service.dart';
|
||||
import 'package:aves/model/settings.dart';
|
||||
|
@ -12,7 +11,6 @@ import 'package:flutter/services.dart';
|
|||
import 'package:outline_material_icons/outline_material_icons.dart';
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:screen/screen.dart';
|
||||
|
||||
void main() {
|
||||
|
@ -51,6 +49,7 @@ class HomePage extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
MediaStoreSource _mediaStore;
|
||||
ImageEntry _sharedEntry;
|
||||
Future<void> _appSetup;
|
||||
|
||||
|
@ -88,6 +87,9 @@ class _HomePageState extends State<HomePage> {
|
|||
// cataloging is essential for geolocation and video rotation
|
||||
await _sharedEntry.catalog();
|
||||
unawaited(_sharedEntry.locate());
|
||||
} else {
|
||||
_mediaStore = MediaStoreSource();
|
||||
unawaited(_mediaStore.fetch());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -103,11 +105,7 @@ class _HomePageState extends State<HomePage> {
|
|||
? SingleFullscreenPage(
|
||||
entry: _sharedEntry,
|
||||
)
|
||||
: MediaStoreCollectionProvider(
|
||||
child: Consumer<CollectionLens>(
|
||||
builder: (context, collection, child) => CollectionPage(collection),
|
||||
),
|
||||
);
|
||||
: CollectionPage(_mediaStore.collection);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -128,14 +128,14 @@ class CollectionLens with ChangeNotifier {
|
|||
case SortFactor.date:
|
||||
_filteredEntries.sort((a, b) {
|
||||
final c = b.bestDate.compareTo(a.bestDate);
|
||||
return c != 0 ? c : compareAsciiUpperCase(a.title, b.title);
|
||||
return c != 0 ? c : compareAsciiUpperCase(a.bestTitle, b.bestTitle);
|
||||
});
|
||||
break;
|
||||
case SortFactor.size:
|
||||
_filteredEntries.sort((a, b) => b.sizeBytes.compareTo(a.sizeBytes));
|
||||
break;
|
||||
case SortFactor.name:
|
||||
_filteredEntries.sort((a, b) => compareAsciiUpperCase(a.title, b.title));
|
||||
_filteredEntries.sort((a, b) => compareAsciiUpperCase(a.bestTitle, b.bestTitle));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -130,11 +130,19 @@ class ImageEntry {
|
|||
|
||||
int get megaPixels => width != null && height != null ? (width * height / 1000000).round() : null;
|
||||
|
||||
DateTime _bestDate;
|
||||
|
||||
DateTime get bestDate {
|
||||
if ((catalogMetadata?.dateMillis ?? 0) > 0) return DateTime.fromMillisecondsSinceEpoch(catalogMetadata.dateMillis);
|
||||
if (sourceDateTakenMillis != null && sourceDateTakenMillis > 0) return DateTime.fromMillisecondsSinceEpoch(sourceDateTakenMillis);
|
||||
if (dateModifiedSecs != null && dateModifiedSecs > 0) return DateTime.fromMillisecondsSinceEpoch(dateModifiedSecs * 1000);
|
||||
return null;
|
||||
if (_bestDate == null) {
|
||||
if ((catalogMetadata?.dateMillis ?? 0) > 0) {
|
||||
_bestDate = DateTime.fromMillisecondsSinceEpoch(catalogMetadata.dateMillis);
|
||||
} else if (sourceDateTakenMillis != null && sourceDateTakenMillis > 0) {
|
||||
_bestDate = DateTime.fromMillisecondsSinceEpoch(sourceDateTakenMillis);
|
||||
} else if (dateModifiedSecs != null && dateModifiedSecs > 0) {
|
||||
_bestDate = DateTime.fromMillisecondsSinceEpoch(dateModifiedSecs * 1000);
|
||||
}
|
||||
}
|
||||
return _bestDate;
|
||||
}
|
||||
|
||||
DateTime get monthTaken {
|
||||
|
@ -159,14 +167,18 @@ class ImageEntry {
|
|||
|
||||
List<String> get xmpSubjects => catalogMetadata?.xmpSubjects?.split(';')?.where((tag) => tag.isNotEmpty)?.toList() ?? [];
|
||||
|
||||
String get title {
|
||||
if (catalogMetadata != null && catalogMetadata.xmpTitleDescription.isNotEmpty) return catalogMetadata.xmpTitleDescription;
|
||||
return sourceTitle;
|
||||
String _bestTitle;
|
||||
|
||||
String get bestTitle {
|
||||
_bestTitle ??= (catalogMetadata != null && catalogMetadata.xmpTitleDescription.isNotEmpty) ? catalogMetadata.xmpTitleDescription : sourceTitle;
|
||||
return _bestTitle;
|
||||
}
|
||||
|
||||
Future<void> catalog() async {
|
||||
if (isCatalogued) return;
|
||||
catalogMetadata = await MetadataService.getCatalogMetadata(this);
|
||||
_bestDate = null;
|
||||
_bestTitle = null;
|
||||
if (catalogMetadata != null) {
|
||||
metadataChangeNotifier.notifyListeners();
|
||||
}
|
||||
|
@ -212,7 +224,7 @@ class ImageEntry {
|
|||
}
|
||||
|
||||
bool search(String query) {
|
||||
if (title?.toUpperCase()?.contains(query) ?? false) return true;
|
||||
if (bestTitle?.toUpperCase()?.contains(query) ?? false) return true;
|
||||
if (catalogMetadata?.xmpSubjects?.toUpperCase()?.contains(query) ?? false) return true;
|
||||
if (addressDetails?.addressLine?.toUpperCase()?.contains(query) ?? false) return true;
|
||||
return false;
|
||||
|
@ -232,6 +244,7 @@ class ImageEntry {
|
|||
if (contentId is int) this.contentId = contentId;
|
||||
final sourceTitle = newFields['sourceTitle'];
|
||||
if (sourceTitle is String) this.sourceTitle = sourceTitle;
|
||||
_bestTitle = null;
|
||||
metadataChangeNotifier.notifyListeners();
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ class CollectionPage extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint('$runtimeType build');
|
||||
return MediaQueryDataProvider(
|
||||
child: ChangeNotifierProvider<CollectionLens>.value(
|
||||
value: collection,
|
||||
|
|
|
@ -86,19 +86,19 @@ class GridThumbnail extends StatelessWidget {
|
|||
return GestureDetector(
|
||||
key: ValueKey(entry.uri),
|
||||
onTap: () => _goToFullscreen(context),
|
||||
child: MetaData(
|
||||
metaData: ThumbnailMetadata(index, entry),
|
||||
child: Selector<MediaQueryData, double>(
|
||||
selector: (c, mq) => mq.size.width,
|
||||
builder: (c, mqWidth, child) {
|
||||
return MetaData(
|
||||
metaData: ThumbnailMetadata(index, entry),
|
||||
child: Thumbnail(
|
||||
return Thumbnail(
|
||||
entry: entry,
|
||||
extent: mqWidth / columnCount,
|
||||
heroTag: collection.heroTag(entry),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -27,23 +27,22 @@ class ThumbnailCollection extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint('$runtimeType build');
|
||||
final collection = Provider.of<CollectionLens>(context);
|
||||
final sections = collection.sections;
|
||||
final sectionKeys = sections.keys.toList();
|
||||
final showHeaders = collection.showHeaders;
|
||||
|
||||
return SafeArea(
|
||||
child: Selector<MediaQueryData, double>(
|
||||
selector: (c, mq) => mq.viewInsets.bottom,
|
||||
builder: (c, mqViewInsetsBottom, child) {
|
||||
return Consumer<CollectionLens>(
|
||||
builder: (context, collection, child) {
|
||||
debugPrint('$runtimeType collection builder entries=${collection.entryCount}');
|
||||
final sectionKeys = collection.sections.keys.toList();
|
||||
final showHeaders = collection.showHeaders;
|
||||
return GridScaleGestureDetector(
|
||||
scrollableKey: _scrollableKey,
|
||||
columnCountNotifier: _columnCountNotifier,
|
||||
child: ValueListenableBuilder<int>(
|
||||
valueListenable: _columnCountNotifier,
|
||||
builder: (context, columnCount, child) {
|
||||
debugPrint('$runtimeType builder columnCount=$columnCount entries=${collection.entryCount}');
|
||||
debugPrint('$runtimeType columnCount builder entries=${collection.entryCount} columnCount=$columnCount');
|
||||
final scrollView = CustomScrollView(
|
||||
key: _scrollableKey,
|
||||
primary: true,
|
||||
|
@ -84,7 +83,10 @@ class ThumbnailCollection extends StatelessWidget {
|
|||
return DraggableScrollbar(
|
||||
heightScrollThumb: avesScrollThumbHeight,
|
||||
backgroundColor: Colors.white,
|
||||
scrollThumbBuilder: avesScrollThumbBuilder(),
|
||||
scrollThumbBuilder: avesScrollThumbBuilder(
|
||||
height: avesScrollThumbHeight,
|
||||
backgroundColor: Colors.white,
|
||||
),
|
||||
controller: PrimaryScrollController.of(context),
|
||||
padding: EdgeInsets.only(
|
||||
// padding to keep scroll thumb between app bar above and nav bar below
|
||||
|
@ -100,6 +102,8 @@ class ThumbnailCollection extends StatelessWidget {
|
|||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,37 +8,26 @@ import 'package:aves/model/settings.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_native_timezone/flutter_native_timezone.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class MediaStoreCollectionProvider extends StatefulWidget {
|
||||
final Widget child;
|
||||
class MediaStoreSource {
|
||||
CollectionSource _source;
|
||||
CollectionLens _baseLens;
|
||||
|
||||
const MediaStoreCollectionProvider({@required this.child});
|
||||
CollectionLens get collection => _baseLens;
|
||||
|
||||
@override
|
||||
_MediaStoreCollectionProviderState createState() => _MediaStoreCollectionProviderState();
|
||||
}
|
||||
static const EventChannel _eventChannel = EventChannel('deckers.thibault/aves/mediastore');
|
||||
|
||||
class _MediaStoreCollectionProviderState extends State<MediaStoreCollectionProvider> {
|
||||
Future<CollectionLens> collectionFuture;
|
||||
|
||||
static const EventChannel eventChannel = EventChannel('deckers.thibault/aves/mediastore');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
collectionFuture = _create();
|
||||
}
|
||||
|
||||
Future<CollectionLens> _create() async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final mediaStoreSource = CollectionSource();
|
||||
final mediaStoreBaseLens = CollectionLens(
|
||||
source: mediaStoreSource,
|
||||
MediaStoreSource() {
|
||||
_source = CollectionSource();
|
||||
_baseLens = CollectionLens(
|
||||
source: _source,
|
||||
groupFactor: settings.collectionGroupFactor,
|
||||
sortFactor: settings.collectionSortFactor,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> fetch() async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
await metadataDb.init(); // <20ms
|
||||
await favourites.init();
|
||||
final currentTimeZone = await FlutterNativeTimezone.getLocalTimezone(); // <20ms
|
||||
|
@ -51,39 +40,31 @@ class _MediaStoreCollectionProviderState extends State<MediaStoreCollectionProvi
|
|||
}
|
||||
|
||||
final allEntries = <ImageEntry>[];
|
||||
eventChannel.receiveBroadcastStream().cast<Map>().listen(
|
||||
(entryMap) => allEntries.add(ImageEntry.fromMap(entryMap)),
|
||||
_eventChannel.receiveBroadcastStream().cast<Map>().listen(
|
||||
(entryMap) {
|
||||
allEntries.add(ImageEntry.fromMap(entryMap));
|
||||
if (allEntries.length >= 100) {
|
||||
_source.addAll(allEntries);
|
||||
allEntries.clear();
|
||||
// debugPrint('$runtimeType streamed ${_source.entries.length} entries at ${stopwatch.elapsed.inMilliseconds}ms');
|
||||
}
|
||||
},
|
||||
onDone: () async {
|
||||
debugPrint('$runtimeType stream complete in ${stopwatch.elapsed.inMilliseconds}ms');
|
||||
mediaStoreSource.addAll(allEntries);
|
||||
debugPrint('$runtimeType stream complete at ${stopwatch.elapsed.inMilliseconds}ms');
|
||||
_source.addAll(allEntries);
|
||||
// TODO reduce setup time until here
|
||||
mediaStoreSource.updateAlbums(); // <50ms
|
||||
await mediaStoreSource.loadCatalogMetadata(); // 650ms
|
||||
await mediaStoreSource.catalogEntries(); // <50ms
|
||||
await mediaStoreSource.loadAddresses(); // 350ms
|
||||
await mediaStoreSource.locateEntries(); // <50ms
|
||||
_source.updateAlbums(); // <50ms
|
||||
await _source.loadCatalogMetadata(); // 650ms
|
||||
await _source.catalogEntries(); // <50ms
|
||||
await _source.loadAddresses(); // 350ms
|
||||
await _source.locateEntries(); // <50ms
|
||||
debugPrint('$runtimeType setup end, elapsed=${stopwatch.elapsed}');
|
||||
},
|
||||
onError: (error) => debugPrint('$runtimeType mediastore stream error=$error'),
|
||||
);
|
||||
|
||||
// TODO split image fetch AND/OR cache fetch across sessions
|
||||
debugPrint('$runtimeType stream start at ${stopwatch.elapsed.inMilliseconds}ms');
|
||||
await ImageFileService.getImageEntries(); // 460ms
|
||||
|
||||
return mediaStoreBaseLens;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: collectionFuture,
|
||||
builder: (futureContext, AsyncSnapshot<CollectionLens> snapshot) {
|
||||
final collection = (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) ? snapshot.data : CollectionLens.empty();
|
||||
return ChangeNotifierProvider<CollectionLens>.value(
|
||||
value: collection,
|
||||
child: widget.child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,14 +3,12 @@ import 'package:flutter/material.dart';
|
|||
|
||||
const double avesScrollThumbHeight = 48;
|
||||
|
||||
ScrollThumbBuilder avesScrollThumbBuilder() {
|
||||
return (
|
||||
Color backgroundColor,
|
||||
Animation<double> thumbAnimation,
|
||||
Animation<double> labelAnimation,
|
||||
double height, {
|
||||
Widget labelText,
|
||||
}) {
|
||||
// height and background color do not change
|
||||
// so we do not rely on the builder props
|
||||
ScrollThumbBuilder avesScrollThumbBuilder({
|
||||
@required double height,
|
||||
@required Color backgroundColor,
|
||||
}) {
|
||||
final scrollThumb = Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black26,
|
||||
|
@ -34,7 +32,13 @@ ScrollThumbBuilder avesScrollThumbBuilder() {
|
|||
clipper: ArrowClipper(),
|
||||
),
|
||||
);
|
||||
|
||||
return (
|
||||
Color backgroundColor,
|
||||
Animation<double> thumbAnimation,
|
||||
Animation<double> labelAnimation,
|
||||
double height, {
|
||||
Widget labelText,
|
||||
}) {
|
||||
return DraggableScrollbar.buildScrollThumbAndLabel(
|
||||
scrollThumb: scrollThumb,
|
||||
backgroundColor: backgroundColor,
|
||||
|
|
|
@ -82,7 +82,7 @@ class FullscreenActionDelegate {
|
|||
Future<void> _print(ImageEntry entry) async {
|
||||
final uri = entry.uri;
|
||||
final mimeType = entry.mimeType;
|
||||
final documentName = entry.title ?? 'Aves';
|
||||
final documentName = entry.bestTitle ?? 'Aves';
|
||||
final doc = pdf.Document(title: documentName);
|
||||
|
||||
PdfImage pdfImage;
|
||||
|
@ -152,7 +152,7 @@ class FullscreenActionDelegate {
|
|||
}
|
||||
|
||||
Future<void> _showRenameDialog(BuildContext context, ImageEntry entry) async {
|
||||
final currentName = entry.title;
|
||||
final currentName = entry.bestTitle;
|
||||
final controller = TextEditingController(text: currentName);
|
||||
final newName = await showDialog<String>(
|
||||
context: context,
|
||||
|
|
|
@ -37,7 +37,7 @@ class BasicSection extends StatelessWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
InfoRowGroup({
|
||||
'Title': entry.title ?? '?',
|
||||
'Title': entry.bestTitle ?? '?',
|
||||
'Date': dateText,
|
||||
if (entry.isVideo) ..._buildVideoRows(),
|
||||
if (!entry.isSvg) 'Resolution': resolutionText,
|
||||
|
|
|
@ -152,7 +152,7 @@ class _FullscreenBottomOverlayContent extends AnimatedWidget {
|
|||
final subRowWidth = twoColumns ? min(_subRowMinWidth, maxWidth / 2) : maxWidth;
|
||||
final positionTitle = [
|
||||
if (position != null) position,
|
||||
if (entry.title != null) entry.title,
|
||||
if (entry.bestTitle != null) entry.bestTitle,
|
||||
].join(' – ');
|
||||
final hasShootingDetails = details != null && !details.isEmpty;
|
||||
return Column(
|
||||
|
|
Loading…
Reference in a new issue