diff --git a/lib/services/remote_sync_service.dart b/lib/services/remote_sync_service.dart index 7b35e2481..a1a137eac 100644 --- a/lib/services/remote_sync_service.dart +++ b/lib/services/remote_sync_service.dart @@ -477,6 +477,7 @@ class RemoteSyncService { final int toBeUploaded = filesToBeUploaded.length + updatedFileIDs.length; if (toBeUploaded > 0) { Bus.instance.fire(SyncStatusUpdate(SyncStatus.preparingForUpload)); + await _uploader.checkNetworkForUpload(); // verify if files upload is allowed based on their subscription plan and // storage limit. To avoid creating new endpoint, we are using // fetchUploadUrls as alternative method. diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index 4ba82928e..721ca4104 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -21,6 +21,7 @@ class EnteColorScheme { // Fill Colors final Color fillBase; + final Color fillBasePressed; final Color fillMuted; final Color fillFaint; final Color fillFaintPressed; @@ -44,6 +45,7 @@ class EnteColorScheme { final Color warning700; final Color warning500; final Color warning400; + final Color warning800; final Color caution500; //other colors @@ -62,6 +64,7 @@ class EnteColorScheme { this.textFaint, this.blurTextBase, this.fillBase, + this.fillBasePressed, this.fillMuted, this.fillFaint, this.fillFaintPressed, @@ -78,9 +81,10 @@ class EnteColorScheme { this.primary500 = _primary500, this.primary400 = _primary400, this.primary300 = _primary300, + this.warning800 = _warning800, this.warning700 = _warning700, this.warning500 = _warning500, - this.warning400 = _warning700, + this.warning400 = _warning400, this.caution500 = _caution500, }); } @@ -97,6 +101,7 @@ const EnteColorScheme lightScheme = EnteColorScheme( textFaintLight, blurTextBaseLight, fillBaseLight, + fillBasePressedLight, fillMutedLight, fillFaintLight, fillFaintPressedLight, @@ -123,6 +128,7 @@ const EnteColorScheme darkScheme = EnteColorScheme( textFaintDark, blurTextBaseDark, fillBaseDark, + fillBasePressedDark, fillMutedDark, fillFaintDark, fillFaintPressedDark, @@ -168,11 +174,13 @@ const Color blurTextBaseDark = Color.fromRGBO(255, 255, 255, 0.95); // Fill Colors const Color fillBaseLight = Color.fromRGBO(0, 0, 0, 1); +const Color fillBasePressedLight = Color.fromRGBO(0, 0, 0, 0.87); const Color fillMutedLight = Color.fromRGBO(0, 0, 0, 0.12); const Color fillFaintLight = Color.fromRGBO(0, 0, 0, 0.04); const Color fillFaintPressedLight = Color.fromRGBO(0, 0, 0, 0.08); const Color fillBaseDark = Color.fromRGBO(255, 255, 255, 1); +const Color fillBasePressedDark = Color.fromRGBO(255, 255, 255, 0.9); const Color fillMutedDark = Color.fromRGBO(255, 255, 255, 0.16); const Color fillFaintDark = Color.fromRGBO(255, 255, 255, 0.12); const Color fillFaintPressedDark = Color.fromRGBO(255, 255, 255, 0.06); @@ -212,6 +220,7 @@ const Color _warning700 = Color.fromRGBO(234, 63, 63, 1); const Color _warning500 = Color.fromRGBO(255, 101, 101, 1); const Color warning500 = Color.fromRGBO(255, 101, 101, 1); const Color _warning400 = Color.fromRGBO(255, 111, 111, 1); +const Color _warning800 = Color(0xFFF53434); const Color _caution500 = Color.fromRGBO(255, 194, 71, 1); diff --git a/lib/ui/collections/collection_item_widget.dart b/lib/ui/collections/collection_item_widget.dart index bcfc0e32b..03bc6105b 100644 --- a/lib/ui/collections/collection_item_widget.dart +++ b/lib/ui/collections/collection_item_widget.dart @@ -4,18 +4,22 @@ import 'package:photos/models/collection.dart'; import 'package:photos/models/collection_items.dart'; import 'package:photos/models/gallery_type.dart'; import 'package:photos/theme/ente_theme.dart'; +import 'package:photos/ui/viewer/file/file_icons_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/navigation_util.dart'; +import 'package:visibility_detector/visibility_detector.dart'; class CollectionItem extends StatelessWidget { final CollectionWithThumbnail c; final double sideOfThumbnail; + final bool shouldRender; CollectionItem( this.c, this.sideOfThumbnail, { + this.shouldRender = false, Key? key, }) : super(key: Key(c.collection.id.toString())); @@ -39,11 +43,10 @@ class CollectionItem extends StatelessWidget { child: Hero( tag: heroTag, child: c.thumbnail != null - ? ThumbnailWidget( - c.thumbnail, - shouldShowArchiveStatus: c.collection.isArchived(), - showFavForAlbumOnly: true, - key: Key(heroTag), + ? CollectionItemThumbnailWidget( + c: c, + heroTag: heroTag, + shouldRender: shouldRender, ) : const NoThumbnailWidget(), ), @@ -98,3 +101,54 @@ class CollectionItem extends StatelessWidget { ); } } + +class CollectionItemThumbnailWidget extends StatefulWidget { + const CollectionItemThumbnailWidget({ + Key? key, + required this.c, + required this.heroTag, + this.shouldRender = false, + }) : super(key: key); + + final CollectionWithThumbnail c; + final String heroTag; + final bool shouldRender; + + @override + State createState() => + _CollectionItemThumbnailWidgetState(); +} + +class _CollectionItemThumbnailWidgetState + extends State { + bool _shouldRender = false; + + @override + void initState() { + super.initState(); + _shouldRender = widget.shouldRender; + } + + @override + Widget build(BuildContext context) { + return VisibilityDetector( + key: Key("collection_item" + widget.c.thumbnail!.tag), + onVisibilityChanged: (visibility) { + final shouldRender = visibility.visibleFraction > 0; + if (mounted && shouldRender && !_shouldRender) { + setState(() { + _shouldRender = shouldRender; + }); + } + }, + child: _shouldRender + ? ThumbnailWidget( + widget.c.thumbnail, + shouldShowArchiveStatus: widget.c.collection.isArchived(), + showFavForAlbumOnly: true, + key: Key(widget.heroTag), + ) + : const ThumbnailPlaceHolder(), + ); + } +} diff --git a/lib/ui/collections/remote_collections_grid_view_widget.dart b/lib/ui/collections/remote_collections_grid_view_widget.dart index 20889f6f3..80efac15d 100644 --- a/lib/ui/collections/remote_collections_grid_view_widget.dart +++ b/lib/ui/collections/remote_collections_grid_view_widget.dart @@ -14,6 +14,7 @@ class RemoteCollectionsGridViewWidget extends StatelessWidget { static const maxThumbnailWidth = 224.0; static const fixedGapBetweenAlbum = 8.0; static const minGapForHorizontalPadding = 8.0; + static const collectionItemsToPreload = 20; final List? collections; @@ -45,7 +46,11 @@ class RemoteCollectionsGridViewWidget extends StatelessWidget { // to disable GridView's scrolling itemBuilder: (context, index) { if (index < collections!.length) { - return CollectionItem(collections![index], sideOfThumbnail); + return CollectionItem( + collections![index], + sideOfThumbnail, + shouldRender: index < collectionItemsToPreload, + ); } else { return const CreateNewAlbumWidget(); } diff --git a/lib/ui/components/action_sheet_widget.dart b/lib/ui/components/action_sheet_widget.dart index db72ba744..737c80acd 100644 --- a/lib/ui/components/action_sheet_widget.dart +++ b/lib/ui/components/action_sheet_widget.dart @@ -152,7 +152,7 @@ class ContentContainerWidget extends StatelessWidget { ? const SizedBox.shrink() : Text( title!, - style: textTheme.h3Bold + style: textTheme.largeBold .copyWith(color: textBaseDark), //constant color ), title == null || body == null diff --git a/lib/ui/components/button_widget.dart b/lib/ui/components/button_widget.dart index 120f41bbe..8109ad491 100644 --- a/lib/ui/components/button_widget.dart +++ b/lib/ui/components/button_widget.dart @@ -115,7 +115,6 @@ class ButtonWidget extends StatelessWidget { buttonType.defaultBorderColor(colorScheme, buttonSize); buttonStyle.pressedBorderColor = buttonType.pressedBorderColor( colorScheme: colorScheme, - inverseColorScheme: inverseColorScheme, buttonSize: buttonSize, ); buttonStyle.disabledBorderColor = @@ -205,27 +204,20 @@ class _ButtonChildWidgetState extends State { final _debouncer = Debouncer(const Duration(milliseconds: 300)); ExecutionState executionState = ExecutionState.idle; + @override + void initState() { + _setButtonTheme(); + super.initState(); + } + + @override + void didUpdateWidget(covariant ButtonChildWidget oldWidget) { + _setButtonTheme(); + super.didUpdateWidget(oldWidget); + } + @override Widget build(BuildContext context) { - progressStatus = widget.progressStatus; - checkIconColor = widget.buttonStyle.checkIconColor ?? - widget.buttonStyle.defaultIconColor; - loadingIconColor = widget.buttonStyle.defaultIconColor; - if (widget.isDisabled) { - buttonColor = widget.buttonStyle.disabledButtonColor ?? - widget.buttonStyle.defaultButtonColor; - borderColor = widget.buttonStyle.disabledBorderColor ?? - widget.buttonStyle.defaultBorderColor; - iconColor = widget.buttonStyle.disabledIconColor ?? - widget.buttonStyle.defaultIconColor; - labelStyle = widget.buttonStyle.disabledLabelStyle ?? - widget.buttonStyle.defaultLabelStyle; - } else { - buttonColor = widget.buttonStyle.defaultButtonColor; - borderColor = widget.buttonStyle.defaultBorderColor; - iconColor = widget.buttonStyle.defaultIconColor; - labelStyle = widget.buttonStyle.defaultLabelStyle; - } if (executionState == ExecutionState.successful) { Future.delayed(Duration(seconds: widget.isInAlert ? 1 : 2), () { setState(() { @@ -241,7 +233,9 @@ class _ButtonChildWidgetState extends State { child: Container( decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(4)), - border: Border.all(color: borderColor), + border: widget.buttonType == ButtonType.tertiaryCritical + ? Border.all(color: borderColor) + : null, ), child: AnimatedContainer( duration: const Duration(milliseconds: 16), @@ -383,6 +377,28 @@ class _ButtonChildWidgetState extends State { ); } + void _setButtonTheme() { + progressStatus = widget.progressStatus; + checkIconColor = widget.buttonStyle.checkIconColor ?? + widget.buttonStyle.defaultIconColor; + loadingIconColor = widget.buttonStyle.defaultIconColor; + if (widget.isDisabled) { + buttonColor = widget.buttonStyle.disabledButtonColor ?? + widget.buttonStyle.defaultButtonColor; + borderColor = widget.buttonStyle.disabledBorderColor ?? + widget.buttonStyle.defaultBorderColor; + iconColor = widget.buttonStyle.disabledIconColor ?? + widget.buttonStyle.defaultIconColor; + labelStyle = widget.buttonStyle.disabledLabelStyle ?? + widget.buttonStyle.defaultLabelStyle; + } else { + buttonColor = widget.buttonStyle.defaultButtonColor; + borderColor = widget.buttonStyle.defaultBorderColor; + iconColor = widget.buttonStyle.defaultIconColor; + labelStyle = widget.buttonStyle.defaultLabelStyle; + } + } + bool get _shouldRegisterGestures => !widget.isDisabled && executionState == ExecutionState.idle; diff --git a/lib/ui/components/dialog_widget.dart b/lib/ui/components/dialog_widget.dart index d2a1b0aa8..0999c3664 100644 --- a/lib/ui/components/dialog_widget.dart +++ b/lib/ui/components/dialog_widget.dart @@ -117,7 +117,7 @@ class ContentContainer extends StatelessWidget { ], ), icon == null ? const SizedBox.shrink() : const SizedBox(height: 19), - Text(title, style: textTheme.h3Bold), + Text(title, style: textTheme.largeBold), body != null ? const SizedBox(height: 19) : const SizedBox.shrink(), body != null ? Text( diff --git a/lib/ui/components/models/button_type.dart b/lib/ui/components/models/button_type.dart index 66f80d56e..f97bc9544 100644 --- a/lib/ui/components/models/button_type.dart +++ b/lib/ui/components/models/button_type.dart @@ -55,6 +55,15 @@ enum ButtonType { if (isPrimary) { return colorScheme.primary700; } + if (isSecondary) { + return colorScheme.fillFaintPressed; + } + if (isNeutral) { + return colorScheme.fillBasePressed; + } + if (this == ButtonType.critical) { + return colorScheme.warning800; + } return null; } @@ -85,20 +94,10 @@ enum ButtonType { //Returning null to fallback to default color Color? pressedBorderColor({ required EnteColorScheme colorScheme, - required EnteColorScheme inverseColorScheme, required ButtonSize buttonSize, }) { - if (isPrimary) { - return colorScheme.strokeMuted; - } - if (buttonSize == ButtonSize.small && this == ButtonType.tertiaryCritical) { - return null; - } - if (isSecondary || isCritical) { - return colorScheme.strokeBase; - } - if (isNeutral) { - return inverseColorScheme.strokeBase; + if (this == ButtonType.tertiaryCritical && buttonSize == ButtonSize.large) { + return colorScheme.warning700; } return null; } @@ -133,8 +132,11 @@ enum ButtonType { //Returning null to fallback to default color Color? pressedIconColor(EnteColorScheme colorScheme, ButtonSize buttonSize) { - if (this == ButtonType.tertiaryCritical && buttonSize == ButtonSize.large) { - return colorScheme.strokeBase; + if (this == ButtonType.tertiaryCritical) { + return colorScheme.warning700; + } + if (this == ButtonType.tertiary && buttonSize == ButtonSize.small) { + return colorScheme.fillBasePressed; } return null; } @@ -176,8 +178,11 @@ enum ButtonType { EnteColorScheme colorScheme, ButtonSize buttonSize, ) { - if (this == ButtonType.tertiaryCritical && buttonSize == ButtonSize.large) { - return textTheme.bodyBold.copyWith(color: colorScheme.strokeBase); + if (this == ButtonType.tertiaryCritical) { + return textTheme.bodyBold.copyWith(color: colorScheme.warning700); + } + if (this == ButtonType.tertiary && buttonSize == ButtonSize.small) { + return textTheme.bodyBold.copyWith(color: colorScheme.fillBasePressed); } return null; } diff --git a/lib/ui/home/status_bar_widget.dart b/lib/ui/home/status_bar_widget.dart index 4d212c886..5115a5deb 100644 --- a/lib/ui/home/status_bar_widget.dart +++ b/lib/ui/home/status_bar_widget.dart @@ -240,34 +240,6 @@ class RefreshIndicatorWidget extends StatelessWidget { } } -class BrandingWidget extends StatelessWidget { - const BrandingWidget({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Container( - height: kContainerHeight, - padding: const EdgeInsets.only(left: 12, top: 4), - child: const Align( - alignment: Alignment.centerLeft, - child: Text( - "ente", - style: TextStyle( - fontWeight: FontWeight.bold, - fontFamily: 'Montserrat', - fontSize: 24, - height: 1, - ), - ), - ), - ), - ], - ); - } -} - class SyncStatusCompletedWidget extends StatelessWidget { const SyncStatusCompletedWidget({Key? key}) : super(key: key); diff --git a/lib/ui/huge_listview/lazy_loading_gallery.dart b/lib/ui/huge_listview/lazy_loading_gallery.dart index 73ed59f05..12d09b2e9 100644 --- a/lib/ui/huge_listview/lazy_loading_gallery.dart +++ b/lib/ui/huge_listview/lazy_loading_gallery.dart @@ -352,7 +352,7 @@ class _LazyLoadingGridViewState extends State { Widget _getRecyclableView() { return VisibilityDetector( - key: UniqueKey(), + key: Key("gallery" + widget.filesInDay.first.tag), onVisibilityChanged: (visibility) { final shouldRender = visibility.visibleFraction > 0; if (mounted && shouldRender != _shouldRender) { @@ -370,7 +370,7 @@ class _LazyLoadingGridViewState extends State { Widget _getNonRecyclableView() { if (!_shouldRender!) { return VisibilityDetector( - key: UniqueKey(), + key: Key("gallery" + widget.filesInDay.first.tag), onVisibilityChanged: (visibility) { if (mounted && visibility.visibleFraction > 0 && !_shouldRender!) { setState(() { diff --git a/lib/ui/viewer/file/file_icons_widget.dart b/lib/ui/viewer/file/file_icons_widget.dart index ae9a19265..11ec540c5 100644 --- a/lib/ui/viewer/file/file_icons_widget.dart +++ b/lib/ui/viewer/file/file_icons_widget.dart @@ -23,7 +23,10 @@ class UnSyncedIcon extends StatelessWidget { @override Widget build(BuildContext context) { - return const _BottomLeftOverlayIcon(Icons.cloud_off_outlined); + return const _BottomLeftOverlayIcon( + Icons.cloud_off_outlined, + baseSize: 18, + ); } } @@ -186,7 +189,7 @@ class _BottomLeftOverlayIcon extends StatelessWidget { if (constraints.hasBoundedWidth) { final w = constraints.maxWidth; - if (w > 120) { + if (w > 125) { size = 24; } else if (w < 75) { inset = 3; diff --git a/lib/ui/viewer/file/zoomable_image.dart b/lib/ui/viewer/file/zoomable_image.dart index 6e53dabe5..79e17c471 100644 --- a/lib/ui/viewer/file/zoomable_image.dart +++ b/lib/ui/viewer/file/zoomable_image.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; @@ -13,6 +14,7 @@ import 'package:photos/events/local_photos_updated_event.dart'; import 'package:photos/models/file.dart'; import 'package:photos/ui/common/loading_widget.dart'; import 'package:photos/utils/file_util.dart'; +import 'package:photos/utils/image_util.dart'; import 'package:photos/utils/thumbnail_util.dart'; class ZoomableImage extends StatefulWidget { @@ -35,7 +37,7 @@ class ZoomableImage extends StatefulWidget { class _ZoomableImageState extends State with SingleTickerProviderStateMixin { - final Logger _logger = Logger("ZoomableImage"); + late Logger _logger; late File _photo; ImageProvider? _imageProvider; bool _loadedSmallThumbnail = false; @@ -45,10 +47,13 @@ class _ZoomableImageState extends State bool _loadedFinalImage = false; ValueChanged? _scaleStateChangedCallback; bool _isZooming = false; + PhotoViewController _photoViewController = PhotoViewController(); + int? _thumbnailWidth; @override void initState() { _photo = widget.photo; + _logger = Logger("ZoomableImage_" + _photo.displayName); debugPrint('initState for ${_photo.toString()}'); _scaleStateChangedCallback = (value) { if (widget.shouldDisableScroll != null) { @@ -61,6 +66,12 @@ class _ZoomableImageState extends State super.initState(); } + @override + void dispose() { + _photoViewController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { if (_photo.isRemoteFile) { @@ -75,6 +86,7 @@ class _ZoomableImageState extends State axis: Axis.vertical, child: PhotoView( imageProvider: _imageProvider, + controller: _photoViewController, scaleStateChangedCallback: _scaleStateChangedCallback, minScale: PhotoViewComputedScale.contained, gaplessPlayback: true, @@ -106,6 +118,7 @@ class _ZoomableImageState extends State if (cachedThumbnail != null) { _imageProvider = Image.memory(cachedThumbnail).image; _loadedSmallThumbnail = true; + _captureThumbnailDimensions(_imageProvider!); } else { getThumbnailFromServer(_photo).then((file) { final imageProvider = Image.memory(file).image; @@ -115,6 +128,7 @@ class _ZoomableImageState extends State setState(() { _imageProvider = imageProvider; _loadedSmallThumbnail = true; + _captureThumbnailDimensions(_imageProvider!); }); } }).catchError((e) { @@ -125,7 +139,8 @@ class _ZoomableImageState extends State }); } } - if (!_loadedFinalImage) { + if (!_loadedFinalImage && !_loadingFinalImage) { + _loadingFinalImage = true; getFileFromServer(_photo).then((file) { _onFinalImageLoaded( Image.file( @@ -209,16 +224,42 @@ class _ZoomableImageState extends State void _onFinalImageLoaded(ImageProvider imageProvider) { if (mounted) { - precacheImage(imageProvider, context).then((value) { + precacheImage(imageProvider, context).then((value) async { if (mounted) { + await _updatePhotoViewController(imageProvider); setState(() { _imageProvider = imageProvider; _loadedFinalImage = true; + _logger.info("Final image loaded"); }); } }); } } + Future _captureThumbnailDimensions(ImageProvider imageProvider) async { + final imageInfo = await getImageInfo(imageProvider); + _thumbnailWidth = imageInfo.image.width; + } + + Future _updatePhotoViewController(ImageProvider imageProvider) async { + if (_thumbnailWidth == null || _photoViewController.scale == null) { + return; + } + final imageInfo = await getImageInfo(imageProvider); + final scale = _photoViewController.scale! / + (imageInfo.image.width / _thumbnailWidth!); + final currentPosition = _photoViewController.value.position; + final positionScaleFactor = 1 / scale; + final newPosition = currentPosition.scale( + positionScaleFactor, + positionScaleFactor, + ); + _photoViewController = PhotoViewController( + initialPosition: newPosition, + initialScale: scale, + ); + } + bool _isGIF() => _photo.displayName.toLowerCase().endsWith(".gif"); } diff --git a/lib/utils/file_uploader.dart b/lib/utils/file_uploader.dart index feb8c78ab..525bd83cc 100644 --- a/lib/utils/file_uploader.dart +++ b/lib/utils/file_uploader.dart @@ -271,18 +271,27 @@ class FileUploader { } } + Future checkNetworkForUpload({bool isForceUpload = false}) async { + // Note: We don't support force uploading currently. During force upload, + // network check is skipped completely + if (isForceUpload) { + return; + } + final connectivityResult = await (Connectivity().checkConnectivity()); + final canUploadUnderCurrentNetworkConditions = + (connectivityResult == ConnectivityResult.wifi || + Configuration.instance.shouldBackupOverMobileData()); + if (!canUploadUnderCurrentNetworkConditions) { + throw WiFiUnavailableError(); + } + } + Future _tryToUpload( File file, int collectionID, bool forcedUpload, ) async { - final connectivityResult = await (Connectivity().checkConnectivity()); - final canUploadUnderCurrentNetworkConditions = - (connectivityResult == ConnectivityResult.wifi || - Configuration.instance.shouldBackupOverMobileData()); - if (!canUploadUnderCurrentNetworkConditions && !forcedUpload) { - throw WiFiUnavailableError(); - } + await checkNetworkForUpload(isForceUpload: forcedUpload); final fileOnDisk = await FilesDB.instance.getFile(file.generatedID!); final wasAlreadyUploaded = fileOnDisk != null && fileOnDisk.uploadedFileID != null && diff --git a/lib/utils/image_util.dart b/lib/utils/image_util.dart new file mode 100644 index 000000000..a5bcb03a7 --- /dev/null +++ b/lib/utils/image_util.dart @@ -0,0 +1,16 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; + +Future getImageInfo(ImageProvider imageProvider) { + final completer = Completer(); + final imageStream = imageProvider.resolve(const ImageConfiguration()); + final listener = ImageStreamListener( + ((imageInfo, _) { + completer.complete(imageInfo); + }), + ); + imageStream.addListener(listener); + completer.future.whenComplete(() => imageStream.removeListener(listener)); + return completer.future; +}