This commit is contained in:
Thibault Deckers 2019-12-24 10:41:43 +09:00
parent f965e329ad
commit cb28ad9272
33 changed files with 212 additions and 209 deletions

View file

@ -3,3 +3,10 @@ include: package:pedantic/analysis_options.yaml
analyzer:
exclude:
- lib/generated_plugin_registrant.dart
linter:
rules:
- always_declare_return_types
- prefer_const_constructors
- prefer_const_constructors_in_immutables
- prefer_const_declarations

View file

@ -16,10 +16,10 @@ import 'package:pedantic/pedantic.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:screen/screen.dart';
final stopwatch = Stopwatch()..start();
final _stopwatch = Stopwatch()..start();
void main() {
debugPrint('main start, elapsed=${stopwatch.elapsed}');
debugPrint('main start, elapsed=${_stopwatch.elapsed}');
// initialize binding/plugins to configure Skia before `runApp`
WidgetsFlutterBinding.ensureInitialized(); // 220ms
// debugPrint('main WidgetsFlutterBinding.ensureInitialized done, elapsed=${stopwatch.elapsed}');
@ -61,7 +61,7 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> {
static const EventChannel eventChannel = EventChannel('deckers.thibault/aves/mediastore');
ImageCollection localMediaCollection = ImageCollection(entries: List());
ImageCollection localMediaCollection = ImageCollection(entries: []);
@override
void initState() {
@ -72,7 +72,7 @@ class _HomePageState extends State<HomePage> {
}
Future<void> setup() async {
debugPrint('$runtimeType setup start, elapsed=${stopwatch.elapsed}');
debugPrint('$runtimeType setup start, elapsed=${_stopwatch.elapsed}');
// TODO reduce permission check time
final permissions = await PermissionHandler().requestPermissions([
PermissionGroup.storage
@ -91,7 +91,7 @@ class _HomePageState extends State<HomePage> {
await settings.init(); // <20ms
localMediaCollection.groupFactor = settings.collectionGroupFactor;
localMediaCollection.sortFactor = settings.collectionSortFactor;
debugPrint('$runtimeType setup settings.init done, elapsed=${stopwatch.elapsed}');
debugPrint('$runtimeType setup settings.init done, elapsed=${_stopwatch.elapsed}');
await metadataDb.init(); // <20ms
final currentTimeZone = await FlutterNativeTimezone.getLocalTimezone(); // <20ms
@ -107,7 +107,7 @@ class _HomePageState extends State<HomePage> {
eventChannel.receiveBroadcastStream().cast<Map>().listen(
(entryMap) => localMediaCollection.add(ImageEntry.fromMap(entryMap)),
onDone: () async {
debugPrint('$runtimeType mediastore stream done, elapsed=${stopwatch.elapsed}');
debugPrint('$runtimeType mediastore stream done, elapsed=${_stopwatch.elapsed}');
localMediaCollection.updateSections(); // <50ms
// TODO reduce setup time until here
localMediaCollection.updateAlbums(); // <50ms
@ -115,7 +115,7 @@ class _HomePageState extends State<HomePage> {
await localMediaCollection.catalogEntries(); // <50ms
await localMediaCollection.loadAddresses(); // 350ms
await localMediaCollection.locateEntries(); // <50ms
debugPrint('$runtimeType setup end, elapsed=${stopwatch.elapsed}');
debugPrint('$runtimeType setup end, elapsed=${_stopwatch.elapsed}');
},
onError: (error) => debugPrint('$runtimeType mediastore stream error=$error'),
);

View file

@ -8,11 +8,11 @@ import 'package:path/path.dart';
class ImageCollection with ChangeNotifier {
final List<ImageEntry> _rawEntries;
Map<dynamic, List<ImageEntry>> sections = Map();
Map<dynamic, List<ImageEntry>> sections = {};
GroupFactor groupFactor = GroupFactor.month;
SortFactor sortFactor = SortFactor.date;
List<String> sortedAlbums = List.unmodifiable(Iterable.empty());
List<String> sortedTags = List.unmodifiable(Iterable.empty());
List<String> sortedAlbums = List.unmodifiable(const Iterable.empty());
List<String> sortedTags = List.unmodifiable(const Iterable.empty());
ImageCollection({
@required List<ImageEntry> entries,
@ -140,7 +140,7 @@ class ImageCollection with ChangeNotifier {
Future<void> catalogEntries() async {
final start = DateTime.now();
final uncataloguedEntries = _rawEntries.where((entry) => !entry.isCatalogued).toList();
final newMetadata = List<CatalogMetadata>();
final newMetadata = <CatalogMetadata>[];
await Future.forEach<ImageEntry>(uncataloguedEntries, (entry) async {
await entry.catalog();
newMetadata.add(entry.catalogMetadata);
@ -153,7 +153,7 @@ class ImageCollection with ChangeNotifier {
Future<void> locateEntries() async {
final start = DateTime.now();
final unlocatedEntries = _rawEntries.where((entry) => entry.hasGps && !entry.isLocated).toList();
final newAddresses = List<AddressDetails>();
final newAddresses = <AddressDetails>[];
await Future.forEach<ImageEntry>(unlocatedEntries, (entry) async {
await entry.locate();
newAddresses.add(entry.addressDetails);

View file

@ -1,5 +1,3 @@
import 'dart:collection';
import 'package:aves/model/image_file_service.dart';
import 'package:aves/model/image_metadata.dart';
import 'package:aves/model/metadata_service.dart';
@ -50,19 +48,19 @@ class ImageEntry {
factory ImageEntry.fromMap(Map map) {
return ImageEntry(
uri: map['uri'],
path: map['path'],
contentId: map['contentId'],
mimeType: map['mimeType'],
width: map['width'],
height: map['height'],
orientationDegrees: map['orientationDegrees'],
sizeBytes: map['sizeBytes'],
title: map['title'],
dateModifiedSecs: map['dateModifiedSecs'],
sourceDateTakenMillis: map['sourceDateTakenMillis'],
bucketDisplayName: map['bucketDisplayName'],
durationMillis: map['durationMillis'],
uri: map['uri'] as String,
path: map['path'] as String,
contentId: map['contentId'] as int,
mimeType: map['mimeType'] as String,
width: map['width'] as int,
height: map['height'] as int,
orientationDegrees: map['orientationDegrees'] as int,
sizeBytes: map['sizeBytes'] as int,
title: map['title'] as String,
dateModifiedSecs: map['dateModifiedSecs'] as int,
sourceDateTakenMillis: map['sourceDateTakenMillis'] as int,
bucketDisplayName: map['bucketDisplayName'] as String,
durationMillis: map['durationMillis'] as int,
);
}
@ -180,13 +178,11 @@ class ImageEntry {
// admin area examples: Seoul, Geneva, null
// locality examples: Mapo-gu, Geneva, Annecy
return LinkedHashSet.of(
[
addressDetails.countryName,
addressDetails.adminArea,
addressDetails.locality
],
).where((part) => part != null && part.isNotEmpty).join(', ');
return {
addressDetails.countryName,
addressDetails.adminArea,
addressDetails.locality
}.where((part) => part != null && part.isNotEmpty).join(', ');
}
bool search(String query) {
@ -203,13 +199,13 @@ class ImageEntry {
if (newFields.isEmpty) return false;
final uri = newFields['uri'];
if (uri != null) this.uri = uri;
if (uri is String) this.uri = uri;
final path = newFields['path'];
if (path != null) this.path = path;
if (path is String) this.path = path;
final contentId = newFields['contentId'];
if (contentId != null) this.contentId = contentId;
if (contentId is int) this.contentId = contentId;
final title = newFields['title'];
if (title != null) this.title = title;
if (title is String) this.title = title;
metadataChangeNotifier.notifyListeners();
return true;
}
@ -223,11 +219,11 @@ class ImageEntry {
if (newFields.isEmpty) return false;
final width = newFields['width'];
if (width != null) this.width = width;
if (width is int) this.width = width;
final height = newFields['height'];
if (height != null) this.height = height;
if (height is int) this.height = height;
final orientationDegrees = newFields['orientationDegrees'];
if (orientationDegrees != null) this.orientationDegrees = orientationDegrees;
if (orientationDegrees is int) this.orientationDegrees = orientationDegrees;
imageChangeNotifier.notifyListeners();
return true;
}

View file

@ -65,7 +65,7 @@ class ImageFileService {
} on PlatformException catch (e) {
debugPrint('rename failed with exception=${e.message}');
}
return Map();
return {};
}
static Future<Map> rotate(ImageEntry entry, {@required bool clockwise}) async {
@ -79,6 +79,6 @@ class ImageFileService {
} on PlatformException catch (e) {
debugPrint('rotate failed with exception=${e.message}');
}
return Map();
return {};
}
}

View file

@ -10,8 +10,8 @@ class MetadataDb {
Future<String> get path async => join(await getDatabasesPath(), 'metadata.db');
static final metadataTable = 'metadata';
static final addressTable = 'address';
static const metadataTable = 'metadata';
static const addressTable = 'address';
MetadataDb._private();

View file

@ -17,7 +17,7 @@ class MetadataService {
} on PlatformException catch (e) {
debugPrint('getAllMetadata failed with exception=${e.message}');
}
return Map();
return {};
}
static Future<CatalogMetadata> getCatalogMetadata(ImageEntry entry) async {

View file

@ -5,12 +5,12 @@ import 'package:shared_preferences/shared_preferences.dart';
final Settings settings = Settings._private();
typedef void SettingsCallback(String key, dynamic oldValue, dynamic newValue);
typedef SettingsCallback = void Function(String key, dynamic oldValue, dynamic newValue);
class Settings {
static SharedPreferences prefs;
ObserverList<SettingsCallback> _listeners = ObserverList<SettingsCallback>();
final ObserverList<SettingsCallback> _listeners = ObserverList<SettingsCallback>();
Settings._private();
@ -32,7 +32,7 @@ class Settings {
debugPrint('$runtimeType notifyListeners key=$key, old=$oldValue, new=$newValue');
if (_listeners != null) {
final List<SettingsCallback> localListeners = _listeners.toList();
for (SettingsCallback listener in localListeners) {
for (final listener in localListeners) {
try {
if (_listeners.contains(listener)) {
listener(key, oldValue, newValue);
@ -66,7 +66,7 @@ class Settings {
T getEnumOrDefault<T>(String key, T defaultValue, Iterable<T> values) {
final valueString = prefs.getString(key);
for (T element in values) {
for (final element in values) {
if (element.toString() == valueString) {
return element;
}

View file

@ -13,7 +13,7 @@ class AndroidAppService {
} on PlatformException catch (e) {
debugPrint('getAppNames failed with exception=${e.message}');
}
return Map();
return {};
}
static Future<Uint8List> getAppIcon(String packageName, int size) async {

View file

@ -27,7 +27,7 @@ String _decimal2sexagesimal(final double dec) {
final List<int> minParts = _split(min);
final int minFractionalPart = minParts[1];
final double sec = (double.parse('0.$minFractionalPart') * 60);
final double sec = double.parse('0.$minFractionalPart') * 60;
return '$deg° ${min.floor()} ${_round(sec, decimals: 2).toStringAsFixed(2)}';
}

View file

@ -6,7 +6,7 @@ bool isAtSameDayAs(DateTime d1, DateTime d2) => isAtSameMonthAs(d1, d2) && d1.da
bool isToday(DateTime d) => isAtSameDayAs(d, DateTime.now());
bool isYesterday(DateTime d) => isAtSameDayAs(d, DateTime.now().subtract(Duration(days: 1)));
bool isYesterday(DateTime d) => isAtSameDayAs(d, DateTime.now().subtract(const Duration(days: 1)));
bool isThisMonth(DateTime d) => isAtSameMonthAs(d, DateTime.now());

View file

@ -21,6 +21,9 @@ class AllCollectionDrawer extends StatelessWidget {
padding: EdgeInsets.only(bottom: window.viewInsets.bottom),
children: [
DrawerHeader(
decoration: BoxDecoration(
color: Theme.of(context).accentColor,
),
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -28,18 +31,18 @@ class AllCollectionDrawer extends StatelessWidget {
Row(
children: [
CircleAvatar(
backgroundColor: Colors.white,
radius: 44,
child: Padding(
padding: EdgeInsets.only(top: 6.0),
padding: const EdgeInsets.only(top: 6.0),
child: SvgPicture.asset(
'assets/aves_logo.svg',
width: 64,
),
),
backgroundColor: Colors.white,
radius: 44,
),
SizedBox(width: 16),
Text(
const SizedBox(width: 16),
const Text(
'Aves',
style: TextStyle(
fontSize: 44,
@ -48,28 +51,28 @@ class AllCollectionDrawer extends StatelessWidget {
),
],
),
SizedBox(height: 16),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(children: [
Icon(Icons.photo_library),
SizedBox(width: 4),
const SizedBox(width: 4),
Text('${collection.imageCount}')
]),
Row(children: [
Icon(Icons.video_library),
SizedBox(width: 4),
const SizedBox(width: 4),
Text('${collection.videoCount}')
]),
Row(children: [
Icon(Icons.photo_album),
SizedBox(width: 4),
const SizedBox(width: 4),
Text('${collection.albumCount}')
]),
Row(children: [
Icon(Icons.label),
SizedBox(width: 4),
const SizedBox(width: 4),
Text('${collection.tagCount}')
]),
],
@ -77,9 +80,6 @@ class AllCollectionDrawer extends StatelessWidget {
],
),
),
decoration: BoxDecoration(
color: Theme.of(context).accentColor,
),
),
_buildFilteredCollectionNavTile(
context: context,
@ -87,14 +87,14 @@ class AllCollectionDrawer extends StatelessWidget {
title: 'Videos',
filter: (entry) => entry.isVideo,
),
Divider(),
const Divider(),
...albums.map((album) => _buildFilteredCollectionNavTile(
context: context,
leading: IconUtils.getAlbumIcon(context, album) ?? Icon(Icons.photo_album),
title: collection.getUniqueAlbumName(album, albums),
filter: (entry) => entry.directory == album,
)),
Divider(),
const Divider(),
...tags.map((tag) => _buildFilteredCollectionNavTile(
context: context,
leading: Icon(Icons.label),
@ -106,7 +106,7 @@ class AllCollectionDrawer extends StatelessWidget {
);
}
_buildFilteredCollectionNavTile({BuildContext context, Widget leading, String title, bool Function(ImageEntry) filter}) {
Widget _buildFilteredCollectionNavTile({BuildContext context, Widget leading, String title, bool Function(ImageEntry) filter}) {
return SafeArea(
top: false,
bottom: false,

View file

@ -16,7 +16,7 @@ class AllCollectionPage extends StatelessWidget {
return ThumbnailCollection(
collection: collection,
appBar: SliverAppBar(
title: Text('All'),
title: const Text('All'),
actions: [
IconButton(
icon: Icon(Icons.search),
@ -35,7 +35,7 @@ class AllCollectionPage extends StatelessWidget {
value: AlbumAction.sortBySize,
child: MenuRow(text: 'Sort by size', checked: collection.sortFactor == SortFactor.size),
),
PopupMenuDivider(),
const PopupMenuDivider(),
if (collection.sortFactor == SortFactor.date) ...[
PopupMenuItem(
value: AlbumAction.groupByAlbum,
@ -49,14 +49,14 @@ class AllCollectionPage extends StatelessWidget {
value: AlbumAction.groupByDay,
child: MenuRow(text: 'Group by day', checked: collection.groupFactor == GroupFactor.day),
),
PopupMenuDivider(),
const PopupMenuDivider(),
],
PopupMenuItem(
value: AlbumAction.debug,
child: MenuRow(text: 'Debug', icon: Icons.whatshot),
),
],
onSelected: (action) => onActionSelected(context, action),
onSelected: (action) => _onActionSelected(context, action),
),
],
floating: true,
@ -64,7 +64,7 @@ class AllCollectionPage extends StatelessWidget {
);
}
onActionSelected(BuildContext context, AlbumAction action) {
void _onActionSelected(BuildContext context, AlbumAction action) {
switch (action) {
case AlbumAction.debug:
goToDebug(context);

View file

@ -43,14 +43,14 @@ class ImageSearchDelegate extends SearchDelegate<ImageEntry> {
@override
Widget buildSuggestions(BuildContext context) {
return SizedBox.shrink();
return const SizedBox.shrink();
}
@override
Widget buildResults(BuildContext context) {
if (query.isEmpty) {
showSuggestions(context);
return SizedBox.shrink();
return const SizedBox.shrink();
}
final lowerQuery = query.toLowerCase();
final matches = collection.sortedEntries.where((entry) => entry.search(lowerQuery)).toList();

View file

@ -7,13 +7,13 @@ class DaySectionHeader extends StatelessWidget {
final String text;
DaySectionHeader({Key key, DateTime date})
: text = formatDate(date),
: text = _formatDate(date),
super(key: key);
static DateFormat md = DateFormat.MMMMd();
static DateFormat ymd = DateFormat.yMMMMd();
static formatDate(DateTime date) {
static String _formatDate(DateTime date) {
if (isToday(date)) return 'Today';
if (isYesterday(date)) return 'Yesterday';
if (isThisYear(date)) return md.format(date);
@ -30,13 +30,13 @@ class MonthSectionHeader extends StatelessWidget {
final String text;
MonthSectionHeader({Key key, DateTime date})
: text = formatDate(date),
: text = _formatDate(date),
super(key: key);
static DateFormat m = DateFormat.MMMM();
static DateFormat ym = DateFormat.yMMMM();
static formatDate(DateTime date) {
static String _formatDate(DateTime date) {
if (isThisMonth(date)) return 'This month';
if (isThisYear(date)) return m.format(date);
return ym.format(date);
@ -64,7 +64,7 @@ class TitleSectionHeader extends StatelessWidget {
fontFamily: 'Concourse Caps',
shadows: [
Shadow(
offset: Offset(0, 2),
offset: const Offset(0, 2),
blurRadius: 3,
color: Colors.grey[900],
),
@ -74,12 +74,12 @@ class TitleSectionHeader extends StatelessWidget {
outlineWidth: 2,
);
return Container(
padding: EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
child: leading != null
? Row(
children: [
leading,
SizedBox(width: 8),
const SizedBox(width: 8),
text,
],
)

View file

@ -13,7 +13,7 @@ class ThumbnailCollection extends AnimatedWidget {
final ImageCollection collection;
final Widget appBar;
ThumbnailCollection({
const ThumbnailCollection({
Key key,
this.collection,
this.appBar,
@ -65,6 +65,12 @@ class ThumbnailCollectionContent extends StatelessWidget {
child: Selector<MediaQueryData, double>(
selector: (c, mq) => mq.viewInsets.bottom,
builder: (c, mqViewInsetsBottom, child) => DraggableScrollbar.arrows(
controller: _scrollController,
padding: EdgeInsets.only(
// top padding to adjust scroll thumb
top: topPadding,
bottom: mqViewInsetsBottom,
),
child: CustomScrollView(
controller: _scrollController,
slivers: [
@ -89,12 +95,6 @@ class ThumbnailCollectionContent extends StatelessWidget {
}),
],
),
controller: _scrollController,
padding: EdgeInsets.only(
// top padding to adjust scroll thumb
top: topPadding,
bottom: mqViewInsetsBottom,
),
),
),
);
@ -117,7 +117,7 @@ class SectionSliver extends StatelessWidget {
@override
Widget build(BuildContext context) {
final columnCount = 4;
const columnCount = 4;
return SliverStickyHeader(
header: SectionHeader(
collection: collection,
@ -143,7 +143,7 @@ class SectionSliver extends StatelessWidget {
addAutomaticKeepAlives: false,
addRepaintBoundaries: true,
),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columnCount,
),
),
@ -151,7 +151,7 @@ class SectionSliver extends StatelessWidget {
);
}
_showFullscreen(BuildContext context, ImageEntry entry) {
void _showFullscreen(BuildContext context, ImageEntry entry) {
Navigator.push(
context,
MaterialPageRoute(
@ -178,11 +178,11 @@ class SectionHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
Widget header = SizedBox.shrink();
Widget header = const SizedBox.shrink();
if (collection.sortFactor == SortFactor.date) {
switch (collection.groupFactor) {
case GroupFactor.album:
Widget albumIcon = IconUtils.getAlbumIcon(context, sectionKey);
Widget albumIcon = IconUtils.getAlbumIcon(context, sectionKey as String);
if (albumIcon != null) {
albumIcon = Material(
type: MaterialType.circle,
@ -194,14 +194,14 @@ class SectionHeader extends StatelessWidget {
}
header = TitleSectionHeader(
leading: albumIcon,
title: collection.getUniqueAlbumName(sectionKey, sections.keys.cast<String>()),
title: collection.getUniqueAlbumName(sectionKey as String, sections.keys.cast<String>()),
);
break;
case GroupFactor.month:
header = MonthSectionHeader(date: sectionKey);
header = MonthSectionHeader(date: sectionKey as DateTime);
break;
case GroupFactor.day:
header = DaySectionHeader(date: sectionKey);
header = DaySectionHeader(date: sectionKey as DateTime);
break;
}
}

View file

@ -42,7 +42,7 @@ class AppIconState extends State<AppIcon> {
width: widget.size,
height: widget.size,
)
: SizedBox.shrink();
: const SizedBox.shrink();
},
);
}

View file

@ -3,7 +3,7 @@ import 'package:flutter/widgets.dart';
class FakeAppBar extends StatelessWidget with PreferredSizeWidget {
@override
Widget build(BuildContext context) {
return SafeArea(child: SizedBox.shrink());
return const SafeArea(child: SizedBox.shrink());
}
@override

View file

@ -61,10 +61,10 @@ class OverlayIcon extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.all(1),
margin: const EdgeInsets.all(1),
padding: text != null ? EdgeInsets.only(right: iconSize / 4) : null,
decoration: BoxDecoration(
color: Color(0xBB000000),
color: const Color(0xBB000000),
borderRadius: BorderRadius.all(
Radius.circular(iconSize),
),
@ -78,7 +78,7 @@ class OverlayIcon extends StatelessWidget {
size: iconSize,
),
if (text != null) ...[
SizedBox(width: 2),
const SizedBox(width: 2),
Text(text),
]
],

View file

@ -40,33 +40,33 @@ class ImagePreviewState extends State<ImagePreview> with AfterInitMixin {
entry.imageChangeNotifier,
entry.metadataChangeNotifier
]);
_entryChangeNotifier.addListener(onEntryChange);
_entryChangeNotifier.addListener(_onEntryChange);
}
@override
void didInitState() {
_devicePixelRatio = Provider.of<MediaQueryData>(context, listen: false).devicePixelRatio;
initByteLoader();
_initByteLoader();
}
@override
void didUpdateWidget(ImagePreview old) {
super.didUpdateWidget(old);
if (widget.width == old.width && widget.height == old.height && uri == old.entry.uri && widget.entry.width == old.entry.width && widget.entry.height == old.entry.height && widget.entry.orientationDegrees == old.entry.orientationDegrees) return;
initByteLoader();
_initByteLoader();
}
initByteLoader() {
void _initByteLoader() {
final width = (widget.width * _devicePixelRatio).round();
final height = (widget.height * _devicePixelRatio).round();
_byteLoader = ImageFileService.getImageBytes(widget.entry, width, height);
}
onEntryChange() => setState(() => initByteLoader());
void _onEntryChange() => setState(() => _initByteLoader());
@override
void dispose() {
_entryChangeNotifier.removeListener(onEntryChange);
_entryChangeNotifier.removeListener(_onEntryChange);
super.dispose();
}

View file

@ -21,11 +21,11 @@ class MenuRow extends StatelessWidget {
opacity: checked ? 1 : 0,
child: Icon(Icons.done),
),
SizedBox(width: 8),
const SizedBox(width: 8),
],
if (icon != null) ...[
Icon(icon),
SizedBox(width: 8),
const SizedBox(width: 8),
],
Text(text),
],

View file

@ -39,48 +39,48 @@ class DebugPageState extends State<DebugPage> {
return MediaQueryDataProvider(
child: Scaffold(
appBar: AppBar(
title: Text('Info'),
title: const Text('Info'),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Paths'),
const Text('Paths'),
Text('DCIM path: ${androidFileUtils.dcimPath}'),
Text('pictures path: ${androidFileUtils.picturesPath}'),
Divider(),
Text('Settings'),
const Divider(),
const Text('Settings'),
Text('collectionGroupFactor: ${settings.collectionGroupFactor}'),
Text('collectionSortFactor: ${settings.collectionSortFactor}'),
Text('infoMapZoom: ${settings.infoMapZoom}'),
Divider(),
const Divider(),
Text('Entries: ${entries.length}'),
...byMimeTypes.keys.map((mimeType) => Text('- $mimeType: ${byMimeTypes[mimeType].length}')),
Text('Catalogued: ${catalogued.length}'),
Text('With GPS: ${withGps.length}'),
Text('With address: ${located.length}'),
Divider(),
const Divider(),
RaisedButton(
onPressed: () => metadataDb.reset(),
child: Text('Reset DB'),
child: const Text('Reset DB'),
),
FutureBuilder(
future: _dbMetadataLoader,
builder: (futureContext, AsyncSnapshot<List<CatalogMetadata>> snapshot) {
if (snapshot.hasError) return Text(snapshot.error);
if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink();
if (snapshot.hasError) return Text(snapshot.error.toString());
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
return Text('DB metadata rows: ${snapshot.data.length}');
},
),
FutureBuilder(
future: _dbAddressLoader,
builder: (futureContext, AsyncSnapshot<List<AddressDetails>> snapshot) {
if (snapshot.hasError) return Text(snapshot.error);
if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink();
if (snapshot.hasError) return Text(snapshot.error.toString());
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
return Text('DB address rows: ${snapshot.data.length}');
},
),
Divider(),
Text('Time dilation'),
const Divider(),
const Text('Time dilation'),
Slider(
value: timeDilation,
onChanged: (v) => setState(() => timeDilation = v),

View file

@ -61,14 +61,14 @@ class FullscreenActionDelegate {
void _showFeedback(BuildContext context, String message) {
Flushbar(
message: message,
margin: EdgeInsets.all(8),
margin: const EdgeInsets.all(8),
borderRadius: 8,
borderColor: Colors.white30,
borderWidth: 0.5,
duration: Duration(seconds: 2),
duration: const Duration(seconds: 2),
flushbarPosition: FlushbarPosition.TOP,
animationDuration: Duration(milliseconds: 600),
)..show(context);
animationDuration: const Duration(milliseconds: 600),
).show(context);
}
Future<void> _print(ImageEntry entry) async {
@ -94,15 +94,15 @@ class FullscreenActionDelegate {
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: Text('Are you sure?'),
content: const Text('Are you sure?'),
actions: [
FlatButton(
onPressed: () => Navigator.pop(context),
child: Text('CANCEL'),
child: const Text('CANCEL'),
),
FlatButton(
onPressed: () => Navigator.pop(context, true),
child: Text('DELETE'),
child: const Text('DELETE'),
),
],
);
@ -130,11 +130,11 @@ class FullscreenActionDelegate {
actions: [
FlatButton(
onPressed: () => Navigator.pop(context),
child: Text('CANCEL'),
child: const Text('CANCEL'),
),
FlatButton(
onPressed: () => Navigator.pop(context, controller.text),
child: Text('APPLY'),
child: const Text('APPLY'),
),
],
);

View file

@ -61,13 +61,13 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
bool _isInitialScale = true;
int _currentHorizontalPage, _currentVerticalPage = imagePage;
PageController _horizontalPager, _verticalPager;
ValueNotifier<bool> _overlayVisible = ValueNotifier(true);
final ValueNotifier<bool> _overlayVisible = ValueNotifier(true);
AnimationController _overlayAnimationController;
Animation<double> _topOverlayScale;
Animation<Offset> _bottomOverlayOffset;
EdgeInsets _frozenViewInsets, _frozenViewPadding;
FullscreenActionDelegate _actionDelegate;
List<Tuple2<String, VideoPlayerController>> _videoControllers = List();
final List<Tuple2<String, VideoPlayerController>> _videoControllers = [];
ImageCollection get collection => widget.collection;
@ -85,14 +85,14 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
_horizontalPager = PageController(initialPage: _currentHorizontalPage);
_verticalPager = PageController(initialPage: _currentVerticalPage);
_overlayAnimationController = AnimationController(
duration: Duration(milliseconds: 400),
duration: const Duration(milliseconds: 400),
vsync: this,
);
_topOverlayScale = CurvedAnimation(
parent: _overlayAnimationController,
curve: Curves.easeOutQuart,
);
_bottomOverlayOffset = Tween(begin: Offset(0, 1), end: Offset(0, 0)).animate(CurvedAnimation(
_bottomOverlayOffset = Tween(begin: const Offset(0, 1), end: const Offset(0, 0)).animate(CurvedAnimation(
parent: _overlayAnimationController,
curve: Curves.easeOutQuart,
));
@ -105,7 +105,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
_initOverlay();
}
_initOverlay() async {
Future<void> _initOverlay() async {
// wait for MaterialPageRoute.transitionDuration
// to show overlay after hero animation is complete
await Future.delayed(Duration(milliseconds: (300 * timeDilation).toInt()));
@ -136,10 +136,10 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
PageView(
scrollDirection: Axis.vertical,
controller: _verticalPager,
physics: _isInitialScale ? PageScrollPhysics() : NeverScrollableScrollPhysics(),
physics: _isInitialScale ? const PageScrollPhysics() : const NeverScrollableScrollPhysics(),
onPageChanged: _onVerticalPageChanged,
children: [
SizedBox(),
const SizedBox(),
Container(
color: Colors.black,
child: ImagePage(
@ -208,12 +208,12 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
Future<void> _goToVerticalPage(int page) {
return _verticalPager.animateToPage(
page,
duration: Duration(milliseconds: 350),
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
);
}
void _onVerticalPageChanged(page) {
void _onVerticalPageChanged(int page) {
setState(() => _currentVerticalPage = page);
if (_currentVerticalPage == transitionPage) {
_onLeave();

View file

@ -51,7 +51,7 @@ class ImagePageState extends State<ImagePage> with AutomaticKeepAliveClientMixin
entry: entry,
controller: videoController,
)
: SizedBox(),
: const SizedBox(),
childSize: mqSize,
// no hero as most videos fullscreen image is different from its thumbnail
minScale: PhotoViewComputedScale.contained,
@ -70,14 +70,14 @@ class ImagePageState extends State<ImagePage> with AutomaticKeepAliveClientMixin
onTapUp: (tapContext, details, value) => widget.onTap?.call(),
);
},
loadingChild: Center(
loadingChild: const Center(
child: CircularProgressIndicator(),
),
backgroundDecoration: BoxDecoration(color: Colors.transparent),
pageController: widget.pageController,
onPageChanged: widget.onPageChanged,
scaleStateChangedCallback: widget.onScaleChanged,
scrollPhysics: BouncingScrollPhysics(),
scrollPhysics: const BouncingScrollPhysics(),
),
);
}

View file

@ -119,7 +119,7 @@ class SectionRow extends StatelessWidget {
padding: const EdgeInsets.all(16.0),
child: Text(
title,
style: TextStyle(
style: const TextStyle(
fontSize: 20,
fontFamily: 'Concourse Caps',
),
@ -142,9 +142,9 @@ class InfoRow extends StatelessWidget {
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: RichText(
text: TextSpan(
style: TextStyle(fontFamily: 'Concourse'),
style: const TextStyle(fontFamily: 'Concourse'),
children: [
TextSpan(text: '$label ', style: TextStyle(color: Colors.white70)),
TextSpan(text: '$label ', style: const TextStyle(color: Colors.white70)),
TextSpan(text: value),
],
),

View file

@ -7,7 +7,7 @@ import 'package:google_maps_flutter/google_maps_flutter.dart';
class LocationSection extends AnimatedWidget {
final ImageEntry entry;
final showTitle;
final bool showTitle;
LocationSection({
Key key,
@ -23,12 +23,12 @@ class LocationSection extends AnimatedWidget {
@override
Widget build(BuildContext context) {
return !entry.hasGps
? SizedBox.shrink()
? const SizedBox.shrink()
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (showTitle)
Padding(
const Padding(
padding: EdgeInsets.only(bottom: 8),
child: SectionRow('Location'),
),
@ -43,7 +43,7 @@ class LocationSection extends AnimatedWidget {
),
if (entry.isLocated)
Padding(
padding: EdgeInsets.only(top: 8),
padding: const EdgeInsets.only(top: 8),
child: InfoRow('Address', entry.addressDetails.addressLine),
),
],
@ -90,7 +90,7 @@ class ImageMapState extends State<ImageMap> with AutomaticKeepAliveClientMixin {
child: SizedBox(
height: 200,
child: ClipRRect(
borderRadius: BorderRadius.all(
borderRadius: const BorderRadius.all(
Radius.circular(16),
),
child: GoogleMap(
@ -104,18 +104,18 @@ class ImageMapState extends State<ImageMap> with AutomaticKeepAliveClientMixin {
zoomGesturesEnabled: false,
tiltGesturesEnabled: false,
myLocationButtonEnabled: false,
markers: [
markers: {
Marker(
markerId: MarkerId(widget.markerId),
icon: BitmapDescriptor.defaultMarkerWithHue(accentHue),
position: widget.latLng,
)
].toSet(),
},
),
),
),
),
SizedBox(width: 8),
const SizedBox(width: 8),
Column(children: [
IconButton(
icon: Icon(Icons.add),
@ -137,7 +137,7 @@ class ImageMapState extends State<ImageMap> with AutomaticKeepAliveClientMixin {
);
}
_zoomBy(double amount) {
void _zoomBy(double amount) {
settings.infoMapZoom += amount;
_controller.animateCamera(CameraUpdate.zoomBy(amount));
}

View file

@ -23,16 +23,16 @@ class MetadataSectionState extends State<MetadataSection> {
@override
void initState() {
super.initState();
initMetadataLoader();
_initMetadataLoader();
}
@override
void didUpdateWidget(MetadataSection oldWidget) {
super.didUpdateWidget(oldWidget);
initMetadataLoader();
_initMetadataLoader();
}
initMetadataLoader() async {
Future<void> _initMetadataLoader() async {
_metadataLoader = MetadataService.getAllMetadata(widget.entry);
}
@ -43,9 +43,9 @@ class MetadataSectionState extends State<MetadataSection> {
builder: (c, mqWidth, child) => FutureBuilder(
future: _metadataLoader,
builder: (futureContext, AsyncSnapshot<Map> snapshot) {
if (snapshot.hasError) return Text(snapshot.error);
if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink();
final metadataMap = snapshot.data.cast<String, Map>();
if (snapshot.hasError) return Text(snapshot.error.toString());
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
final directoryNames = metadataMap.keys.toList()..sort();
Widget content;
@ -68,7 +68,7 @@ class MetadataSectionState extends State<MetadataSection> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: getMetadataColumn(metadataMap, first)),
SizedBox(width: 8),
const SizedBox(width: 8),
Expanded(child: getMetadataColumn(metadataMap, second)),
],
);
@ -79,7 +79,7 @@ class MetadataSectionState extends State<MetadataSection> {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SectionRow('Metadata'),
const SectionRow('Metadata'),
content,
],
);
@ -98,19 +98,19 @@ class MetadataSectionState extends State<MetadataSection> {
return [
if (directoryName.isNotEmpty)
Padding(
padding: EdgeInsets.symmetric(vertical: 4.0),
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Text(directoryName,
style: TextStyle(
style: const TextStyle(
fontSize: 18,
fontFamily: 'Concourse Caps',
)),
),
...tagKeys.map((tagKey) {
final value = directory[tagKey] as String;
if (value == null || value.isEmpty) return SizedBox.shrink();
if (value == null || value.isEmpty) return const SizedBox.shrink();
return InfoRow(tagKey, value.length > maxValueLength ? '${value.substring(0, maxValueLength)}' : value);
}),
SizedBox(height: 16),
const SizedBox(height: 16),
];
}),
],

View file

@ -18,15 +18,15 @@ class XmpTagSection extends AnimatedWidget {
Widget build(BuildContext context) {
final tags = entry.xmpSubjects;
return tags.isEmpty
? SizedBox.shrink()
? const SizedBox.shrink()
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionRow('XMP Tags'),
const SectionRow('XMP Tags'),
Wrap(
children: tags
.map((tag) => Padding(
padding: EdgeInsets.symmetric(horizontal: 4.0),
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: ActionChip(
label: Text(tag),
onPressed: () => Navigator.push(

View file

@ -45,16 +45,16 @@ class _FullscreenBottomOverlayState extends State<FullscreenBottomOverlay> {
@override
void initState() {
super.initState();
initDetailLoader();
_initDetailLoader();
}
@override
void didUpdateWidget(FullscreenBottomOverlay oldWidget) {
super.didUpdateWidget(oldWidget);
initDetailLoader();
_initDetailLoader();
}
initDetailLoader() {
void _initDetailLoader() {
_detailLoader = MetadataService.getOverlayMetadata(entry);
}
@ -86,7 +86,7 @@ class _FullscreenBottomOverlayState extends State<FullscreenBottomOverlay> {
_lastEntry = entry;
}
return _lastEntry == null
? SizedBox.shrink()
? const SizedBox.shrink()
: _FullscreenBottomOverlayContent(
entry: _lastEntry,
details: _lastDetails,
@ -115,7 +115,7 @@ class _FullscreenBottomOverlayContent extends StatelessWidget {
final String position;
final double maxWidth;
_FullscreenBottomOverlayContent({
const _FullscreenBottomOverlayContent({
this.entry,
this.details,
this.position,

View file

@ -28,14 +28,14 @@ class FullscreenTopOverlay extends StatelessWidget {
return SafeArea(
minimum: (viewInsets ?? EdgeInsets.zero) + (viewPadding ?? EdgeInsets.zero),
child: Padding(
padding: EdgeInsets.all(8.0),
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
OverlayButton(
scale: scale,
child: BackButton(),
child: const BackButton(),
),
Spacer(),
const Spacer(),
OverlayButton(
scale: scale,
child: IconButton(
@ -44,7 +44,7 @@ class FullscreenTopOverlay extends StatelessWidget {
tooltip: 'Share',
),
),
SizedBox(width: 8),
const SizedBox(width: 8),
OverlayButton(
scale: scale,
child: IconButton(
@ -53,7 +53,7 @@ class FullscreenTopOverlay extends StatelessWidget {
tooltip: 'Delete',
),
),
SizedBox(width: 8),
const SizedBox(width: 8),
OverlayButton(
scale: scale,
child: PopupMenuButton<FullscreenAction>(
@ -81,21 +81,21 @@ class FullscreenTopOverlay extends StatelessWidget {
value: FullscreenAction.print,
child: MenuRow(text: 'Print', icon: Icons.print),
),
PopupMenuDivider(),
PopupMenuItem(
const PopupMenuDivider(),
const PopupMenuItem(
value: FullscreenAction.edit,
child: Text('Edit with…'),
),
PopupMenuItem(
const PopupMenuItem(
value: FullscreenAction.open,
child: Text('Open with…'),
),
PopupMenuItem(
const PopupMenuItem(
value: FullscreenAction.setAs,
child: Text('Set as…'),
),
if (entry.hasGps)
PopupMenuItem(
const PopupMenuItem(
value: FullscreenAction.openMap,
child: Text('Show on map…'),
),

View file

@ -46,32 +46,32 @@ class VideoControlOverlayState extends State<VideoControlOverlay> with SingleTic
void initState() {
super.initState();
_playPauseAnimation = AnimationController(
duration: Duration(milliseconds: 300),
duration: const Duration(milliseconds: 300),
vsync: this,
);
registerWidget(widget);
_registerWidget(widget);
_onValueChange();
}
@override
void didUpdateWidget(VideoControlOverlay oldWidget) {
super.didUpdateWidget(oldWidget);
unregisterWidget(oldWidget);
registerWidget(widget);
_unregisterWidget(oldWidget);
_registerWidget(widget);
}
@override
void dispose() {
unregisterWidget(widget);
_unregisterWidget(widget);
_playPauseAnimation.dispose();
super.dispose();
}
void registerWidget(VideoControlOverlay widget) {
void _registerWidget(VideoControlOverlay widget) {
widget.controller.addListener(_onValueChange);
}
void unregisterWidget(VideoControlOverlay widget) {
void _unregisterWidget(VideoControlOverlay widget) {
widget.controller.removeListener(_onValueChange);
}
@ -86,7 +86,7 @@ class VideoControlOverlayState extends State<VideoControlOverlay> with SingleTic
final viewInsets = widget.viewInsets ?? mqViewInsets;
final viewPadding = widget.viewPadding ?? mqViewPadding;
final safePadding = (viewInsets + viewPadding).copyWith(bottom: 8) + EdgeInsets.symmetric(horizontal: 8.0);
final safePadding = (viewInsets + viewPadding).copyWith(bottom: 8) + const EdgeInsets.symmetric(horizontal: 8.0);
return Padding(
padding: safePadding,
@ -109,7 +109,7 @@ class VideoControlOverlayState extends State<VideoControlOverlay> with SingleTic
Expanded(
child: _buildProgressBar(),
),
SizedBox(width: 8),
const SizedBox(width: 8),
OverlayButton(
scale: scale,
child: IconButton(
@ -130,7 +130,7 @@ class VideoControlOverlayState extends State<VideoControlOverlay> with SingleTic
}
Widget _buildProgressBar() {
final progressBarBorderRadius = 123.0;
const progressBarBorderRadius = 123.0;
return SizeTransition(
sizeFactor: scale,
child: BlurredRRect(
@ -150,11 +150,11 @@ class VideoControlOverlayState extends State<VideoControlOverlay> with SingleTic
if (_playingOnDragStart) controller.play();
},
child: Container(
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 16) + EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16) + const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.black26,
border: Border.all(color: Colors.white30, width: 0.5),
borderRadius: BorderRadius.all(
borderRadius: const BorderRadius.all(
Radius.circular(progressBarBorderRadius),
),
),
@ -164,7 +164,7 @@ class VideoControlOverlayState extends State<VideoControlOverlay> with SingleTic
Row(
children: [
Text(formatDuration(value.position ?? Duration.zero)),
Spacer(),
const Spacer(),
Text(formatDuration(value.duration ?? Duration.zero)),
],
),

View file

@ -29,34 +29,34 @@ class AvesVideoState extends State<AvesVideo> {
@override
void initState() {
super.initState();
registerWidget(widget);
_registerWidget(widget);
_onValueChange();
}
@override
void didUpdateWidget(AvesVideo oldWidget) {
super.didUpdateWidget(oldWidget);
unregisterWidget(oldWidget);
registerWidget(widget);
_unregisterWidget(oldWidget);
_registerWidget(widget);
}
@override
void dispose() {
unregisterWidget(widget);
_unregisterWidget(widget);
super.dispose();
}
registerWidget(AvesVideo widget) {
void _registerWidget(AvesVideo widget) {
widget.controller.addListener(_onValueChange);
}
unregisterWidget(AvesVideo widget) {
void _unregisterWidget(AvesVideo widget) {
widget.controller.removeListener(_onValueChange);
}
@override
Widget build(BuildContext context) {
if (value == null) return SizedBox();
if (value == null) return const SizedBox();
if (value.hasError) {
return Selector<MediaQueryData, double>(
selector: (c, mq) => mq.size.width,
@ -79,12 +79,12 @@ class AvesVideoState extends State<AvesVideo> {
);
}
_onValueChange() {
if (!value.isPlaying && value.position == value.duration) goToBeginning();
void _onValueChange() {
if (!value.isPlaying && value.position == value.duration) _goToStart();
setState(() {});
}
goToBeginning() async {
Future<void> _goToStart() async {
await widget.controller.seekTo(Duration.zero);
await widget.controller.pause();
}