Browse Source

[mob][photos] Add support for casting (#1556)

## Description

## Tests
Vishnu Mohandas 1 năm trước cách đây
mục cha
commit
569f7c0c47

+ 9 - 0
mobile/ios/Runner/Info.plist

@@ -105,5 +105,14 @@
 		<true/>
 		<true/>
 		<key>UIApplicationSupportsIndirectInputEvents</key>
 		<key>UIApplicationSupportsIndirectInputEvents</key>
 		<true/>
 		<true/>
+		<key>NSBonjourServices</key>
+                <array>
+                  <string>_googlecast._tcp</string>
+                  <string>F5BCEC64._googlecast._tcp</string>
+                </array>
+
+                <key>NSLocalNetworkUsageDescription</key>
+                <string>${PRODUCT_NAME} uses the local network to discover Cast-enabled devices on your WiFi
+                network.</string>
 	</dict>
 	</dict>
 </plist>
 </plist>

+ 12 - 4
mobile/lib/gateways/cast_gw.dart

@@ -12,10 +12,14 @@ class CastGateway {
       );
       );
       return response.data["publicKey"];
       return response.data["publicKey"];
     } catch (e) {
     } catch (e) {
-      if (e is DioError &&
-          e.response != null &&
-          e.response!.statusCode == 404) {
-        return null;
+      if (e is DioError && e.response != null) {
+        if (e.response!.statusCode == 404) {
+          return null;
+        } else if (e.response!.statusCode == 403) {
+          throw CastIPMismatchException();
+        } else {
+          rethrow;
+        }
       }
       }
       rethrow;
       rethrow;
     }
     }
@@ -48,3 +52,7 @@ class CastGateway {
     }
     }
   }
   }
 }
 }
+
+class CastIPMismatchException implements Exception {
+  CastIPMismatchException();
+}

+ 24 - 0
mobile/lib/generated/intl/messages_en.dart

@@ -357,6 +357,13 @@ class MessageLookup extends MessageLookupByLibrary {
                 "Authentication failed, please try again"),
                 "Authentication failed, please try again"),
         "authenticationSuccessful":
         "authenticationSuccessful":
             MessageLookupByLibrary.simpleMessage("Authentication successful!"),
             MessageLookupByLibrary.simpleMessage("Authentication successful!"),
+        "autoCastDialogBody": MessageLookupByLibrary.simpleMessage(
+            "You\'ll see available Cast devices here."),
+        "autoCastiOSPermission": MessageLookupByLibrary.simpleMessage(
+            "Make sure Local Network permissions are turned on for the Ente Photos app, in Settings."),
+        "autoPair": MessageLookupByLibrary.simpleMessage("Auto pair"),
+        "autoPairGoogle": MessageLookupByLibrary.simpleMessage(
+            "Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos."),
         "available": MessageLookupByLibrary.simpleMessage("Available"),
         "available": MessageLookupByLibrary.simpleMessage("Available"),
         "backedUpFolders":
         "backedUpFolders":
             MessageLookupByLibrary.simpleMessage("Backed up folders"),
             MessageLookupByLibrary.simpleMessage("Backed up folders"),
@@ -387,6 +394,10 @@ class MessageLookup extends MessageLookupByLibrary {
         "cannotAddMorePhotosAfterBecomingViewer": m9,
         "cannotAddMorePhotosAfterBecomingViewer": m9,
         "cannotDeleteSharedFiles":
         "cannotDeleteSharedFiles":
             MessageLookupByLibrary.simpleMessage("Cannot delete shared files"),
             MessageLookupByLibrary.simpleMessage("Cannot delete shared files"),
+        "castIPMismatchBody": MessageLookupByLibrary.simpleMessage(
+            "Please make sure you are on the same network as the TV."),
+        "castIPMismatchTitle":
+            MessageLookupByLibrary.simpleMessage("Failed to cast album"),
         "castInstruction": MessageLookupByLibrary.simpleMessage(
         "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."),
             "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"),
         "centerPoint": MessageLookupByLibrary.simpleMessage("Center point"),
@@ -460,6 +471,8 @@ class MessageLookup extends MessageLookupByLibrary {
             MessageLookupByLibrary.simpleMessage("Confirm recovery key"),
             MessageLookupByLibrary.simpleMessage("Confirm recovery key"),
         "confirmYourRecoveryKey":
         "confirmYourRecoveryKey":
             MessageLookupByLibrary.simpleMessage("Confirm your recovery key"),
             MessageLookupByLibrary.simpleMessage("Confirm your recovery key"),
+        "connectToDevice":
+            MessageLookupByLibrary.simpleMessage("Connect to device"),
         "contactFamilyAdmin": m12,
         "contactFamilyAdmin": m12,
         "contactSupport":
         "contactSupport":
             MessageLookupByLibrary.simpleMessage("Contact support"),
             MessageLookupByLibrary.simpleMessage("Contact support"),
@@ -904,6 +917,8 @@ class MessageLookup extends MessageLookupByLibrary {
         "manageParticipants": MessageLookupByLibrary.simpleMessage("Manage"),
         "manageParticipants": MessageLookupByLibrary.simpleMessage("Manage"),
         "manageSubscription":
         "manageSubscription":
             MessageLookupByLibrary.simpleMessage("Manage subscription"),
             MessageLookupByLibrary.simpleMessage("Manage subscription"),
+        "manualPairDesc": MessageLookupByLibrary.simpleMessage(
+            "Pair with PIN works for any large screen device you want to play your album on."),
         "map": MessageLookupByLibrary.simpleMessage("Map"),
         "map": MessageLookupByLibrary.simpleMessage("Map"),
         "maps": MessageLookupByLibrary.simpleMessage("Maps"),
         "maps": MessageLookupByLibrary.simpleMessage("Maps"),
         "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"),
         "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"),
