Merge branch 'main' into 30_million
This commit is contained in:
commit
f2c05c49a3
11 changed files with 284 additions and 52 deletions
50
lib/gateways/cast_gw.dart
Normal file
50
lib/gateways/cast_gw.dart
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
8
lib/generated/intl/messages_en.dart
generated
8
lib/generated/intl/messages_en.dart
generated
|
@ -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"),
|
||||
|
|
50
lib/generated/l10n.dart
generated
50
lib/generated/l10n.dart
generated
|
@ -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> {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Reference in a new issue