[mobile][photos] Home Widget fixes (#992)
## Description - Upscale default placeholder - Keep image ready to be rendered in home widget before adding the home widget. - Change the photo in home widget every time it's clicked. - Open favourites when home widget is clicked. - Fix multiple issues ## Tests Did a good amount of testing. --------- Co-authored-by: ashilkn <ashilkn99@gmail.com> Co-authored-by: Ashil <77285023+ashilkn@users.noreply.github.com>
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 75 KiB |
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 114 KiB |
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 216 KiB |
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 343 KiB |
|
@ -7,17 +7,24 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:home_widget/home_widget.dart' as hw;
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:media_extension/media_extension_action_types.dart';
|
||||
import 'package:photos/ente_theme_data.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/l10n/l10n.dart";
|
||||
import "package:photos/models/collection/collection_items.dart";
|
||||
import 'package:photos/services/app_lifecycle_service.dart';
|
||||
import "package:photos/services/collections_service.dart";
|
||||
import "package:photos/services/favorites_service.dart";
|
||||
import "package:photos/services/home_widget_service.dart";
|
||||
import "package:photos/services/machine_learning/machine_learning_controller.dart";
|
||||
import 'package:photos/services/sync_service.dart';
|
||||
import 'package:photos/ui/tabs/home_widget.dart';
|
||||
import "package:photos/ui/viewer/actions/file_viewer.dart";
|
||||
import "package:photos/ui/viewer/gallery/collection_page.dart";
|
||||
import "package:photos/utils/intent_util.dart";
|
||||
import "package:photos/utils/navigation_util.dart";
|
||||
|
||||
class EnteApp extends StatefulWidget {
|
||||
final Future<void> Function(String) runBackgroundTask;
|
||||
|
@ -55,6 +62,46 @@ class _EnteAppState extends State<EnteApp> with WidgetsBindingObserver {
|
|||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_checkForWidgetLaunch();
|
||||
hw.HomeWidget.widgetClicked.listen(_launchedFromWidget);
|
||||
}
|
||||
|
||||
void _checkForWidgetLaunch() {
|
||||
hw.HomeWidget.initiallyLaunchedFromHomeWidget().then(_launchedFromWidget);
|
||||
}
|
||||
|
||||
Future<void> _launchedFromWidget(Uri? uri) async {
|
||||
if (uri == null) return;
|
||||
final collectionID =
|
||||
await FavoritesService.instance.getFavoriteCollectionID();
|
||||
if (collectionID == null) {
|
||||
return;
|
||||
}
|
||||
final collection = CollectionsService.instance.getCollectionByID(
|
||||
collectionID,
|
||||
);
|
||||
if (collection == null) {
|
||||
return;
|
||||
}
|
||||
unawaited(HomeWidgetService.instance.initHomeWidget());
|
||||
|
||||
final thumbnail = await CollectionsService.instance.getCover(collection);
|
||||
unawaited(
|
||||
routeToPage(
|
||||
context,
|
||||
CollectionPage(
|
||||
CollectionWithThumbnail(
|
||||
collection,
|
||||
thumbnail,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
setLocale(Locale newLocale) {
|
||||
setState(() {
|
||||
locale = newLocale;
|
||||
|
|
|
@ -24,6 +24,7 @@ import 'package:photos/models/private_key_attributes.dart';
|
|||
import 'package:photos/services/billing_service.dart';
|
||||
import 'package:photos/services/collections_service.dart';
|
||||
import 'package:photos/services/favorites_service.dart';
|
||||
import "package:photos/services/home_widget_service.dart";
|
||||
import 'package:photos/services/ignored_files_service.dart';
|
||||
import 'package:photos/services/machine_learning/semantic_search/semantic_search_service.dart';
|
||||
import 'package:photos/services/memories_service.dart';
|
||||
|
@ -31,7 +32,6 @@ import 'package:photos/services/search_service.dart';
|
|||
import 'package:photos/services/sync_service.dart';
|
||||
import 'package:photos/utils/crypto_util.dart';
|
||||
import 'package:photos/utils/file_uploader.dart';
|
||||
import "package:photos/utils/home_widget_util.dart";
|
||||
import 'package:photos/utils/validator_util.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import "package:tuple/tuple.dart";
|
||||
|
@ -175,7 +175,7 @@ class Configuration {
|
|||
MemoriesService.instance.clearCache();
|
||||
BillingService.instance.clearCache();
|
||||
SearchService.instance.clearCache();
|
||||
unawaited(clearHomeWidget());
|
||||
unawaited(HomeWidgetService.instance.clearHomeWidget());
|
||||
Bus.instance.fire(UserLoggedOutEvent());
|
||||
} else {
|
||||
await _preferences.setBool("auto_logout", true);
|
||||
|
|
|
@ -27,6 +27,7 @@ import 'package:photos/services/collections_service.dart';
|
|||
import "package:photos/services/entity_service.dart";
|
||||
import 'package:photos/services/favorites_service.dart';
|
||||
import 'package:photos/services/feature_flag_service.dart';
|
||||
import 'package:photos/services/home_widget_service.dart';
|
||||
import 'package:photos/services/local_file_update_service.dart';
|
||||
import 'package:photos/services/local_sync_service.dart';
|
||||
import "package:photos/services/location_service.dart";
|
||||
|
@ -46,7 +47,6 @@ import 'package:photos/ui/tools/app_lock.dart';
|
|||
import 'package:photos/ui/tools/lock_screen.dart';
|
||||
import 'package:photos/utils/crypto_util.dart';
|
||||
import 'package:photos/utils/file_uploader.dart';
|
||||
import "package:photos/utils/home_widget_util.dart";
|
||||
import 'package:photos/utils/local_settings.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
|
@ -110,8 +110,8 @@ ThemeMode _themeMode(AdaptiveThemeMode? savedThemeMode) {
|
|||
Future<void> _homeWidgetSync() async {
|
||||
if (!Platform.isAndroid) return;
|
||||
try {
|
||||
if (await countHomeWidgets() != 0) {
|
||||
await initHomeWidget();
|
||||
if (await HomeWidgetService.instance.countHomeWidgets() != 0) {
|
||||
await HomeWidgetService.instance.initHomeWidget();
|
||||
}
|
||||
} catch (e, s) {
|
||||
_logger.severe("Error in initSlideshowWidget", e, s);
|
||||
|
@ -210,6 +210,12 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
|
|||
LocalFileUpdateService.instance.init(preferences);
|
||||
SearchService.instance.init();
|
||||
StorageBonusService.instance.init(preferences);
|
||||
if (!isBackground &&
|
||||
Platform.isAndroid &&
|
||||
await HomeWidgetService.instance.countHomeWidgets() == 0) {
|
||||
unawaited(HomeWidgetService.instance.initHomeWidget());
|
||||
}
|
||||
|
||||
if (Platform.isIOS) {
|
||||
// ignore: unawaited_futures
|
||||
PushService.instance.init().then((_) {
|
||||
|
@ -275,9 +281,15 @@ Future<void> _scheduleHeartBeat(
|
|||
}
|
||||
|
||||
Future<void> _scheduleFGHomeWidgetSync() async {
|
||||
Future.delayed(kFGHomeWidgetSyncFrequency, () async {
|
||||
unawaited(_homeWidgetSyncPeriodic());
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _homeWidgetSyncPeriodic() async {
|
||||
await _homeWidgetSync();
|
||||
Future.delayed(kFGHomeWidgetSyncFrequency, () async {
|
||||
unawaited(_scheduleFGHomeWidgetSync());
|
||||
unawaited(_homeWidgetSyncPeriodic());
|
||||
});
|
||||
}
|
||||
|
||||
|
|
173
mobile/lib/services/home_widget_service.dart
Normal file
|
@ -0,0 +1,173 @@
|
|||
import "dart:math";
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
import 'package:home_widget/home_widget.dart' as hw;
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photos/core/configuration.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/db/files_db.dart";
|
||||
import "package:photos/models/file/file_type.dart";
|
||||
import "package:photos/services/favorites_service.dart";
|
||||
import "package:photos/utils/file_util.dart";
|
||||
import "package:photos/utils/preload_util.dart";
|
||||
|
||||
class HomeWidgetService {
|
||||
final Logger _logger = Logger((HomeWidgetService).toString());
|
||||
|
||||
HomeWidgetService._privateConstructor();
|
||||
|
||||
static final HomeWidgetService instance =
|
||||
HomeWidgetService._privateConstructor();
|
||||
|
||||
Future<void> initHomeWidget() async {
|
||||
final isLoggedIn = Configuration.instance.isLoggedIn();
|
||||
|
||||
if (!isLoggedIn) {
|
||||
await clearHomeWidget();
|
||||
_logger.info("user not logged in");
|
||||
return;
|
||||
}
|
||||
|
||||
final collectionID =
|
||||
await FavoritesService.instance.getFavoriteCollectionID();
|
||||
if (collectionID == null) {
|
||||
await clearHomeWidget();
|
||||
_logger.info("Favorite collection not found");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await hw.HomeWidget.setAppGroupId(iOSGroupID);
|
||||
final res = await FilesDB.instance.getFilesInCollection(
|
||||
collectionID,
|
||||
galleryLoadStartTime,
|
||||
galleryLoadEndTime,
|
||||
);
|
||||
|
||||
final previousGeneratedId =
|
||||
await hw.HomeWidget.getWidgetData<int>("home_widget_last_img");
|
||||
|
||||
if (res.files.length == 1 &&
|
||||
res.files[0].generatedID == previousGeneratedId) {
|
||||
_logger
|
||||
.info("Only one image found and it's the same as the previous one");
|
||||
return;
|
||||
}
|
||||
if (res.files.isEmpty) {
|
||||
await clearHomeWidget();
|
||||
_logger.info("No images found");
|
||||
return;
|
||||
}
|
||||
final files = res.files.where(
|
||||
(element) =>
|
||||
element.generatedID != previousGeneratedId &&
|
||||
element.fileType == FileType.image,
|
||||
);
|
||||
|
||||
final randomNumber = Random().nextInt(files.length);
|
||||
final randomFile = files.elementAt(randomNumber);
|
||||
final fullImage = await getFileFromServer(randomFile);
|
||||
if (fullImage == null) throw Exception("File not found");
|
||||
|
||||
final image = await decodeImageFromList(await fullImage.readAsBytes());
|
||||
final width = image.width.toDouble();
|
||||
final height = image.height.toDouble();
|
||||
final size = min(min(width, height), 1024.0);
|
||||
final aspectRatio = width / height;
|
||||
late final int cacheWidth;
|
||||
late final int cacheHeight;
|
||||
if (aspectRatio > 1) {
|
||||
cacheWidth = 1024;
|
||||
cacheHeight = (1024 / aspectRatio).round();
|
||||
} else if (aspectRatio < 1) {
|
||||
cacheHeight = 1024;
|
||||
cacheWidth = (1024 * aspectRatio).round();
|
||||
} else {
|
||||
cacheWidth = 1024;
|
||||
cacheHeight = 1024;
|
||||
}
|
||||
final Image img = Image.file(
|
||||
fullImage,
|
||||
fit: BoxFit.cover,
|
||||
cacheWidth: cacheWidth,
|
||||
cacheHeight: cacheHeight,
|
||||
);
|
||||
|
||||
await PreloadImage.loadImage(img.image);
|
||||
|
||||
final widget = ClipRRect(
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
child: Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black,
|
||||
image: DecorationImage(image: img.image, fit: BoxFit.cover),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await hw.HomeWidget.renderFlutterWidget(
|
||||
widget,
|
||||
logicalSize: Size(size, size),
|
||||
key: "slideshow",
|
||||
);
|
||||
|
||||
if (randomFile.generatedID != null) {
|
||||
await hw.HomeWidget.saveWidgetData<int>(
|
||||
"home_widget_last_img",
|
||||
randomFile.generatedID!,
|
||||
);
|
||||
}
|
||||
|
||||
await hw.HomeWidget.updateWidget(
|
||||
name: 'SlideshowWidgetProvider',
|
||||
androidName: 'SlideshowWidgetProvider',
|
||||
qualifiedAndroidName: 'io.ente.photos.SlideshowWidgetProvider',
|
||||
iOSName: 'SlideshowWidget',
|
||||
);
|
||||
_logger.info(
|
||||
">>> OG size of SlideshowWidget image: ${width} x $height",
|
||||
);
|
||||
_logger.info(
|
||||
">>> SlideshowWidget image rendered with size ${cacheWidth} x $cacheHeight",
|
||||
);
|
||||
} catch (e) {
|
||||
_logger.severe("Error rendering widget", e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> countHomeWidgets() async {
|
||||
return await hw.HomeWidget.getWidgetCount(
|
||||
name: 'SlideshowWidgetProvider',
|
||||
androidName: 'SlideshowWidgetProvider',
|
||||
qualifiedAndroidName: 'io.ente.photos.SlideshowWidgetProvider',
|
||||
iOSName: 'SlideshowWidget',
|
||||
) ??
|
||||
0;
|
||||
}
|
||||
|
||||
Future<void> clearHomeWidget() async {
|
||||
final previousGeneratedId =
|
||||
await hw.HomeWidget.getWidgetData<int>("home_widget_last_img");
|
||||
if (previousGeneratedId == null) return;
|
||||
|
||||
_logger.info("Clearing SlideshowWidget");
|
||||
await hw.HomeWidget.saveWidgetData(
|
||||
"slideshow",
|
||||
null,
|
||||
);
|
||||
|
||||
await hw.HomeWidget.updateWidget(
|
||||
name: 'SlideshowWidgetProvider',
|
||||
androidName: 'SlideshowWidgetProvider',
|
||||
qualifiedAndroidName: 'io.ente.photos.SlideshowWidgetProvider',
|
||||
iOSName: 'SlideshowWidget',
|
||||
);
|
||||
await hw.HomeWidget.saveWidgetData<int>(
|
||||
"home_widget_last_img",
|
||||
null,
|
||||
);
|
||||
_logger.info(">>> SlideshowWidget cleared");
|
||||
}
|
||||
}
|
|
@ -1,147 +0,0 @@
|
|||
import "dart:math";
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
import 'package:home_widget/home_widget.dart' as hw;
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photos/core/configuration.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/db/files_db.dart";
|
||||
import "package:photos/models/file/file_type.dart";
|
||||
import "package:photos/services/favorites_service.dart";
|
||||
import "package:photos/utils/file_util.dart";
|
||||
import "package:photos/utils/preload_util.dart";
|
||||
|
||||
Future<int> countHomeWidgets() async {
|
||||
return await hw.HomeWidget.getWidgetCount(
|
||||
name: 'SlideshowWidgetProvider',
|
||||
androidName: 'SlideshowWidgetProvider',
|
||||
qualifiedAndroidName: 'io.ente.photos.SlideshowWidgetProvider',
|
||||
iOSName: 'SlideshowWidget',
|
||||
) ??
|
||||
0;
|
||||
}
|
||||
|
||||
Future<void> initHomeWidget() async {
|
||||
final Logger logger = Logger("initHomeWidget");
|
||||
final user = Configuration.instance.getUserID();
|
||||
|
||||
if (user == null) {
|
||||
await clearHomeWidget();
|
||||
throw Exception("User not found");
|
||||
}
|
||||
|
||||
final collectionID =
|
||||
await FavoritesService.instance.getFavoriteCollectionID();
|
||||
if (collectionID == null) {
|
||||
await clearHomeWidget();
|
||||
throw Exception("Collection not found");
|
||||
}
|
||||
|
||||
try {
|
||||
await hw.HomeWidget.setAppGroupId(iOSGroupID);
|
||||
final res = await FilesDB.instance.getFilesInCollection(
|
||||
collectionID,
|
||||
galleryLoadStartTime,
|
||||
galleryLoadEndTime,
|
||||
);
|
||||
|
||||
final previousGeneratedId =
|
||||
await hw.HomeWidget.getWidgetData<int>("home_widget_last_img");
|
||||
|
||||
if (res.files.length == 1 &&
|
||||
res.files[0].generatedID == previousGeneratedId) {
|
||||
logger.info("Only one image found and it's the same as the previous one");
|
||||
return;
|
||||
}
|
||||
if (res.files.isEmpty) {
|
||||
await clearHomeWidget();
|
||||
return;
|
||||
}
|
||||
final files = res.files.where(
|
||||
(element) =>
|
||||
element.generatedID != previousGeneratedId &&
|
||||
element.fileType == FileType.image,
|
||||
);
|
||||
|
||||
final randomNumber = Random().nextInt(files.length);
|
||||
final randomFile = files.elementAt(randomNumber);
|
||||
final fullImage = await getFileFromServer(randomFile);
|
||||
if (fullImage == null) throw Exception("File not found");
|
||||
|
||||
Image img = Image.file(fullImage);
|
||||
var imgProvider = img.image;
|
||||
await PreloadImage.loadImage(imgProvider);
|
||||
|
||||
img = Image.file(fullImage);
|
||||
imgProvider = img.image;
|
||||
|
||||
final image = await decodeImageFromList(await fullImage.readAsBytes());
|
||||
final width = image.width.toDouble();
|
||||
final height = image.height.toDouble();
|
||||
final size = min(min(width, height), 1024.0);
|
||||
|
||||
final widget = ClipRRect(
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
child: Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black,
|
||||
image: DecorationImage(image: imgProvider, fit: BoxFit.cover),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await hw.HomeWidget.renderFlutterWidget(
|
||||
widget,
|
||||
logicalSize: Size(size, size),
|
||||
key: "slideshow",
|
||||
);
|
||||
|
||||
await hw.HomeWidget.updateWidget(
|
||||
name: 'SlideshowWidgetProvider',
|
||||
androidName: 'SlideshowWidgetProvider',
|
||||
qualifiedAndroidName: 'io.ente.photos.SlideshowWidgetProvider',
|
||||
iOSName: 'SlideshowWidget',
|
||||
);
|
||||
|
||||
if (randomFile.generatedID != null) {
|
||||
await hw.HomeWidget.saveWidgetData<int>(
|
||||
"home_widget_last_img",
|
||||
randomFile.generatedID!,
|
||||
);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
">>> SlideshowWidget rendered with size ${width}x$height",
|
||||
);
|
||||
} catch (_) {
|
||||
throw Exception("Error rendering widget");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clearHomeWidget() async {
|
||||
final previousGeneratedId =
|
||||
await hw.HomeWidget.getWidgetData<int>("home_widget_last_img");
|
||||
if (previousGeneratedId == null) return;
|
||||
|
||||
final Logger logger = Logger("clearHomeWidget");
|
||||
|
||||
logger.info("Clearing SlideshowWidget");
|
||||
await hw.HomeWidget.saveWidgetData(
|
||||
"slideshow",
|
||||
null,
|
||||
);
|
||||
|
||||
await hw.HomeWidget.updateWidget(
|
||||
name: 'SlideshowWidgetProvider',
|
||||
androidName: 'SlideshowWidgetProvider',
|
||||
qualifiedAndroidName: 'io.ente.photos.SlideshowWidgetProvider',
|
||||
iOSName: 'SlideshowWidget',
|
||||
);
|
||||
await hw.HomeWidget.saveWidgetData<int>(
|
||||
"home_widget_last_img",
|
||||
null,
|
||||
);
|
||||
logger.info(">>> SlideshowWidget cleared");
|
||||
}
|