@@ -938,6 +953,8 @@ class MessageLookup extends MessageLookupByLibrary {
         "no": MessageLookupByLibrary.simpleMessage("No"),
         "no": MessageLookupByLibrary.simpleMessage("No"),
         "noAlbumsSharedByYouYet":
         "noAlbumsSharedByYouYet":
             MessageLookupByLibrary.simpleMessage("No albums shared by you yet"),
             MessageLookupByLibrary.simpleMessage("No albums shared by you yet"),
+        "noDeviceFound":
+            MessageLookupByLibrary.simpleMessage("No device found"),
         "noDeviceLimit": MessageLookupByLibrary.simpleMessage("None"),
         "noDeviceLimit": MessageLookupByLibrary.simpleMessage("None"),
         "noDeviceThatCanBeDeleted": MessageLookupByLibrary.simpleMessage(
         "noDeviceThatCanBeDeleted": MessageLookupByLibrary.simpleMessage(
             "You\'ve no files on this device that can be deleted"),
             "You\'ve no files on this device that can be deleted"),
@@ -984,6 +1001,9 @@ class MessageLookup extends MessageLookupByLibrary {
         "orPickAnExistingOne":
         "orPickAnExistingOne":
             MessageLookupByLibrary.simpleMessage("Or pick an existing one"),
             MessageLookupByLibrary.simpleMessage("Or pick an existing one"),
         "pair": MessageLookupByLibrary.simpleMessage("Pair"),
         "pair": MessageLookupByLibrary.simpleMessage("Pair"),
+        "pairWithPin": MessageLookupByLibrary.simpleMessage("Pair with PIN"),
+        "pairingComplete":
+            MessageLookupByLibrary.simpleMessage("Pairing complete"),
         "passkey": MessageLookupByLibrary.simpleMessage("Passkey"),
         "passkey": MessageLookupByLibrary.simpleMessage("Passkey"),
         "passkeyAuthTitle":
         "passkeyAuthTitle":
             MessageLookupByLibrary.simpleMessage("Passkey verification"),
             MessageLookupByLibrary.simpleMessage("Passkey verification"),
@@ -1330,6 +1350,10 @@ class MessageLookup extends MessageLookupByLibrary {
         "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Success"),
         "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Success"),
         "startBackup": MessageLookupByLibrary.simpleMessage("Start backup"),
         "startBackup": MessageLookupByLibrary.simpleMessage("Start backup"),
         "status": MessageLookupByLibrary.simpleMessage("Status"),
         "status": MessageLookupByLibrary.simpleMessage("Status"),
+        "stopCastingBody": MessageLookupByLibrary.simpleMessage(
+            "Do you want to stop casting?"),
+        "stopCastingTitle":
+            MessageLookupByLibrary.simpleMessage("Stop casting"),
         "storage": MessageLookupByLibrary.simpleMessage("Storage"),
         "storage": MessageLookupByLibrary.simpleMessage("Storage"),
         "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Family"),
         "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Family"),
         "storageBreakupYou": MessageLookupByLibrary.simpleMessage("You"),
         "storageBreakupYou": MessageLookupByLibrary.simpleMessage("You"),

+ 130 - 0
mobile/lib/generated/l10n.dart

@@ -8388,6 +8388,26 @@ class S {
     );
     );
   }
   }
 
 
