album: group by album/date
This commit is contained in:
parent
af1b86dfaa
commit
5bb2e914c6
6 changed files with 173 additions and 105 deletions
|
@ -2,13 +2,31 @@ import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/image_file_service.dart';
|
import 'package:aves/model/image_file_service.dart';
|
||||||
import 'package:aves/model/image_metadata.dart';
|
import 'package:aves/model/image_metadata.dart';
|
||||||
import 'package:aves/model/metadata_db.dart';
|
import 'package:aves/model/metadata_db.dart';
|
||||||
|
import "package:collection/collection.dart";
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class ImageCollection with ChangeNotifier {
|
class ImageCollection with ChangeNotifier {
|
||||||
final List<ImageEntry> entries;
|
final List<ImageEntry> entries;
|
||||||
|
|
||||||
|
GroupFactor groupFactor = GroupFactor.date;
|
||||||
|
|
||||||
ImageCollection(this.entries);
|
ImageCollection(this.entries);
|
||||||
|
|
||||||
|
Map<dynamic, List<ImageEntry>> get sections {
|
||||||
|
switch (groupFactor) {
|
||||||
|
case GroupFactor.album:
|
||||||
|
return groupBy(entries, (entry) => entry.bucketDisplayName);
|
||||||
|
case GroupFactor.date:
|
||||||
|
return groupBy(entries, (entry) => entry.monthTaken);
|
||||||
|
}
|
||||||
|
return Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
group(GroupFactor groupFactor) {
|
||||||
|
this.groupFactor = groupFactor;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> delete(ImageEntry entry) async {
|
Future<bool> delete(ImageEntry entry) async {
|
||||||
final success = await ImageFileService.delete(entry);
|
final success = await ImageFileService.delete(entry);
|
||||||
if (success) {
|
if (success) {
|
||||||
|
@ -77,3 +95,5 @@ class ImageCollection with ChangeNotifier {
|
||||||
debugPrint('$runtimeType locateEntries complete in ${DateTime.now().difference(start).inSeconds}s');
|
debugPrint('$runtimeType locateEntries complete in ${DateTime.now().difference(start).inSeconds}s');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum GroupFactor { album, date }
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:aves/model/image_collection.dart';
|
import 'package:aves/model/image_collection.dart';
|
||||||
import 'package:aves/widgets/album/search_delegate.dart';
|
import 'package:aves/widgets/album/search_delegate.dart';
|
||||||
import 'package:aves/widgets/album/thumbnail_collection.dart';
|
import 'package:aves/widgets/album/thumbnail_collection.dart';
|
||||||
|
import 'package:aves/widgets/common/menu_row.dart';
|
||||||
import 'package:aves/widgets/debug_page.dart';
|
import 'package:aves/widgets/debug_page.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
@ -23,13 +24,44 @@ class AllCollectionPage extends StatelessWidget {
|
||||||
delegate: ImageSearchDelegate(collection),
|
delegate: ImageSearchDelegate(collection),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(icon: Icon(Icons.whatshot), onPressed: () => goToDebug(context)),
|
PopupMenuButton<AlbumAction>(
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
PopupMenuItem(
|
||||||
|
value: AlbumAction.groupByAlbum,
|
||||||
|
child: MenuRow(text: 'Group by album', checked: collection.groupFactor == GroupFactor.album),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: AlbumAction.groupByDate,
|
||||||
|
child: MenuRow(text: 'Group by date', checked: collection.groupFactor == GroupFactor.date),
|
||||||
|
),
|
||||||
|
PopupMenuDivider(),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: AlbumAction.groupByAlbum,
|
||||||
|
child: MenuRow(text: 'Debug', icon: Icons.whatshot),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onSelected: (action) => onActionSelected(context, action),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
floating: true,
|
floating: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onActionSelected(BuildContext context, AlbumAction action) {
|
||||||
|
switch (action) {
|
||||||
|
case AlbumAction.groupByAlbum:
|
||||||
|
collection.group(GroupFactor.album);
|
||||||
|
break;
|
||||||
|
case AlbumAction.groupByDate:
|
||||||
|
collection.group(GroupFactor.date);
|
||||||
|
break;
|
||||||
|
case AlbumAction.debug:
|
||||||
|
goToDebug(context);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future goToDebug(BuildContext context) {
|
Future goToDebug(BuildContext context) {
|
||||||
return Navigator.push(
|
return Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
@ -41,3 +73,5 @@ class AllCollectionPage extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum AlbumAction { groupByDate, groupByAlbum, debug }
|
||||||
|
|
77
lib/widgets/album/sections.dart
Normal file
77
lib/widgets/album/sections.dart
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import 'package:aves/utils/date_utils.dart';
|
||||||
|
import 'package:aves/widgets/common/outlined_text.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
class DaySectionHeader extends StatelessWidget {
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
DaySectionHeader({Key key, DateTime date})
|
||||||
|
: text = formatDate(date),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
static DateFormat md = DateFormat.MMMMd();
|
||||||
|
static DateFormat ymd = DateFormat.yMMMMd();
|
||||||
|
|
||||||
|
static formatDate(DateTime date) {
|
||||||
|
if (isToday(date)) return 'Today';
|
||||||
|
if (isThisYear(date)) return md.format(date);
|
||||||
|
return ymd.format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SectionHeader(text: text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MonthSectionHeader extends StatelessWidget {
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
MonthSectionHeader({Key key, DateTime date})
|
||||||
|
: text = formatDate(date),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
static DateFormat m = DateFormat.MMMM();
|
||||||
|
static DateFormat ym = DateFormat.yMMMM();
|
||||||
|
|
||||||
|
static formatDate(DateTime date) {
|
||||||
|
if (isThisMonth(date)) return 'This month';
|
||||||
|
if (isThisYear(date)) return m.format(date);
|
||||||
|
return ym.format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SectionHeader(text: text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SectionHeader extends StatelessWidget {
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
const SectionHeader({Key key, this.text}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: OutlinedText(
|
||||||
|
text,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey[200],
|
||||||
|
fontSize: 20,
|
||||||
|
shadows: [
|
||||||
|
Shadow(
|
||||||
|
offset: Offset(0, 2),
|
||||||
|
blurRadius: 3,
|
||||||
|
color: Colors.grey[900],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
outlineColor: Colors.black87,
|
||||||
|
outlineWidth: 2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,11 @@
|
||||||
import 'package:aves/model/image_collection.dart';
|
import 'package:aves/model/image_collection.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/utils/date_utils.dart';
|
import 'package:aves/widgets/album/sections.dart';
|
||||||
import 'package:aves/widgets/album/thumbnail.dart';
|
import 'package:aves/widgets/album/thumbnail.dart';
|
||||||
import 'package:aves/widgets/common/draggable_scrollbar.dart';
|
import 'package:aves/widgets/common/draggable_scrollbar.dart';
|
||||||
import 'package:aves/widgets/common/outlined_text.dart';
|
|
||||||
import 'package:aves/widgets/fullscreen/image_page.dart';
|
import 'package:aves/widgets/fullscreen/image_page.dart';
|
||||||
import "package:collection/collection.dart";
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
|
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
|
|
||||||
class ThumbnailCollection extends AnimatedWidget {
|
class ThumbnailCollection extends AnimatedWidget {
|
||||||
final ImageCollection collection;
|
final ImageCollection collection;
|
||||||
|
@ -33,14 +30,14 @@ class ThumbnailCollectionContent extends StatelessWidget {
|
||||||
final ImageCollection collection;
|
final ImageCollection collection;
|
||||||
final Widget appBar;
|
final Widget appBar;
|
||||||
|
|
||||||
final Map<DateTime, List<ImageEntry>> _sections;
|
final Map<dynamic, List<ImageEntry>> _sections;
|
||||||
final ScrollController _scrollController = ScrollController();
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
ThumbnailCollectionContent({
|
ThumbnailCollectionContent({
|
||||||
Key key,
|
Key key,
|
||||||
this.collection,
|
this.collection,
|
||||||
this.appBar,
|
this.appBar,
|
||||||
}) : _sections = groupBy(collection.entries, (entry) => entry.monthTaken),
|
}) : _sections = collection.sections,
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -82,8 +79,8 @@ class ThumbnailCollectionContent extends StatelessWidget {
|
||||||
|
|
||||||
class SectionSliver extends StatelessWidget {
|
class SectionSliver extends StatelessWidget {
|
||||||
final ImageCollection collection;
|
final ImageCollection collection;
|
||||||
final Map<DateTime, List<ImageEntry>> sections;
|
final Map<dynamic, List<ImageEntry>> sections;
|
||||||
final DateTime sectionKey;
|
final dynamic sectionKey;
|
||||||
|
|
||||||
const SectionSliver({
|
const SectionSliver({
|
||||||
Key key,
|
Key key,
|
||||||
|
@ -97,7 +94,7 @@ class SectionSliver extends StatelessWidget {
|
||||||
// debugPrint('$runtimeType build with sectionKey=$sectionKey');
|
// debugPrint('$runtimeType build with sectionKey=$sectionKey');
|
||||||
final columnCount = 4;
|
final columnCount = 4;
|
||||||
return SliverStickyHeader(
|
return SliverStickyHeader(
|
||||||
header: MonthSectionHeader(date: sectionKey),
|
header: collection.groupFactor == GroupFactor.date ? MonthSectionHeader(date: sectionKey) : SectionHeader(text: sectionKey),
|
||||||
sliver: SliverGrid(
|
sliver: SliverGrid(
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
(sliverContext, index) {
|
(sliverContext, index) {
|
||||||
|
@ -135,76 +132,3 @@ class SectionSliver extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DaySectionHeader extends StatelessWidget {
|
|
||||||
final String text;
|
|
||||||
|
|
||||||
DaySectionHeader({Key key, DateTime date})
|
|
||||||
: text = formatDate(date),
|
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
static DateFormat md = DateFormat.MMMMd();
|
|
||||||
static DateFormat ymd = DateFormat.yMMMMd();
|
|
||||||
|
|
||||||
static formatDate(DateTime date) {
|
|
||||||
if (isToday(date)) return 'Today';
|
|
||||||
if (isThisYear(date)) return md.format(date);
|
|
||||||
return ymd.format(date);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SectionHeader(text: text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MonthSectionHeader extends StatelessWidget {
|
|
||||||
final String text;
|
|
||||||
|
|
||||||
MonthSectionHeader({Key key, DateTime date})
|
|
||||||
: text = formatDate(date),
|
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
static DateFormat m = DateFormat.MMMM();
|
|
||||||
static DateFormat ym = DateFormat.yMMMM();
|
|
||||||
|
|
||||||
static formatDate(DateTime date) {
|
|
||||||
if (isThisMonth(date)) return 'This month';
|
|
||||||
if (isThisYear(date)) return m.format(date);
|
|
||||||
return ym.format(date);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SectionHeader(text: text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SectionHeader extends StatelessWidget {
|
|
||||||
final String text;
|
|
||||||
|
|
||||||
const SectionHeader({Key key, this.text}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
padding: EdgeInsets.all(16),
|
|
||||||
child: OutlinedText(
|
|
||||||
text,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.grey[200],
|
|
||||||
fontSize: 20,
|
|
||||||
shadows: [
|
|
||||||
Shadow(
|
|
||||||
offset: Offset(0, 2),
|
|
||||||
blurRadius: 3,
|
|
||||||
color: Colors.grey[900],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
outlineColor: Colors.black87,
|
|
||||||
outlineWidth: 2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
34
lib/widgets/common/menu_row.dart
Normal file
34
lib/widgets/common/menu_row.dart
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class MenuRow extends StatelessWidget {
|
||||||
|
final String text;
|
||||||
|
final IconData icon;
|
||||||
|
final bool checked;
|
||||||
|
|
||||||
|
const MenuRow({
|
||||||
|
Key key,
|
||||||
|
this.text,
|
||||||
|
this.icon,
|
||||||
|
this.checked,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
if (checked != null) ...[
|
||||||
|
Opacity(
|
||||||
|
opacity: checked ? 1 : 0,
|
||||||
|
child: Icon(Icons.done),
|
||||||
|
),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
],
|
||||||
|
if (icon != null) ...[
|
||||||
|
Icon(icon),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
],
|
||||||
|
Text(text),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/widgets/common/blurred.dart';
|
import 'package:aves/widgets/common/blurred.dart';
|
||||||
|
import 'package:aves/widgets/common/menu_row.dart';
|
||||||
import 'package:aves/widgets/fullscreen/image_page.dart';
|
import 'package:aves/widgets/fullscreen/image_page.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
@ -99,28 +100,6 @@ class FullscreenTopOverlay extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MenuRow extends StatelessWidget {
|
|
||||||
final String text;
|
|
||||||
final IconData icon;
|
|
||||||
|
|
||||||
const MenuRow({
|
|
||||||
Key key,
|
|
||||||
this.text,
|
|
||||||
this.icon,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Icon(icon),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Text(text),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class OverlayButton extends StatelessWidget {
|
class OverlayButton extends StatelessWidget {
|
||||||
final Animation<double> scale;
|
final Animation<double> scale;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
Loading…
Reference in a new issue