misc fixes

This commit is contained in:
Thibault Deckers 2020-04-07 14:50:23 +09:00
parent bc38edfea1
commit 48a62e85c5
11 changed files with 172 additions and 173 deletions

View file

@ -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);
});
}
}

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -15,7 +15,6 @@ class CollectionPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
debugPrint('$runtimeType build');
return MediaQueryDataProvider(
child: ChangeNotifierProvider<CollectionLens>.value(
value: collection,

View file

@ -86,18 +86,18 @@ class GridThumbnail extends StatelessWidget {
return GestureDetector(
key: ValueKey(entry.uri),
onTap: () => _goToFullscreen(context),
child: Selector<MediaQueryData, double>(
selector: (c, mq) => mq.size.width,
builder: (c, mqWidth, child) {
return MetaData(
metaData: ThumbnailMetadata(index, entry),
child: Thumbnail(
child: MetaData(
metaData: ThumbnailMetadata(index, entry),
child: Selector<MediaQueryData, double>(
selector: (c, mq) => mq.size.width,
builder: (c, mqWidth, child) {
return Thumbnail(
entry: entry,
extent: mqWidth / columnCount,
heroTag: collection.heroTag(entry),
),
);
},
);
},
),
),
);
}

View file

@ -27,77 +27,81 @@ 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 GridScaleGestureDetector(
scrollableKey: _scrollableKey,
columnCountNotifier: _columnCountNotifier,
child: ValueListenableBuilder<int>(
valueListenable: _columnCountNotifier,
builder: (context, columnCount, child) {
debugPrint('$runtimeType builder columnCount=$columnCount entries=${collection.entryCount}');
final scrollView = CustomScrollView(
key: _scrollableKey,
primary: true,
// workaround to prevent scrolling the app bar away
// when there is no content and we use `SliverFillRemaining`
physics: collection.isEmpty ? const NeverScrollableScrollPhysics() : null,
slivers: [
CollectionAppBar(
stateNotifier: stateNotifier,
appBarHeightNotifier: _appBarHeightNotifier,
collection: collection,
),
if (collection.isEmpty)
SliverFillRemaining(
child: _buildEmptyCollectionPlaceholder(collection),
hasScrollBody: false,
),
...sectionKeys.map((sectionKey) => SectionSliver(
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 columnCount builder entries=${collection.entryCount} columnCount=$columnCount');
final scrollView = CustomScrollView(
key: _scrollableKey,
primary: true,
// workaround to prevent scrolling the app bar away
// when there is no content and we use `SliverFillRemaining`
physics: collection.isEmpty ? const NeverScrollableScrollPhysics() : null,
slivers: [
CollectionAppBar(
stateNotifier: stateNotifier,
appBarHeightNotifier: _appBarHeightNotifier,
collection: collection,
sectionKey: sectionKey,
columnCount: columnCount,
showHeader: showHeaders,
)),
SliverToBoxAdapter(
child: Selector<MediaQueryData, double>(
selector: (c, mq) => mq.viewInsets.bottom,
builder: (c, mqViewInsetsBottom, child) {
return SizedBox(height: mqViewInsetsBottom);
},
),
),
],
);
),
if (collection.isEmpty)
SliverFillRemaining(
child: _buildEmptyCollectionPlaceholder(collection),
hasScrollBody: false,
),
...sectionKeys.map((sectionKey) => SectionSliver(
collection: collection,
sectionKey: sectionKey,
columnCount: columnCount,
showHeader: showHeaders,
)),
SliverToBoxAdapter(
child: Selector<MediaQueryData, double>(
selector: (c, mq) => mq.viewInsets.bottom,
builder: (c, mqViewInsetsBottom, child) {
return SizedBox(height: mqViewInsetsBottom);
},
),
),
],
);
return ValueListenableBuilder<double>(
valueListenable: _appBarHeightNotifier,
builder: (context, appBarHeight, child) {
return DraggableScrollbar(
heightScrollThumb: avesScrollThumbHeight,
backgroundColor: Colors.white,
scrollThumbBuilder: avesScrollThumbBuilder(),
controller: PrimaryScrollController.of(context),
padding: EdgeInsets.only(
// padding to keep scroll thumb between app bar above and nav bar below
top: appBarHeight,
bottom: mqViewInsetsBottom,
),
child: child,
return ValueListenableBuilder<double>(
valueListenable: _appBarHeightNotifier,
builder: (context, appBarHeight, child) {
return DraggableScrollbar(
heightScrollThumb: avesScrollThumbHeight,
backgroundColor: Colors.white,
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
top: appBarHeight,
bottom: mqViewInsetsBottom,
),
child: child,
);
},
child: scrollView,
);
},
child: scrollView,
);
},
),
),
);
},
);
},
),

View file

@ -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)),
onDone: () async {
debugPrint('$runtimeType stream complete in ${stopwatch.elapsed.inMilliseconds}ms');
mediaStoreSource.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
debugPrint('$runtimeType setup end, elapsed=${stopwatch.elapsed}');
},
onError: (error) => debugPrint('$runtimeType mediastore stream error=$error'),
);
_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 at ${stopwatch.elapsed.inMilliseconds}ms');
_source.addAll(allEntries);
// TODO reduce setup time until here
_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,
);
},
);
}
}

View file

@ -3,7 +3,35 @@ import 'package:flutter/material.dart';
const double avesScrollThumbHeight = 48;
ScrollThumbBuilder avesScrollThumbBuilder() {
// 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,
borderRadius: const BorderRadius.all(
Radius.circular(12.0),
),
),
height: height,
margin: const EdgeInsets.only(right: .5),
padding: const EdgeInsets.all(2),
child: ClipPath(
child: Container(
width: 20.0,
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: const BorderRadius.all(
Radius.circular(12.0),
),
),
),
clipper: ArrowClipper(),
),
);
return (
Color backgroundColor,
Animation<double> thumbAnimation,
@ -11,30 +39,6 @@ ScrollThumbBuilder avesScrollThumbBuilder() {
double height, {
Widget labelText,
}) {
final scrollThumb = Container(
decoration: BoxDecoration(
color: Colors.black26,
borderRadius: const BorderRadius.all(
Radius.circular(12.0),
),
),
height: height,
margin: const EdgeInsets.only(right: .5),
padding: const EdgeInsets.all(2),
child: ClipPath(
child: Container(
width: 20.0,
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: const BorderRadius.all(
Radius.circular(12.0),
),
),
),
clipper: ArrowClipper(),
),
);
return DraggableScrollbar.buildScrollThumbAndLabel(
scrollThumb: scrollThumb,
backgroundColor: backgroundColor,

View file

@ -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,

View file

@ -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,

View file

@ -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(