+  /// `Auto pair`
+  String get autoPair {
+    return Intl.message(
+      'Auto pair',
+      name: 'autoPair',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Pair with PIN`
+  String get pairWithPin {
+    return Intl.message(
+      'Pair with PIN',
+      name: 'pairWithPin',
+      desc: '',
+      args: [],
+    );
+  }
+
   /// `Device not found`
   /// `Device not found`
   String get deviceNotFound {
   String get deviceNotFound {
     return Intl.message(
     return Intl.message(
@@ -8573,6 +8593,116 @@ class S {
       args: [],
       args: [],
     );
     );
   }
   }
+
+  /// `Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos.`
+  String get autoPairGoogle {
+    return Intl.message(
+      'Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos.',
+      name: 'autoPairGoogle',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Pair with PIN works for any large screen device you want to play your album on.`
+  String get manualPairDesc {
+    return Intl.message(
+      'Pair with PIN works for any large screen device you want to play your album on.',
+      name: 'manualPairDesc',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Connect to device`
+  String get connectToDevice {
+    return Intl.message(
+      'Connect to device',
+      name: 'connectToDevice',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `You'll see available Cast devices here.`
+  String get autoCastDialogBody {
+    return Intl.message(
+      'You\'ll see available Cast devices here.',
+      name: 'autoCastDialogBody',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Make sure Local Network permissions are turned on for the Ente Photos app, in Settings.`
+  String get autoCastiOSPermission {
+    return Intl.message(
+      'Make sure Local Network permissions are turned on for the Ente Photos app, in Settings.',
+      name: 'autoCastiOSPermission',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `No device found`
+  String get noDeviceFound {
+    return Intl.message(
+      'No device found',
+      name: 'noDeviceFound',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Stop casting`
+  String get stopCastingTitle {
+    return Intl.message(
+      'Stop casting',
+      name: 'stopCastingTitle',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Do you want to stop casting?`
+  String get stopCastingBody {
+    return Intl.message(
+      'Do you want to stop casting?',
+      name: 'stopCastingBody',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Failed to cast album`
+  String get castIPMismatchTitle {
+    return Intl.message(
+      'Failed to cast album',
+      name: 'castIPMismatchTitle',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Please make sure you are on the same network as the TV.`
+  String get castIPMismatchBody {
+    return Intl.message(
+      'Please make sure you are on the same network as the TV.',
+      name: 'castIPMismatchBody',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Pairing complete`
+  String get pairingComplete {
+    return Intl.message(
+      'Pairing complete',
+      name: 'pairingComplete',
+      desc: '',
+      args: [],
+    );
+  }
 }
 }
 
 
 class AppLocalizationDelegate extends LocalizationsDelegate<S> {
 class AppLocalizationDelegate extends LocalizationsDelegate<S> {

+ 14 - 1
mobile/lib/l10n/intl_en.arb

@@ -1196,6 +1196,8 @@
   "verifyPasskey": "Verify passkey",
   "verifyPasskey": "Verify passkey",
   "playOnTv": "Play album on TV",
   "playOnTv": "Play album on TV",
   "pair": "Pair",
   "pair": "Pair",
+  "autoPair": "Auto pair",
+  "pairWithPin": "Pair with PIN",
   "deviceNotFound": "Device not found",
   "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.",
   "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",
   "deviceCodeHint": "Enter the code",
@@ -1213,5 +1215,16 @@
   "endpointUpdatedMessage": "Endpoint updated successfully",
   "endpointUpdatedMessage": "Endpoint updated successfully",
   "customEndpoint": "Connected to {endpoint}",
   "customEndpoint": "Connected to {endpoint}",
   "createCollaborativeLink": "Create collaborative link",
   "createCollaborativeLink": "Create collaborative link",
-  "search": "Search"
+  "search": "Search",
+  "autoPairGoogle": "Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos.",
+  "manualPairDesc": "Pair with PIN works for any large screen device you want to play your album on.",
+  "connectToDevice": "Connect to device",
+  "autoCastDialogBody": "You'll see available Cast devices here.",
+  "autoCastiOSPermission": "Make sure Local Network permissions are turned on for the Ente Photos app, in Settings.",
+  "noDeviceFound": "No device found",
+  "stopCastingTitle": "Stop casting",
+  "stopCastingBody": "Do you want to stop casting?",
+  "castIPMismatchTitle": "Failed to cast album",
+  "castIPMismatchBody": "Please make sure you are on the same network as the TV.",
+  "pairingComplete": "Pairing complete"
 }
 }

+ 8 - 0
mobile/lib/service_locator.dart

@@ -1,4 +1,6 @@
 import "package:dio/dio.dart";
 import "package:dio/dio.dart";
+import "package:ente_cast/ente_cast.dart";
+import "package:ente_cast_normal/ente_cast_normal.dart";
 import "package:ente_feature_flag/ente_feature_flag.dart";
 import "package:ente_feature_flag/ente_feature_flag.dart";
 import "package:shared_preferences/shared_preferences.dart";
 import "package:shared_preferences/shared_preferences.dart";
 
 
@@ -26,3 +28,9 @@ FlagService get flagService {
   );
   );
   return _flagService!;
   return _flagService!;
 }
 }
+
+CastService? _castService;
+CastService get castService {
+  _castService ??= CastServiceImpl();
+  return _castService!;
+}

+ 128 - 0
mobile/lib/ui/cast/auto.dart

@@ -0,0 +1,128 @@
+import "dart:io";
+
+import "package:ente_cast/ente_cast.dart";
+import "package:flutter/material.dart";
+import "package:photos/generated/l10n.dart";
+import "package:photos/service_locator.dart";
+import "package:photos/theme/ente_theme.dart";
+import "package:photos/ui/common/loading_widget.dart";
+import "package:photos/utils/dialog_util.dart";
+
+class AutoCastDialog extends StatefulWidget {
+  // async method that takes string as input
+  // and returns void
+  final void Function(String) onConnect;
+  AutoCastDialog(
+    this.onConnect, {
+    Key? key,
+  }) : super(key: key) {}
+
+  @override
+  State<AutoCastDialog> createState() => _AutoCastDialogState();
+}
+
+class _AutoCastDialogState extends State<AutoCastDialog> {
+  final bool doesUserExist = true;
+  final Set<Object> _isDeviceTapInProgress = {};
+
+  @override
+  Widget build(BuildContext context) {
+    final textStyle = getEnteTextTheme(context);
+    final AlertDialog alert = AlertDialog(
+      title: Text(
+        S.of(context).connectToDevice,
+        style: textStyle.largeBold,
+      ),
+      content: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        mainAxisSize: MainAxisSize.min,
+        children: [
+          Text(
+            S.of(context).autoCastDialogBody,
+            style: textStyle.bodyMuted,
+          ),
+          if (Platform.isIOS)
+            Text(
+              S.of(context).autoCastiOSPermission,
+              style: textStyle.bodyMuted,
+            ),
+          const SizedBox(height: 16),
+          FutureBuilder<List<(String, Object)>>(
+            future: castService.searchDevices(),
+            builder: (context, snapshot) {
+              if (snapshot.hasError) {
+                return Center(
+                  child: Text(
+                    'Error: ${snapshot.error.toString()}',
+                  ),
+                );
+              } else if (!snapshot.hasData) {
+                return const EnteLoadingWidget();
+              }
+
+              if (snapshot.data!.isEmpty) {
+                return Center(child: Text(S.of(context).noDeviceFound));
+              }
+
+              return Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: snapshot.data!.map((result) {
+                  final device = result.$2;
+                  final name = result.$1;
+                  return GestureDetector(
+                    onTap: () async {
+                      if (_isDeviceTapInProgress.contains(device)) {
+                        return;
+                      }
+                      setState(() {
+                        _isDeviceTapInProgress.add(device);
+                      });
+                      try {
+                        await _connectToYourApp(context, device);
+                      } catch (e) {
+                        showGenericErrorDialog(context: context, error: e)
+                            .ignore();
+                      } finally {
+                        setState(() {
+                          _isDeviceTapInProgress.remove(device);
+                        });
+                      }
+                    },
+                    child: Padding(
+                      padding: const EdgeInsets.symmetric(vertical: 8.0),
+                      child: Row(
+                        children: [
+                          Expanded(child: Text(name)),
+                          if (_isDeviceTapInProgress.contains(device))
+                            const EnteLoadingWidget(),
+                        ],
+                      ),
+                    ),
+                  );
+                }).toList(),
+              );
+            },
+          ),
+        ],
+      ),
+    );
+    return alert;
+  }
+
+  Future<void> _connectToYourApp(
+    BuildContext context,
+    Object castDevice,
+  ) async {
+    await castService.connectDevice(
+      context,
+      castDevice,
+      onMessage: (message) {
+        if (message.containsKey(CastMessageType.pairCode)) {
+          final code = message[CastMessageType.pairCode]!['code'];
+          widget.onConnect(code);
+          Navigator.of(context).pop();
+        }
+      },
+    );
+  }
+}

+ 76 - 0
mobile/lib/ui/cast/choose.dart

@@ -0,0 +1,76 @@
+import "package:flutter/material.dart";
+import "package:photos/generated/l10n.dart";
+import "package:photos/l10n/l10n.dart";
+import "package:photos/theme/ente_theme.dart";
+import "package:photos/ui/components/buttons/button_widget.dart";
+import "package:photos/ui/components/models/button_type.dart";
+
+class CastChooseDialog extends StatefulWidget {
+  const CastChooseDialog({
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<CastChooseDialog> createState() => _CastChooseDialogState();
+}
+
+class _CastChooseDialogState extends State<CastChooseDialog> {
+  final bool doesUserExist = true;
+
+  @override
+  Widget build(BuildContext context) {
+    final textStyle = getEnteTextTheme(context);
+    final AlertDialog alert = AlertDialog(
+      title: Text(
+        context.l10n.playOnTv,
+        style: textStyle.largeBold,
+      ),
+      content: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        mainAxisSize: MainAxisSize.min,
+        children: [
+          const SizedBox(height: 8),
+          Text(
+            S.of(context).autoPairGoogle,
+            style: textStyle.bodyMuted,
+          ),
+          const SizedBox(height: 12),
+          ButtonWidget(
+            labelText: S.of(context).autoPair,
+            icon: Icons.cast_outlined,
+            buttonType: ButtonType.neutral,
+            buttonSize: ButtonSize.large,
+            shouldStickToDarkTheme: true,
+            buttonAction: ButtonAction.first,
+            shouldSurfaceExecutionStates: false,
+            isInAlert: true,
+            onTap: () async {
+              Navigator.of(context).pop(ButtonAction.first);
+            },
+          ),
+          const SizedBox(height: 36),
+          Text(
+            S.of(context).manualPairDesc,
+            style: textStyle.bodyMuted,
+          ),
+          const SizedBox(height: 12),
+          ButtonWidget(
+            labelText: S.of(context).pairWithPin,
+            buttonType: ButtonType.neutral,
+            // icon for pairing with TV manually
+            icon: Icons.tv_outlined,
+            buttonSize: ButtonSize.large,
+            isInAlert: true,
+            onTap: () async {
+              Navigator.of(context).pop(ButtonAction.second);
+            },
+            shouldStickToDarkTheme: true,
+            buttonAction: ButtonAction.second,
+            shouldSurfaceExecutionStates: false,
+          ),
+        ],
+      ),
+    );
+    return alert;
+  }
+}

+ 109 - 21
mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart

@@ -24,6 +24,8 @@ import 'package:photos/services/collections_service.dart';
 import 'package:photos/services/sync_service.dart';
 import 'package:photos/services/sync_service.dart';
 import 'package:photos/services/update_service.dart';
 import 'package:photos/services/update_service.dart';
 import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
 import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
+import "package:photos/ui/cast/auto.dart";
+import "package:photos/ui/cast/choose.dart";
 import "package:photos/ui/common/popup_item.dart";
 import "package:photos/ui/common/popup_item.dart";
 import 'package:photos/ui/components/action_sheet_widget.dart';
 import 'package:photos/ui/components/action_sheet_widget.dart';
 import 'package:photos/ui/components/buttons/button_widget.dart';
 import 'package:photos/ui/components/buttons/button_widget.dart';
@@ -320,6 +322,25 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
         ),
         ),
       );
       );
     }
     }
