Merge pull request #583 from ente-io/empty_albums

Empty albums
This commit is contained in:
Neeraj Gupta 2022-11-15 10:54:56 +05:30 committed by GitHub
commit 34de3f5fc3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 165 additions and 122 deletions

View file

@ -41,7 +41,7 @@ analyzer:
curly_braces_in_flow_control_structures: error
directives_ordering: error
require_trailing_commas: warning
always_use_package_imports: error
always_use_package_imports: warning
prefer_final_fields: error
unused_import: error
camel_case_types: error

View file

@ -43,3 +43,5 @@ class FFDefault {
}
const kDefaultProductionEndpoint = 'https://api.ente.io';
const int intMaxValue = 9223372036854775807;

View file

@ -18,8 +18,8 @@ class AlbumSearchResult extends SearchResult {
}
@override
File previewThumbnail() {
return collectionWithThumbnail.thumbnail!;
File? previewThumbnail() {
return collectionWithThumbnail.thumbnail;
}
@override

View file

@ -19,7 +19,7 @@ class GenericSearchResult extends SearchResult {
}
@override
File previewThumbnail() {
File? previewThumbnail() {
return _files.first;
}

View file

@ -5,7 +5,7 @@ abstract class SearchResult {
String name();
File previewThumbnail();
File? previewThumbnail();
String heroTag() {
return '${type().toString()}_${name()}';

View file

@ -25,6 +25,7 @@ import 'package:photos/extensions/stop_watch.dart';
import 'package:photos/models/api/collection/create_request.dart';
import 'package:photos/models/collection.dart';
import 'package:photos/models/collection_file_item.dart';
import 'package:photos/models/collection_items.dart';
import 'package:photos/models/file.dart';
import 'package:photos/models/magic_metadata.dart';
import 'package:photos/services/app_lifecycle_service.dart';
@ -216,6 +217,29 @@ class CollectionsService {
.toList();
}
Future<List<CollectionWithThumbnail>> getCollectionsWithThumbnails({
bool includedOwnedByOthers = false,
}) async {
final List<CollectionWithThumbnail> collectionsWithThumbnail = [];
final usersCollection = getActiveCollections();
// remove any hidden collection to avoid accidental rendering on UI
usersCollection.removeWhere((element) => element.isHidden());
if (!includedOwnedByOthers) {
final userID = Configuration.instance.getUserID();
usersCollection.removeWhere((c) => c.owner.id != userID);
}
final latestCollectionFiles = await getLatestCollectionFiles();
final Map<int, File> collectionToThumbnailMap = Map.fromEntries(
latestCollectionFiles.map((e) => MapEntry(e.collectionID, e)),
);
for (final c in usersCollection) {
final File thumbnail = collectionToThumbnailMap[c.id];
collectionsWithThumbnail.add(CollectionWithThumbnail(c, thumbnail));
}
return collectionsWithThumbnail;
}
Future<List<User>> getSharees(int collectionID) {
return _enteDio.get(
"/collections/sharees",

View file

@ -8,7 +8,6 @@ import 'package:photos/data/months.dart';
import 'package:photos/data/years.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/events/local_photos_updated_event.dart';
import 'package:photos/models/collection.dart';
import 'package:photos/models/collection_items.dart';
import 'package:photos/models/file.dart';
import 'package:photos/models/file_type.dart';
@ -118,23 +117,23 @@ class SearchService {
Future<List<AlbumSearchResult>> getCollectionSearchResults(
String query,
) async {
/*latestCollectionFiles is to identify collections which have at least one file as we don't display
empty collections and to get the file to pass for tumbnail */
final List<File> latestCollectionFiles =
await _collectionService.getLatestCollectionFiles();
final List<CollectionWithThumbnail> collectionWithThumbnails =
await _collectionService.getCollectionsWithThumbnails(
includedOwnedByOthers: true,
);
final List<AlbumSearchResult> collectionSearchResults = [];
for (var file in latestCollectionFiles) {
for (var c in collectionWithThumbnails) {
if (collectionSearchResults.length >= _maximumResultsLimit) {
break;
}
final Collection collection =
CollectionsService.instance.getCollectionByID(file.collectionID);
if (!collection.isHidden() &&
collection.name.toLowerCase().contains(query.toLowerCase())) {
collectionSearchResults
.add(AlbumSearchResult(CollectionWithThumbnail(collection, file)));
if (!c.collection.isHidden() &&
c.collection.name.toLowerCase().contains(
query.toLowerCase(),
)) {
collectionSearchResults.add(AlbumSearchResult(c));
}
}

View file

@ -1,34 +1,28 @@
// @dart=2.9
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/models/collection_items.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/viewer/file/no_thumbnail_widget.dart';
import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
import 'package:photos/ui/viewer/gallery/collection_page.dart';
import 'package:photos/utils/navigation_util.dart';
class CollectionItem extends StatelessWidget {
CollectionItem(
this.c, {
Key key,
}) : super(key: Key(c.collection.id.toString()));
final CollectionWithThumbnail c;
final double sideOfThumbnail;
CollectionItem(
this.c,
this.sideOfThumbnail, {
Key? key,
}) : super(key: Key(c.collection.id.toString()));
@override
Widget build(BuildContext context) {
const double horizontalPaddingOfGridRow = 16;
const double crossAxisSpacingOfGrid = 9;
final Size size = MediaQuery.of(context).size;
final int albumsCountInOneRow = max(size.width ~/ 220.0, 2);
final double totalWhiteSpaceOfRow = (horizontalPaddingOfGridRow * 2) +
(albumsCountInOneRow - 1) * crossAxisSpacingOfGrid;
final TextStyle albumTitleTextStyle =
Theme.of(context).textTheme.subtitle1.copyWith(fontSize: 14);
final double sideOfThumbnail = (size.width / albumsCountInOneRow) -
(totalWhiteSpaceOfRow / albumsCountInOneRow);
final enteColorScheme = getEnteColorScheme(context);
final enteTextTheme = getEnteTextTheme(context);
final String heroTag =
"collection" + (c.thumbnail?.tag ?? c.collection.id.toString());
return GestureDetector(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -39,45 +33,42 @@ class CollectionItem extends StatelessWidget {
height: sideOfThumbnail,
width: sideOfThumbnail,
child: Hero(
tag: "collection" + c.thumbnail.tag,
child: ThumbnailWidget(
c.thumbnail,
shouldShowArchiveStatus: c.collection.isArchived(),
key: Key(
"collection" + c.thumbnail.tag,
),
),
tag: heroTag,
child: c.thumbnail != null
? ThumbnailWidget(
c.thumbnail,
shouldShowArchiveStatus: c.collection.isArchived(),
key: Key(heroTag),
)
: const NoThumbnailWidget(),
),
),
),
const SizedBox(height: 4),
Row(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
constraints: BoxConstraints(maxWidth: sideOfThumbnail - 40),
child: Text(
c.collection.name,
style: albumTitleTextStyle,
overflow: TextOverflow.ellipsis,
),
Text(
c.collection.name ?? "Unnamed",
style: enteTextTheme.small,
overflow: TextOverflow.ellipsis,
),
FutureBuilder<int>(
future: FilesDB.instance.collectionFileCount(c.collection.id),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data > 0) {
return RichText(
text: TextSpan(
style: albumTitleTextStyle.copyWith(
color: albumTitleTextStyle.color.withOpacity(0.5),
),
children: [
const TextSpan(text: " \u2022 "),
TextSpan(text: snapshot.data.toString()),
],
if (snapshot.hasData) {
return Text(
snapshot.data.toString(),
style: enteTextTheme.small.copyWith(
color: enteColorScheme.textMuted,
),
);
} else {
return const SizedBox.shrink();
return Text(
"",
style: enteTextTheme.small.copyWith(
color: enteColorScheme.textMuted,
),
);
}
},
),

View file

@ -48,6 +48,7 @@ class DeviceFolderIcon extends StatelessWidget {
height: 140,
width: 120,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
ClipRRect(
borderRadius: BorderRadius.circular(4),
@ -79,6 +80,7 @@ class DeviceFolderIcon extends StatelessWidget {
padding: const EdgeInsets.only(top: 10),
child: Text(
deviceCollection.name,
textAlign: TextAlign.left,
style: Theme.of(context)
.textTheme
.subtitle1

View file

@ -8,6 +8,15 @@ import 'package:photos/ui/collections/collection_item_widget.dart';
import 'package:photos/ui/collections/create_new_album_widget.dart';
class RemoteCollectionsGridViewWidget extends StatelessWidget {
/*
Aspect ratio 1:1 Max width 224 Fixed gap 8
Width changes dynamically with screen width such that we can fit 2 in one row.
Keep the width integral (center the albums to distribute excess pixels)
*/
static const maxThumbnailWidth = 224.0;
static const fixedGapBetweenAlbum = 8.0;
static const minGapForHorizontalPadding = 8.0;
final List<CollectionWithThumbnail> collections;
const RemoteCollectionsGridViewWidget(
@ -17,13 +26,18 @@ class RemoteCollectionsGridViewWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
const double horizontalPaddingOfGridRow = 16;
const double crossAxisSpacingOfGrid = 9;
final Size size = MediaQuery.of(context).size;
final int albumsCountInOneRow = max(size.width ~/ 220.0, 2);
final double sideOfThumbnail = (size.width / albumsCountInOneRow) -
horizontalPaddingOfGridRow -
((crossAxisSpacingOfGrid / 2) * (albumsCountInOneRow - 1));
final double screenWidth = MediaQuery.of(context).size.width;
final int albumsCountInOneRow = max(screenWidth ~/ maxThumbnailWidth, 2);
final double gapBetweenAlbums =
(albumsCountInOneRow - 1) * fixedGapBetweenAlbum;
// gapOnSizeOfAlbums will be
final double gapOnSizeOfAlbums = minGapForHorizontalPadding +
(screenWidth - gapBetweenAlbums - (2 * minGapForHorizontalPadding)) %
albumsCountInOneRow;
final double sideOfThumbnail =
(screenWidth - gapOnSizeOfAlbums - gapBetweenAlbums) /
albumsCountInOneRow;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
@ -33,7 +47,7 @@ class RemoteCollectionsGridViewWidget extends StatelessWidget {
// to disable GridView's scrolling
itemBuilder: (context, index) {
if (index < collections.length) {
return CollectionItem(collections[index]);
return CollectionItem(collections[index], sideOfThumbnail);
} else {
return const CreateNewAlbumWidget();
}
@ -42,9 +56,9 @@ class RemoteCollectionsGridViewWidget extends StatelessWidget {
// To include the + button
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: albumsCountInOneRow,
mainAxisSpacing: 12,
crossAxisSpacing: crossAxisSpacingOfGrid,
childAspectRatio: sideOfThumbnail / (sideOfThumbnail + 24),
mainAxisSpacing: 4,
crossAxisSpacing: gapBetweenAlbums,
childAspectRatio: sideOfThumbnail / (sideOfThumbnail + 50),
), //24 is height of album title
),
);

View file

@ -6,6 +6,7 @@ import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/core/constants.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/events/collection_updated_event.dart';
import 'package:photos/events/local_photos_updated_event.dart';
@ -79,17 +80,8 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
}
Future<List<CollectionWithThumbnail>> _getCollections() async {
final collectionsService = CollectionsService.instance;
final userID = Configuration.instance.getUserID();
final List<CollectionWithThumbnail> collectionsWithThumbnail = [];
final latestCollectionFiles =
await collectionsService.getLatestCollectionFiles();
for (final file in latestCollectionFiles) {
final c = collectionsService.getCollectionByID(file.collectionID);
if (c.owner.id == userID && !c.isHidden()) {
collectionsWithThumbnail.add(CollectionWithThumbnail(c, file));
}
}
final List<CollectionWithThumbnail> collectionsWithThumbnail =
await CollectionsService.instance.getCollectionsWithThumbnails();
collectionsWithThumbnail.sort(
(first, second) {
if (second.collection.type == CollectionType.favorites &&
@ -105,8 +97,8 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
second.collection.name,
);
} else if (sortKey == AlbumSortKey.newestPhoto) {
return second.thumbnail.creationTime
.compareTo(first.thumbnail.creationTime);
return (second.thumbnail?.creationTime ?? -1 * intMaxValue)
.compareTo(first.thumbnail?.creationTime ?? -1 * intMaxValue);
} else {
return second.collection.updationTime
.compareTo(first.collection.updationTime);

View file

@ -3,7 +3,6 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/models/collection.dart';
@ -15,6 +14,7 @@ import 'package:photos/services/ignored_files_service.dart';
import 'package:photos/services/remote_sync_service.dart';
import 'package:photos/ui/common/gradient_button.dart';
import 'package:photos/ui/common/loading_widget.dart';
import 'package:photos/ui/viewer/file/no_thumbnail_widget.dart';
import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
import 'package:photos/ui/viewer/gallery/collection_page.dart';
import 'package:photos/utils/dialog_util.dart';
@ -159,8 +159,10 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
child: SizedBox(
height: 64,
width: 64,
key: Key("collection_item:" + item.thumbnail.tag),
child: ThumbnailWidget(item.thumbnail),
key: Key("collection_item:" + (item.thumbnail?.tag ?? "")),
child: item.thumbnail != null
? ThumbnailWidget(item.thumbnail)
: const NoThumbnailWidget(),
),
),
const Padding(padding: EdgeInsets.all(8)),
@ -190,16 +192,8 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
}
Future<List<CollectionWithThumbnail>> _getCollectionsWithThumbnail() async {
final List<CollectionWithThumbnail> collectionsWithThumbnail = [];
final latestCollectionFiles =
await CollectionsService.instance.getLatestCollectionFiles();
for (final file in latestCollectionFiles) {
final c =
CollectionsService.instance.getCollectionByID(file.collectionID);
if (c.owner.id == Configuration.instance.getUserID() && !c.isHidden()) {
collectionsWithThumbnail.add(CollectionWithThumbnail(c, file));
}
}
final List<CollectionWithThumbnail> collectionsWithThumbnail =
await CollectionsService.instance.getCollectionsWithThumbnails();
collectionsWithThumbnail.sort((first, second) {
return compareAsciiLowerCaseNatural(
first.collection.name ?? "",

View file

@ -1,12 +1,10 @@
// @dart=2.9
import 'package:flutter/material.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/models/trash_file.dart';
import 'package:photos/utils/date_time_util.dart';
class ThumbnailPlaceHolder extends StatelessWidget {
const ThumbnailPlaceHolder({Key key}) : super(key: key);
const ThumbnailPlaceHolder({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -18,7 +16,7 @@ class ThumbnailPlaceHolder extends StatelessWidget {
}
class UnSyncedIcon extends StatelessWidget {
const UnSyncedIcon({Key key}) : super(key: key);
const UnSyncedIcon({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -50,7 +48,7 @@ class UnSyncedIcon extends StatelessWidget {
}
class VideoOverlayIcon extends StatelessWidget {
const VideoOverlayIcon({Key key}) : super(key: key);
const VideoOverlayIcon({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -66,7 +64,7 @@ class VideoOverlayIcon extends StatelessWidget {
}
class LivePhotoOverlayIcon extends StatelessWidget {
const LivePhotoOverlayIcon({Key key}) : super(key: key);
const LivePhotoOverlayIcon({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -87,7 +85,7 @@ class LivePhotoOverlayIcon extends StatelessWidget {
class TrashedFileOverlayText extends StatelessWidget {
final TrashFile file;
const TrashedFileOverlayText(this.file, {Key key}) : super(key: key);
const TrashedFileOverlayText(this.file, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -105,7 +103,7 @@ class TrashedFileOverlayText extends StatelessWidget {
daysLeft(file.deleteBy),
style: Theme.of(context)
.textTheme
.subtitle2
.subtitle2!
.copyWith(color: Colors.white), //same for both themes
),
);
@ -113,7 +111,7 @@ class TrashedFileOverlayText extends StatelessWidget {
}
class ArchiveOverlayIcon extends StatelessWidget {
const ArchiveOverlayIcon({Key key}) : super(key: key);
const ArchiveOverlayIcon({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {

View file

@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
import 'package:photos/theme/ente_theme.dart';
class NoThumbnailWidget extends StatelessWidget {
const NoThumbnailWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final enteColorScheme = getEnteColorScheme(context);
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(1),
border: Border.all(
color: enteColorScheme.strokeFaint,
width: 1,
),
color: enteColorScheme.fillFaint,
),
child: Center(
child: Icon(
Icons.photo_outlined,
color: enteColorScheme.strokeMuted,
size: 24,
),
),
);
}
}

View file

@ -1,12 +1,10 @@
// @dart=2.9
import 'package:flutter/material.dart';
import 'package:photos/ente_theme_data.dart';
class EmptyState extends StatelessWidget {
final String text;
const EmptyState({Key key, this.text = "Nothing to see here! 👀"})
const EmptyState({Key? key, this.text = "Nothing to see here! 👀"})
: super(key: key);
@override

View file

@ -51,7 +51,7 @@ class SearchResultPage extends StatelessWidget {
},
tagPrefix: searchResult.heroTag(),
selectedFiles: _selectedFiles,
initialFiles: [searchResult.previewThumbnail()],
initialFiles: [],
);
return Scaffold(
appBar: PreferredSize(

View file

@ -1,31 +1,32 @@
// @dart=2.9
import 'package:flutter/widgets.dart';
import 'package:photos/models/file.dart';
import 'package:photos/ui/viewer/file/no_thumbnail_widget.dart';
import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
class SearchThumbnailWidget extends StatelessWidget {
final File file;
final File? file;
final String tagPrefix;
const SearchThumbnailWidget(
this.file,
this.tagPrefix, {
Key key,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Hero(
tag: tagPrefix + file.tag,
tag: tagPrefix + (file?.tag ?? ""),
child: SizedBox(
height: 58,
width: 58,
child: ClipRRect(
borderRadius: BorderRadius.circular(3),
child: ThumbnailWidget(
file,
),
child: file != null
? ThumbnailWidget(
file,
)
: const NoThumbnailWidget(),
),
),
);