diff --git a/assets/kakaotalk.svg b/assets/kakaotalk.svg new file mode 100644 index 000000000..e721ec2b4 --- /dev/null +++ b/assets/kakaotalk.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 1c7057fa4..b48eba16e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_file_service.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/settings.dart'; +import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/album/all_collection_drawer.dart'; import 'package:aves/widgets/album/all_collection_page.dart'; import 'package:aves/widgets/common/fake_app_bar.dart'; @@ -12,6 +13,7 @@ import 'package:permission_handler/permission_handler.dart'; void main() async { await settings.init(); + await androidFileUtils.init(); runApp(AvesApp()); } diff --git a/lib/model/image_collection.dart b/lib/model/image_collection.dart index 793b8f9b3..4390e1439 100644 --- a/lib/model/image_collection.dart +++ b/lib/model/image_collection.dart @@ -4,6 +4,7 @@ import 'package:aves/model/image_metadata.dart'; import 'package:aves/model/metadata_db.dart'; import "package:collection/collection.dart"; import 'package:flutter/material.dart'; +import 'package:path/path.dart'; class ImageCollection with ChangeNotifier { final List _rawEntries; @@ -46,7 +47,7 @@ class ImageCollection with ChangeNotifier { case SortFactor.date: switch (groupFactor) { case GroupFactor.album: - sections = groupBy(_rawEntries, (entry) => entry.bucketDisplayName); + sections = groupBy(_rawEntries, (entry) => entry.directory); break; case GroupFactor.date: sections = groupBy(_rawEntries, (entry) => entry.monthTaken); @@ -82,7 +83,7 @@ class ImageCollection with ChangeNotifier { return success; } - updateAlbums() => albums = _rawEntries.map((entry) => entry.bucketDisplayName).toSet(); + updateAlbums() => albums = _rawEntries.map((entry) => entry.directory).toSet(); updateTags() => tags = _rawEntries.expand((entry) => entry.xmpSubjects).toSet(); @@ -153,6 +154,17 @@ class ImageCollection with ChangeNotifier { sortFactor: sortFactor, ); } + + String getUniqueAlbumName(String album, List albums) { + final otherAlbums = albums.where((item) => item != album); + final parts = album.split(separator); + int partCount = 0; + String testName; + do { + testName = separator + parts.skip(parts.length - ++partCount).join(separator); + } while (otherAlbums.any((item) => item.endsWith(testName))); + return parts.skip(parts.length - partCount).join(separator); + } } enum SortFactor { date, size } diff --git a/lib/model/image_entry.dart b/lib/model/image_entry.dart index cbcaaf58d..64116ef30 100644 --- a/lib/model/image_entry.dart +++ b/lib/model/image_entry.dart @@ -13,6 +13,7 @@ import 'mime_types.dart'; class ImageEntry with ChangeNotifier { String uri; String path; + String directory; int contentId; final String mimeType; int width; @@ -41,7 +42,7 @@ class ImageEntry with ChangeNotifier { this.sourceDateTakenMillis, this.bucketDisplayName, this.durationMillis, - }); + }) : directory = path != null ? dirname(path) : null; factory ImageEntry.fromMap(Map map) { return ImageEntry( diff --git a/lib/utils/android_file_utils.dart b/lib/utils/android_file_utils.dart new file mode 100644 index 000000000..fa464c3bf --- /dev/null +++ b/lib/utils/android_file_utils.dart @@ -0,0 +1,26 @@ +import 'package:path/path.dart'; + +final AndroidFileUtils androidFileUtils = AndroidFileUtils._private(); + +typedef void AndroidFileUtilsCallback(String key, dynamic oldValue, dynamic newValue); + +class AndroidFileUtils { + String dcimPath; + String picturesPath; + + AndroidFileUtils._private(); + + init() async { + // TODO TLAD find storage root + // getExternalStorageDirectory() gives '/storage/emulated/0/Android/data/deckers.thibault.aves/files' + final ext = '/storage/emulated/0'; + dcimPath = join(ext, 'DCIM'); + picturesPath = join(ext, 'Pictures'); + } + + bool isCameraPath(String path) => path != null && path.startsWith(dcimPath) && (path.endsWith('Camera') || path.endsWith('100ANDRO')); + + bool isScreenshotsPath(String path) => path != null && path.startsWith(dcimPath) && path.endsWith('Screenshots'); + + bool isKakaoTalkPath(String path) => path != null && path.startsWith(picturesPath) && path.endsWith('KakaoTalk'); +} diff --git a/lib/widgets/album/all_collection_drawer.dart b/lib/widgets/album/all_collection_drawer.dart index cf9b33991..fb4816f13 100644 --- a/lib/widgets/album/all_collection_drawer.dart +++ b/lib/widgets/album/all_collection_drawer.dart @@ -71,8 +71,8 @@ class AllCollectionDrawer extends StatelessWidget { ...albums.map((album) => _buildFilteredCollectionNavTile( context: context, leading: Icon(Icons.photo_album), - title: album, - filter: (entry) => entry.bucketDisplayName == album, + title: collection.getUniqueAlbumName(album, albums), + filter: (entry) => entry.directory == album, )), Divider(), ...tags.map((tag) => _buildFilteredCollectionNavTile( diff --git a/lib/widgets/album/sections.dart b/lib/widgets/album/sections.dart index ee8a22073..1e18b747c 100644 --- a/lib/widgets/album/sections.dart +++ b/lib/widgets/album/sections.dart @@ -21,7 +21,7 @@ class DaySectionHeader extends StatelessWidget { @override Widget build(BuildContext context) { - return SectionHeader(text: text); + return SectionHeader(title: text); } } @@ -43,36 +43,46 @@ class MonthSectionHeader extends StatelessWidget { @override Widget build(BuildContext context) { - return SectionHeader(text: text); + return SectionHeader(title: text); } } class SectionHeader extends StatelessWidget { - final String text; + final Widget leading; + final String title; - const SectionHeader({Key key, this.text}) : super(key: key); + const SectionHeader({Key key, this.leading, this.title}) : super(key: key); @override Widget build(BuildContext context) { + final text = OutlinedText( + title, + style: TextStyle( + color: Colors.grey[200], + fontSize: 20, + fontFamily: 'Concourse Caps', + shadows: [ + Shadow( + offset: Offset(0, 2), + blurRadius: 3, + color: Colors.grey[900], + ), + ], + ), + outlineColor: Colors.black87, + outlineWidth: 2, + ); return Container( padding: EdgeInsets.all(16), - child: OutlinedText( - text, - style: TextStyle( - color: Colors.grey[200], - fontSize: 20, - fontFamily: 'Concourse Caps', - shadows: [ - Shadow( - offset: Offset(0, 2), - blurRadius: 3, - color: Colors.grey[900], - ), - ], - ), - outlineColor: Colors.black87, - outlineWidth: 2, - ), + child: leading != null + ? Row( + children: [ + leading, + SizedBox(width: 8), + text, + ], + ) + : text, ); } } diff --git a/lib/widgets/album/thumbnail_collection.dart b/lib/widgets/album/thumbnail_collection.dart index 1808fda3b..5fc3692ad 100644 --- a/lib/widgets/album/thumbnail_collection.dart +++ b/lib/widgets/album/thumbnail_collection.dart @@ -1,11 +1,13 @@ import 'package:aves/model/image_collection.dart'; import 'package:aves/model/image_entry.dart'; +import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/album/sections.dart'; import 'package:aves/widgets/album/thumbnail.dart'; import 'package:aves/widgets/common/draggable_scrollbar.dart'; import 'package:aves/widgets/fullscreen/image_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_sticky_header/flutter_sticky_header.dart'; +import 'package:flutter_svg/flutter_svg.dart'; class ThumbnailCollection extends AnimatedWidget { final ImageCollection collection; @@ -96,7 +98,10 @@ class SectionSliver extends StatelessWidget { if (collection.sortFactor == SortFactor.date) { switch (collection.groupFactor) { case GroupFactor.album: - header = SectionHeader(text: sectionKey); + header = SectionHeader( + leading: getAlbumIcon(context, sectionKey), + title: collection.getUniqueAlbumName(sectionKey, sections.keys.toList()), + ); break; case GroupFactor.date: header = MonthSectionHeader(date: sectionKey); @@ -130,6 +135,17 @@ class SectionSliver extends StatelessWidget { ); } + Widget getAlbumIcon(BuildContext context, String albumDirectory) { + if (androidFileUtils.isCameraPath(albumDirectory)) { + return Icon(Icons.photo_camera); + } else if (androidFileUtils.isScreenshotsPath(albumDirectory)) { + return Icon(Icons.smartphone); + } else if (androidFileUtils.isKakaoTalkPath(albumDirectory)) { + return SvgPicture.asset('assets/kakaotalk.svg', width: IconTheme.of(context).size); + } + return null; + } + Future _showFullscreen(BuildContext context, ImageEntry entry) { return Navigator.push( context, diff --git a/lib/widgets/debug_page.dart b/lib/widgets/debug_page.dart index 52b030848..96d96c481 100644 --- a/lib/widgets/debug_page.dart +++ b/lib/widgets/debug_page.dart @@ -2,6 +2,7 @@ import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_metadata.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/settings.dart'; +import 'package:aves/utils/android_file_utils.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -40,6 +41,10 @@ class DebugPageState extends State { body: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('Paths'), + Text('DCIM path: ${androidFileUtils.dcimPath}'), + Text('pictures path: ${androidFileUtils.picturesPath}'), + Divider(), Text('Settings'), Text('collectionGroupFactor: ${settings.collectionGroupFactor}'), Text('collectionSortFactor: ${settings.collectionSortFactor}'),