+
+    if (widget.collection != null && castService.isSupported) {
+      actions.add(
+        Tooltip(
+          message: "Cast album",
+          child: IconButton(
+            icon: castService.getActiveSessions().isNotEmpty
+                ? const Icon(Icons.cast_connected_rounded)
+                : const Icon(Icons.cast_outlined),
+            onPressed: () async {
+              await _castChoiceDialog();
+              if (mounted) {
+                setState(() {});
+              }
+            },
+          ),
+        ),
+      );
+    }
     final List<EntePopupMenuItem<AlbumPopupAction>> items = [];
     final List<EntePopupMenuItem<AlbumPopupAction>> items = [];
     items.addAll([
     items.addAll([
       if (galleryType.canRename())
       if (galleryType.canRename())
@@ -458,7 +479,7 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
             } else if (value == AlbumPopupAction.leave) {
             } else if (value == AlbumPopupAction.leave) {
               await _leaveAlbum(context);
               await _leaveAlbum(context);
             } else if (value == AlbumPopupAction.playOnTv) {
             } else if (value == AlbumPopupAction.playOnTv) {
-              await castAlbum();
+              await _castChoiceDialog();
             } else if (value == AlbumPopupAction.freeUpSpace) {
             } else if (value == AlbumPopupAction.freeUpSpace) {
               await _deleteBackedUpFiles(context);
               await _deleteBackedUpFiles(context);
             } else if (value == AlbumPopupAction.setCover) {
             } else if (value == AlbumPopupAction.setCover) {
@@ -693,10 +714,56 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
     setState(() {});
     setState(() {});
   }
   }
 
 
-  Future<void> castAlbum() async {
+  Future<void> _castChoiceDialog() async {
     final gw = CastGateway(NetworkClient.instance.enteDio);
     final gw = CastGateway(NetworkClient.instance.enteDio);
+    if (castService.getActiveSessions().isNotEmpty) {
+      await showChoiceDialog(
+        context,
+        title: S.of(context).stopCastingTitle,
+        firstButtonLabel: S.of(context).yes,
+        secondButtonLabel: S.of(context).no,
+        body: S.of(context).stopCastingBody,
+        firstButtonOnTap: () async {
+          gw.revokeAllTokens().ignore();
+          await castService.closeActiveCasts();
+        },
+      );
+      return;
+    }
+
     // stop any existing cast session
     // stop any existing cast session
     gw.revokeAllTokens().ignore();
     gw.revokeAllTokens().ignore();
+    final result = await showDialog<ButtonAction?>(
+      context: context,
+      barrierDismissible: true,
+      builder: (BuildContext context) {
+        return const CastChooseDialog();
+      },
+    );
+    if (result == null) {
+      return;
+    }
+    // wait to allow the dialog to close
+    await Future.delayed(const Duration(milliseconds: 100));
+    if (result == ButtonAction.first) {
+      await showDialog(
+        context: context,
+        barrierDismissible: true,
+        builder: (BuildContext context) {
+          return AutoCastDialog(
+            (device) async {
+              await _castPair(gw, device);
+            },
+          );
+        },
+      );
+    }
+    if (result == ButtonAction.second) {
+      await _pairWithPin(gw, '');
+    }
+  }
+
+  Future<void> _pairWithPin(CastGateway gw, String code) async {
     await showTextInputDialog(
     await showTextInputDialog(
       context,
       context,
       title: context.l10n.playOnTv,
       title: context.l10n.playOnTv,
@@ -704,28 +771,49 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
       submitButtonLabel: S.of(context).pair,
       submitButtonLabel: S.of(context).pair,
       textInputType: TextInputType.streetAddress,
       textInputType: TextInputType.streetAddress,
       hintText: context.l10n.deviceCodeHint,
       hintText: context.l10n.deviceCodeHint,
+      showOnlyLoadingState: true,
+      alwaysShowSuccessState: false,
+      initialValue: code,
       onSubmit: (String text) async {
       onSubmit: (String text) async {
-        try {
-          final code = text.trim();
-          final String? publicKey = await gw.getPublicKey(code);
-          if (publicKey == null) {
-            showToast(context, S.of(context).deviceNotFound);
-            return;
-          }
-          final String castToken = const 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);
+        final bool paired = await _castPair(gw, text);
+        if (!paired) {
+          Future.delayed(Duration.zero, () => _pairWithPin(gw, code));
         }
         }
       },
       },
     );
     );
   }
   }
+
+  Future<bool> _castPair(CastGateway gw, String code) async {
+    try {
+      final String? publicKey = await gw.getPublicKey(code);
+      if (publicKey == null) {
+        showToast(context, S.of(context).deviceNotFound);
+
+        return false;
+      }
+      final String castToken = const Uuid().v4().toString();
+      final castPayload = CollectionsService.instance
+          .getCastData(castToken, widget.collection!, publicKey);
+      await gw.publishCastPayload(
+        code,
+        castPayload,
+        widget.collection!.id,
+        castToken,
+      );
+      showToast(context, S.of(context).pairingComplete);
+      return true;
+    } catch (e, s) {
+      _logger.severe("Failed to cast album", e, s);
+      if (e is CastIPMismatchException) {
+        await showErrorDialog(
+          context,
+          S.of(context).castIPMismatchTitle,
+          S.of(context).castIPMismatchBody,
+        );
+      } else {
+        await showGenericErrorDialog(context: context, error: e);
+      }
+      return false;
+    }
+  }
 }
 }

