mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
initial cast framework complete and mocked cast dialog working
This commit is contained in:
parent
a02fe89ec9
commit
6c3087b585
@ -1287,6 +1287,7 @@
|
||||
"no_archived_assets_message": "Archive photos and videos to hide them from your Photos view",
|
||||
"no_assets_message": "CLICK TO UPLOAD YOUR FIRST PHOTO",
|
||||
"no_assets_to_show": "No assets to show",
|
||||
"no_cast_devices_found": "No cast devices found",
|
||||
"no_duplicates_found": "No duplicates were found.",
|
||||
"no_exif_info_available": "No exif info available",
|
||||
"no_explore_results_message": "Upload more photos to explore your collection.",
|
||||
|
@ -1,6 +1,9 @@
|
||||
PODS:
|
||||
- background_downloader (0.0.1):
|
||||
- Flutter
|
||||
- bonsoir_darwin (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- device_info_plus (0.0.1):
|
||||
@ -129,6 +132,7 @@ PODS:
|
||||
|
||||
DEPENDENCIES:
|
||||
- background_downloader (from `.symlinks/plugins/background_downloader/ios`)
|
||||
- bonsoir_darwin (from `.symlinks/plugins/bonsoir_darwin/darwin`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||
@ -173,6 +177,8 @@ SPEC REPOS:
|
||||
EXTERNAL SOURCES:
|
||||
background_downloader:
|
||||
:path: ".symlinks/plugins/background_downloader/ios"
|
||||
bonsoir_darwin:
|
||||
:path: ".symlinks/plugins/bonsoir_darwin/darwin"
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
device_info_plus:
|
||||
@ -235,44 +241,45 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad
|
||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||
background_downloader: a05c77d32a0d70615b9c04577aa203535fc924ff
|
||||
bonsoir_darwin: e3b8526c42ca46a885142df84229131dfabea842
|
||||
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
|
||||
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
|
||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
|
||||
flutter_web_auth_2: 5c8d9dcd7848b5a9efb086d24e7a9adcae979c80
|
||||
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
|
||||
geolocator_apple: 1560c3c875af2a412242c7a923e15d0d401966ff
|
||||
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
||||
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
||||
isar_flutter_libs: bc909e72c3d756c2759f14c8776c13b5b0556e26
|
||||
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
|
||||
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
|
||||
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
|
||||
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
||||
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
|
||||
flutter_web_auth_2: 06d500582775790a0d4c323222fcb6d7990f9603
|
||||
fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f
|
||||
geolocator_apple: 9bcea1918ff7f0062d98345d238ae12718acfbc1
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
|
||||
isar_flutter_libs: fdf730ca925d05687f36d7f1d355e482529ed097
|
||||
local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3
|
||||
MapLibre: 0ebfa9329d313cec8bf0a5ba5a336a1dc903785e
|
||||
maplibre_gl: eab61cca6e1cfa9187249bacd3f08b51e8cd8ae9
|
||||
native_video_player: b65c58951ede2f93d103a25366bdebca95081265
|
||||
network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413
|
||||
maplibre_gl: be7b98f1c3ed75bf77f321eec04df359d0ff6f62
|
||||
native_video_player: d12af78a1a4a8cf09775a5177d5b392def6fd23c
|
||||
network_info_plus: 6613d9d7cdeb0e6f366ed4dbe4b3c51c52d567a9
|
||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
|
||||
share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb
|
||||
share_handler_ios: 6dd3a4ac5ca0d955274aec712ba0ecdcaf583e7c
|
||||
share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
||||
sqlite3_flutter_libs: f8fc13346870e73fe35ebf6dbb997fbcd156b241
|
||||
sqlite3_flutter_libs: cc304edcb8e1d8c595d1b08c7aeb46a47691d9db
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||
wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
wakelock_plus: 373cfe59b235a6dd5837d0fb88791d2f13a90d56
|
||||
|
||||
PODFILE CHECKSUM: 7ce312f2beab01395db96f6969d90a447279cf45
|
||||
|
||||
|
@ -113,6 +113,11 @@
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true />
|
||||
</dict>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_googlecast._tcp</string>
|
||||
<string>_CC1AD845._googlecast._tcp</string>
|
||||
</array>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>We need to access the camera to let you take beautiful video using this app</string>
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
@ -164,4 +169,4 @@
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>We need to use FaceID to allow access to your locked folder</string>
|
||||
</dict>
|
||||
</plist>
|
||||
</plist>
|
||||
|
@ -0,0 +1,25 @@
|
||||
import 'package:immich_mobile/models/cast_manager_state.dart';
|
||||
|
||||
abstract interface class ICastDestinationService {
|
||||
Future<bool> initialize();
|
||||
CastDestinationType getType();
|
||||
|
||||
bool isAvailable();
|
||||
|
||||
void Function(bool)? onConnectionState;
|
||||
|
||||
void Function(Duration)? onCurrentTime;
|
||||
void Function(Duration)? onDuration;
|
||||
|
||||
void Function(String)? onReceiverName;
|
||||
void Function(CastState)? onCastState;
|
||||
|
||||
void loadMedia(String url, String sessionKey, bool reload);
|
||||
|
||||
void play();
|
||||
void pause();
|
||||
void seekTo(Duration position);
|
||||
void disconnect();
|
||||
|
||||
Future<List<(String, CastDestinationType, dynamic)>> getDevices();
|
||||
}
|
87
mobile/lib/models/cast_manager_state.dart
Normal file
87
mobile/lib/models/cast_manager_state.dart
Normal file
@ -0,0 +1,87 @@
|
||||
import 'dart:convert';
|
||||
|
||||
enum CastDestinationType { googleCast }
|
||||
|
||||
enum CastState { idle, playing, paused, buffering }
|
||||
|
||||
class CastManagerState {
|
||||
final bool isCasting;
|
||||
final String receiverName;
|
||||
final CastState castState;
|
||||
final Duration currentTime;
|
||||
final Duration duration;
|
||||
|
||||
CastManagerState({
|
||||
required this.isCasting,
|
||||
required this.receiverName,
|
||||
required this.castState,
|
||||
required this.currentTime,
|
||||
required this.duration,
|
||||
});
|
||||
|
||||
CastManagerState copyWith({
|
||||
bool? isCasting,
|
||||
String? receiverName,
|
||||
CastState? castState,
|
||||
Duration? currentTime,
|
||||
Duration? duration,
|
||||
}) {
|
||||
return CastManagerState(
|
||||
isCasting: isCasting ?? this.isCasting,
|
||||
receiverName: receiverName ?? this.receiverName,
|
||||
castState: castState ?? this.castState,
|
||||
currentTime: currentTime ?? this.currentTime,
|
||||
duration: duration ?? this.duration,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
final result = <String, dynamic>{};
|
||||
|
||||
result.addAll({'isCasting': isCasting});
|
||||
result.addAll({'receiverName': receiverName});
|
||||
result.addAll({'castState': castState});
|
||||
result.addAll({'currentTime': currentTime.inSeconds});
|
||||
result.addAll({'duration': duration.inSeconds});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
factory CastManagerState.fromMap(Map<String, dynamic> map) {
|
||||
return CastManagerState(
|
||||
isCasting: map['isCasting'] ?? false,
|
||||
receiverName: map['receiverName'] ?? '',
|
||||
castState: map['castState'] ?? CastState.idle,
|
||||
currentTime: Duration(seconds: map['currentTime']?.toInt() ?? 0),
|
||||
duration: Duration(seconds: map['duration']?.toInt() ?? 0));
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory CastManagerState.fromJson(String source) =>
|
||||
CastManagerState.fromMap(json.decode(source));
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'CastManagerState(isCasting: $isCasting, receiverName: $receiverName, castState: $castState, currentTime: $currentTime, duration: $duration)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is CastManagerState &&
|
||||
other.isCasting == isCasting &&
|
||||
other.receiverName == receiverName &&
|
||||
other.castState == castState &&
|
||||
other.currentTime == currentTime &&
|
||||
other.duration == duration;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
isCasting.hashCode ^
|
||||
receiverName.hashCode ^
|
||||
castState.hashCode ^
|
||||
currentTime.hashCode ^
|
||||
duration.hashCode;
|
||||
}
|
89
mobile/lib/providers/cast.provider.dart
Normal file
89
mobile/lib/providers/cast.provider.dart
Normal file
@ -0,0 +1,89 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/models/cast_manager_state.dart';
|
||||
import 'package:immich_mobile/services/gcast.service.dart';
|
||||
|
||||
final castProvider = StateNotifierProvider<CastNotifier, CastManagerState>(
|
||||
(ref) => CastNotifier(ref.watch(gCastServiceProvider)),
|
||||
);
|
||||
|
||||
class CastNotifier extends StateNotifier<CastManagerState> {
|
||||
final GCastService _gCastService;
|
||||
|
||||
CastNotifier(this._gCastService)
|
||||
: super(
|
||||
CastManagerState(
|
||||
isCasting: false,
|
||||
currentTime: Duration.zero,
|
||||
duration: Duration.zero,
|
||||
receiverName: '',
|
||||
castState: CastState.idle,
|
||||
),
|
||||
) {
|
||||
_gCastService.onConnectionState = _onConnectionState;
|
||||
_gCastService.onCurrentTime = _onCurrentTime;
|
||||
_gCastService.onDuration = _onDuration;
|
||||
_gCastService.onReceiverName = _onReceiverName;
|
||||
_gCastService.onCastState = _onCastState;
|
||||
}
|
||||
|
||||
void _onConnectionState(bool isCasting) {
|
||||
state = state.copyWith(isCasting: isCasting);
|
||||
}
|
||||
|
||||
void _onCurrentTime(Duration currentTime) {
|
||||
state = state.copyWith(currentTime: currentTime);
|
||||
}
|
||||
|
||||
void _onDuration(Duration duration) {
|
||||
state = state.copyWith(duration: duration);
|
||||
}
|
||||
|
||||
void _onReceiverName(String receiverName) {
|
||||
state = state.copyWith(receiverName: receiverName);
|
||||
}
|
||||
|
||||
void _onCastState(CastState castState) {
|
||||
state = state.copyWith(castState: castState);
|
||||
}
|
||||
|
||||
void loadMedia(String url, String sessionKey, bool reload) {
|
||||
_gCastService.loadMedia(url, sessionKey, reload);
|
||||
}
|
||||
|
||||
Future<void> connect(CastDestinationType type, dynamic device) async {
|
||||
switch (type) {
|
||||
case CastDestinationType.googleCast:
|
||||
await _gCastService.connect(device);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<(String, CastDestinationType, dynamic)>> getDevices() async {
|
||||
// return _gCastService.getDevices();
|
||||
// delay for 2 seconds to simulate loading
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
||||
return Future<List<(String, CastDestinationType, dynamic)>>.value([
|
||||
('Google Cast', CastDestinationType.googleCast, null),
|
||||
('Apple TV', CastDestinationType.googleCast, null),
|
||||
('Roku', CastDestinationType.googleCast, null),
|
||||
('Fire TV', CastDestinationType.googleCast, null),
|
||||
]);
|
||||
}
|
||||
|
||||
void play() {
|
||||
_gCastService.play();
|
||||
}
|
||||
|
||||
void pause() {
|
||||
_gCastService.pause();
|
||||
}
|
||||
|
||||
void seekTo(Duration position) {
|
||||
_gCastService.seekTo(position);
|
||||
}
|
||||
|
||||
void disconnect() {
|
||||
_gCastService.disconnect();
|
||||
}
|
||||
}
|
56
mobile/lib/repositories/gcast.repository.dart
Normal file
56
mobile/lib/repositories/gcast.repository.dart
Normal file
@ -0,0 +1,56 @@
|
||||
import 'package:cast/device.dart';
|
||||
import 'package:cast/session.dart';
|
||||
import 'package:cast/session_manager.dart';
|
||||
import 'package:cast/discovery_service.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
final gCastRepositoryProvider = Provider((_) {
|
||||
return GCastRepository();
|
||||
});
|
||||
|
||||
class GCastRepository {
|
||||
CastSession? _castSession;
|
||||
|
||||
void Function(CastSessionState)? onCastStatus;
|
||||
void Function(Map<String, dynamic>)? onCastMessage;
|
||||
|
||||
GCastRepository();
|
||||
|
||||
Future<void> connect(CastDevice device) async {
|
||||
print("Connecting to ${device.name}");
|
||||
_castSession = await CastSessionManager().startSession(device);
|
||||
|
||||
_castSession?.stateStream.listen((state) {
|
||||
onCastStatus?.call(state);
|
||||
});
|
||||
|
||||
_castSession?.messageStream.listen((message) {
|
||||
onCastMessage?.call(message);
|
||||
});
|
||||
|
||||
// open the default receiver
|
||||
sendMessage({
|
||||
'type': 'LAUNCH',
|
||||
'appId': 'CC1AD845',
|
||||
});
|
||||
|
||||
print("Connected to ${device.name}");
|
||||
}
|
||||
|
||||
Future<void> disconnect() async {
|
||||
await _castSession?.close();
|
||||
}
|
||||
|
||||
void sendMessage(Map<String, dynamic> message) {
|
||||
if (_castSession == null) {
|
||||
throw Exception("Cast session is not established");
|
||||
}
|
||||
|
||||
_castSession!.sendMessage(CastSession.kNamespaceReceiver, message);
|
||||
}
|
||||
|
||||
Future<List<CastDevice>> listDestinations() async {
|
||||
return await CastDiscoveryService()
|
||||
.search(timeout: const Duration(seconds: 3));
|
||||
}
|
||||
}
|
109
mobile/lib/services/gcast.service.dart
Normal file
109
mobile/lib/services/gcast.service.dart
Normal file
@ -0,0 +1,109 @@
|
||||
import 'package:cast/device.dart';
|
||||
import 'package:cast/session.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/cast_destination_service.interface.dart';
|
||||
import 'package:immich_mobile/models/cast_manager_state.dart';
|
||||
import 'package:immich_mobile/repositories/gcast.repository.dart';
|
||||
import 'package:immich_mobile/utils/url_helper.dart';
|
||||
|
||||
final gCastServiceProvider =
|
||||
Provider((ref) => GCastService(ref.watch(gCastRepositoryProvider)));
|
||||
|
||||
class GCastService implements ICastDestinationService {
|
||||
final GCastRepository _gCastRepository;
|
||||
|
||||
@override
|
||||
void Function(bool)? onConnectionState;
|
||||
@override
|
||||
void Function(Duration)? onCurrentTime;
|
||||
@override
|
||||
void Function(Duration)? onDuration;
|
||||
@override
|
||||
void Function(String)? onReceiverName;
|
||||
@override
|
||||
void Function(CastState)? onCastState;
|
||||
|
||||
GCastService(this._gCastRepository) {
|
||||
_gCastRepository.onCastStatus = _onCastStatusCallback;
|
||||
_gCastRepository.onCastMessage = _onCastMessageCallback;
|
||||
}
|
||||
|
||||
void _onCastStatusCallback(CastSessionState state) {
|
||||
if (state == CastSessionState.connected) {
|
||||
onConnectionState?.call(true);
|
||||
} else if (state == CastSessionState.closed) {
|
||||
onConnectionState?.call(false);
|
||||
}
|
||||
}
|
||||
|
||||
void _onCastMessageCallback(Map<String, dynamic> message) {
|
||||
final msgType = message['type'];
|
||||
|
||||
print(message);
|
||||
|
||||
if (msgType == 'RECEIVER_STATUS') {
|
||||
print("Got receiver status");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> connect(CastDevice device) async {
|
||||
await _gCastRepository.connect(device);
|
||||
}
|
||||
|
||||
@override
|
||||
CastDestinationType getType() {
|
||||
return CastDestinationType.googleCast;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> initialize() async {
|
||||
// check if server URL is https
|
||||
final serverUrl = punycodeDecodeUrl(Store.tryGet(StoreKey.serverEndpoint));
|
||||
|
||||
return serverUrl?.startsWith("https://") ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
void disconnect() {
|
||||
_gCastRepository.disconnect();
|
||||
}
|
||||
|
||||
@override
|
||||
bool isAvailable() {
|
||||
// check if server URL is https
|
||||
final serverUrl = punycodeDecodeUrl(Store.tryGet(StoreKey.serverEndpoint));
|
||||
|
||||
return serverUrl?.startsWith("https://") ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
void loadMedia(String url, String sessionKey, bool reload) {
|
||||
// TODO: implement loadMedia
|
||||
}
|
||||
|
||||
@override
|
||||
void play() {
|
||||
// TODO: implement play
|
||||
}
|
||||
|
||||
@override
|
||||
void pause() {
|
||||
// TODO: implement pause
|
||||
}
|
||||
|
||||
@override
|
||||
void seekTo(Duration position) {
|
||||
// TODO: implement seekTo
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<(String, CastDestinationType, dynamic)>> getDevices() async {
|
||||
final dests = await _gCastRepository.listDestinations();
|
||||
|
||||
return dests
|
||||
.map((device) => (device.name, CastDestinationType.googleCast, device))
|
||||
.toList(growable: false);
|
||||
}
|
||||
}
|
82
mobile/lib/widgets/asset_viewer/cast_dialog.dart
Normal file
82
mobile/lib/widgets/asset_viewer/cast_dialog.dart
Normal file
@ -0,0 +1,82 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/models/cast_manager_state.dart';
|
||||
import 'package:immich_mobile/providers/cast.provider.dart';
|
||||
|
||||
class CastDialog extends ConsumerWidget {
|
||||
const CastDialog({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return AlertDialog(
|
||||
title: const Text(
|
||||
"cast",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
).tr(),
|
||||
content: SizedBox(
|
||||
width: 250,
|
||||
height: 250,
|
||||
child: FutureBuilder<List<(String, CastDestinationType, dynamic)>>(
|
||||
future: ref.read(castProvider.notifier).getDevices(),
|
||||
builder: (context, snapshot) {
|
||||
final cast = ref.read(castProvider.notifier);
|
||||
|
||||
if (snapshot.hasError) {
|
||||
return Text(
|
||||
'Error: ${snapshot.error.toString()}',
|
||||
);
|
||||
} else if (!snapshot.hasData) {
|
||||
return const SizedBox(
|
||||
height: 48,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot.data!.isEmpty) {
|
||||
return const Text(
|
||||
'no_cast_devices_found',
|
||||
).tr();
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: snapshot.data!.length,
|
||||
itemBuilder: (context, index) {
|
||||
final found = snapshot.data![index];
|
||||
final deviceName = found.$1;
|
||||
final type = found.$2;
|
||||
final deviceObj = found.$3;
|
||||
|
||||
return ListTile(
|
||||
title: Text(deviceName),
|
||||
leading: Icon(
|
||||
type == CastDestinationType.googleCast
|
||||
? Icons.cast
|
||||
: Icons.cast_connected,
|
||||
),
|
||||
onTap: () {
|
||||
cast.connect(type, deviceObj);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(
|
||||
"close",
|
||||
style: TextStyle(
|
||||
color: context.colorScheme.tertiary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/motion_photo_button.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
||||
|
||||
@ -169,6 +170,20 @@ class TopControlAppBar extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildCastButton() {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context, builder: (context) => const CastDialog());
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.cast,
|
||||
size: 20.0,
|
||||
color: Colors.grey[200],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool isInHomePage = ref.read(tabProvider.notifier).state == TabEnum.home;
|
||||
bool? isInTrash = ref.read(currentAssetProvider)?.isTrashed;
|
||||
|
||||
@ -193,6 +208,7 @@ class TopControlAppBar extends HookConsumerWidget {
|
||||
!asset.isTrashed &&
|
||||
!isInLockedView)
|
||||
buildAddToAlbumButton(),
|
||||
buildCastButton(),
|
||||
if (asset.isTrashed) buildRestoreButton(),
|
||||
if (album != null && album.shared && !isInLockedView)
|
||||
buildActivitiesButton(),
|
||||
|
@ -86,6 +86,54 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.2.0"
|
||||
bonsoir:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bonsoir
|
||||
sha256: "2e2cf3be580deccad9a48dcaddddf90de092e74b7de2015ef58fb24e11d66496"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.11"
|
||||
bonsoir_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bonsoir_android
|
||||
sha256: "9a65b6e50c5718c3f1a7ed6ff57ab9ed8ae990ff9c36d2b1ab3d1b90f28f7d1b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.6"
|
||||
bonsoir_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bonsoir_darwin
|
||||
sha256: "2d25c70f0d09260be1c2ab583b80dd89cbbfd59997579dadf789c5af00c7b2e4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.3"
|
||||
bonsoir_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bonsoir_linux
|
||||
sha256: f2639aded6e15943a9822de98a663a1056f37cbfd0a74d72c9eaa941965945c2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.3"
|
||||
bonsoir_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bonsoir_platform_interface
|
||||
sha256: "08bb8b35d0198168b3bce87dbc718e4e510336cff1d97e43762e030c01636d45"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.3"
|
||||
bonsoir_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bonsoir_windows
|
||||
sha256: d4a0ca479d4f3679487a61f3174fb9fe1651e323c778b02dfa630490366be65d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.5"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -198,6 +246,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
cast:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cast
|
||||
sha256: de1856e1a31aa60a6fed627f827921f7ec6539c67c60d0c899e89646dcbe773e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1400,6 +1456,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.3"
|
||||
protobuf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: protobuf
|
||||
sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -19,6 +19,7 @@ dependencies:
|
||||
background_downloader: ^9.2.0
|
||||
cached_network_image: ^3.4.1
|
||||
cancellation_token_http: ^2.1.0
|
||||
cast: ^2.1.0
|
||||
collection: ^1.18.0
|
||||
connectivity_plus: ^6.1.3
|
||||
crop_image: ^1.0.16
|
||||
|
Loading…
x
Reference in New Issue
Block a user