Skip to content
This repository has been archived by the owner on Mar 14, 2024. It is now read-only.

Commit

Permalink
Add support for playing album on cast.ente.io (#1694)
Browse files Browse the repository at this point in the history
  • Loading branch information
ua741 authored Feb 2, 2024
2 parents 94f4351 + e64bfaf commit 86ae38b
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 1 deletion.
50 changes: 50 additions & 0 deletions lib/gateways/cast_gw.dart
Original file line number Diff line number Diff line change
@@ -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 changes: 8 additions & 0 deletions lib/generated/intl/messages_en.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 50 additions & 0 deletions lib/generated/l10n.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion lib/l10n/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
17 changes: 17 additions & 0 deletions lib/services/collections_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
60 changes: 60 additions & 0 deletions lib/ui/viewer/gallery/gallery_app_bar_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -64,6 +69,7 @@ enum AlbumPopupAction {
ownedArchive,
sharedArchive,
ownedHide,
playOnTv,
sort,
leave,
freeUpSpace,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}
},
);
}
}

0 comments on commit 86ae38b

Please sign in to comment.