+ 10 - 0
mobile/plugins/ente_cast/.metadata

@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: 0b8abb4724aa590dd0f429683339b1e045a1594d
+  channel: stable
+
+project_type: plugin

+ 1 - 0
mobile/plugins/ente_cast/analysis_options.yaml

@@ -0,0 +1 @@
+include: ../../analysis_options.yaml

+ 2 - 0
mobile/plugins/ente_cast/lib/ente_cast.dart

@@ -0,0 +1,2 @@
+export 'src/model.dart';
+export 'src/service.dart';

+ 5 - 0
mobile/plugins/ente_cast/lib/src/model.dart

@@ -0,0 +1,5 @@
+// create enum for type of message for cast
+enum CastMessageType {
+  pairCode,
+  alreadyCasting,
+}

+ 18 - 0
mobile/plugins/ente_cast/lib/src/service.dart

@@ -0,0 +1,18 @@
+import "package:ente_cast/src/model.dart";
+import "package:flutter/widgets.dart";
+
+abstract class CastService {
+  bool get isSupported;
+  Future<List<(String, Object)>> searchDevices();
+  Future<void> connectDevice(
+    BuildContext context,
+    Object device, {
+    int? collectionID,
+    // callback that take a map of string, dynamic
+    void Function(Map<CastMessageType, Map<String, dynamic>>)? onMessage,
+  });
+  // returns a map of sessionID to deviceNames
+  Map<String, String> getActiveSessions();
+
+  Future<void> closeActiveCasts();
+}

