Merge branch 'main' into 30_million

This commit is contained in:
Vishnu Mohandas 2024-02-04 19:48:13 +05:30 committed by GitHub
commit f2c05c49a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 284 additions and 52 deletions

50
lib/gateways/cast_gw.dart Normal file
View file

@ -0,0 +1,50 @@
import "package:dio/dio.dart";
class CastGateway {
final Dio _enteDio;
CastGateway(this._enteDio);
Future<String?> getPublicKey(String deviceCode) async {
try {
final response = await _enteDio.get(
"/cast/device-info/$deviceCode",
);
return response.data["publicKey"];
} catch (e) {
if (e is DioError &&
e.response != null &&
e.response!.statusCode == 404) {
return null;
}
rethrow;
}
}
Future<void> publishCastPayload(
String code,
String castPayload,
int collectionID,
String castToken,
) {
return _enteDio.post(
"/cast/cast-data/",
data: {
"deviceCode": code,
"encPayload": castPayload,
"collectionID": collectionID,
"castToken": castToken,
},
);
}
Future<void> revokeAllTokens() async {
try {
await _enteDio.delete(
"/cast/revoke-all-tokens/",
);
} catch (e) {
// swallow error
}
}
}

View file

@ -380,6 +380,8 @@ class MessageLookup extends MessageLookupByLibrary {
"cannotAddMorePhotosAfterBecomingViewer": m7,
"cannotDeleteSharedFiles":
MessageLookupByLibrary.simpleMessage("Cannot delete shared files"),
"castInstruction": MessageLookupByLibrary.simpleMessage(
"Visit cast.ente.io on the device you want to pair.\n\nEnter the code below to play the album on your TV."),
"centerPoint": MessageLookupByLibrary.simpleMessage("Center point"),
"changeEmail": MessageLookupByLibrary.simpleMessage("Change email"),
"changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage(
@ -552,10 +554,14 @@ class MessageLookup extends MessageLookupByLibrary {
"details": MessageLookupByLibrary.simpleMessage("Details"),
"devAccountChanged": MessageLookupByLibrary.simpleMessage(
"The developer account we use to publish ente on App Store has changed. Because of this, you will need to login again.\n\nOur apologies for the inconvenience, but this was unavoidable."),
"deviceCodeHint":
MessageLookupByLibrary.simpleMessage("Enter the code"),
"deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage(
"Files added to this device album will automatically get uploaded to ente."),
"deviceLockExplanation": MessageLookupByLibrary.simpleMessage(
"Disable the device screen lock when ente is in the foreground and there is a backup in progress. This is normally not needed, but may help big uploads and initial imports of large libraries complete faster."),
"deviceNotFound":
MessageLookupByLibrary.simpleMessage("Device not found"),
"didYouKnow": MessageLookupByLibrary.simpleMessage("Did you know?"),
"disableAutoLock":
MessageLookupByLibrary.simpleMessage("Disable auto lock"),
@ -946,6 +952,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Optional, as short as you like..."),
"orPickAnExistingOne":
MessageLookupByLibrary.simpleMessage("Or pick an existing one"),
"pair": MessageLookupByLibrary.simpleMessage("Pair"),
"password": MessageLookupByLibrary.simpleMessage("Password"),
"passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage(
"Password changed successfully"),
@ -980,6 +987,7 @@ class MessageLookup extends MessageLookupByLibrary {
"pickCenterPoint":
MessageLookupByLibrary.simpleMessage("Pick center point"),
"pinAlbum": MessageLookupByLibrary.simpleMessage("Pin album"),
"playOnTv": MessageLookupByLibrary.simpleMessage("Play album on TV"),
"playStoreFreeTrialValidTill": m37,
"playstoreSubscription":
MessageLookupByLibrary.simpleMessage("PlayStore subscription"),

View file

@ -8307,6 +8307,56 @@ class S {
args: [],
);
}
/// `Play album on TV`
String get playOnTv {
return Intl.message(
'Play album on TV',
name: 'playOnTv',
desc: '',
args: [],
);
}
/// `Pair`
String get pair {
return Intl.message(
'Pair',
name: 'pair',
desc: '',
args: [],
);
}
/// `Device not found`
String get deviceNotFound {
return Intl.message(
'Device not found',
name: 'deviceNotFound',
desc: '',
args: [],
);
}
/// `Visit cast.ente.io on the device you want to pair.\n\nEnter the code below to play the album on your TV.`
String get castInstruction {
return Intl.message(
'Visit cast.ente.io on the device you want to pair.\n\nEnter the code below to play the album on your TV.',
name: 'castInstruction',
desc: '',
args: [],
);
}
/// `Enter the code`
String get deviceCodeHint {
return Intl.message(
'Enter the code',
name: 'deviceCodeHint',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<S> {

View file

@ -1187,5 +1187,10 @@
"selectALocationFirst": "Select a location first",
"changeLocationOfSelectedItems": "Change location of selected items?",
"editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente",
"cleanUncategorized": "Clean Uncategorized"
"cleanUncategorized": "Clean Uncategorized",
"playOnTv": "Play album on TV",
"pair": "Pair",
"deviceNotFound": "Device not found",
"castInstruction": "Visit cast.ente.io on the device you want to pair.\n\nEnter the code below to play the album on your TV.",
"deviceCodeHint": "Enter the code"
}

View file

@ -473,6 +473,23 @@ class CollectionsService {
});
}
String getCastData(
String castToken,
Collection collection,
String publicKey,
) {
final String payload = jsonEncode({
"collectionID": collection.id,
"castToken": castToken,
"collectionKey": CryptoUtil.bin2base64(getCollectionKey(collection.id)),
});
final encPayload = CryptoUtil.sealSync(
CryptoUtil.base642bin(base64Encode(payload.codeUnits)),
CryptoUtil.base642bin(publicKey),
);
return CryptoUtil.bin2base64(encPayload);
}
Future<List<User>> share(
int collectionID,
String email,

View file

@ -13,7 +13,6 @@ import "package:photos/models/local_entity_data.dart";
import "package:photos/models/location/location.dart";
import 'package:photos/models/location_tag/location_tag.dart';
import "package:photos/services/entity_service.dart";
import "package:photos/services/feature_flag_service.dart";
import "package:photos/services/remote_assets_service.dart";
import "package:shared_preferences/shared_preferences.dart";
@ -32,9 +31,7 @@ class LocationService {
void init(SharedPreferences preferences) {
prefs = preferences;
if (FeatureFlagService.instance.isInternalUserOrDebugBuild()) {
_loadCities();
}
_loadCities();
}
Future<Iterable<LocalEntity<LocationTag>>> _getStoredLocationTags() async {

View file

@ -2,6 +2,7 @@ import "dart:async";
import "dart:isolate";
import "package:collection/collection.dart";
import "package:computer/computer.dart";
import "package:flutter/foundation.dart";
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
@ -79,43 +80,15 @@ class _MapScreenState extends State<MapScreen> {
}
Future<void> processFiles(List<EnteFile> files) async {
final List<ImageMarker> tempMarkers = [];
bool hasAnyLocation = false;
EnteFile? mostRecentFile;
for (var file in files) {
if (file.hasLocation) {
if (!Location.isValidRange(
latitude: file.location!.latitude!,
longitude: file.location!.longitude!,
)) {
_logger.warning(
'Skipping file with invalid location ${file.toString()}',
);
continue;
}
hasAnyLocation = true;
final result = await Computer.shared().compute(
_findRecentFileAndGenerateTempMarkers,
param: {"files": files, "center": widget.center},
);
if (widget.center == null) {
if (mostRecentFile == null) {
mostRecentFile = file;
} else {
if ((mostRecentFile.creationTime ?? 0) < (file.creationTime ?? 0)) {
mostRecentFile = file;
}
}
}
final EnteFile? mostRecentFile = result.$1;
final List<ImageMarker> tempMarkers = result.$2;
tempMarkers.add(
ImageMarker(
latitude: file.location!.latitude!,
longitude: file.location!.longitude!,
imageFile: file,
),
);
}
}
if (hasAnyLocation) {
if (tempMarkers.isNotEmpty) {
center = widget.center ??
LatLng(
mostRecentFile!.location!.latitude!,
@ -173,6 +146,50 @@ class _MapScreenState extends State<MapScreen> {
});
}
static (EnteFile?, List<ImageMarker>) _findRecentFileAndGenerateTempMarkers(
Map<String, dynamic> args,
) {
final Logger logger = Logger("_MapScreenState");
final files = args["files"] as List<EnteFile>;
final center = args["center"] as LatLng?;
final List<ImageMarker> tempMarkers = [];
EnteFile? mostRecentFile;
for (var file in files) {
if (file.hasLocation) {
if (!Location.isValidRange(
latitude: file.location!.latitude!,
longitude: file.location!.longitude!,
)) {
logger.warning(
'Skipping file with invalid location ${file.toString()}',
);
continue;
}
if (center == null) {
if (mostRecentFile == null) {
mostRecentFile = file;
} else {
if ((mostRecentFile.creationTime ?? 0) < (file.creationTime ?? 0)) {
mostRecentFile = file;
}
}
}
tempMarkers.add(
ImageMarker(
latitude: file.location!.latitude!,
longitude: file.location!.longitude!,
imageFile: file,
),
);
}
}
return (mostRecentFile, tempMarkers);
}
@pragma('vm:entry-point')
static void _calculateMarkersIsolate(MapIsolate message) async {
final bounds = message.bounds;

View file

@ -14,9 +14,7 @@ import "package:photos/services/location_service.dart";
import "package:photos/services/search_service.dart";
import "package:photos/services/user_remote_flag_service.dart";
import "package:photos/states/location_screen_state.dart";
import "package:photos/theme/colors.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/common/loading_widget.dart";
import "package:photos/ui/components/buttons/chip_button_widget.dart";
import "package:photos/ui/components/info_item_widget.dart";
import "package:photos/ui/map/enable_map.dart";
@ -255,14 +253,6 @@ class _InfoMapState extends State<InfoMap> {
),
),
),
_tappedToOpenMap
? const EnteLoadingWidget(
alignment: Alignment.topLeft,
padding: 19,
size: 11,
color: strokeSolidMutedLight,
)
: const SizedBox.shrink(),
],
)
: ValueListenableBuilder(

View file

@ -6,10 +6,14 @@ import "package:flutter/cupertino.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/core/network/network.dart";
import "package:photos/db/files_db.dart";
import 'package:photos/events/subscription_purchased_event.dart';
import "package:photos/gateways/cast_gw.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/l10n/l10n.dart";
import 'package:photos/models/backup_status.dart';
import 'package:photos/models/collection/collection.dart';
import 'package:photos/models/device_collection.dart';
@ -36,6 +40,7 @@ import 'package:photos/utils/dialog_util.dart';
import 'package:photos/utils/magic_util.dart';
import 'package:photos/utils/navigation_util.dart';
import 'package:photos/utils/toast_util.dart';
import "package:uuid/uuid.dart";
class GalleryAppBarWidget extends StatefulWidget {
final GalleryType type;
@ -64,6 +69,7 @@ enum AlbumPopupAction {
ownedArchive,
sharedArchive,
ownedHide,
playOnTv,
sort,
leave,
freeUpSpace,
@ -472,6 +478,22 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
),
);
}
if (widget.collection != null && isInternalUser) {
items.add(
PopupMenuItem(
value: AlbumPopupAction.playOnTv,
child: Row(
children: [
const Icon(Icons.tv_outlined),
const Padding(
padding: EdgeInsets.all(8),
),
Text(context.l10n.playOnTv),
],
),
),
);
}
if (galleryType.canDelete()) {
items.add(
@ -579,6 +601,8 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
await _removeQuickLink();
} else if (value == AlbumPopupAction.leave) {
await _leaveAlbum(context);
} else if (value == AlbumPopupAction.playOnTv) {
await castAlbum();
} else if (value == AlbumPopupAction.freeUpSpace) {
await _deleteBackedUpFiles(context);
} else if (value == AlbumPopupAction.setCover) {
@ -797,4 +821,40 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
);
setState(() {});
}
Future<void> castAlbum() async {
final gw = CastGateway(NetworkClient.instance.enteDio);
// stop any existing cast session
gw.revokeAllTokens().ignore();
await showTextInputDialog(
context,
title: context.l10n.playOnTv,
body: S.of(context).castInstruction,
submitButtonLabel: S.of(context).pair,
textInputType: TextInputType.streetAddress,
hintText: context.l10n.deviceCodeHint,
onSubmit: (String text) async {
try {
String code = text.trim();
final String? publicKey = await gw.getPublicKey(code);
if (publicKey == null) {
showToast(context, S.of(context).deviceNotFound);
return;
}
final String castToken = Uuid().v4().toString();
final castPayload = CollectionsService.instance
.getCastData(castToken, widget.collection!, publicKey);
await gw.publishCastPayload(
code,
castPayload,
widget.collection!.id,
castToken,
);
} catch (e, s) {
_logger.severe("Failed to cast album", e, s);
await showGenericErrorDialog(context: context, error: e);
}
},
);
}
}

View file

@ -11,6 +11,11 @@ import 'package:photos/utils/file_util.dart';
const kDateTimeOriginal = "EXIF DateTimeOriginal";
const kImageDateTime = "Image DateTime";
const kExifOffSetKeys = [
"EXIF OffsetTime",
"EXIF OffsetTimeOriginal",
"EXIF OffsetTimeDigitized",
];
const kExifDateTimePattern = "yyyy:MM:dd HH:mm:ss";
const kEmptyExifDateTime = "0000:00:00 00:00:00";
@ -56,7 +61,14 @@ Future<DateTime?> getCreationTimeFromEXIF(
? exif[kImageDateTime]!.printable
: null;
if (exifTime != null && exifTime != kEmptyExifDateTime) {
return DateFormat(kExifDateTimePattern).parse(exifTime);
String? exifOffsetTime;
for (final key in kExifOffSetKeys) {
if (exif.containsKey(key)) {
exifOffsetTime = exif[key]!.printable;
break;
}
}
return getDateTimeInDeviceTimezone(exifTime, exifOffsetTime);
}
} catch (e) {
_logger.severe("failed to getCreationTimeFromEXIF", e);
@ -64,6 +76,32 @@ Future<DateTime?> getCreationTimeFromEXIF(
return null;
}
DateTime getDateTimeInDeviceTimezone(String exifTime, String? offsetString) {
final DateTime result = DateFormat(kExifDateTimePattern).parse(exifTime);
if (offsetString == null) {
return result;
}
try {
final List<String> splitHHMM = offsetString.split(":");
// Parse the offset from the photo's time zone
final int offsetHours = int.parse(splitHHMM[0]);
final int offsetMinutes =
int.parse(splitHHMM[1]) * (offsetHours.isNegative ? -1 : 1);
// Adjust the date for the offset to get the photo's correct UTC time
final photoUtcDate =
result.add(Duration(hours: -offsetHours, minutes: -offsetMinutes));
// Getting the current device's time zone offset from UTC
final now = DateTime.now();
final localOffset = now.timeZoneOffset;
// Adjusting the photo's UTC time to the device's local time
final deviceLocalTime = photoUtcDate.add(localOffset);
return deviceLocalTime;
} catch (e, s) {
_logger.severe("tz offset adjust failed $offsetString", e, s);
}
return result;
}
Location? locationFromExif(Map<String, IfdTag> exif) {
try {
return gpsDataFromExif(exif).toLocationObj();

View file

@ -12,7 +12,7 @@ description: ente photos application
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.8.51+571
version: 0.8.52+572
environment:
sdk: ">=3.0.0 <4.0.0"