+ 19 - 0
mobile/plugins/ente_cast/pubspec.yaml

@@ -0,0 +1,19 @@
+name: ente_cast
+version: 0.0.1
+publish_to: none
+
+environment:
+  sdk: '>=3.3.0 <4.0.0'
+
+dependencies:
+  collection:
+  dio: ^4.0.6
+  flutter:
+    sdk: flutter
+  shared_preferences: ^2.0.5
+  stack_trace:
+
+dev_dependencies:
+  flutter_lints:
+
+flutter:

+ 10 - 0
mobile/plugins/ente_cast_none/.metadata

@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: 0b8abb4724aa590dd0f429683339b1e045a1594d
+  channel: stable
+
+project_type: plugin

+ 1 - 0
mobile/plugins/ente_cast_none/analysis_options.yaml

@@ -0,0 +1 @@
+include: ../../analysis_options.yaml

+ 1 - 0
mobile/plugins/ente_cast_none/lib/ente_cast_none.dart

@@ -0,0 +1 @@
+export 'src/service.dart';

+ 35 - 0
mobile/plugins/ente_cast_none/lib/src/service.dart

@@ -0,0 +1,35 @@
+import "package:ente_cast/ente_cast.dart";
+import "package:flutter/widgets.dart";
+
+class CastServiceImpl extends CastService {
+  @override
+  Future<void> connectDevice(
+    BuildContext context,
+    Object device, {
+    int? collectionID,
+    void Function(Map<CastMessageType, Map<String, dynamic>>)? onMessage,
+  }) {
+    throw UnimplementedError();
+  }
+
+  @override
+  bool get isSupported => false;
+
+  @override
+  Future<List<(String, Object)>> searchDevices() {
+    // TODO: implement searchDevices
+    throw UnimplementedError();
+  }
+
+  @override
+  Future<void> closeActiveCasts() {
+    // TODO: implement closeActiveCasts
+    throw UnimplementedError();
+  }
+
+  @override
+  Map<String, String> getActiveSessions() {
+    // TODO: implement getActiveSessions
+    throw UnimplementedError();
+  }
+}

+ 18 - 0
mobile/plugins/ente_cast_none/pubspec.yaml

@@ -0,0 +1,18 @@
+name: ente_cast_none
+version: 0.0.1
+publish_to: none
+
+environment:
+  sdk: '>=3.3.0 <4.0.0'
+
+dependencies:
+  ente_cast:
+    path: ../ente_cast
+  flutter:
+    sdk: flutter
+  stack_trace:
+
+dev_dependencies:
+  flutter_lints:
+
+flutter:

+ 10 - 0
mobile/plugins/ente_cast_normal/.metadata

@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: 0b8abb4724aa590dd0f429683339b1e045a1594d
+  channel: stable
+
+project_type: plugin

+ 1 - 0
mobile/plugins/ente_cast_normal/analysis_options.yaml

@@ -0,0 +1 @@
+include: ../../analysis_options.yaml

+ 1 - 0
mobile/plugins/ente_cast_normal/lib/ente_cast_normal.dart

@@ -0,0 +1 @@
+export 'src/service.dart';

+ 100 - 0
mobile/plugins/ente_cast_normal/lib/src/service.dart

@@ -0,0 +1,100 @@
+import "dart:developer" as dev;
+
+import "package:cast/cast.dart";
+import "package:ente_cast/ente_cast.dart";
+import "package:flutter/material.dart";
+
+class CastServiceImpl extends CastService {
+  final String _appId = 'F5BCEC64';
+  final String _pairRequestNamespace = 'urn:x-cast:pair-request';
+  final Map<int, String> collectionIDToSessions = {};
+
+  @override
+  Future<void> connectDevice(
+    BuildContext context,
+    Object device, {
+    int? collectionID,
+    void Function(Map<CastMessageType, Map<String, dynamic>>)? onMessage,
+  }) async {
+    final CastDevice castDevice = device as CastDevice;
+    final session = await CastSessionManager().startSession(castDevice);
+    session.messageStream.listen((message) {
+      if (message['type'] == "RECEIVER_STATUS") {
+        dev.log(
+          "got RECEIVER_STATUS, Send request to pair",
+          name: "CastServiceImpl",
+        );
+        session.sendMessage(_pairRequestNamespace, {});
+      } else {
+        if (onMessage != null && message.containsKey("code")) {
+          onMessage(
+            {
+              CastMessageType.pairCode: message,
+            },
+          );
+        }
+        print('receive message: $message');
+      }
+    });
+
+    session.stateStream.listen((state) {
+      if (state == CastSessionState.connected) {
+        debugPrint("Send request to pair");
+        session.sendMessage(_pairRequestNamespace, {});
+      } else if (state == CastSessionState.closed) {
+        dev.log('Session closed', name: 'CastServiceImpl');
+      }
+    });
+
+    debugPrint("Send request to launch");
+    session.sendMessage(CastSession.kNamespaceReceiver, {
+      'type': 'LAUNCH',
+      'appId': _appId, // set the appId of your app here
+    });
+    // session.sendMessage('urn:x-cast:pair-request', {});
+  }
+
+  @override
+  Future<List<(String, Object)>> searchDevices() {
+    return CastDiscoveryService().search().then((devices) {
+      return devices.map((device) => (device.name, device)).toList();
+    });
+  }
+
+  @override
+  bool get isSupported => true;
+
+  @override
+  Future<void> closeActiveCasts() {
+    final sessions = CastSessionManager().sessions;
+    for (final session in sessions) {
+      debugPrint("send close message for ${session.sessionId}");
+      Future(() {
+        session.sendMessage(CastSession.kNamespaceConnection, {
+          'type': 'CLOSE',
+        });
+      }).timeout(
+        const Duration(seconds: 5),
+        onTimeout: () {
+          debugPrint('sendMessage timed out after 5 seconds');
+        },
+      );
+      debugPrint("close session ${session.sessionId}");
+      session.close();
+    }
+    CastSessionManager().sessions.clear();
+    return Future.value();
+  }
+
+  @override
+  Map<String, String> getActiveSessions() {
+    final sessions = CastSessionManager().sessions;
+    final Map<String, String> result = {};
+    for (final session in sessions) {
+      if (session.state == CastSessionState.connected) {
+        result[session.sessionId] = session.state.toString();
+      }
+    }
+    return result;
+  }
+}

+ 333 - 0
mobile/plugins/ente_cast_normal/pubspec.lock

@@ -0,0 +1,333 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+  async:
+    dependency: transitive
+    description:
+      name: async
+      sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.11.0"
+  cast:
+    dependency: "direct main"
+    description:
+      path: "."
+      ref: multicast_version
+      resolved-ref: "1f39cd4d6efa9363e77b2439f0317bae0c92dda1"
+      url: "https://github.com/guyluz11/flutter_cast.git"
+    source: git
+    version: "2.0.9"
+  characters:
+    dependency: transitive
+    description:
+      name: characters
+      sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.0"
+  collection:
+    dependency: transitive
+    description:
+      name: collection
+      sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.18.0"
+  dio:
+    dependency: transitive
+    description:
+      name: dio
+      sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8"
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.0.6"
+  ente_cast:
+    dependency: "direct main"
+    description:
+      path: "../ente_cast"
+      relative: true
+    source: path
+    version: "0.0.1"
+  ffi:
+    dependency: transitive
+    description:
+      name: ffi
+      sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.2"
+  file:
+    dependency: transitive
+    description:
+      name: file
+      sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "7.0.0"
+  fixnum:
+    dependency: transitive
+    description:
+      name: fixnum
+      sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.0"
+  flutter:
+    dependency: "direct main"
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  flutter_lints:
+    dependency: "direct dev"
+    description:
+      name: flutter_lints
+      sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.2"
+  flutter_web_plugins:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  http:
+    dependency: transitive
+    description:
+      name: http
+      sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.1"
+  http_parser:
+    dependency: transitive
+    description:
+      name: http_parser
+      sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.0.2"
+  lints:
+    dependency: transitive
+    description:
+      name: lints
+      sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.0"
+  material_color_utilities:
+    dependency: transitive
+    description:
+      name: material_color_utilities
+      sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.8.0"
+  meta:
+    dependency: transitive
+    description:
+      name: meta
+      sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.11.0"
+  multicast_dns:
+    dependency: transitive
+    description:
+      name: multicast_dns
+      sha256: "316cc47a958d4bd3c67bd238fe8b44fdfb6133bad89cb191c0c3bd3edb14e296"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.3.2+6"
+  path:
+    dependency: transitive
+    description:
+      name: path
+      sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.9.0"
+  path_provider_linux:
+    dependency: transitive
+    description:
+      name: path_provider_linux
+      sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.2.1"
+  path_provider_platform_interface:
+    dependency: transitive
+    description:
+      name: path_provider_platform_interface
+      sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.2"
+  path_provider_windows:
+    dependency: transitive
+    description:
+      name: path_provider_windows
+      sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.2.1"
+  platform:
+    dependency: transitive
+    description:
+      name: platform
+      sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.4"
+  plugin_platform_interface:
+    dependency: transitive
+    description:
+      name: plugin_platform_interface
+      sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.8"
+  protobuf:
+    dependency: transitive
+    description:
+      name: protobuf
+      sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.0"
+  shared_preferences:
+    dependency: transitive
+    description:
+      name: shared_preferences
+      sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.2.3"
+  shared_preferences_android:
+    dependency: transitive
+    description:
+      name: shared_preferences_android
+      sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.2.2"
+  shared_preferences_foundation:
+    dependency: transitive
+    description:
+      name: shared_preferences_foundation
+      sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.3.5"
+  shared_preferences_linux:
+    dependency: transitive
+    description:
+      name: shared_preferences_linux
+      sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.3.2"
+  shared_preferences_platform_interface:
+    dependency: transitive
+    description:
+      name: shared_preferences_platform_interface
+      sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.3.2"
+  shared_preferences_web:
+    dependency: transitive
+    description:
+      name: shared_preferences_web
+      sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.3.0"
+  shared_preferences_windows:
+    dependency: transitive
+    description:
+      name: shared_preferences_windows
+      sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.3.2"
+  sky_engine:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.99"
+  source_span:
+    dependency: transitive
+    description:
+      name: source_span
+      sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.10.0"
+  stack_trace:
+    dependency: "direct main"
+    description:
+      name: stack_trace
+      sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.11.1"
+  string_scanner:
+    dependency: transitive
+    description:
+      name: string_scanner
+      sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.0"
+  term_glyph:
+    dependency: transitive
+    description:
+      name: term_glyph
+      sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.1"
+  typed_data:
+    dependency: transitive
+    description:
+      name: typed_data
+      sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.2"
+  vector_math:
+    dependency: transitive
+    description:
+      name: vector_math
+      sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.4"
+  web:
+    dependency: transitive
+    description:
+      name: web
+      sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.5.1"
+  win32:
+    dependency: transitive
+    description:
+      name: win32
+      sha256: "0a989dc7ca2bb51eac91e8fd00851297cfffd641aa7538b165c62637ca0eaa4a"
+      url: "https://pub.dev"
+    source: hosted
+    version: "5.4.0"
+  xdg_directories:
+    dependency: transitive
+    description:
+      name: xdg_directories
+      sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.0.4"
+sdks:
+  dart: ">=3.3.0 <4.0.0"
+  flutter: ">=3.19.0"

+ 22 - 0
mobile/plugins/ente_cast_normal/pubspec.yaml

@@ -0,0 +1,22 @@
+name: ente_cast_normal
+version: 0.0.1
+publish_to: none
+
+environment:
+  sdk: '>=3.3.0 <4.0.0'
+
+dependencies:
+  cast:
+    git:
+      url: https://github.com/guyluz11/flutter_cast.git
+      ref: multicast_version
+  ente_cast:
+    path: ../ente_cast
+  flutter:
+    sdk: flutter
+  stack_trace:
+
+dev_dependencies:
+  flutter_lints:
+
+flutter:

+ 39 - 0
mobile/pubspec.lock

@@ -209,6 +209,15 @@ packages:
       url: "https://pub.dev"
       url: "https://pub.dev"
     source: hosted
     source: hosted
     version: "1.1.1"
     version: "1.1.1"
+  cast:
+    dependency: transitive
+    description:
+      path: "."
+      ref: multicast_version
+      resolved-ref: "1f39cd4d6efa9363e77b2439f0317bae0c92dda1"
+      url: "https://github.com/guyluz11/flutter_cast.git"
+    source: git
+    version: "2.0.9"
   characters:
   characters:
     dependency: transitive
     dependency: transitive
     description:
     description:
@@ -434,6 +443,20 @@ packages:
       url: "https://pub.dev"
       url: "https://pub.dev"
     source: hosted
     source: hosted
     version: "2.1.17"
     version: "2.1.17"
+  ente_cast:
+    dependency: "direct main"
+    description:
+      path: "plugins/ente_cast"
+      relative: true
+    source: path
+    version: "0.0.1"
+  ente_cast_normal:
+    dependency: "direct main"
+    description:
+      path: "plugins/ente_cast_normal"
+      relative: true
+    source: path
+    version: "0.0.1"
   ente_feature_flag:
   ente_feature_flag:
     dependency: "direct main"
     dependency: "direct main"
     description:
     description:
@@ -1423,6 +1446,14 @@ packages:
       url: "https://pub.dev"
       url: "https://pub.dev"
     source: hosted
     source: hosted
     version: "1.0.2"
     version: "1.0.2"
+  multicast_dns:
+    dependency: transitive
+    description:
+      name: multicast_dns
+      sha256: "316cc47a958d4bd3c67bd238fe8b44fdfb6133bad89cb191c0c3bd3edb14e296"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.3.2+6"
   nested:
   nested:
     dependency: transitive
     dependency: transitive
     description:
     description:
@@ -1736,6 +1767,14 @@ packages:
       url: "https://pub.dev"
       url: "https://pub.dev"
     source: hosted
     source: hosted
     version: "2.1.0"
     version: "2.1.0"
+  protobuf:
+    dependency: transitive
+    description:
+      name: protobuf
+      sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.0"
   provider:
   provider:
     dependency: "direct main"
     dependency: "direct main"
     description:
     description:

+ 4 - 0
mobile/pubspec.yaml

@@ -47,6 +47,10 @@ dependencies:
   dotted_border: ^2.1.0
   dotted_border: ^2.1.0
   dropdown_button2: ^2.0.0
   dropdown_button2: ^2.0.0
   email_validator: ^2.0.1
   email_validator: ^2.0.1
+  ente_cast:
+    path: plugins/ente_cast
+  ente_cast_normal:
+    path: plugins/ente_cast_normal
   ente_feature_flag:
   ente_feature_flag:
     path: plugins/ente_feature_flag
     path: plugins/ente_feature_flag
   equatable: ^2.0.5
   equatable: ^2.0.5