diff --git a/lib/common/environment.dart b/lib/common/environment.dart index 4b6ac5cbf..55727eab8 100644 --- a/lib/common/environment.dart +++ b/lib/common/environment.dart @@ -44,6 +44,10 @@ class Environment { static String get auClaimSecretKey => dotenv.env['AU_CLAIM_SECRET_KEY'] ?? ''; + static String get tvKey => dotenv.env['TV_API_KEY'] ?? ''; + + static String get tvCastApiUrl => dotenv.env['TV_CAST_API_URL'] ?? ''; + static String get tokenWebviewPrefix => dotenv.env['TOKEN_WEBVIEW_PREFIX'] ?? ''; diff --git a/lib/common/injector.dart b/lib/common/injector.dart index e49515952..a1c917d92 100644 --- a/lib/common/injector.dart +++ b/lib/common/injector.dart @@ -23,6 +23,7 @@ import 'package:autonomy_flutter/gateway/merchandise_api.dart'; import 'package:autonomy_flutter/gateway/postcard_api.dart'; import 'package:autonomy_flutter/gateway/pubdoc_api.dart'; import 'package:autonomy_flutter/gateway/source_exhibition_api.dart'; +import 'package:autonomy_flutter/gateway/tv_cast_api.dart'; import 'package:autonomy_flutter/gateway/tzkt_api.dart'; import 'package:autonomy_flutter/screen/bloc/identity/identity_bloc.dart'; import 'package:autonomy_flutter/screen/bloc/subscription/subscription_bloc.dart'; @@ -41,8 +42,6 @@ import 'package:autonomy_flutter/service/audit_service.dart'; import 'package:autonomy_flutter/service/auth_service.dart'; import 'package:autonomy_flutter/service/autonomy_service.dart'; import 'package:autonomy_flutter/service/backup_service.dart'; -import 'package:autonomy_flutter/service/canvas_channel_service.dart'; -import 'package:autonomy_flutter/service/canvas_client_service.dart'; import 'package:autonomy_flutter/service/canvas_client_service_v2.dart'; import 'package:autonomy_flutter/service/chat_auth_service.dart'; import 'package:autonomy_flutter/service/chat_service.dart'; @@ -57,8 +56,8 @@ import 'package:autonomy_flutter/service/domain_service.dart'; import 'package:autonomy_flutter/service/ethereum_service.dart'; import 'package:autonomy_flutter/service/feralfile_service.dart'; import 'package:autonomy_flutter/service/hive_service.dart'; +import 'package:autonomy_flutter/service/hive_store_service.dart'; import 'package:autonomy_flutter/service/iap_service.dart'; -import 'package:autonomy_flutter/service/mdns_service.dart'; import 'package:autonomy_flutter/service/merchandise_service.dart'; import 'package:autonomy_flutter/service/metric_client_service.dart'; import 'package:autonomy_flutter/service/mix_panel_client_service.dart'; @@ -80,9 +79,10 @@ import 'package:autonomy_flutter/util/dio_interceptors.dart'; import 'package:autonomy_flutter/util/dio_util.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:dio/dio.dart'; +import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:get_it/get_it.dart'; -import 'package:http/http.dart'; +import 'package:http/http.dart' as http; import 'package:logging/logging.dart'; import 'package:nft_collection/data/api/indexer_api.dart'; import 'package:nft_collection/graphql/clients/indexer_client.dart'; @@ -126,6 +126,7 @@ Future setup() async { migrateV15ToV16, migrateV16ToV17, migrateV17ToV18, + migrateV18ToV19, ]).build(); final cloudDB = await $FloorCloudDatabase @@ -179,8 +180,7 @@ Future setup() async { injector.registerSingleton( ConfigurationServiceImpl(sharedPreferences)); - - injector.registerLazySingleton(() => Client()); + injector.registerLazySingleton(() => http.Client()); injector.registerLazySingleton( () => AutonomyServiceImpl(injector(), injector())); injector @@ -247,6 +247,8 @@ Future setup() async { injector.registerLazySingleton( () => IAPServiceImpl(injector(), injector())); + injector.registerLazySingleton(() => + TvCastApi(tvCastDio(dioOptions), baseUrl: Environment.tvCastApiUrl)); injector.registerLazySingleton(() => Wc2Service( injector(), injector(), @@ -321,13 +323,12 @@ Future setup() async { injector.registerLazySingleton(() => mainnetDB); injector.registerLazySingleton( () => PlayListServiceImp(injector(), injector(), injector(), injector())); - - injector.registerLazySingleton( - () => CanvasChannelService(injector())); injector.registerLazySingleton(() => DeviceInfoService()); - injector.registerLazySingleton( - () => CanvasClientService(injector(), injector(), injector())); - injector.registerLazySingleton(() => MDnsService(injector())); + + injector.registerLazySingleton>( + () => HiveStoreObjectServiceImpl()); + await injector>() + .init('local.canvas_device'); injector.registerLazySingleton(() => CanvasClientServiceV2(injector(), injector(), injector(), injector())); @@ -388,7 +389,7 @@ Future setup() async { () => IdentityBloc(injector(), injector())); injector.registerFactory(() => AuChatBloc(injector())); injector.registerLazySingleton( - () => CanvasDeviceBloc(injector(), injector(), injector())); + () => CanvasDeviceBloc(injector())); injector .registerLazySingleton(() => ExhibitionBloc(injector())); injector.registerLazySingleton( diff --git a/lib/database/app_database.dart b/lib/database/app_database.dart index ed0509e5a..10c4df8c2 100644 --- a/lib/database/app_database.dart +++ b/lib/database/app_database.dart @@ -8,14 +8,12 @@ import 'dart:async'; import 'package:autonomy_flutter/database/dao/announcement_dao.dart'; -import 'package:autonomy_flutter/database/dao/canvas_device_dao.dart'; import 'package:autonomy_flutter/database/dao/draft_customer_support_dao.dart'; import 'package:autonomy_flutter/database/dao/identity_dao.dart'; import 'package:autonomy_flutter/database/entity/announcement_local.dart'; import 'package:autonomy_flutter/database/entity/draft_customer_support.dart'; import 'package:autonomy_flutter/database/entity/identity.dart'; import 'package:autonomy_flutter/util/log.dart'; -import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; import 'package:floor/floor.dart'; import 'package:nft_collection/models/token.dart'; import 'package:sqflite/sqflite.dart' as sqflite; @@ -25,12 +23,10 @@ import 'package:sqflite/sqflite.dart' as sqflite; part 'app_database.g.dart'; // the generated code will be there @TypeConverters([DateTimeConverter, TokenOwnersConverter]) -@Database(version: 17, entities: [ +@Database(version: 19, entities: [ Identity, DraftCustomerSupport, AnnouncementLocal, - CanvasDevice, - Scene, ]) abstract class AppDatabase extends FloorDatabase { IdentityDao get identityDao; @@ -39,16 +35,10 @@ abstract class AppDatabase extends FloorDatabase { AnnouncementLocalDao get announcementDao; - CanvasDeviceDao get canvasDeviceDao; - - SceneDao get sceneDao; - Future removeAll() async { await identityDao.removeAll(); await draftCustomerSupportDao.removeAll(); await announcementDao.removeAll(); - await canvasDeviceDao.removeAll(); - await sceneDao.removeAll(); } } @@ -164,3 +154,8 @@ final migrateV17ToV18 = Migration(17, 18, (database) async { await database.execute('DROP TABLE IF EXISTS Followee;'); log.info('Migrated App database from version 17 to 18'); }); + +final migrateV18ToV19 = Migration(18, 19, (database) async { + await database.execute('DROP TABLE IF EXISTS CanvasDevice;'); + await database.execute('DROP TABLE IF EXISTS Scene;'); +}); diff --git a/lib/database/app_database.g.dart b/lib/database/app_database.g.dart index 4a7d5ceb0..65bbc68c4 100644 --- a/lib/database/app_database.g.dart +++ b/lib/database/app_database.g.dart @@ -67,17 +67,13 @@ class _$AppDatabase extends AppDatabase { AnnouncementLocalDao? _announcementDaoInstance; - CanvasDeviceDao? _canvasDeviceDaoInstance; - - SceneDao? _sceneDaoInstance; - Future open( String path, List migrations, [ Callback? callback, ]) async { final databaseOptions = sqflite.OpenDatabaseOptions( - version: 17, + version: 19, onConfigure: (database) async { await database.execute('PRAGMA foreign_keys = ON'); await callback?.onConfigure?.call(database); @@ -98,10 +94,6 @@ class _$AppDatabase extends AppDatabase { 'CREATE TABLE IF NOT EXISTS `DraftCustomerSupport` (`uuid` TEXT NOT NULL, `issueID` TEXT NOT NULL, `type` TEXT NOT NULL, `data` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `reportIssueType` TEXT NOT NULL, `mutedMessages` TEXT NOT NULL, PRIMARY KEY (`uuid`))'); await database.execute( 'CREATE TABLE IF NOT EXISTS `AnnouncementLocal` (`announcementContextId` TEXT NOT NULL, `title` TEXT NOT NULL, `body` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `announceAt` INTEGER NOT NULL, `type` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY (`announcementContextId`))'); - await database.execute( - 'CREATE TABLE IF NOT EXISTS `CanvasDevice` (`id` TEXT NOT NULL, `ip` TEXT NOT NULL, `port` INTEGER NOT NULL, `name` TEXT NOT NULL, `isConnecting` INTEGER NOT NULL, `playingSceneId` TEXT, PRIMARY KEY (`id`))'); - await database.execute( - 'CREATE TABLE IF NOT EXISTS `Scene` (`id` TEXT NOT NULL, `deviceId` TEXT NOT NULL, `isPlaying` INTEGER NOT NULL, `metadata` TEXT NOT NULL, PRIMARY KEY (`id`))'); await callback?.onCreate?.call(database, version); }, @@ -125,17 +117,6 @@ class _$AppDatabase extends AppDatabase { return _announcementDaoInstance ??= _$AnnouncementLocalDao(database, changeListener); } - - @override - CanvasDeviceDao get canvasDeviceDao { - return _canvasDeviceDaoInstance ??= - _$CanvasDeviceDao(database, changeListener); - } - - @override - SceneDao get sceneDao { - return _sceneDaoInstance ??= _$SceneDao(database, changeListener); - } } class _$IdentityDao extends IdentityDao { @@ -438,198 +419,6 @@ class _$AnnouncementLocalDao extends AnnouncementLocalDao { } } -class _$CanvasDeviceDao extends CanvasDeviceDao { - _$CanvasDeviceDao( - this.database, - this.changeListener, - ) : _queryAdapter = QueryAdapter(database), - _canvasDeviceInsertionAdapter = InsertionAdapter( - database, - 'CanvasDevice', - (CanvasDevice item) => { - 'id': item.id, - 'ip': item.ip, - 'port': item.port, - 'name': item.name, - 'isConnecting': item.isConnecting ? 1 : 0, - 'playingSceneId': item.playingSceneId - }), - _canvasDeviceUpdateAdapter = UpdateAdapter( - database, - 'CanvasDevice', - ['id'], - (CanvasDevice item) => { - 'id': item.id, - 'ip': item.ip, - 'port': item.port, - 'name': item.name, - 'isConnecting': item.isConnecting ? 1 : 0, - 'playingSceneId': item.playingSceneId - }), - _canvasDeviceDeletionAdapter = DeletionAdapter( - database, - 'CanvasDevice', - ['id'], - (CanvasDevice item) => { - 'id': item.id, - 'ip': item.ip, - 'port': item.port, - 'name': item.name, - 'isConnecting': item.isConnecting ? 1 : 0, - 'playingSceneId': item.playingSceneId - }); - - final sqflite.DatabaseExecutor database; - - final StreamController changeListener; - - final QueryAdapter _queryAdapter; - - final InsertionAdapter _canvasDeviceInsertionAdapter; - - final UpdateAdapter _canvasDeviceUpdateAdapter; - - final DeletionAdapter _canvasDeviceDeletionAdapter; - - @override - Future> getCanvasDevices() async { - return _queryAdapter.queryList('SELECT * FROM CanvasDevice', - mapper: (Map row) => CanvasDevice( - id: row['id'] as String, - ip: row['ip'] as String, - port: row['port'] as int, - name: row['name'] as String, - isConnecting: (row['isConnecting'] as int) != 0, - playingSceneId: row['playingSceneId'] as String?)); - } - - @override - Future removeAll() async { - await _queryAdapter.queryNoReturn('DELETE FROM CanvasDevice'); - } - - @override - Future insertCanvasDevice(CanvasDevice canvasDevice) async { - await _canvasDeviceInsertionAdapter.insert( - canvasDevice, OnConflictStrategy.replace); - } - - @override - Future insertCanvasDevices(List canvasDevices) async { - await _canvasDeviceInsertionAdapter.insertList( - canvasDevices, OnConflictStrategy.replace); - } - - @override - Future updateCanvasDevice(CanvasDevice canvasDevice) async { - await _canvasDeviceUpdateAdapter.update( - canvasDevice, OnConflictStrategy.abort); - } - - @override - Future deleteCanvasDevice(CanvasDevice canvasDevice) async { - await _canvasDeviceDeletionAdapter.delete(canvasDevice); - } -} - -class _$SceneDao extends SceneDao { - _$SceneDao( - this.database, - this.changeListener, - ) : _queryAdapter = QueryAdapter(database), - _sceneInsertionAdapter = InsertionAdapter( - database, - 'Scene', - (Scene item) => { - 'id': item.id, - 'deviceId': item.deviceId, - 'isPlaying': item.isPlaying ? 1 : 0, - 'metadata': item.metadata - }), - _sceneUpdateAdapter = UpdateAdapter( - database, - 'Scene', - ['id'], - (Scene item) => { - 'id': item.id, - 'deviceId': item.deviceId, - 'isPlaying': item.isPlaying ? 1 : 0, - 'metadata': item.metadata - }); - - final sqflite.DatabaseExecutor database; - - final StreamController changeListener; - - final QueryAdapter _queryAdapter; - - final InsertionAdapter _sceneInsertionAdapter; - - final UpdateAdapter _sceneUpdateAdapter; - - @override - Future> getScenes() async { - return _queryAdapter.queryList('SELECT * FROM Scene', - mapper: (Map row) => Scene( - id: row['id'] as String, - deviceId: row['deviceId'] as String, - metadata: row['metadata'] as String, - isPlaying: (row['isPlaying'] as int) != 0)); - } - - @override - Future> getScenesByDeviceId(String deviceId) async { - return _queryAdapter.queryList('SELECT * FROM Scene WHERE deviceId = ?1', - mapper: (Map row) => Scene( - id: row['id'] as String, - deviceId: row['deviceId'] as String, - metadata: row['metadata'] as String, - isPlaying: (row['isPlaying'] as int) != 0), - arguments: [deviceId]); - } - - @override - Future getSceneById(String id) async { - return _queryAdapter.query('SELECT * FROM Scene WHERE id = ?1', - mapper: (Map row) => Scene( - id: row['id'] as String, - deviceId: row['deviceId'] as String, - metadata: row['metadata'] as String, - isPlaying: (row['isPlaying'] as int) != 0), - arguments: [id]); - } - - @override - Future updateSceneMetadata( - String id, - String metadata, - ) async { - await _queryAdapter.queryNoReturn( - 'UPDATE Scene SET metadata = ?2 WHERE id = ?1', - arguments: [id, metadata]); - } - - @override - Future removeAll() async { - await _queryAdapter.queryNoReturn('DELETE FROM Scene'); - } - - @override - Future insertScene(Scene scene) async { - await _sceneInsertionAdapter.insert(scene, OnConflictStrategy.replace); - } - - @override - Future insertScenes(List scenes) async { - await _sceneInsertionAdapter.insertList(scenes, OnConflictStrategy.replace); - } - - @override - Future updateScene(Scene scene) async { - await _sceneUpdateAdapter.update(scene, OnConflictStrategy.abort); - } -} - // ignore_for_file: unused_element final _dateTimeConverter = DateTimeConverter(); final _tokenOwnersConverter = TokenOwnersConverter(); diff --git a/lib/database/dao/canvas_device_dao.dart b/lib/database/dao/canvas_device_dao.dart deleted file mode 100644 index 63f29bb92..000000000 --- a/lib/database/dao/canvas_device_dao.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; -import 'package:floor/floor.dart'; - -@dao -abstract class CanvasDeviceDao { - @Query('SELECT * FROM CanvasDevice') - Future> getCanvasDevices(); - - @Insert(onConflict: OnConflictStrategy.replace) - Future insertCanvasDevice(CanvasDevice canvasDevice); - - @Insert(onConflict: OnConflictStrategy.replace) - Future insertCanvasDevices(List canvasDevices); - - @update - Future updateCanvasDevice(CanvasDevice canvasDevice); - - @delete - Future deleteCanvasDevice(CanvasDevice canvasDevice); - - @Query('DELETE FROM CanvasDevice') - Future removeAll(); -} - -@dao -abstract class SceneDao { - @Query('SELECT * FROM Scene') - Future> getScenes(); - - @Query('SELECT * FROM Scene WHERE deviceId = :deviceId') - Future> getScenesByDeviceId(String deviceId); - - @Query('SELECT * FROM Scene WHERE id = :id') - Future getSceneById(String id); - - @Query('UPDATE Scene SET metadata = :metadata WHERE id = :id') - Future updateSceneMetadata(String id, String metadata); - - @Insert(onConflict: OnConflictStrategy.replace) - Future insertScene(Scene scene); - - @Insert(onConflict: OnConflictStrategy.replace) - Future insertScenes(List scenes); - - @update - Future updateScene(Scene scene); - - @Query('DELETE FROM Scene') - Future removeAll(); -} diff --git a/lib/gateway/tv_cast_api.dart b/lib/gateway/tv_cast_api.dart new file mode 100644 index 000000000..afc68a6c5 --- /dev/null +++ b/lib/gateway/tv_cast_api.dart @@ -0,0 +1,16 @@ +import 'package:dio/dio.dart'; +import 'package:retrofit/retrofit.dart'; + +part 'tv_cast_api.g.dart'; + +@RestApi(baseUrl: '') +abstract class TvCastApi { + factory TvCastApi(Dio dio, {String baseUrl}) = _TvCastApi; + + @GET('/api/cast') + Future request({ + @Query('locationID') required String locationId, + @Query('topicID') required String topicId, + @Body() required Map body, + }); +} diff --git a/lib/gateway/tv_cast_api.g.dart b/lib/gateway/tv_cast_api.g.dart new file mode 100644 index 000000000..53d82fc33 --- /dev/null +++ b/lib/gateway/tv_cast_api.g.dart @@ -0,0 +1,84 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tv_cast_api.dart'; + +// ************************************************************************** +// RetrofitGenerator +// ************************************************************************** + +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers + +class _TvCastApi implements TvCastApi { + _TvCastApi( + this._dio, { + this.baseUrl, + }); + + final Dio _dio; + + String? baseUrl; + + @override + Future request({ + required String locationId, + required String topicId, + required Map body, + }) async { + const _extra = {}; + final queryParameters = { + r'locationID': locationId, + r'topicID': topicId, + }; + final _headers = {}; + final _data = {}; + _data.addAll(body); + final _result = await _dio.fetch(_setStreamType(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/cast', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = _result.data; + return value; + } + + RequestOptions _setStreamType(RequestOptions requestOptions) { + if (T != dynamic && + !(requestOptions.responseType == ResponseType.bytes || + requestOptions.responseType == ResponseType.stream)) { + if (T == String) { + requestOptions.responseType = ResponseType.plain; + } else { + requestOptions.responseType = ResponseType.json; + } + } + return requestOptions; + } + + String _combineBaseUrls( + String dioBaseUrl, + String? baseUrl, + ) { + if (baseUrl == null || baseUrl.trim().isEmpty) { + return dioBaseUrl; + } + + final url = Uri.parse(baseUrl); + + if (url.isAbsolute) { + return url.toString(); + } + + return Uri.parse(dioBaseUrl).resolveUri(url).toString(); + } +} diff --git a/lib/main.dart b/lib/main.dart index 98b3323e3..62751ee8e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -24,6 +24,7 @@ import 'package:autonomy_flutter/service/navigation_service.dart'; import 'package:autonomy_flutter/service/notification_service.dart'; import 'package:autonomy_flutter/service/remote_config_service.dart'; import 'package:autonomy_flutter/util/au_file_service.dart'; +import 'package:autonomy_flutter/util/canvas_device_adapter.dart'; import 'package:autonomy_flutter/util/custom_route_observer.dart'; import 'package:autonomy_flutter/util/device.dart'; import 'package:autonomy_flutter/util/error_handler.dart'; @@ -116,7 +117,8 @@ Future runFeralFileApp() async { void _registerHiveAdapter() { Hive ..registerAdapter(EthereumPendingTxAmountAdapter()) - ..registerAdapter(EthereumPendingTxListAdapter()); + ..registerAdapter(EthereumPendingTxListAdapter()) + ..registerAdapter(CanvasDeviceAdapter()); } Future _setupApp() async { @@ -154,9 +156,7 @@ Future _setupApp() async { Sentry.configureScope((scope) async { final deviceID = await getDeviceID(); - if (deviceID != null) { - scope.setUser(SentryUser(id: deviceID)); - } + scope.setUser(SentryUser(id: deviceID)); }); //safe delay to wait for onboarding finished diff --git a/lib/screen/detail/artwork_detail_page.dart b/lib/screen/detail/artwork_detail_page.dart index 9803b0771..c4b962ade 100644 --- a/lib/screen/detail/artwork_detail_page.dart +++ b/lib/screen/detail/artwork_detail_page.dart @@ -34,6 +34,7 @@ import 'package:autonomy_flutter/util/au_icons.dart'; import 'package:autonomy_flutter/util/constants.dart'; import 'package:autonomy_flutter/util/file_helper.dart'; import 'package:autonomy_flutter/util/log.dart'; +import 'package:autonomy_flutter/util/playlist_ext.dart'; import 'package:autonomy_flutter/util/string_ext.dart'; import 'package:autonomy_flutter/util/style.dart'; import 'package:autonomy_flutter/util/ui_helper.dart'; @@ -316,10 +317,12 @@ class _ArtworkDetailPageState extends State backgroundColor: Colors.transparent, actions: [ FFCastButton( + displayKey: _getDisplayKey(asset), onDeviceSelected: (device) { if (widget.payload.playlist == null) { final artwork = PlayArtworkV2( token: CastAssetToken(id: asset.id), + duration: 0, ); _canvasDeviceBloc.add( CanvasDeviceCastListArtworkEvent( @@ -427,6 +430,14 @@ class _ArtworkDetailPageState extends State }); } + String _getDisplayKey(AssetToken asset) { + final playlistDisplayKey = widget.payload.playlist?.displayKey; + if (playlistDisplayKey != null) { + return playlistDisplayKey; + } + return asset.id.hashCode.toString(); + } + Widget _artworkInfoIcon() => Semantics( label: 'artworkInfoIcon', child: GestureDetector( @@ -596,7 +607,9 @@ class _ArtworkDetailPageState extends State final showKeyboard = (asset.medium == 'software' || asset.medium == 'other' || (asset.medium?.isEmpty ?? true) || - canvasDeviceState.isCasting) && + canvasDeviceState + .lastSelectedActiveDeviceForKey(_getDisplayKey(asset)) != + null) && !asset.isPostcard; if (!context.mounted) { return; @@ -619,12 +632,14 @@ class _ArtworkDetailPageState extends State icon: SvgPicture.asset('assets/images/keyboard_icon.svg'), onTap: () { Navigator.of(context).pop(); - if (canvasDeviceState.isCasting) { + final castingDevice = canvasDeviceState + .lastSelectedActiveDeviceForKey(_getDisplayKey(asset)); + if (castingDevice != null) { unawaited(Navigator.of(context).pushNamed( AppRouter.keyboardControlPage, arguments: KeyboardControlPagePayload( asset, - [canvasDeviceState.controllingDevice!], + [castingDevice], ), )); } else { diff --git a/lib/screen/detail/preview/canvas_device_bloc.dart b/lib/screen/detail/preview/canvas_device_bloc.dart index 110792d9d..e86a66e56 100644 --- a/lib/screen/detail/preview/canvas_device_bloc.dart +++ b/lib/screen/detail/preview/canvas_device_bloc.dart @@ -8,15 +8,12 @@ import 'dart:async'; import 'package:autonomy_flutter/au_bloc.dart'; -import 'package:autonomy_flutter/model/play_list_model.dart'; -import 'package:autonomy_flutter/service/canvas_client_service.dart'; import 'package:autonomy_flutter/service/canvas_client_service_v2.dart'; -import 'package:autonomy_flutter/service/network_service.dart'; +import 'package:autonomy_flutter/util/cast_request_ext.dart'; import 'package:autonomy_flutter/util/constants.dart'; import 'package:autonomy_flutter/util/device_status_ext.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:collection/collection.dart'; -import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rxdart/transformers.dart'; @@ -37,35 +34,6 @@ class CanvasDeviceAppendDeviceEvent extends CanvasDeviceEvent { CanvasDeviceAppendDeviceEvent(this.device); } -class CanvasDeviceGetControllingStatusEvent extends CanvasDeviceEvent {} - -class CanvasDeviceAddEvent extends CanvasDeviceEvent { - final DeviceState device; - - CanvasDeviceAddEvent(this.device); -} - -class CanvasDeviceCastSingleEvent extends CanvasDeviceEvent { - final CanvasDevice device; - final String tokenId; - - CanvasDeviceCastSingleEvent(this.device, this.tokenId); -} - -class CanvasDeviceCastCollectionEvent extends CanvasDeviceEvent { - final CanvasDevice device; - final PlayListModel playlist; - - CanvasDeviceCastCollectionEvent(this.device, this.playlist); -} - -class CanvasDeviceUnCastingEvent extends CanvasDeviceEvent { - final CanvasDevice device; - final bool isCollection; - - CanvasDeviceUnCastingEvent(this.device, this.isCollection); -} - class CanvasDeviceRotateEvent extends CanvasDeviceEvent { final CanvasDevice device; final bool clockwise; @@ -104,12 +72,6 @@ class CanvasDeviceChangeControlDeviceEvent extends CanvasDeviceEvent { CanvasDeviceChangeControlDeviceEvent(this.newDevice, this.artwork); } -class CanvasDeviceCancelCastingEvent extends CanvasDeviceEvent { - final CanvasDevice device; - - CanvasDeviceCancelCastingEvent(this.device); -} - class CanvasDevicePauseCastingEvent extends CanvasDeviceEvent { final CanvasDevice device; @@ -150,27 +112,41 @@ class CanvasDeviceCastExhibitionEvent extends CanvasDeviceEvent { class CanvasDeviceState { final List devices; - final Map? controllingDeviceStatus; + final Map canvasDeviceStatus; + final Map lastSelectedActiveDeviceMap; // final String sceneId; final RPCError? rpcError; CanvasDeviceState({ required this.devices, - this.controllingDeviceStatus, + Map? canvasDeviceStatus, + Map? lastSelectedActiveDeviceMap, this.rpcError, - }); + }) : canvasDeviceStatus = canvasDeviceStatus ?? {}, + lastSelectedActiveDeviceMap = lastSelectedActiveDeviceMap ?? {}; CanvasDeviceState copyWith( {List? devices, Map? controllingDeviceStatus, + Map? lastActiveDevice, RPCError? rpcError}) => CanvasDeviceState( devices: devices ?? this.devices, - controllingDeviceStatus: - controllingDeviceStatus ?? this.controllingDeviceStatus, + canvasDeviceStatus: controllingDeviceStatus ?? canvasDeviceStatus, + lastSelectedActiveDeviceMap: + lastActiveDevice ?? lastSelectedActiveDeviceMap, rpcError: rpcError ?? this.rpcError); + CanvasDeviceState updateOnCast( + {required CanvasDevice device, required String displayKey}) { + lastSelectedActiveDeviceMap.removeWhere((key, value) => value == device); + lastSelectedActiveDeviceMap[displayKey] = device; + return copyWith( + lastActiveDevice: lastSelectedActiveDeviceMap, + ); + } + CanvasDeviceState replaceDeviceState( {required CanvasDevice device, required DeviceState deviceState}) { final newDeviceState = devices.map((e) { @@ -182,32 +158,50 @@ class CanvasDeviceState { return copyWith(devices: newDeviceState); } - Duration? get castingSpeed { - final controllingDevice = controllingDeviceStatus?.keys.first; - if (controllingDevice == null) { - return null; + CanvasDevice? lastSelectedActiveDeviceForKey(String key) { + final lastActiveDevice = lastSelectedActiveDeviceMap[key]; + if (lastActiveDevice != null) { + if (isDeviceAlive(lastActiveDevice)) { + return lastActiveDevice; + } else { + lastSelectedActiveDeviceMap.remove(key); + } } - final status = controllingDeviceStatus![controllingDevice]; - if (status == null || status.artworks.isEmpty) { - return null; + final activeDevice = _activeDeviceForKey(key); + if (activeDevice != null) { + lastSelectedActiveDeviceMap[key] = activeDevice; } - return Duration(milliseconds: status.artworks.first.duration); + return activeDevice; } - CanvasDevice? get controllingDevice => devices - .firstWhereOrNull((deviceState) => - controllingDeviceStatus?.keys.contains(deviceState.device.id) ?? - false) - ?.device; + Duration? castingSpeed(String key) { + CanvasDevice? lastActiveDevice = lastSelectedActiveDeviceForKey(key); + final lastActiveDeviceStatus = + canvasDeviceStatus[lastActiveDevice?.deviceId]; + final durationInMilisecond = + lastActiveDeviceStatus?.artworks.first.duration; + if (durationInMilisecond != null) { + return Duration(milliseconds: durationInMilisecond); + } + return null; + } - bool isDeviceControlling(CanvasDevice device) => - controllingDeviceStatus?.keys.contains(device.id) ?? false; + CheckDeviceStatusReply? statusOf(CanvasDevice device) => + canvasDeviceStatus[device.deviceId]; - List get controllingDevices => - devices.where((element) => isDeviceControlling(element.device)).toList(); + bool isDeviceAlive(CanvasDevice device) { + final status = statusOf(device); + return status != null; + } - bool get isCasting => - devices.any((element) => element.device == controllingDevice); + CanvasDevice? _activeDeviceForKey(String key) { + final id = canvasDeviceStatus.entries + .firstWhereOrNull((element) => element.value.playingArtworkKey == key) + ?.key; + return devices + .firstWhereOrNull((element) => element.device.deviceId == id) + ?.device; + } } class DeviceState { @@ -246,62 +240,30 @@ EventTransformer debounceSequential(Duration duration) => (events, mapper) => events.throttleTime(duration).asyncExpand(mapper); class CanvasDeviceBloc extends AuBloc { - final CanvasClientService _canvasClientService; final CanvasClientServiceV2 _canvasClientServiceV2; - final NetworkService _networkService; final Map _deviceRetryCount = {}; // constructor - CanvasDeviceBloc(this._canvasClientService, this._canvasClientServiceV2, - this._networkService) + CanvasDeviceBloc(this._canvasClientServiceV2) : super(CanvasDeviceState(devices: [])) { on( (event, emit) async { - if (!_networkService.isWifi) { - if (event.retry) { - _networkService.addListener((result) { - if (result == ConnectivityResult.wifi) { - log.info('CanvasDeviceBloc: retry get devices'); - add(CanvasDeviceGetDevicesEvent()); - } - }, id: NetworkService.canvasBlocListenerId); - } - - log.info('CanvasDeviceBloc: not using wifi, skip getting devices'); - return; - } log.info('CanvasDeviceBloc: adding devices'); - unawaited(_networkService - .removeListener(NetworkService.canvasBlocListenerId)); try { final devices = await _canvasClientServiceV2.scanDevices(); - final stateControllingDeviceStatus = state.controllingDeviceStatus; - - final controllingdevice = state.controllingDevice; Map? controllingDeviceStatus = {}; - if (controllingdevice == null) { - controllingDeviceStatus = devices.controllingDevices; - } else { - if (devices - .any((element) => element.first.id == controllingdevice.id)) { - controllingDeviceStatus = stateControllingDeviceStatus; - } else { - controllingDeviceStatus = devices.controllingDevices; - } - } + controllingDeviceStatus = devices.controllingDevices; final newState = state.copyWith( devices: devices.map((e) => DeviceState(device: e.first)).toList(), controllingDeviceStatus: controllingDeviceStatus, ); log.info('CanvasDeviceBloc: get devices: ${newState.devices.length}, ' - 'controllingDeviceStatus: ${newState.controllingDeviceStatus}'); + 'controllingDeviceStatus: ${newState.canvasDeviceStatus}'); emit(newState); - await _canvasClientServiceV2.connectToDevice( - newState.controllingDevices.map((e) => e.device).toList().first); } catch (e) { log.info('CanvasDeviceBloc: error while get devices: $e'); unawaited(Sentry.captureException(e)); @@ -312,82 +274,18 @@ class CanvasDeviceBloc extends AuBloc { ); on((event, emit) async { - final newState = state.copyWith( - devices: state.devices - ..removeWhere((element) => element.device.id == event.device.id) - ..add(DeviceState(device: event.device))); - emit(newState); - }); - - on((event, emit) async { final newState = state.copyWith( devices: state.devices ..removeWhere( - (element) => element.device.id == event.device.device.id) - ..add(DeviceState(device: event.device.device, isPlaying: false))); + (element) => element.device.deviceId == event.device.deviceId) + ..add(DeviceState(device: event.device))); emit(newState); }); - on((event, emit) async { - final device = event.device; - try { - emit(state.replaceDeviceState( - device: device, - deviceState: DeviceState( - device: device, - ))); - final connected = await _canvasClientService.connectToDevice(device); - if (!connected) { - throw Exception('Failed to connect to device'); - } - final ok = - await _canvasClientService.castSingleArtwork(device, event.tokenId); - if (!ok) { - throw Exception('Failed to cast to device'); - } - emit(state.replaceDeviceState( - device: device, deviceState: DeviceState(device: device))); - } catch (_) { - emit(state.replaceDeviceState( - device: device, deviceState: DeviceState(device: device))); - } - }); - - on((event, emit) async { - final device = event.device; - try { - emit(state.replaceDeviceState( - device: device, deviceState: DeviceState(device: device))); - final connected = await _canvasClientService.connectToDevice(device); - if (!connected) { - throw Exception('Failed to connect to device'); - } - final ok = - await _canvasClientService.castCollection(device, event.playlist); - if (!ok) { - throw Exception('Failed to cast to device'); - } - emit(state.replaceDeviceState( - device: device, deviceState: DeviceState(device: device))); - } catch (_) { - emit(state.replaceDeviceState( - device: device, deviceState: DeviceState(device: device))); - } - }); - - on((event, emit) async { - final device = event.device; - try { - await _canvasClientService.unCastSingleArtwork(device); - emit(state.replaceDeviceState( - device: device, deviceState: DeviceState(device: device))); - } catch (_) {} - }); - on((event, emit) async { final device = event.device; try { - await _canvasClientService.rotateCanvas(device, + await _canvasClientServiceV2.rotateCanvas(device, clockwise: event.clockwise); } catch (_) {} }); @@ -401,7 +299,7 @@ class CanvasDeviceBloc extends AuBloc { await Future.forEach(devices, (device) async { try { log.info('CanvasDeviceBloc: disconnect device: ' - '${device.id}, ${device.name}, ${device.ip}'); + '${device.deviceId}, ${device.name}, ${device.deviceId}'); if (event.callRPC) { await _canvasClientServiceV2.disconnectDevice(device); } @@ -415,16 +313,16 @@ class CanvasDeviceBloc extends AuBloc { on((event, emit) async { final controllingDevice = event.device; - final numberOfRetry = _deviceRetryCount[controllingDevice.id] ?? 0; + final numberOfRetry = _deviceRetryCount[controllingDevice.deviceId] ?? 0; log.info('CanvasDeviceBloc: retry connect to device: $numberOfRetry'); if (numberOfRetry < maxRetryCount) { await Future.delayed(const Duration(milliseconds: 500)); - _deviceRetryCount[controllingDevice.id] = numberOfRetry + 1; + _deviceRetryCount[controllingDevice.deviceId] = numberOfRetry + 1; final isSuccess = await _canvasClientServiceV2.connectToDevice(controllingDevice); log.info('CanvasDeviceBloc: retry connect to device: $isSuccess'); if (isSuccess) { - _deviceRetryCount.remove(controllingDevice.id); + _deviceRetryCount.remove(controllingDevice.deviceId); } else { add(CanvasDeviceDisconnectEvent([controllingDevice])); } @@ -441,19 +339,23 @@ class CanvasDeviceBloc extends AuBloc { if (!ok) { throw Exception('Failed to cast to device'); } - final currentDeviceState = state.devices - .firstWhereOrNull((element) => element.device.id == device.id); + final currentDeviceState = state.devices.firstWhereOrNull( + (element) => element.device.deviceId == device.deviceId); if (currentDeviceState == null) { throw Exception('Device not found'); } final status = await _canvasClientServiceV2.getDeviceCastingStatus(device); + final newStatus = state.canvasDeviceStatus; + newStatus[device.deviceId] = status; + final displayKey = event.artwork.playArtworksHashCode.toString(); emit( state + .updateOnCast(device: device, displayKey: displayKey) .replaceDeviceState( device: device, deviceState: currentDeviceState.copyWith(isPlaying: true)) - .copyWith(controllingDeviceStatus: {device.id: status}), + .copyWith(controllingDeviceStatus: newStatus), ); } catch (_) { emit(state.replaceDeviceState( @@ -469,19 +371,23 @@ class CanvasDeviceBloc extends AuBloc { if (!ok) { throw Exception('Failed to cast to device'); } - final currentDeviceState = state.devices - .firstWhereOrNull((element) => element.device.id == device.id); + final currentDeviceState = state.devices.firstWhereOrNull( + (element) => element.device.deviceId == device.deviceId); if (currentDeviceState == null) { throw Exception('Device not found'); } final status = await _canvasClientServiceV2.getDeviceCastingStatus(device); + final newStatus = state.canvasDeviceStatus; + newStatus[device.deviceId] = status; + final displayKey = event.castRequest.displayKey; emit( state + .updateOnCast(device: device, displayKey: displayKey) .replaceDeviceState( device: device, deviceState: currentDeviceState.copyWith(isPlaying: true)) - .copyWith(controllingDeviceStatus: {device.id: status}), + .copyWith(controllingDeviceStatus: newStatus), ); } catch (_) { emit(state.replaceDeviceState( @@ -489,26 +395,11 @@ class CanvasDeviceBloc extends AuBloc { } }); - on((event, emit) async { - final device = event.device; - try { - final currentDeviceState = state.devices - .firstWhereOrNull((element) => element.device.id == device.id); - if (currentDeviceState == null) { - throw Exception('Device not found'); - } - await _canvasClientServiceV2.cancelCasting(device); - emit(state.replaceDeviceState( - device: device, - deviceState: currentDeviceState.copyWith(isPlaying: false))); - } catch (_) {} - }); - on((event, emit) async { final device = event.device; try { - final currentDeviceState = state.devices - .firstWhereOrNull((element) => element.device.id == device.id); + final currentDeviceState = state.devices.firstWhereOrNull( + (element) => element.device.deviceId == device.deviceId); if (currentDeviceState == null) { throw Exception('Device not found'); } @@ -522,8 +413,8 @@ class CanvasDeviceBloc extends AuBloc { on((event, emit) async { final device = event.device; try { - final currentDeviceState = state.devices - .firstWhereOrNull((element) => element.device.id == device.id); + final currentDeviceState = state.devices.firstWhereOrNull( + (element) => element.device.deviceId == device.deviceId); if (currentDeviceState == null) { throw Exception('Device not found'); } @@ -537,8 +428,8 @@ class CanvasDeviceBloc extends AuBloc { on((event, emit) async { final device = event.device; try { - final currentDeviceState = state.devices - .firstWhereOrNull((element) => element.device.id == device.id); + final currentDeviceState = state.devices.firstWhereOrNull( + (element) => element.device.deviceId == device.deviceId); if (currentDeviceState == null) { throw Exception('Device not found'); } @@ -552,8 +443,8 @@ class CanvasDeviceBloc extends AuBloc { on((event, emit) async { final device = event.device; try { - final currentDeviceState = state.devices - .firstWhereOrNull((element) => element.device.id == device.id); + final currentDeviceState = state.devices.firstWhereOrNull( + (element) => element.device.deviceId == device.deviceId); if (currentDeviceState == null) { throw Exception('Device not found'); } @@ -574,25 +465,22 @@ class CanvasDeviceBloc extends AuBloc { try { final response = await _canvasClientServiceV2.updateDuration(device, artworks); - final currentDeviceState = state.devices - .firstWhereOrNull((element) => element.device.id == device.id); + final currentDeviceState = state.devices.firstWhereOrNull( + (element) => element.device.deviceId == device.deviceId); if (currentDeviceState == null) { throw Exception('Device not found'); } - final controllingStatus = state.controllingDeviceStatus?[device.id]; + final controllingStatus = state.canvasDeviceStatus[device.deviceId]; if (controllingStatus == null) { throw Exception('Device not found'); } - final newControllingStatus = CheckDeviceStatusReply(); - newControllingStatus.artworks.clear(); - newControllingStatus.artworks.addAll(artworks); - newControllingStatus + final newControllingStatus = CheckDeviceStatusReply(artworks: artworks) ..startTime = response.startTime ..connectedDevice = controllingStatus.connectedDevice; final controllingDeviceStatus = - state.controllingDeviceStatus?.map((key, value) { - if (key == device.id) { + state.canvasDeviceStatus.map((key, value) { + if (key == device.deviceId) { return MapEntry(key, newControllingStatus); } return MapEntry(key, value); diff --git a/lib/screen/detail/preview/keyboard_control_page.dart b/lib/screen/detail/preview/keyboard_control_page.dart index 98c586120..c3920dcf6 100644 --- a/lib/screen/detail/preview/keyboard_control_page.dart +++ b/lib/screen/detail/preview/keyboard_control_page.dart @@ -5,7 +5,7 @@ import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/main.dart'; import 'package:autonomy_flutter/screen/app_router.dart'; import 'package:autonomy_flutter/screen/detail/preview/touchpad_page.dart'; -import 'package:autonomy_flutter/service/canvas_client_service.dart'; +import 'package:autonomy_flutter/service/canvas_client_service_v2.dart'; import 'package:autonomy_flutter/util/style.dart'; import 'package:autonomy_flutter/view/artwork_common_widget.dart'; import 'package:autonomy_flutter/view/responsive.dart'; @@ -157,7 +157,7 @@ class _KeyboardControlPageState extends State final code = text[text.length - 1]; _textController.text = ''; final devices = widget.payload.devices; - await injector() + await injector() .sendKeyBoard(devices, code.codeUnitAt(0)); }, ), diff --git a/lib/screen/exhibition_details/exhibition_detail_page.dart b/lib/screen/exhibition_details/exhibition_detail_page.dart index ca161050a..1ab38e56f 100644 --- a/lib/screen/exhibition_details/exhibition_detail_page.dart +++ b/lib/screen/exhibition_details/exhibition_detail_page.dart @@ -9,6 +9,7 @@ import 'package:autonomy_flutter/screen/exhibition_details/exhibition_detail_blo import 'package:autonomy_flutter/screen/exhibition_details/exhibition_detail_state.dart'; import 'package:autonomy_flutter/service/metric_client_service.dart'; import 'package:autonomy_flutter/util/constants.dart'; +import 'package:autonomy_flutter/util/exhibition_ext.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/view/back_appbar.dart'; import 'package:autonomy_flutter/view/cast_button.dart'; @@ -37,13 +38,12 @@ class _ExhibitionDetailPageState extends State with AfterLayoutMixin { late final ExhibitionDetailBloc _exBloc; - // late final CanvasDeviceBloc _canvasDeviceBloc; final _metricClientService = injector(); final _canvasDeviceBloc = injector(); late final PageController _controller; int _currentIndex = 0; - late int _carouselIndex; + int _carouselIndex = 0; @override void initState() { @@ -52,7 +52,6 @@ class _ExhibitionDetailPageState extends State _exBloc.add(GetExhibitionDetailEvent( widget.payload.exhibitions[widget.payload.index].id)); _controller = PageController(); - _carouselIndex = 0; } @override @@ -134,13 +133,15 @@ class _ExhibitionDetailPageState extends State } void _stream(Exhibition exhibition) { - final controllingDevice = _canvasDeviceBloc.state.controllingDevice; log.info('onPageChanged: $_currentIndex'); - if (controllingDevice != null) { + final displayKey = exhibition.displayKey; + final lastSelectedDevice = + _canvasDeviceBloc.state.lastSelectedActiveDeviceForKey(displayKey); + if (lastSelectedDevice != null) { final request = _getCastExhibitionRequest(exhibition); log.info('onPageChanged: request: $request'); _canvasDeviceBloc.add( - CanvasDeviceCastExhibitionEvent(controllingDevice, request), + CanvasDeviceCastExhibitionEvent(lastSelectedDevice, request), ); } } @@ -190,16 +191,10 @@ class _ExhibitionDetailPageState extends State viewportFraction: 0.76, enableInfiniteScroll: false, enlargeCenterPage: true, + initialPage: _carouselIndex, onPageChanged: (index, reason) { _carouselIndex = index; - final controllingDevice = - _canvasDeviceBloc.state.controllingDevice; - final request = _getCastExhibitionRequest(exhibition); - if (controllingDevice != null) { - _canvasDeviceBloc.add( - CanvasDeviceCastExhibitionEvent(controllingDevice, request), - ); - } + _stream(exhibition); }, ), ), @@ -214,6 +209,7 @@ class _ExhibitionDetailPageState extends State ? Padding( padding: const EdgeInsets.only(right: 14, bottom: 10, top: 10), child: FFCastButton( + displayKey: exhibition.id, onDeviceSelected: (device) async { final request = _getCastExhibitionRequest(exhibition); _canvasDeviceBloc.add( @@ -225,22 +221,22 @@ class _ExhibitionDetailPageState extends State : null, ); - Pair _getCurrentCatalogInfo( + Pair _getCurrentCatalogInfo( Exhibition exhibition) { - ExhibitionKatalog? catalog; + ExhibitionCatalog? catalog; String? catalogId; switch (_currentIndex) { case 0: - catalog = ExhibitionKatalog.HOME; + catalog = ExhibitionCatalog.home; case 1: if (_carouselIndex == 0) { - catalog = ExhibitionKatalog.CURATOR_NOTE; + catalog = ExhibitionCatalog.curatorNote; } else { - catalog = ExhibitionKatalog.RESOURCE; + catalog = ExhibitionCatalog.resource; catalogId = exhibition.posts![_carouselIndex - 1].id; } default: - catalog = ExhibitionKatalog.ARTWORK; + catalog = ExhibitionCatalog.artwork; final seriesIndex = _currentIndex - 2; final currentArtwork = exhibition.series?[seriesIndex].artwork?.id; catalogId = currentArtwork; @@ -250,57 +246,17 @@ class _ExhibitionDetailPageState extends State CastExhibitionRequest _getCastExhibitionRequest(Exhibition exhibition) { final exhibitionId = exhibition.id; - final katalogInfo = _getCurrentCatalogInfo(exhibition); - final katalog = katalogInfo.first; - final katalogId = katalogInfo.second; + final catalogInfo = _getCurrentCatalogInfo(exhibition); + final catalog = catalogInfo.first; + final catalogId = catalogInfo.second; CastExhibitionRequest request = CastExhibitionRequest( exhibitionId: exhibitionId, - katalog: katalog, - katalogId: katalogId, + catalog: catalog, + catalogId: catalogId, ); return request; } - // Future _onCastTap( - // BuildContext context, ExhibitionDetail exhibitionDetail) async { - // if (exhibitionDetail.artworks == null || - // exhibitionDetail.artworks!.isEmpty) { - // return; - // } - // final tokenIds = exhibitionDetail.artworks - // ?.map((e) => exhibitionDetail.getArtworkTokenId(e)!) - // .toList(); - // final sceneId = exhibitionDetail.exhibition.id; - // final playlistModel = PlayListModel( - // name: exhibitionDetail.exhibition.title, - // id: sceneId, - // thumbnailURL: exhibitionDetail.exhibition.coverUrl, - // tokenIDs: tokenIds, - // playControlModel: PlayControlModel(timer: 30), - // ); - // await UIHelper.showFlexibleDialog( - // context, - // BlocProvider.value( - // value: _canvasDeviceBloc, - // child: CanvasDeviceView( - // sceneId: sceneId, - // isCollection: true, - // playlist: playlistModel, - // onClose: () { - // Navigator.of(context).pop(); - // }, - // ), - // ), - // isDismissible: true, - // ); - // await _fetchDevice(sceneId); - // } - - // Future _fetchDevice(String exhibitionId) async { - // _canvasDeviceBloc - // .add(CanvasDeviceGetDevicesEvent(exhibitionId, syncAll: false)); - // } - @override FutureOr afterFirstLayout(BuildContext context) { _metricClientService.addEvent( diff --git a/lib/screen/exhibitions/exhibitions_page.dart b/lib/screen/exhibitions/exhibitions_page.dart index c54814855..4f2271066 100644 --- a/lib/screen/exhibitions/exhibitions_page.dart +++ b/lib/screen/exhibitions/exhibitions_page.dart @@ -7,7 +7,6 @@ import 'package:autonomy_flutter/model/ff_exhibition.dart'; import 'package:autonomy_flutter/screen/app_router.dart'; import 'package:autonomy_flutter/screen/bloc/subscription/subscription_bloc.dart'; import 'package:autonomy_flutter/screen/bloc/subscription/subscription_state.dart'; -import 'package:autonomy_flutter/screen/detail/preview/canvas_device_bloc.dart'; import 'package:autonomy_flutter/screen/exhibition_details/exhibition_detail_page.dart'; import 'package:autonomy_flutter/screen/exhibitions/exhibitions_bloc.dart'; import 'package:autonomy_flutter/screen/exhibitions/exhibitions_state.dart'; @@ -22,7 +21,6 @@ import 'package:autonomy_flutter/view/primary_button.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; -import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -43,7 +41,6 @@ class ExhibitionsPageState extends State with RouteAware { final _navigationService = injector(); final _iapService = injector(); static const _padding = 14.0; - final _canvasDeviceBloc = injector(); static const _exhibitionInfoDivideWidth = 20.0; // initState @@ -137,21 +134,10 @@ class ExhibitionsPageState extends State with RouteAware { } } + if (!context.mounted) { + return; + } if (exhibition.canViewDetails && index >= 0) { - final device = _canvasDeviceBloc.state.controllingDevice; - if (device != null) { - final castRequest = CastExhibitionRequest( - exhibitionId: exhibition.id, - katalog: ExhibitionKatalog.HOME, - ); - _canvasDeviceBloc.add( - CanvasDeviceCastExhibitionEvent(device, castRequest)); - } - - if (!context.mounted) { - return; - } - await Navigator.of(context).pushNamed( AppRouter.exhibitionDetailPage, arguments: ExhibitionDetailPayload( diff --git a/lib/screen/feralfile_artwork_preview/feralfile_artwork_preview_page.dart b/lib/screen/feralfile_artwork_preview/feralfile_artwork_preview_page.dart index bd75214d5..cdeb0558e 100644 --- a/lib/screen/feralfile_artwork_preview/feralfile_artwork_preview_page.dart +++ b/lib/screen/feralfile_artwork_preview/feralfile_artwork_preview_page.dart @@ -48,6 +48,9 @@ class _FeralFileArtworkPreviewPageState context, onBack: () => Navigator.pop(context), action: FFCastButton( + /// can not get url of PlayArtworkV2, + /// so use exhibitionId as a temporary solution + displayKey: widget.payload.artwork.series?.exhibitionID ?? '', onDeviceSelected: _onDeviceSelected, ), ), @@ -76,8 +79,8 @@ class _FeralFileArtworkPreviewPageState final artworkId = widget.payload.artwork.id; final request = CastExhibitionRequest( exhibitionId: exhibitionId, - katalog: ExhibitionKatalog.ARTWORK, - katalogId: artworkId, + catalog: ExhibitionCatalog.artwork, + catalogId: artworkId, ); _canvasDeviceBloc.add(CanvasDeviceCastExhibitionEvent(device, request)); } diff --git a/lib/screen/feralfile_series/feralfile_series_page.dart b/lib/screen/feralfile_series/feralfile_series_page.dart index a112f3258..0e2fc4dbc 100644 --- a/lib/screen/feralfile_series/feralfile_series_page.dart +++ b/lib/screen/feralfile_series/feralfile_series_page.dart @@ -8,6 +8,7 @@ import 'package:autonomy_flutter/screen/feralfile_artwork_preview/feralfile_artw import 'package:autonomy_flutter/screen/feralfile_series/feralfile_series_bloc.dart'; import 'package:autonomy_flutter/screen/feralfile_series/feralfile_series_state.dart'; import 'package:autonomy_flutter/service/feralfile_service.dart'; +import 'package:autonomy_flutter/util/series_ext.dart'; import 'package:autonomy_flutter/util/style.dart'; import 'package:autonomy_flutter/view/back_appbar.dart'; import 'package:autonomy_flutter/view/ff_artwork_thumbnail_view.dart'; @@ -130,16 +131,18 @@ class _FeralFileSeriesPageState extends State { _axisSpacing * 2) ~/ 3, onTap: () async { - final controllingDevice = - _canvasDeviceBloc.state.controllingDevice; - if (controllingDevice != null) { + final displayKey = series.displayKey; + final lastSelectedCanvasDevice = _canvasDeviceBloc.state + .lastSelectedActiveDeviceForKey(displayKey); + + if (lastSelectedCanvasDevice != null) { final castRequest = CastExhibitionRequest( exhibitionId: series.exhibitionID, - katalog: ExhibitionKatalog.ARTWORK, - katalogId: artwork.id); + catalog: ExhibitionCatalog.artwork, + catalogId: artwork.id); _canvasDeviceBloc.add( CanvasDeviceCastExhibitionEvent( - controllingDevice, + lastSelectedCanvasDevice, castRequest, ), ); diff --git a/lib/screen/home/collection_home_page.dart b/lib/screen/home/collection_home_page.dart index e24e32738..346ee4028 100644 --- a/lib/screen/home/collection_home_page.dart +++ b/lib/screen/home/collection_home_page.dart @@ -14,7 +14,6 @@ import 'package:autonomy_flutter/model/blockchain.dart'; import 'package:autonomy_flutter/screen/app_router.dart'; import 'package:autonomy_flutter/screen/bloc/identity/identity_bloc.dart'; import 'package:autonomy_flutter/screen/detail/artwork_detail_page.dart'; -import 'package:autonomy_flutter/screen/detail/preview/canvas_device_bloc.dart'; import 'package:autonomy_flutter/screen/home/home_bloc.dart'; import 'package:autonomy_flutter/screen/home/home_state.dart'; import 'package:autonomy_flutter/screen/interactive_postcard/postcard_detail_page.dart'; @@ -37,15 +36,12 @@ import 'package:autonomy_flutter/util/style.dart'; import 'package:autonomy_flutter/util/token_ext.dart'; import 'package:autonomy_flutter/view/artwork_common_widget.dart'; import 'package:autonomy_flutter/view/back_appbar.dart'; -import 'package:autonomy_flutter/view/cast_button.dart'; import 'package:autonomy_flutter/view/get_started_banner.dart'; import 'package:autonomy_flutter/view/header.dart'; import 'package:autonomy_flutter/view/responsive.dart'; -import 'package:autonomy_flutter/view/stream_common_widget.dart'; import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; -import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_fgbg/flutter_fgbg.dart'; @@ -77,7 +73,6 @@ class CollectionHomePageState extends State final nftBloc = injector().nftBloc; late bool _showPostcardBanner; final _identityBloc = injector(); - late CanvasDeviceBloc _canvasDeviceBloc; @override void initState() { @@ -89,7 +84,6 @@ class CollectionHomePageState extends State _controller = ScrollController()..addListener(_scrollListenerToLoadMore); unawaited(_configurationService.setAutoShowPostcard(true)); context.read().add(CheckReviewAppEvent()); - _canvasDeviceBloc = injector.get(); unawaited(injector().setup()); } @@ -223,34 +217,12 @@ class CollectionHomePageState extends State ); } - void castToken(CanvasDevice device, String tokenId) { - final token = CastAssetToken(id: tokenId); - final playArtwork = PlayArtworkV2(token: token); - _canvasDeviceBloc.add(CanvasDeviceChangeControlDeviceEvent( - device, - [playArtwork], - )); - } - Widget _header(BuildContext context) { final paddingTop = MediaQuery.of(context).viewPadding.top; return Padding( padding: EdgeInsets.only(top: paddingTop), child: HeaderView( title: 'collection'.tr(), - action: FFCastButton( - onDeviceSelected: (device) async { - try { - final firstTokens = - _updateTokens(nftBloc.state.tokens.items).firstOrNull; - if (firstTokens != null) { - castToken(device, firstTokens.id); - } - } catch (e) { - log.info('Failed to cast artwork: $e'); - } - }, - ), ), ); } @@ -467,18 +439,6 @@ class CollectionHomePageState extends State return; } - final id = asset.id; - final duration = speedValues.values.first.inMilliseconds; - final playArtwork = - PlayArtworkV2(token: CastAssetToken(id: id), duration: duration); - final device = _canvasDeviceBloc.state.controllingDevice; - if (device != null) { - _canvasDeviceBloc.add(CanvasDeviceChangeControlDeviceEvent( - device, - [playArtwork], - )); - } - final index = tokens .where((e) => e.pending != true || e.hasMetadata) .toList() diff --git a/lib/screen/home/home_navigation_page.dart b/lib/screen/home/home_navigation_page.dart index 8a704289c..5c46659ff 100644 --- a/lib/screen/home/home_navigation_page.dart +++ b/lib/screen/home/home_navigation_page.dart @@ -6,7 +6,6 @@ // import 'dart:async'; -import 'dart:io'; import 'package:after_layout/after_layout.dart'; import 'package:autonomy_flutter/common/injector.dart'; @@ -45,7 +44,6 @@ import 'package:autonomy_flutter/util/au_icons.dart'; import 'package:autonomy_flutter/util/constants.dart'; import 'package:autonomy_flutter/util/dio_util.dart'; import 'package:autonomy_flutter/util/inapp_notifications.dart'; -import 'package:autonomy_flutter/util/local_network_helper.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/util/style.dart'; import 'package:autonomy_flutter/util/ui_helper.dart'; @@ -316,18 +314,6 @@ class HomeNavigationPageState extends State WidgetsBinding.instance.addObserver(this); _fgbgSubscription = FGBGEvents.stream.listen(_handleForeBackground); unawaited(_syncArtist()); - - unawaited(_requestLocalNetworkPermission()); - } - - Future _requestLocalNetworkPermission() async { - if (Platform.isIOS) { - final didRequest = _configurationService.didGetLocalNetworkPermission(); - if (!didRequest) { - final result = await LocalNetworkHelper.requestLocalNetworkPermission(); - await _configurationService.setDidGetLocalNetworkPermission(result); - } - } } Future _syncArtist() async { diff --git a/lib/screen/playlists/add_new_playlist/add_new_playlist.dart b/lib/screen/playlists/add_new_playlist/add_new_playlist.dart index 5b61421da..19357eae9 100644 --- a/lib/screen/playlists/add_new_playlist/add_new_playlist.dart +++ b/lib/screen/playlists/add_new_playlist/add_new_playlist.dart @@ -166,7 +166,7 @@ class _AddNewPlaylistScreenState extends State final isDone = playlistName.isNotEmpty && selectedIDs?.isNotEmpty == true; return Scaffold( - backgroundColor: theme.colorScheme.background, + backgroundColor: AppColor.white, appBar: getCustomDoneAppBar( context, title: TextFieldWidget( diff --git a/lib/screen/playlists/list_playlists/list_playlists.dart b/lib/screen/playlists/list_playlists/list_playlists.dart index e591e7dba..3a71e392a 100644 --- a/lib/screen/playlists/list_playlists/list_playlists.dart +++ b/lib/screen/playlists/list_playlists/list_playlists.dart @@ -4,18 +4,15 @@ import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/main.dart'; import 'package:autonomy_flutter/model/play_list_model.dart'; import 'package:autonomy_flutter/screen/app_router.dart'; -import 'package:autonomy_flutter/screen/detail/preview/canvas_device_bloc.dart'; import 'package:autonomy_flutter/screen/playlists/view_playlist/view_playlist.dart'; import 'package:autonomy_flutter/service/configuration_service.dart'; import 'package:autonomy_flutter/util/au_icons.dart'; import 'package:autonomy_flutter/util/collection_ext.dart'; import 'package:autonomy_flutter/view/artwork_common_widget.dart'; -import 'package:autonomy_flutter/view/stream_common_widget.dart'; import 'package:autonomy_flutter/view/title_text.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; -import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; import 'package:flutter/material.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; @@ -129,22 +126,6 @@ class _ListPlaylistsScreenState extends State AppRouter.viewPlayListPage, arguments: ViewPlaylistScreenPayload(playListModel: playlist), )); - final tokenIds = playlist.tokenIDs; - if (tokenIds != null && tokenIds.isNotEmpty) { - final bloc = injector.get(); - final controllingDevice = bloc.state.controllingDevice; - if (controllingDevice != null) { - final duration = speedValues.values.first; - final List castArtworks = tokenIds - .map((e) => PlayArtworkV2( - token: CastAssetToken(id: e), - duration: duration.inMilliseconds, - )) - .toList(); - bloc.add(CanvasDeviceChangeControlDeviceEvent( - controllingDevice, castArtworks)); - } - } } } diff --git a/lib/screen/playlists/view_playlist/view_playlist.dart b/lib/screen/playlists/view_playlist/view_playlist.dart index 70928df9d..be3a95bda 100644 --- a/lib/screen/playlists/view_playlist/view_playlist.dart +++ b/lib/screen/playlists/view_playlist/view_playlist.dart @@ -18,6 +18,7 @@ import 'package:autonomy_flutter/util/asset_token_ext.dart'; import 'package:autonomy_flutter/util/constants.dart'; import 'package:autonomy_flutter/util/iterable_ext.dart'; import 'package:autonomy_flutter/util/log.dart'; +import 'package:autonomy_flutter/util/playlist_ext.dart'; import 'package:autonomy_flutter/util/token_ext.dart'; import 'package:autonomy_flutter/util/ui_helper.dart'; import 'package:autonomy_flutter/view/artwork_common_widget.dart'; @@ -231,23 +232,26 @@ class _ViewPlaylistScreenState extends State { width: 24, )), ], - const SizedBox(width: 15), - FFCastButton( - onDeviceSelected: (device) async { - final listTokenIds = playList.tokenIDs; - if (listTokenIds == null) { - log.info('Playlist tokenIds is null'); - return; - } - final duration = speedValues.values.first.inMilliseconds; - final listPlayArtwork = listTokenIds - .map((e) => PlayArtworkV2( - token: CastAssetToken(id: e), duration: duration)) - .toList(); - _canvasDeviceBloc.add( - CanvasDeviceChangeControlDeviceEvent(device, listPlayArtwork)); - }, - ), + if (_getDisplayKey(playList) != null) ...[ + const SizedBox(width: 15), + FFCastButton( + displayKey: _getDisplayKey(playList)!, + onDeviceSelected: (device) async { + final listTokenIds = playList.tokenIDs; + if (listTokenIds == null) { + log.info('Playlist tokenIds is null'); + return; + } + final duration = speedValues.values.first.inMilliseconds; + final listPlayArtwork = listTokenIds + .map((e) => PlayArtworkV2( + token: CastAssetToken(id: e), duration: duration)) + .toList(); + _canvasDeviceBloc.add(CanvasDeviceChangeControlDeviceEvent( + device, listPlayArtwork)); + }, + ), + ], const SizedBox(width: 15), ]; @@ -262,7 +266,7 @@ class _ViewPlaylistScreenState extends State { return const SizedBox(); } - final playList = state.playListModel!; + final PlayListModel playList = state.playListModel!; return Scaffold( backgroundColor: AppColor.primaryBlack, appBar: getPlaylistAppBar( @@ -277,12 +281,16 @@ class _ViewPlaylistScreenState extends State { BlocBuilder( bloc: _canvasDeviceBloc, builder: (context, canvasDeviceState) { - final isPlaylistCasting = - _canvasDeviceBloc.state.controllingDevice != null; + final displayKey = _getDisplayKey(playList); + final isPlaylistCasting = canvasDeviceState + .lastSelectedActiveDeviceForKey(displayKey ?? '') != + null; if (isPlaylistCasting) { - return const Padding( - padding: EdgeInsets.all(15), - child: PlaylistControl(), + return Padding( + padding: const EdgeInsets.all(15), + child: PlaylistControl( + displayKey: displayKey!, + ), ); } else { return const SizedBox(); @@ -315,10 +323,19 @@ class _ViewPlaylistScreenState extends State { ); } + String? _getDisplayKey(PlayListModel playList) => playList.displayKey; + Future _moveToArtwork(CompactedAssetToken compactedAssetToken) { - final controllingDevice = _canvasDeviceBloc.state.controllingDevice; - if (controllingDevice != null) { - return _canvasClientServiceV2.moveToArtwork(controllingDevice, + final playlist = widget.payload.playListModel; + final displayKey = playlist?.displayKey; + if (displayKey == null) { + return Future.value(false); + } + + final lastSelectedCanvasDevice = + _canvasDeviceBloc.state.lastSelectedActiveDeviceForKey(displayKey); + if (lastSelectedCanvasDevice != null) { + return _canvasClientServiceV2.moveToArtwork(lastSelectedCanvasDevice, artworkId: compactedAssetToken.id); } return Future.value(false); diff --git a/lib/screen/scan_qr/scan_qr_page.dart b/lib/screen/scan_qr/scan_qr_page.dart index d0a101b65..ca92dee9b 100644 --- a/lib/screen/scan_qr/scan_qr_page.dart +++ b/lib/screen/scan_qr/scan_qr_page.dart @@ -6,7 +6,6 @@ // import 'dart:async'; -import 'dart:convert'; import 'dart:io'; import 'package:autonomy_flutter/common/injector.dart'; @@ -18,10 +17,8 @@ import 'package:autonomy_flutter/screen/bloc/accounts/accounts_bloc.dart' import 'package:autonomy_flutter/screen/bloc/ethereum/ethereum_bloc.dart'; import 'package:autonomy_flutter/screen/bloc/persona/persona_bloc.dart'; import 'package:autonomy_flutter/screen/bloc/tezos/tezos_bloc.dart'; -import 'package:autonomy_flutter/screen/detail/preview/canvas_device_bloc.dart'; import 'package:autonomy_flutter/screen/global_receive/receive_page.dart'; import 'package:autonomy_flutter/service/audit_service.dart'; -import 'package:autonomy_flutter/service/canvas_client_service_v2.dart'; import 'package:autonomy_flutter/service/deeplink_service.dart'; import 'package:autonomy_flutter/service/metric_client_service.dart'; import 'package:autonomy_flutter/service/navigation_service.dart'; @@ -31,20 +28,17 @@ import 'package:autonomy_flutter/util/constants.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/util/route_ext.dart'; import 'package:autonomy_flutter/util/style.dart'; -import 'package:autonomy_flutter/util/ui_helper.dart'; import 'package:autonomy_flutter/view/back_appbar.dart'; import 'package:autonomy_flutter/view/header.dart'; import 'package:autonomy_flutter/view/primary_button.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; -import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:qr_code_scanner/qr_code_scanner.dart'; -import 'package:synchronized/synchronized.dart'; // ignore_for_file: constant_identifier_names @@ -279,8 +273,8 @@ enum ScannerItem { BEACON_CONNECT, ETH_ADDRESS, XTZ_ADDRESS, - CANVAS_DEVICE, - GLOBAL + GLOBAL, + CANVAS, } class QRScanView extends StatefulWidget { @@ -303,8 +297,6 @@ class QRScanViewState extends State bool? _cameraPermission; String? currentCode; final metricClient = injector(); - final _navigationService = injector(); - late Lock _lock; Timer? _timer; late bool _shouldPop; @@ -312,9 +304,12 @@ class QRScanViewState extends State @override void initState() { super.initState(); - _shouldPop = !(widget.scannerItem == ScannerItem.GLOBAL); + _shouldPop = !(widget.scannerItem == ScannerItem.GLOBAL || + + /// handle canvas deeplink will pop the screen, + /// therefore no need to pop here + widget.scannerItem == ScannerItem.CANVAS); unawaited(_checkPermission()); - _lock = Lock(); } @override @@ -625,7 +620,23 @@ class QRScanViewState extends State ), ), ); - + case ScannerItem.CANVAS: + return RichText( + text: TextSpan( + text: 'display_with_ff'.tr(), + children: [ + TextSpan( + text: ' ', + style: theme.textTheme.ppMori400Grey14, + ), + TextSpan( + text: 'on_tv_or_desktop'.tr(), + style: theme.textTheme.ppMori400Grey14, + ), + ], + style: theme.textTheme.ppMori400White14, + ), + ); case ScannerItem.ETH_ADDRESS: case ScannerItem.XTZ_ADDRESS: return Column( @@ -633,8 +644,6 @@ class QRScanViewState extends State Text('scan_qr'.tr(), style: theme.primaryTextTheme.labelLarge), ], ); - case ScannerItem.CANVAS_DEVICE: - return const SizedBox(); } } @@ -681,13 +690,15 @@ class QRScanViewState extends State return; } else { switch (widget.scannerItem) { + case ScannerItem.CANVAS: + + /// handled with deeplink case ScannerItem.WALLET_CONNECT: if (code.startsWith('wc:')) { await _handleAutonomyConnect(code); } else { _handleError(code); } - break; case ScannerItem.BEACON_CONNECT: if (code.startsWith('tezos://')) { @@ -695,7 +706,6 @@ class QRScanViewState extends State } else { _handleError(code); } - break; case ScannerItem.ETH_ADDRESS: case ScannerItem.XTZ_ADDRESS: @@ -710,40 +720,14 @@ class QRScanViewState extends State Navigator.pop(context, code); } await Future.delayed(const Duration(milliseconds: 300)); - break; case ScannerItem.GLOBAL: if (code.startsWith('wc:')) { await _handleAutonomyConnect(code); } else if (code.startsWith('tezos:')) { await _handleBeaconConnect(code); - /* TODO: Remove or support for multiple wallets - } else if (code.startsWith("tz1")) { - Navigator.of(context).popAndPushNamed(SendCryptoPage.tag, - arguments: SendData(CryptoType.XTZ, code)); - } else { - try { - final _ = EthereumAddress.fromHex(code); - Navigator.of(context).popAndPushNamed(SendCryptoPage.tag, - arguments: SendData(CryptoType.ETH, code)); - } catch (err) { - log(err.toString()); - } - */ - } else if (_isCanvasQrCode(code)) { - unawaited(_lock.synchronized(() async { - await _handleCanvasQrCode(code); - })); - } else { - _handleError(code); - } - break; - case ScannerItem.CANVAS_DEVICE: - if (_isCanvasQrCode(code)) { - unawaited(_lock.synchronized(() => _handleCanvasQrCode(code))); } else { _handleError(code); } - break; } if (mounted) { await resumeCamera(); @@ -758,54 +742,6 @@ class QRScanViewState extends State }); } - bool _isCanvasQrCode(String code) { - try { - CanvasDevice.fromJson(jsonDecode(code)); - return true; - } catch (err) { - return false; - } - } - - Future _handleCanvasQrCode(String code) async { - log.info('Canvas device scanned: $code'); - setState(() { - _isLoading = true; - }); - await pauseCamera(); - if (!mounted) { - return false; - } - try { - final device = CanvasDevice.fromJson(jsonDecode(code)); - final canvasClient = injector(); - final result = await canvasClient.addQrDevice(device); - final isSuccessful = result != null; - if (!mounted) { - return false; - } - if (_shouldPop) { - Navigator.pop(context, isSuccessful); - } - injector().add(CanvasDeviceAppendDeviceEvent(device)); - return isSuccessful; - } catch (e) { - if (mounted) { - if (_shouldPop) { - Navigator.pop(context, false); - } - if (e.toString().contains('DEADLINE_EXCEEDED') || true) { - await UIHelper.showInfoDialog( - _navigationService.navigatorKey.currentContext!, - 'failed_to_connect'.tr(), - 'canvas_ip_fail'.tr(), - closeButton: 'close'.tr()); - } - } - } - return false; - } - void _handleError(String data) { setState(() { isScanDataError = true; diff --git a/lib/service/canvas_channel_service.dart b/lib/service/canvas_channel_service.dart deleted file mode 100644 index 0aaf13e2f..000000000 --- a/lib/service/canvas_channel_service.dart +++ /dev/null @@ -1,52 +0,0 @@ -// -// SPDX-License-Identifier: BSD-2-Clause-Patent -// Copyright © 2022 Bitmark. All rights reserved. -// Use of this source code is governed by the BSD-2-Clause Plus Patent License -// that can be found in the LICENSE file. -// - -import 'package:autonomy_flutter/service/network_service.dart'; -import 'package:autonomy_flutter/util/log.dart'; -import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; - -class CanvasChannelService { - final NetworkService _networkService; - - CanvasChannelService(this._networkService) { - _networkService.addListener((result) async { - log.info('[CanvasChannelService] shut down all channel'); - await Future.wait(_channels.values.map((e) async { - await e.shutdown(); - })); - _channels.clear(); - }); - } - - final Map _channels = {}; - - CanvasControlV2Client getStubV2(CanvasDevice device) { - final channel = _getChannel(device); - return CanvasControlV2Client(channel); - } - - CanvasControlClient getStubV1(CanvasDevice device) { - final channel = _getChannel(device); - return CanvasControlClient(channel); - } - - ClientChannel _getChannel(CanvasDevice device) => - _channels[device.ip] ?? _createNewChannel(device); - - ClientChannel _createNewChannel(CanvasDevice device) { - final newChannel = ClientChannel( - device.ip, - port: device.port, - options: const ChannelOptions( - credentials: ChannelCredentials.insecure(), - keepAlive: ClientKeepAliveOptions(pingInterval: Duration(seconds: 2)), - ), - ); - _channels[device.ip] = newChannel; - return newChannel; - } -} diff --git a/lib/service/canvas_client_service.dart b/lib/service/canvas_client_service.dart deleted file mode 100644 index 95830b85a..000000000 --- a/lib/service/canvas_client_service.dart +++ /dev/null @@ -1,351 +0,0 @@ -// -// SPDX-License-Identifier: BSD-2-Clause-Patent -// Copyright © 2022 Bitmark. All rights reserved. -// Use of this source code is governed by the BSD-2-Clause Plus Patent License -// that can be found in the LICENSE file. -// - -import 'dart:async'; - -import 'package:autonomy_flutter/common/injector.dart'; -import 'package:autonomy_flutter/database/app_database.dart'; -import 'package:autonomy_flutter/model/pair.dart'; -import 'package:autonomy_flutter/model/play_list_model.dart'; -import 'package:autonomy_flutter/screen/detail/preview/canvas_device_bloc.dart'; -import 'package:autonomy_flutter/service/canvas_channel_service.dart'; -import 'package:autonomy_flutter/service/device_info_service.dart'; -import 'package:autonomy_flutter/service/navigation_service.dart'; -import 'package:autonomy_flutter/util/log.dart'; -import 'package:collection/collection.dart'; -import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; -import 'package:flutter/material.dart'; -import 'package:synchronized/synchronized.dart'; -import 'package:uuid/uuid.dart'; - -class CanvasClientService { - final AppDatabase _db; - final DeviceInfoService _deviceInfoService; - final CanvasChannelService _channelService; - - CanvasClientService(this._db, this._deviceInfoService, this._channelService); - - final List _viewingDevices = []; - - final _connectDevice = Lock(); - final NavigationService _navigationService = injector(); - - Offset currentCursorOffset = Offset.zero; - - CallOptions get _callOptions => CallOptions( - compression: const GzipCodec(), timeout: const Duration(seconds: 10)); - - CanvasControlClient _getStub(CanvasDevice device) => - _channelService.getStubV1(device); - - Future connectToDevice(CanvasDevice device, - {bool isLocal = false}) async => - _connectDevice - .synchronized(() async => await _connectToDevice(device, isLocal)); - - Future _connectToDevice(CanvasDevice device, bool isLocal) async { - final stub = _getStub(device); - try { - final request = ConnectRequest() - ..device = (DeviceInfo() - ..deviceId = _deviceInfoService.deviceId - ..deviceName = _deviceInfoService.deviceName); - - final response = await stub.connect( - request, - options: _callOptions, - ); - log.info('CanvasClientService connect: ${response.ok}'); - final index = - _viewingDevices.indexWhere((element) => element.ip == device.ip); - if (response.ok) { - log.info('CanvasClientService: Connected to device'); - device.isConnecting = true; - if (index == -1) { - _viewingDevices.add(device); - } else { - _viewingDevices[index].isConnecting = true; - } - if (!isLocal) { - await _db.canvasDeviceDao.insertCanvasDevice(device); - } - return true; - } else { - log.info('CanvasClientService: Failed to connect to device'); - if (index != -1) { - _viewingDevices[index].isConnecting = false; - } - return false; - } - } catch (e) { - log.info('CanvasClientService: Caught error: $e'); - rethrow; - } - } - - Future> checkDeviceStatus( - CanvasDevice device) async { - final stub = _getStub(device); - String? sceneId; - late CanvasServerStatus status; - try { - final request = CheckingStatus()..deviceId = _deviceInfoService.deviceId; - final response = await stub.status( - request, - options: _callOptions, - ); - log.info('CanvasClientService received: ${response.status}'); - switch (response.status) { - case ResponseStatus_ServingStatus.NOT_SERVING: - case ResponseStatus_ServingStatus.SERVICE_UNKNOWN: - status = CanvasServerStatus.notServing; - case ResponseStatus_ServingStatus.SERVING: - if (response.sceneId.isNotEmpty) { - status = CanvasServerStatus.playing; - sceneId = response.sceneId; - } else { - status = CanvasServerStatus.connected; - } - case ResponseStatus_ServingStatus.UNKNOWN: - status = CanvasServerStatus.open; - } - } catch (e) { - log.info('CanvasClientService: Caught error: $e'); - status = CanvasServerStatus.error; - } - return Pair(status, sceneId); - } - - Future> _findRawDevices() async { - final devices = []; - final localDevices = await _db.canvasDeviceDao.getCanvasDevices(); - devices.addAll(await refreshDevices(localDevices)); - return devices; - } - - /// This method will check the status of the devices by calling grpc - Future> refreshDevices(List devices) async { - final List workingDevices = []; - await Future.forEach(devices, (device) async { - final status = await checkDeviceStatus(device); - switch (status.first) { - case CanvasServerStatus.playing: - case CanvasServerStatus.connected: - device.playingSceneId = status.second; - device.isConnecting = true; - workingDevices.add(device); - case CanvasServerStatus.open: - device.playingSceneId = status.second; - device.isConnecting = false; - workingDevices.add(device); - case CanvasServerStatus.notServing: - break; - case CanvasServerStatus.error: - break; - } - }); - log.info('CanvasClientService refresh device ${workingDevices.length}'); - return workingDevices; - } - - /// This method will get devices saved in memory, no status check - Future> getConnectingDevices() async => _viewingDevices; - - /// This method will get devices via mDNS and local db, for local db devices - /// it will check the status of the device by calling grpc, - /// it will return the devices that are available and save in memory - Future> scanDevices() async { - final devices = await _findRawDevices(); - - // remove devices that are not available - _viewingDevices.removeWhere( - (element) => !devices.any((current) => current.ip == element.ip)); - - // add new devices - for (var element in devices) { - final index = - _viewingDevices.indexWhere((current) => current.ip == element.ip); - if (index == -1) { - _viewingDevices.add(element); - } - } - - return _viewingDevices; - } - - Future castSingleArtwork(CanvasDevice device, String tokenId) async { - final stub = _getStub(device); - final size = - MediaQuery.of(_navigationService.navigatorKey.currentContext!).size; - final playingDevice = _viewingDevices.firstWhereOrNull( - (element) => element.playingSceneId != null, - ); - if (playingDevice != null) { - currentCursorOffset = await getCursorOffset(playingDevice); - } - final castRequest = CastSingleRequest() - ..id = tokenId - ..cursorDrag = (DragGestureRequest() - ..dx = currentCursorOffset.dx - ..dy = currentCursorOffset.dy - ..coefficientX = 1 / size.width - ..coefficientY = 1 / size.height); - final response = await stub.castSingleArtwork(castRequest); - if (response.ok) { - final lst = _viewingDevices.firstWhereOrNull( - (element) { - final isEqual = element == device; - return isEqual; - }, - ); - lst?.playingSceneId = tokenId; - } else { - log.info('CanvasClientService: Failed to cast single artwork'); - } - return response.ok; - } - - Future unCastSingleArtwork(CanvasDevice device) async { - final stub = _getStub(device); - final unCastRequest = UncastSingleRequest()..id = ''; - final response = await stub.uncastSingleArtwork(unCastRequest); - if (response.ok) { - _viewingDevices - .firstWhereOrNull((element) => element == device) - ?.playingSceneId = null; - } - } - - Future castCollection( - CanvasDevice device, PlayListModel playlist) async { - if (playlist.tokenIDs == null || playlist.tokenIDs!.isEmpty) { - return false; - } - final stub = _getStub(device); - - final castRequest = CastCollectionRequest() - ..id = playlist.id ?? const Uuid().v4() - ..artworks.addAll(playlist.tokenIDs!.map((e) => PlayArtwork() - ..id = e - ..duration = playlist.playControlModel?.timer ?? 10)); - final response = await stub.castCollection(castRequest); - if (response.ok) { - _viewingDevices - .firstWhereOrNull((element) => element == device) - ?.playingSceneId = playlist.id; - } else { - log.info('CanvasClientService: Failed to cast collection'); - } - return response.ok; - } - - Future unCast(CanvasDevice device) async { - final stub = _getStub(device); - final unCastRequest = UnCastRequest()..id = ''; - final response = await stub.unCastArtwork(unCastRequest); - if (response.ok) { - _viewingDevices - .firstWhereOrNull((element) => element == device) - ?.playingSceneId = null; - } - } - - Future sendKeyBoard(List devices, int code) async { - for (var device in devices) { - final stub = _getStub(device); - final sendKeyboardRequest = KeyboardEventRequest()..code = code; - final response = await stub.keyboardEvent(sendKeyboardRequest); - if (response.ok) { - log.info('CanvasClientService: Keyboard Event Success $code'); - } else { - log.info('CanvasClientService: Keyboard Event Failed $code'); - } - } - } - - // function to rotate canvas - Future rotateCanvas(CanvasDevice device, - {bool clockwise = true}) async { - final stub = _getStub(device); - final rotateCanvasRequest = RotateRequest()..clockwise = clockwise; - try { - final response = await stub.rotate(rotateCanvasRequest); - log.info('CanvasClientService: Rotate Canvas Success ${response.degree}'); - } catch (e) { - log.info('CanvasClientService: Rotate Canvas Failed'); - } - } - - Future tap(List devices) async { - for (var device in devices) { - final stub = _getStub(device); - final tapRequest = TapGestureRequest(); - await stub.tapGesture(tapRequest); - } - } - - Future drag( - List devices, Offset offset, Size touchpadSize) async { - final dragRequest = DragGestureRequest() - ..dx = offset.dx - ..dy = offset.dy - ..coefficientX = 1 / touchpadSize.width - ..coefficientY = 1 / touchpadSize.height; - currentCursorOffset += offset; - for (var device in devices) { - final stub = _getStub(device); - await stub.dragGesture(dragRequest); - } - } - - Future getCursorOffset(CanvasDevice device) async { - final stub = _getStub(device); - final response = await stub.getCursorOffset(Empty()); - final size = - MediaQuery.of(_navigationService.navigatorKey.currentContext!).size; - final dx = size.width * response.coefficientX * response.dx; - final dy = size.height * response.coefficientY * response.dy; - return Offset(dx, dy); - } - - Future setCursorOffset(CanvasDevice device) async { - final stub = _getStub(device); - final size = - MediaQuery.of(_navigationService.navigatorKey.currentContext!).size; - final dx = currentCursorOffset.dx / size.width; - final dy = currentCursorOffset.dy / size.height; - final request = CursorOffset() - ..dx = dx - ..dy = dy - ..coefficientX = 1 / size.width - ..coefficientY = 1 / size.height; - - await stub.setCursorOffset(request); - } -} - -enum CanvasServerStatus { - open, - connected, - playing, - notServing, - error; - - DeviceStatus get toDeviceStatus { - switch (this) { - case CanvasServerStatus.error: - case CanvasServerStatus.notServing: - case CanvasServerStatus.open: - case CanvasServerStatus.connected: - return DeviceStatus.connected; - case CanvasServerStatus.playing: - return DeviceStatus.playing; - } - } -} - -extension CanvasServerStatusExt on CanvasServerStatus {} diff --git a/lib/service/canvas_client_service_v2.dart b/lib/service/canvas_client_service_v2.dart index b7dbbb375..11c9c6b40 100644 --- a/lib/service/canvas_client_service_v2.dart +++ b/lib/service/canvas_client_service_v2.dart @@ -6,69 +6,62 @@ // import 'dart:async'; -import 'dart:io'; -import 'package:autonomy_flutter/common/injector.dart'; -import 'package:autonomy_flutter/database/app_database.dart'; +import 'package:autonomy_flutter/gateway/tv_cast_api.dart'; import 'package:autonomy_flutter/model/pair.dart'; -import 'package:autonomy_flutter/screen/detail/preview/canvas_device_bloc.dart'; -import 'package:autonomy_flutter/service/canvas_channel_service.dart'; import 'package:autonomy_flutter/service/device_info_service.dart'; -import 'package:autonomy_flutter/service/mdns_service.dart'; +import 'package:autonomy_flutter/service/hive_store_service.dart'; +import 'package:autonomy_flutter/service/navigation_service.dart'; +import 'package:autonomy_flutter/service/tv_cast_service.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/view/user_agent_utils.dart' as my_device; import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; -import 'package:fixnum/fixnum.dart' as $fixnum; import 'package:flutter/material.dart'; -import 'package:retry/retry.dart'; class CanvasClientServiceV2 { - final AppDatabase _db; - final MDnsService _mdnsService; + final HiveStoreObjectService _db; final DeviceInfoService _deviceInfoService; - final CanvasChannelService _channelService; + final TvCastApi _tvCastApi; + final NavigationService _navigationService; + Timer? _timer; + final dragOffsets = []; - CanvasClientServiceV2(this._db, this._mdnsService, this._deviceInfoService, - this._channelService); - - final _retry = const RetryOptions(maxAttempts: 3); + CanvasClientServiceV2(this._db, this._deviceInfoService, this._tvCastApi, + this._navigationService); Offset currentCursorOffset = Offset.zero; - CallOptions get _callOptions => CallOptions( - compression: const IdentityCodec(), - timeout: const Duration(seconds: 60), - providers: [_grpcLoggingProvider]); - - DeviceInfoV2 get clientDeviceInfo => DeviceInfoV2() - ..deviceId = _deviceInfoService.deviceId - ..deviceName = _deviceInfoService.deviceName - ..platform = _platform; + DeviceInfoV2 get clientDeviceInfo => DeviceInfoV2( + deviceId: _deviceInfoService.deviceId, + deviceName: _deviceInfoService.deviceName, + platform: _platform, + ); - CanvasControlV2Client _getStub(CanvasDevice device) => - _channelService.getStubV2(device); + TvCastService _getStub(CanvasDevice device) => + TvCastServiceImpl(_tvCastApi, device); Future getDeviceCastingStatus( + CanvasDevice device) async => + _getDeviceCastingStatus(device); + + Future _getDeviceCastingStatus( CanvasDevice device) async { final stub = _getStub(device); final request = CheckDeviceStatusRequest(); - final response = await _retryWrapper(() => stub.status( - request, - options: _callOptions, - )); + final response = await stub.status(request); log.info( - 'CanvasClientService2 status ok: ${response.connectedDevice.deviceId}'); + 'CanvasClientService2 status: ${response.connectedDevice?.deviceId}'); return response; } - DeviceInfoV2_DevicePlatform get _platform { + DevicePlatform get _platform { final device = my_device.DeviceInfo.instance; if (device.isAndroid) { - return DeviceInfoV2_DevicePlatform.ANDROID; + return DevicePlatform.android; } else if (device.isIOS) { - return DeviceInfoV2_DevicePlatform.IOS; + return DevicePlatform.iOS; } else { - return DeviceInfoV2_DevicePlatform.OTHER; + return DevicePlatform.other; } } @@ -76,257 +69,243 @@ class CanvasClientServiceV2 { CanvasDevice device) async { final deviceStatus = await _getDeviceStatus(device); if (deviceStatus != null) { - await _db.canvasDeviceDao.insertCanvasDevice(device); + await _db.save(device, device.deviceId); log.info('CanvasClientService: Added device to db ${device.name}'); return deviceStatus; } return null; } - Future> connect( - CanvasDevice device, { - Function(CanvasDevice device, ConnectReplyV2 reply)? onEvent, - Function(CanvasDevice device)? onDone, - Function(CanvasDevice device)? onError, - }) async { + Future connect(CanvasDevice device) async { final stub = _getStub(device); final deviceInfo = clientDeviceInfo; - final request = ConnectRequestV2()..clientDevice = deviceInfo; - final response = await _retryWrapper>( - () => stub.connect(request, options: _callOptions)); - response.listen((reply) { - log.info('CanvasClientService: connect $reply'); - onEvent?.call(device, reply); - }, onDone: () { - log.info('CanvasClientService: onDone'); - onDone?.call(device); - }, onError: (e) { - log.info('CanvasClientService: onError $e'); - onError?.call(device); - }); + final request = ConnectRequestV2(clientDevice: deviceInfo); + final response = await stub.connect(request); return response; } Future connectToDevice(CanvasDevice device) async { - final completer = Completer(); try { - await connect( - device, - onEvent: (device, event) { - if (event.ok) { - completer.complete(true); - } else { - injector().add( - CanvasDeviceDisconnectEvent( - [device], - callRPC: false, - ), - ); - completer.complete(false); - } - }, - onDone: (device) { - completer.complete(true); - }, - onError: (device) { - injector().add( - CanvasDeviceOnRPCErrorEvent(device), - ); - completer.complete(false); - }, - ); - return completer.future; + final response = await connect(device); + return response.ok; } catch (e) { - log.info('CanvasClientService: Caught error: $e'); - rethrow; + log.info('CanvasClientService: connectToDevice error: $e'); + return false; } } Future disconnectDevice(CanvasDevice device) async { final stub = _getStub(device); - final response = await _retryWrapper(() => stub.disconnect( - DisconnectRequestV2(), - options: _callOptions, - )); - if (response.ok) { - //TODO: implement on disconnected - } + await stub.disconnect(DisconnectRequestV2()); } Future castListArtwork( CanvasDevice device, List artworks) async { try { - await connectToDevice(device); + final canConnect = await connectToDevice(device); + if (!canConnect) { + return false; + } final stub = _getStub(device); - final castRequest = CastListArtworkRequest()..artworks.addAll(artworks); + final castRequest = CastListArtworkRequest(artworks: artworks); - final response = await _retryWrapper(() => stub.castListArtwork( - castRequest, - options: _callOptions, - )); + final response = await stub.castListArtwork(castRequest); return response.ok; } catch (e) { - log.info('CanvasClientService: Caught error: $e'); + log.info('CanvasClientService: castListArtwork error: $e'); return false; } } - Future cancelCasting(CanvasDevice device) async { - final stub = _getStub(device); - final response = await _retryWrapper(() => stub.cancelCasting( - CancelCastingRequest(), - options: _callOptions, - )); - return response.ok; - } - Future pauseCasting(CanvasDevice device) async { final stub = _getStub(device); - final response = await _retryWrapper(() => stub.pauseCasting( - PauseCastingRequest(), - options: _callOptions, - )); + final response = await stub.pauseCasting(PauseCastingRequest()); return response.ok; } Future resumeCasting(CanvasDevice device) async { final stub = _getStub(device); - final response = await _retryWrapper(() => stub.resumeCasting( - ResumeCastingRequest(), - options: _callOptions, - )); + final response = await stub.resumeCasting(ResumeCastingRequest()); return response.ok; } Future nextArtwork(CanvasDevice device, {String? startTime}) async { final stub = _getStub(device); - final request = NextArtworkRequest(); - if (startTime != null) { - request.startTime = $fixnum.Int64(int.parse(startTime)); - } - final response = await _retryWrapper(() => stub.nextArtwork( - request, - options: _callOptions, - )); + final request = NextArtworkRequest( + startTime: startTime == null ? null : int.tryParse(startTime)); + + final response = await stub.nextArtwork(request); return response.ok; } Future moveToArtwork(CanvasDevice device, - {String? startTime, String? artworkId}) async { + {required String artworkId, String? startTime}) async { final stub = _getStub(device); - final artwork = PlayArtworkV2(token: CastAssetToken(id: artworkId)); + final artwork = + PlayArtworkV2(token: CastAssetToken(id: artworkId), duration: 0); final request = MoveToArtworkRequest(artwork: artwork); - final reply = await _retryWrapper(() => stub.moveToArtwork( - request, - options: _callOptions, - )); + final reply = await stub.moveToArtwork(request); return reply.ok; } Future previousArtwork(CanvasDevice device, {String? startTime}) async { final stub = _getStub(device); - final request = PreviousArtwortRequest(); - if (startTime != null) { - request.startTime = $fixnum.Int64(int.parse(startTime)); - } - final response = await _retryWrapper(() => stub.previousArtwork( - request, - options: _callOptions, - )); + final request = PreviousArtworkRequest( + startTime: startTime == null ? null : int.tryParse(startTime)); + final response = await stub.previousArtwork(request); return response.ok; } Future appendListArtwork( CanvasDevice device, List artworks) async { final stub = _getStub(device); - final response = await _retryWrapper(() => stub.appendListArtwork( - AppendArtworkToCastingListRequest()..artworks.addAll(artworks), - options: _callOptions, - )); + final response = await stub.appendListArtwork( + AppendArtworkToCastingListRequest(artworks: artworks)); return response.ok; } Future castExhibition( CanvasDevice device, CastExhibitionRequest castRequest) async { - await connectToDevice(device); + final canConnect = await connectToDevice(device); + if (!canConnect) { + return false; + } final stub = _getStub(device); - final response = await _retryWrapper(() => stub.castExhibition( - castRequest, - options: _callOptions, - )); + final response = await stub.castExhibition(castRequest); return response.ok; } Future updateDuration( CanvasDevice device, List artworks) async { final stub = _getStub(device); - final response = await _retryWrapper(() => stub.updateDuration( - UpdateDurationRequest()..artworks.addAll(artworks), - options: _callOptions, - )); + final response = + await stub.updateDuration(UpdateDurationRequest(artworks: artworks)); return response; } - Future> _findRawDevices() async { - final devices = []; - final futures = await Future.wait( - [_mdnsService.findCanvas(), _db.canvasDeviceDao.getCanvasDevices()]); - final localDevices = futures[1]; - final discoverDevices = futures[0]; - localDevices.removeWhere((l) => discoverDevices.any((d) => d.ip == l.ip)); - devices - ..addAll(discoverDevices) - ..addAll(localDevices); + List _findRawDevices() { + final devices = _db.getAll(); return devices; } /// This method will get devices via mDNS and local db, for local db devices /// it will check the status of the device by calling grpc Future>> scanDevices() async { - final rawDevices = await _findRawDevices(); + final rawDevices = _findRawDevices(); final List> devices = - await getDeviceStatuses(rawDevices); + await _getDeviceStatuses(rawDevices); devices.sort((a, b) => a.first.name.compareTo(b.first.name)); return devices; } - Future>> getDeviceStatuses( + Future>> _getDeviceStatuses( List devices) async { final List> statuses = []; - await Future.forEach(devices, (device) async { + await Future.wait(devices.map((device) async { try { final status = await _getDeviceStatus(device); if (status != null) { statuses.add(status); } } catch (e) { - log.info('CanvasClientService: Caught error: $e'); + log.info('CanvasClientService: _getDeviceStatus error: $e'); } - }); + })); return statuses; } Future?> _getDeviceStatus( CanvasDevice device) async { try { - final status = await getDeviceCastingStatus(device); + final status = await _getDeviceCastingStatus(device); return Pair(device, status); } catch (e) { - log.info('CanvasClientService: Caught error: $e'); + log.info('CanvasClientService: _getDeviceStatus error: $e'); return null; } } - Future addLocalDevice(CanvasDevice device) async { - await _db.canvasDeviceDao.insertCanvasDevice(device); + Future sendKeyBoard(List devices, int code) async { + for (var device in devices) { + final stub = _getStub(device); + final sendKeyboardRequest = KeyboardEventRequest(code: code); + final response = await stub.keyboardEvent(sendKeyboardRequest); + if (response.ok) { + log.info('CanvasClientService: Keyboard Event Success $code'); + } else { + log.info('CanvasClientService: Keyboard Event Failed $code'); + } + } + } + + // function to rotate canvas + Future rotateCanvas(CanvasDevice device, + {bool clockwise = true}) async { + final stub = _getStub(device); + final rotateCanvasRequest = RotateRequest(clockwise: clockwise); + try { + final response = await stub.rotate(rotateCanvasRequest); + log.info('CanvasClientService: Rotate Canvas Success ${response.degree}'); + } catch (e) { + log.info('CanvasClientService: Rotate Canvas Failed'); + } } - Future _retryWrapper(FutureOr Function() fn) => - _retry.retry(() => fn.call(), - retryIf: (e) => e is SocketException || e is TimeoutException, - onRetry: (e) => log.info('CanvasClientService retry stub error $e')); + Future tap(List devices) async { + for (var device in devices) { + final stub = _getStub(device); + final tapRequest = TapGestureRequest(); + await stub.tap(tapRequest); + } + } + + Future drag( + List devices, Offset offset, Size touchpadSize) async { + final dragOffset = CursorOffset( + dx: offset.dx, + dy: offset.dy, + coefficientX: 1 / touchpadSize.width, + coefficientY: 1 / touchpadSize.height); + + currentCursorOffset += offset; + dragOffsets.add(dragOffset); + if (_timer == null || !_timer!.isActive) { + _timer = Timer(const Duration(milliseconds: 300), () { + for (var device in devices) { + final stub = _getStub(device); + final dragRequest = DragGestureRequest(cursorOffsets: dragOffsets); + stub.drag(dragRequest); + } + dragOffsets.clear(); + }); + } + } - void _grpcLoggingProvider(Map metadata, String uri) { - log.info('CanvasClientService call gRPC: metadata: $metadata, uri: $uri}'); + Future getCursorOffset(CanvasDevice device) async { + final stub = _getStub(device); + final response = await stub.getCursorOffset(GetCursorOffsetRequest()); + final size = + MediaQuery.of(_navigationService.navigatorKey.currentContext!).size; + final cursorOffset = response.cursorOffset; + final dx = size.width * cursorOffset.coefficientX * cursorOffset.dx; + final dy = size.height * cursorOffset.coefficientY * cursorOffset.dy; + return Offset(dx, dy); + } + + Future setCursorOffset(CanvasDevice device) async { + final stub = _getStub(device); + final size = + MediaQuery.of(_navigationService.navigatorKey.currentContext!).size; + final dx = currentCursorOffset.dx / size.width; + final dy = currentCursorOffset.dy / size.height; + final cursorOffset = CursorOffset( + dx: dx, + dy: dy, + coefficientX: 1 / size.width, + coefficientY: 1 / size.height); + + final request = SetCursorOffsetRequest(cursorOffset: cursorOffset); + + await stub.setCursorOffset(request); } } diff --git a/lib/service/configuration_service.dart b/lib/service/configuration_service.dart index c6223c669..1f3b6bf9b 100644 --- a/lib/service/configuration_service.dart +++ b/lib/service/configuration_service.dart @@ -29,10 +29,6 @@ import 'package:uuid/uuid.dart'; //ignore_for_file: constant_identifier_names abstract class ConfigurationService { - bool didGetLocalNetworkPermission(); - - Future setDidGetLocalNetworkPermission(bool value); - Future setRecordOwners(List owners, {bool override = false}); List getRecordOwners(); @@ -281,8 +277,6 @@ abstract class ConfigurationService { } class ConfigurationServiceImpl implements ConfigurationService { - static const String keyDidGetLocalNetWorkPermission = - 'did_get_local_network_permission'; static const String keyRecordOwners = 'yoko_ono_record_owners'; static const String KEY_HAS_MERCHANDISE_SUPPORT_INDEX_ID = 'has_merchandise_support'; @@ -1227,15 +1221,6 @@ class ConfigurationServiceImpl implements ConfigurationService { await _preferences.setStringList(keyRecordOwners, currentOwners.toList()); } } - - @override - bool didGetLocalNetworkPermission() => - _preferences.getBool(keyDidGetLocalNetWorkPermission) ?? false; - - @override - Future setDidGetLocalNetworkPermission(bool value) async { - await _preferences.setBool(keyDidGetLocalNetWorkPermission, value); - } } enum ConflictAction { diff --git a/lib/service/deeplink_service.dart b/lib/service/deeplink_service.dart index 6bc781bc1..815b933be 100644 --- a/lib/service/deeplink_service.dart +++ b/lib/service/deeplink_service.dart @@ -16,8 +16,10 @@ import 'package:autonomy_flutter/main.dart'; import 'package:autonomy_flutter/model/otp.dart'; import 'package:autonomy_flutter/model/postcard_claim.dart'; import 'package:autonomy_flutter/screen/app_router.dart'; +import 'package:autonomy_flutter/screen/detail/preview/canvas_device_bloc.dart'; import 'package:autonomy_flutter/screen/irl_screen/webview_irl_screen.dart'; import 'package:autonomy_flutter/service/account_service.dart'; +import 'package:autonomy_flutter/service/canvas_client_service_v2.dart'; import 'package:autonomy_flutter/service/configuration_service.dart'; import 'package:autonomy_flutter/service/metric_client_service.dart'; import 'package:autonomy_flutter/service/navigation_service.dart'; @@ -26,12 +28,17 @@ import 'package:autonomy_flutter/service/remote_config_service.dart'; import 'package:autonomy_flutter/service/tezos_beacon_service.dart'; import 'package:autonomy_flutter/service/wc2_service.dart'; import 'package:autonomy_flutter/util/constants.dart'; +import 'package:autonomy_flutter/util/custom_route_observer.dart'; import 'package:autonomy_flutter/util/dio_exception_ext.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/util/string_ext.dart'; +import 'package:autonomy_flutter/util/ui_helper.dart'; +import 'package:autonomy_flutter/view/stream_device_view.dart'; import 'package:collection/collection.dart'; import 'package:dio/dio.dart'; +import 'package:feralfile_app_tv_proto/models/canvas_device.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_branch_sdk/flutter_branch_sdk.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:uni_links/uni_links.dart'; @@ -150,11 +157,9 @@ class DeeplinkServiceImpl extends DeeplinkService { switch (data) { case 'home': _navigationService.restorablePushHomePage(); - break; case 'support': unawaited( _navigationService.navigateTo(AppRouter.supportCustomerPage)); - break; default: return false; } @@ -342,7 +347,11 @@ class DeeplinkServiceImpl extends DeeplinkService { .firstWhereOrNull((prefix) => link.startsWith(prefix)); if (callingBranchDeepLinkPrefix != null) { final response = await _branchApi.getParams(Environment.branchKey, link); - await handleBranchDeeplinkData(response['data']); + try { + await handleBranchDeeplinkData(response['data']); + } catch (e) { + log.info('[DeeplinkService] _handleBranchDeeplink error $e'); + } return true; } return false; @@ -393,7 +402,6 @@ class DeeplinkServiceImpl extends DeeplinkService { log.info('[DeeplinkService] _handlePostcardDeeplink $sharedCode'); await _handlePostcardDeeplink(sharedCode); } - break; case 'autonomy_irl': final url = data['irl_url']; @@ -402,17 +410,42 @@ class DeeplinkServiceImpl extends DeeplinkService { await _handleIRL(url); memoryValues.irlLink.value = null; } - break; case 'moma_postcard_purchase': await _handlePayToMint(); - break; case 'autonomy_connect': final wcUri = data['uri']; final decodedWcUri = Uri.decodeFull(wcUri); await _walletConnect2Service.connect(decodedWcUri); - break; + + case 'feralfile_display': + final payload = data['device'] as Map; + final device = CanvasDevice.fromJson(payload); + final canvasClient = injector(); + final result = await canvasClient.addQrDevice(device); + final isSuccessful = result != null; + if (!_navigationService.context.mounted) { + return; + } + if (isSuccessful) { + if (CustomRouteObserver.currentRoute?.settings.name == + AppRouter.scanQRPage) { + /// in case scan when open scanQRPage, + /// scan with navigation home page does not go to this flow + _navigationService.goBack(result: device); + } else { + await UIHelper.showFlexibleDialog( + _navigationService.context, + BlocProvider.value( + value: injector(), + child: const StreamDeviceView(), + ), + isDismissible: true, + autoDismissAfter: 3); + } + } + default: memoryValues.branchDeeplinkData.value = null; } diff --git a/lib/service/hive_store_service.dart b/lib/service/hive_store_service.dart new file mode 100644 index 000000000..1612bae71 --- /dev/null +++ b/lib/service/hive_store_service.dart @@ -0,0 +1,50 @@ +import 'dart:async'; + +import 'package:autonomy_flutter/util/log.dart'; +import 'package:hive_flutter/hive_flutter.dart'; + +abstract class HiveStoreObjectService { + Future init(String key); + + Future save(T obj, String objId); + + Future delete(String objId); + + T? get(String objId); + + List getAll(); +} + +class HiveStoreObjectServiceImpl implements HiveStoreObjectService { + late Box _box; + + @override + Future init(String key) async { + _box = await Hive.openBox(key); + } + + @override + Future delete(String objId) => _box.delete(objId); + + @override + T? get(String objId) { + try { + return _box.get(objId); + } catch (e) { + log.info('Hive error getting object from Hive: $e'); + return null; + } + } + + @override + List getAll() => _box.values.toList(); + + @override + Future save(T obj, String objId) async { + try { + await _box.put(objId, obj); + } catch (e) { + log.info('Hive error saving object to Hive: $e'); + } + } +} diff --git a/lib/service/mdns_service.dart b/lib/service/mdns_service.dart deleted file mode 100644 index 096563aaf..000000000 --- a/lib/service/mdns_service.dart +++ /dev/null @@ -1,64 +0,0 @@ -// -// SPDX-License-Identifier: BSD-2-Clause-Patent -// Copyright © 2022 Bitmark. All rights reserved. -// Use of this source code is governed by the BSD-2-Clause Plus Patent License -// that can be found in the LICENSE file. -// - -import 'package:autonomy_flutter/service/network_service.dart'; -import 'package:autonomy_flutter/util/log.dart'; -import 'package:bonsoir/bonsoir.dart'; -import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; - -class MDnsService { - static const String _serviceType = '_feralFileCanvas._tcp'; - static const int _scanningTime = 3; - final NetworkService _networkService; - - MDnsService(this._networkService); - - Future> findCanvas() async { - final devices = []; - if (!_networkService.isWifi) { - return devices; - } - BonsoirDiscovery discovery = BonsoirDiscovery(type: _serviceType); - log.info('[MDnsService] Looking for devices'); - await discovery.ready; - discovery.eventStream!.listen((event) { - log.info(event.type); - switch (event.type) { - case BonsoirDiscoveryEventType.discoveryServiceFound: - log.info('[MDnsService] Service found : ${event.service}'); - event.service!.resolve(discovery.serviceResolver); - case BonsoirDiscoveryEventType.discoveryServiceResolved: - final attribute = event.service!.attributes; - log.info('[MDnsService] Service resolved : ${event.service}'); - final name = event.service!.name; - final ip = attribute['ip']; - final port = int.tryParse(attribute['port'] ?? ''); - final id = attribute['id']; - if (ip != null && port != null && id != null) { - if (devices - .any((element) => element.id == id && element.ip == ip)) { - return; - } - devices.add(CanvasDevice( - id: id, - ip: ip, - port: port, - name: name, - )); - } - case BonsoirDiscoveryEventType.discoveryServiceResolveFailed: - log.info('[MDnsService] Service resolved failed : ${event.service}'); - default: - } - }); - await discovery.start(); - await Future.delayed(const Duration(seconds: _scanningTime), () { - discovery.stop(); - }); - return devices; - } -} diff --git a/lib/service/tv_cast_service.dart b/lib/service/tv_cast_service.dart new file mode 100644 index 000000000..b03b650d1 --- /dev/null +++ b/lib/service/tv_cast_service.dart @@ -0,0 +1,176 @@ +import 'package:autonomy_flutter/gateway/tv_cast_api.dart'; +import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; + +abstract class TvCastService { + Future status(CheckDeviceStatusRequest request); + + Future connect(ConnectRequestV2 request); + + Future disconnect(DisconnectRequestV2 request); + + Future castListArtwork(CastListArtworkRequest request); + + Future pauseCasting(PauseCastingRequest request); + + Future resumeCasting(ResumeCastingRequest request); + + Future nextArtwork(NextArtworkRequest request); + + Future moveToArtwork(MoveToArtworkRequest request); + + Future previousArtwork(PreviousArtworkRequest request); + + Future appendListArtwork( + AppendArtworkToCastingListRequest request); + + Future updateDuration(UpdateDurationRequest request); + + Future keyboardEvent(KeyboardEventRequest request); + + Future rotate(RotateRequest request); + + Future castExhibition(CastExhibitionRequest request); + + Future tap(TapGestureRequest request); + + Future drag(DragGestureRequest request); + + Future getCursorOffset(GetCursorOffsetRequest request); + + Future setCursorOffset(SetCursorOffsetRequest request); +} + +class TvCastServiceImpl implements TvCastService { + final TvCastApi _api; + final CanvasDevice _device; + + TvCastServiceImpl(this._api, this._device); + + Future _cast(Map body) async { + final result = await _api.request( + locationId: _device.locationId, + topicId: _device.topicId, + body: body, + ); + return result['message']; + } + + Map _getBody(Request request) => + RequestBody(request).toJson(); + + @override + Future status( + CheckDeviceStatusRequest request) async { + final result = await _cast(_getBody(request)); + return CheckDeviceStatusReply.fromJson(result); + } + + @override + Future connect(ConnectRequestV2 request) async { + final result = await _cast(_getBody(request)); + return ConnectReplyV2.fromJson(result); + } + + @override + Future disconnect(DisconnectRequestV2 request) async { + final result = await _cast(_getBody(request)); + return DisconnectReplyV2.fromJson(result); + } + + @override + Future castListArtwork( + CastListArtworkRequest request) async { + final result = await _cast(_getBody(request)); + return CastListArtworkReply.fromJson(result); + } + + @override + Future pauseCasting(PauseCastingRequest request) async { + final result = await _cast(_getBody(request)); + return PauseCastingReply.fromJson(result); + } + + @override + Future resumeCasting(ResumeCastingRequest request) async { + final result = await _cast(_getBody(request)); + return ResumeCastingReply.fromJson(result); + } + + @override + Future nextArtwork(NextArtworkRequest request) async { + final result = await _cast(_getBody(request)); + return NextArtworkReply.fromJson(result); + } + + @override + Future moveToArtwork(MoveToArtworkRequest request) async { + final result = await _cast(_getBody(request)); + return MoveToArtworkReply.fromJson(result); + } + + @override + Future previousArtwork( + PreviousArtworkRequest request) async { + final result = await _cast(_getBody(request)); + return PreviousArtworkReply.fromJson(result); + } + + @override + Future appendListArtwork( + AppendArtworkToCastingListRequest request) async { + final result = await _cast(_getBody(request)); + return AppendArtworkToCastingListReply.fromJson(result); + } + + @override + Future updateDuration( + UpdateDurationRequest request) async { + final result = await _cast(_getBody(request)); + return UpdateDurationReply.fromJson(result); + } + + @override + Future keyboardEvent(KeyboardEventRequest request) async { + final result = await _cast(_getBody(request)); + return KeyboardEventReply.fromJson(result); + } + + @override + Future rotate(RotateRequest request) async { + final result = await _cast(_getBody(request)); + return RotateReply.fromJson(result); + } + + @override + Future castExhibition( + CastExhibitionRequest request) async { + final result = await _cast(_getBody(request)); + return CastExhibitionReply.fromJson(result); + } + + @override + Future tap(TapGestureRequest request) async { + final result = await _cast(_getBody(request)); + return GestureReply.fromJson(result); + } + + @override + Future drag(DragGestureRequest request) async { + final result = await _cast(_getBody(request)); + return GestureReply.fromJson(result); + } + + @override + Future getCursorOffset( + GetCursorOffsetRequest request) async { + final result = await _cast(_getBody(request)); + return GetCursorOffsetReply.fromJson(result); + } + + @override + Future setCursorOffset( + SetCursorOffsetRequest request) async { + final result = await _cast(_getBody(request)); + return SetCursorOffsetReply.fromJson(result); + } +} diff --git a/lib/util/canvas_device_adapter.dart b/lib/util/canvas_device_adapter.dart new file mode 100644 index 000000000..43e5f75f3 --- /dev/null +++ b/lib/util/canvas_device_adapter.dart @@ -0,0 +1,24 @@ +import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; +import 'package:hive/hive.dart'; + +class CanvasDeviceAdapter extends TypeAdapter { + @override + final int typeId = 0; + + @override + CanvasDevice read(BinaryReader reader) => CanvasDevice( + deviceId: reader.readString(), + locationId: reader.readString(), + topicId: reader.readString(), + name: reader.readString(), + ); + + @override + void write(BinaryWriter writer, CanvasDevice obj) { + writer + ..writeString(obj.deviceId) + ..writeString(obj.locationId) + ..writeString(obj.topicId) + ..writeString(obj.name); + } +} diff --git a/lib/util/cast_request_ext.dart b/lib/util/cast_request_ext.dart new file mode 100644 index 000000000..a74cca599 --- /dev/null +++ b/lib/util/cast_request_ext.dart @@ -0,0 +1,5 @@ +import 'package:feralfile_app_tv_proto/models/model.dart'; + +extension CastExhibitionRequestExt on CastExhibitionRequest { + String get displayKey => exhibitionId ?? ''; +} diff --git a/lib/util/device_status_ext.dart b/lib/util/device_status_ext.dart index 7f4b80261..cc7fc3b74 100644 --- a/lib/util/device_status_ext.dart +++ b/lib/util/device_status_ext.dart @@ -11,11 +11,39 @@ extension ListDeviceStatusExtension final thisDevice = canvasClientServiceV2.clientDeviceInfo; for (final devicePair in this) { final status = devicePair.second; - if (status.connectedDevice.deviceId == thisDevice.deviceId) { - controllingDeviceStatus[devicePair.first.id] = status; - break; + if (status.connectedDevice?.deviceId == thisDevice.deviceId) { + controllingDeviceStatus[devicePair.first.deviceId] = status; } } return controllingDeviceStatus; } } + +extension DeviceStatusExtension on CheckDeviceStatusReply { + String? get playingArtworkKey { + if (artworks.isEmpty && exhibitionId == null) { + return null; + } + if (exhibitionId != null) { + return exhibitionId.toString(); + } + + final hashCode = artworks.playArtworksHashCode; + return hashCode.toString(); + } +} + +extension PlayArtworksExtension on List { + int get playArtworksHashCode { + final hashCodes = map((e) => e.playArtworkHashCode); + final hashCode = hashCodes.reduce((value, element) => value ^ element); + return hashCode; + } +} + +extension PlayArtworkExtension on PlayArtworkV2 { + int get playArtworkHashCode { + final id = token?.id ?? artwork?.url ?? ''; + return id.hashCode; + } +} diff --git a/lib/util/dio_interceptors.dart b/lib/util/dio_interceptors.dart index dfa2d99fe..a635ea339 100644 --- a/lib/util/dio_interceptors.dart +++ b/lib/util/dio_interceptors.dart @@ -251,3 +251,15 @@ class ConnectingExceptionInterceptor extends Interceptor { handler.next(err); } } + +class TVKeyInterceptor extends Interceptor { + final String tvKey; + + TVKeyInterceptor(this.tvKey); + + @override + void onRequest(RequestOptions options, RequestInterceptorHandler handler) { + options.headers['API-KEY'] = tvKey; + handler.next(options); + } +} diff --git a/lib/util/dio_util.dart b/lib/util/dio_util.dart index f9eb6f796..c3e383840 100644 --- a/lib/util/dio_util.dart +++ b/lib/util/dio_util.dart @@ -36,6 +36,14 @@ Dio postcardDio(BaseOptions options) { return dio; } +Dio tvCastDio(BaseOptions options) { + final dio = Dio(options); + dio.interceptors.add(TVKeyInterceptor(Environment.tvKey)); + dio.interceptors.add(LoggingInterceptor()); + dio.interceptors.add(ConnectingExceptionInterceptor()); + return dio; +} + Dio chatDio(BaseOptions options) { final dio = baseDio(options); dio.interceptors.add(HmacAuthInterceptor(Environment.chatServerHmacKey)); diff --git a/lib/util/exhibition_ext.dart b/lib/util/exhibition_ext.dart index e4e1716ec..4064c6e95 100644 --- a/lib/util/exhibition_ext.dart +++ b/lib/util/exhibition_ext.dart @@ -30,6 +30,8 @@ extension ExhibitionExt on Exhibition { id == exhibitionBloc.state.featuredExhibition?.id; } + String get displayKey => id; + //TODO: implement this bool get isOnGoing => true; diff --git a/lib/util/image_ext.dart b/lib/util/image_ext.dart index 51ecff9aa..51e2ea4b6 100644 --- a/lib/util/image_ext.dart +++ b/lib/util/image_ext.dart @@ -1,11 +1,10 @@ -import 'dart:async'; - +import 'package:autonomy_flutter/view/image_background.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; extension ImageExt on CachedNetworkImage { - static CachedNetworkImage customNetwork( + static Widget customNetwork( String src, { Duration fadeInDuration = const Duration(milliseconds: 300), BoxFit? fit, @@ -19,8 +18,25 @@ extension ImageExt on CachedNetworkImage { bool shouldRefreshCache = false, }) { if (shouldRefreshCache) { - unawaited(cacheManager?.removeFile(src)); + Image.network( + src, + fit: fit, + errorBuilder: (context, error, stackTrace) => + errorWidget!(context, src, error), + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) { + return ImageBackground(child: child); + } + if (placeholder != null) { + return placeholder(context, src); + } + return const ImageBackground(child: SizedBox()); + }, + cacheHeight: memCacheHeight, + cacheWidth: memCacheWidth, + ); } + return CachedNetworkImage( imageUrl: src, fadeInDuration: Duration.zero, diff --git a/lib/util/local_network_helper.dart b/lib/util/local_network_helper.dart deleted file mode 100644 index 5e3ae199e..000000000 --- a/lib/util/local_network_helper.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'dart:io'; - -import 'package:autonomy_flutter/util/log.dart'; -import 'package:network_info_plus/network_info_plus.dart'; - -class LocalNetworkHelper { - static Future requestLocalNetworkPermission() async { - bool isGranted = false; - try { - final wifiIp = await NetworkInfo().getWifiIP(); - log.info('[LocalNetworkHelper] wifiIp: $wifiIp'); - await Socket.connect(wifiIp, 80, - timeout: const Duration(milliseconds: 100)); - isGranted = true; - } catch (e) { - log.info('[LocalNetworkHelper] requestLocalNetworkPermission Error: $e'); - isGranted = false; - } - return isGranted; - } -} diff --git a/lib/util/play_control.dart b/lib/util/play_control.dart deleted file mode 100644 index 074e2dec0..000000000 --- a/lib/util/play_control.dart +++ /dev/null @@ -1,183 +0,0 @@ -// import 'package:autonomy_flutter/model/play_control_model.dart'; -// import 'package:autonomy_flutter/view/cast_button.dart'; -// import 'package:feralfile_app_theme/feral_file_app_theme.dart'; -// import 'package:flutter/material.dart'; -// import 'package:flutter_svg/svg.dart'; - -// class PlaylistControl extends StatelessWidget { -// final PlayControlModel playControl; -// final Function()? onPlayTap; -// final Function()? onTimerTap; -// final Function()? onShuffleTap; -// final Function()? onCastTap; -// final bool showPlay; -// final bool isCasting; - -// const PlaylistControl({ -// required this.playControl, -// required this.isCasting, -// super.key, -// this.onPlayTap, -// this.onTimerTap, -// this.onShuffleTap, -// this.showPlay = true, -// this.onCastTap, -// }); - -// @override -// Widget build(BuildContext context) { -// final theme = Theme.of(context); - -// return Container( -// color: theme.colorScheme.primary, -// child: Padding( -// padding: const EdgeInsets.symmetric(vertical: 15), -// child: Row( -// mainAxisAlignment: MainAxisAlignment.spaceEvenly, -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// ControlItem( -// icon: SvgPicture.asset( -// 'assets/images/time_off_icon.svg', -// width: 24, -// colorFilter: -// ColorFilter.mode(theme.disableColor, BlendMode.srcIn), -// ), -// iconFocus: Stack( -// children: [ -// Padding( -// padding: const EdgeInsets.fromLTRB(0, 0, 4, 2), -// child: SvgPicture.asset( -// 'assets/images/time_off_icon.svg', -// width: 24, -// colorFilter: ColorFilter.mode( -// theme.colorScheme.secondary, BlendMode.srcIn), -// ), -// ), -// Positioned( -// bottom: 0, -// right: 0, -// child: Visibility( -// visible: playControl.timer != 0, -// child: Container( -// padding: const EdgeInsets.fromLTRB(3, 2, 3, 0), -// decoration: BoxDecoration( -// color: theme.colorScheme.secondary, -// borderRadius: BorderRadius.circular(6), -// ), -// child: Text( -// playControl.timer.toString(), -// style: TextStyle( -// fontFamily: 'PPMori', -// color: theme.colorScheme.primary, -// fontSize: 8, -// height: 1, -// fontWeight: FontWeight.w700, -// ), -// ), -// ), -// ), -// ) -// ], -// ), -// isActive: playControl.timer != 0, -// onTap: () { -// onTimerTap?.call(); -// }, -// ), -// ControlItem( -// icon: SvgPicture.asset( -// 'assets/images/shuffle_icon.svg', -// width: 24, -// colorFilter: -// ColorFilter.mode(theme.disableColor, BlendMode.srcIn), -// ), -// iconFocus: SvgPicture.asset( -// 'assets/images/shuffle_icon.svg', -// width: 24, -// colorFilter: ColorFilter.mode( -// theme.colorScheme.secondary, BlendMode.srcIn), -// ), -// isActive: playControl.isShuffle, -// onTap: () { -// onShuffleTap?.call(); -// }, -// ), -// if (showPlay) ...[ -// ControlItem( -// icon: SvgPicture.asset( -// 'assets/images/play_icon.svg', -// width: 24, -// colorFilter: ColorFilter.mode( -// theme.colorScheme.secondary, BlendMode.srcIn), -// ), -// iconFocus: SvgPicture.asset( -// 'assets/images/play_icon.svg', -// width: 24, -// colorFilter: ColorFilter.mode( -// theme.colorScheme.secondary, BlendMode.srcIn), -// ), -// onTap: () { -// onPlayTap?.call(); -// }, -// ), -// Padding( -// padding: const EdgeInsets.all(8), -// child: FFCastButton( -// onDeviceSelected: (device) => {onCastTap?.call()}, -// isCasting: isCasting, -// ), -// ) -// ] -// ], -// ), -// ), -// ); -// } -// } - -// class ControlItem extends StatefulWidget { -// final Widget icon; -// final Widget iconFocus; -// final bool isActive; -// final Function()? onTap; - -// const ControlItem({ -// required this.icon, -// required this.iconFocus, -// super.key, -// this.isActive = false, -// this.onTap, -// }); - -// @override -// State createState() => _ControlItemState(); -// } - -// class _ControlItemState extends State { -// @override -// Widget build(BuildContext context) { -// final theme = Theme.of(context); -// return GestureDetector( -// onTap: widget.onTap, -// child: Column( -// children: [ -// Padding( -// padding: const EdgeInsets.all(8), -// child: widget.isActive ? widget.iconFocus : widget.icon, -// ), -// Container( -// width: 4, -// height: 4, -// decoration: widget.isActive -// ? BoxDecoration( -// shape: BoxShape.circle, -// color: theme.colorScheme.secondary, -// ) -// : null, -// ) -// ], -// ), -// ); -// } -// } diff --git a/lib/util/playlist_ext.dart b/lib/util/playlist_ext.dart new file mode 100644 index 000000000..8879d318c --- /dev/null +++ b/lib/util/playlist_ext.dart @@ -0,0 +1,13 @@ +import 'package:autonomy_flutter/model/play_list_model.dart'; + +extension PlaylistExt on PlayListModel { + String? get displayKey { + final listTokenIds = tokenIDs ?? []; + if (listTokenIds.isEmpty) { + return null; + } + final hashCodes = listTokenIds.map((e) => e.hashCode).toList(); + final hashCode = hashCodes.reduce((value, element) => value ^ element); + return hashCode.toString(); + } +} diff --git a/lib/util/series_ext.dart b/lib/util/series_ext.dart index 416c36054..d19e397c8 100644 --- a/lib/util/series_ext.dart +++ b/lib/util/series_ext.dart @@ -1,4 +1,5 @@ import 'package:autonomy_flutter/model/ff_series.dart'; +import 'package:autonomy_flutter/util/exhibition_ext.dart'; import 'package:autonomy_flutter/util/john_gerrard_hepler.dart'; extension FFSeriesExt on FFSeries { @@ -12,4 +13,6 @@ extension FFSeriesExt on FFSeries { int? get latestRevealedArtworkIndex => metadata?['latestRevealedArtworkIndex']; + + String get displayKey => exhibition?.displayKey ?? exhibitionID; } diff --git a/lib/view/back_appbar.dart b/lib/view/back_appbar.dart index 33cc70d77..6293cf178 100644 --- a/lib/view/back_appbar.dart +++ b/lib/view/back_appbar.dart @@ -356,7 +356,7 @@ AppBar getCustomDoneAppBar( ), ), ], - backgroundColor: theme.colorScheme.surface, + backgroundColor: AppColor.white, automaticallyImplyLeading: false, centerTitle: true, title: title, diff --git a/lib/view/cast_button.dart b/lib/view/cast_button.dart index adbc22fbd..bf7dc22b8 100644 --- a/lib/view/cast_button.dart +++ b/lib/view/cast_button.dart @@ -1,11 +1,8 @@ import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/screen/detail/preview/artwork_preview_page.dart'; import 'package:autonomy_flutter/screen/detail/preview/canvas_device_bloc.dart'; -import 'package:autonomy_flutter/service/network_issue_manager.dart'; -import 'package:autonomy_flutter/service/network_service.dart'; import 'package:autonomy_flutter/util/ui_helper.dart'; import 'package:autonomy_flutter/view/stream_device_view.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; import 'package:feralfile_app_tv_proto/models/canvas_device.dart'; import 'package:flutter/material.dart'; @@ -14,11 +11,16 @@ import 'package:flutter_svg/flutter_svg.dart'; class FFCastButton extends StatefulWidget { final Function(CanvasDevice device)? onDeviceSelected; + final String displayKey; final String? text; final String? type; const FFCastButton( - {this.type = '', super.key, this.onDeviceSelected, this.text}); + {required this.displayKey, + this.type = '', + super.key, + this.onDeviceSelected, + this.text}); @override State createState() => _FFCastButtonState(); @@ -40,25 +42,12 @@ class _FFCastButtonState extends State { return BlocBuilder( bloc: _canvasDeviceBloc, builder: (context, state) { - final isCasting = state.isCasting; + final castingDevice = + state.lastSelectedActiveDeviceForKey(widget.displayKey); + final isCasting = castingDevice != null; return GestureDetector( onTap: () async { - if (!injector.get().isWifi) { - final isRetry = - await injector().showRetryDialog( - context, - description: 'network_error_canvas_desc'.tr(), - dynamicRetryNotifier: injector().isWifiNotifier, - onRetry: () async => true, - ); - if (!(isRetry ?? false)) { - return; - } - } - if (!context.mounted) { - return; - } - await _showStreamAction(context); + await _showStreamAction(context, widget.displayKey); }, child: Semantics( label: 'cast_icon', @@ -116,14 +105,19 @@ class _FFCastButtonState extends State { ); } - Future _showStreamAction(BuildContext context) async { + Future _showStreamAction( + BuildContext context, String displayKey) async { keyboardManagerKey.currentState?.hideKeyboard(); await UIHelper.showFlexibleDialog( context, BlocProvider.value( value: _canvasDeviceBloc, child: StreamDeviceView( - onDeviceSelected: widget.onDeviceSelected, + displayKey: displayKey, + onDeviceSelected: (canvasDevice) { + widget.onDeviceSelected?.call(canvasDevice); + Navigator.pop(context); + }, ), ), isDismissible: true, diff --git a/lib/view/stream_common_widget.dart b/lib/view/stream_common_widget.dart index 0bed011da..9f92af8a5 100644 --- a/lib/view/stream_common_widget.dart +++ b/lib/view/stream_common_widget.dart @@ -4,6 +4,7 @@ import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/screen/detail/preview/canvas_device_bloc.dart'; import 'package:autonomy_flutter/util/range_input_formatter.dart'; import 'package:autonomy_flutter/util/ui_helper.dart'; +import 'package:autonomy_flutter/view/responsive.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; @@ -33,9 +34,18 @@ final speedValues = { class StreamDrawerItem extends StatelessWidget { final OptionItem item; final Color backgroundColor; + final Function()? onRotateClicked; + final bool isControlling; - const StreamDrawerItem( - {required this.item, required this.backgroundColor, super.key}); + static const double rotateIconSize = 22; + + const StreamDrawerItem({ + required this.item, + required this.backgroundColor, + required this.isControlling, + super.key, + this.onRotateClicked, + }); @override Widget build(BuildContext context) => Container( @@ -46,45 +56,68 @@ class StreamDrawerItem extends StatelessWidget { width: MediaQuery.of(context).size.width, child: Material( type: MaterialType.transparency, - child: InkWell( - splashFactory: InkSparkle.splashFactory, - borderRadius: BorderRadius.circular(50), - child: Padding( - padding: const EdgeInsets.all(12), - child: Center( - child: Text( - item.title ?? '', - style: Theme.of(context).textTheme.ppMori400Black14, + child: Stack( + children: [ + InkWell( + splashFactory: InkSparkle.splashFactory, + borderRadius: BorderRadius.circular(50), + child: Padding( + padding: EdgeInsets.symmetric( + vertical: 12, + horizontal: ResponsiveLayout.padding + rotateIconSize + 10, + ), + child: Center( + child: Text( + item.title ?? '', + style: Theme.of(context).textTheme.ppMori400Black14, + ), + ), ), + onTap: () => item.onTap?.call(), ), - ), - onTap: () => item.onTap?.call(), + if (isControlling) + Positioned( + top: 0, + bottom: 0, + right: ResponsiveLayout.padding, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: onRotateClicked, + child: SvgPicture.asset( + 'assets/images/icon_rotate.svg', + width: rotateIconSize, + height: rotateIconSize, + ), + ), + ], + ), + ) + ], ), ), ); } class PlaylistControl extends StatefulWidget { - const PlaylistControl({super.key}); + final String displayKey; + + const PlaylistControl({required this.displayKey, super.key}); @override State createState() => _PlaylistControlState(); } class _PlaylistControlState extends State { - late double _currentSliderValue; Timer? _timer; late CanvasDeviceBloc _canvasDeviceBloc; + CanvasDevice? _controllingDevice; @override void initState() { super.initState(); _canvasDeviceBloc = injector.get(); - final castingDuration = _canvasDeviceBloc.state.castingSpeed; - final index = castingDuration != null - ? speedValues.values.toList().indexOf(castingDuration) - : 0; - _currentSliderValue = index.toDouble(); } @override @@ -95,32 +128,25 @@ class _PlaylistControlState extends State { @override Widget build(BuildContext context) => - BlocConsumer( + BlocBuilder( bloc: _canvasDeviceBloc, - listener: (context, state) { - final castingSpeed = state.controllingDeviceStatus?.values.firstOrNull - ?.artworks.firstOrNull?.duration; - if (castingSpeed != null) { - final castingDuration = Duration(milliseconds: castingSpeed); - final index = speedValues.values.toList().indexOf(castingDuration); - setState(() { - _currentSliderValue = index.toDouble(); - }); - } + builder: (context, state) { + _controllingDevice = + state.lastSelectedActiveDeviceForKey(widget.displayKey); + return Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: AppColor.auGreyBackground, + ), + child: Column( + children: [ + _buildPlayControls(context, state), + const SizedBox(height: 15), + _buildSpeedControl(context, state), + ], + )); }, - builder: (context, state) => Container( - padding: const EdgeInsets.all(15), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: AppColor.auGreyBackground, - ), - child: Column( - children: [ - _buildPlayControls(context, state), - const SizedBox(height: 15), - _buildSpeedControl(context, state), - ], - )), ); Widget _buildPlayButton({required String icon, required Function() onTap}) => @@ -149,7 +175,7 @@ class _PlaylistControlState extends State { ); Widget _buildPlayControls(BuildContext context, CanvasDeviceState state) { - final isCasting = state.isCasting; + final isCasting = _controllingDevice != null; return Row( children: [ _buildPlayButton( @@ -191,47 +217,45 @@ class _PlaylistControlState extends State { color: AppColor.primaryBlack, ), child: ArtworkDurationControl( - duration: state.castingSpeed, + duration: state.castingSpeed(widget.displayKey), + displayKey: widget.displayKey, ), ) ], ); void onPrevious(BuildContext context) { - final controllingDevice = _canvasDeviceBloc.state.controllingDevice; - if (controllingDevice == null) { + if (_controllingDevice == null) { return; } - _canvasDeviceBloc.add(CanvasDevicePreviousArtworkEvent(controllingDevice)); + _canvasDeviceBloc + .add(CanvasDevicePreviousArtworkEvent(_controllingDevice!)); } void onNext(BuildContext context) { - final controllingDevice = _canvasDeviceBloc.state.controllingDevice; - if (controllingDevice == null) { + if (_controllingDevice == null) { return; } - _canvasDeviceBloc.add(CanvasDeviceNextArtworkEvent(controllingDevice)); + _canvasDeviceBloc.add(CanvasDeviceNextArtworkEvent(_controllingDevice!)); } void onPause(BuildContext context) { - final controllingDevice = _canvasDeviceBloc.state.controllingDevice; - if (controllingDevice == null) { + if (_controllingDevice == null) { return; } - _canvasDeviceBloc.add(CanvasDevicePauseCastingEvent(controllingDevice)); + _canvasDeviceBloc.add(CanvasDevicePauseCastingEvent(_controllingDevice!)); } void onResume(BuildContext context) { - final controllingDevice = _canvasDeviceBloc.state.controllingDevice; - if (controllingDevice == null) { + if (_controllingDevice == null) { return; } - _canvasDeviceBloc.add(CanvasDeviceResumeCastingEvent(controllingDevice)); + _canvasDeviceBloc.add(CanvasDeviceResumeCastingEvent(_controllingDevice!)); } void onPauseOrResume(BuildContext context) { // final _canvasDeviceBloc = context.read(); - final isCasting = _canvasDeviceBloc.state.isCasting; + final isCasting = _controllingDevice != null; if (isCasting) { onPause(context); } else { @@ -242,8 +266,10 @@ class _PlaylistControlState extends State { class ArtworkDurationControl extends StatefulWidget { final Duration? duration; + final String displayKey; - const ArtworkDurationControl({super.key, this.duration}); + const ArtworkDurationControl( + {required this.displayKey, super.key, this.duration}); @override State createState() => _ArtworkDurationControlState(); @@ -329,12 +355,13 @@ class _ArtworkDurationControlState extends State { } void changeSpeed(Duration duration) { - final controllingDevice = _canvasDeviceBloc.state.controllingDevice; - if (controllingDevice == null) { + final lastSelectedCanvasDevice = _canvasDeviceBloc.state + .lastSelectedActiveDeviceForKey(widget.displayKey); + if (lastSelectedCanvasDevice == null) { return; } final canvasStatus = - _canvasDeviceBloc.state.controllingDeviceStatus?.values.firstOrNull; + _canvasDeviceBloc.state.statusOf(lastSelectedCanvasDevice); if (canvasStatus == null) { return; } @@ -344,7 +371,7 @@ class _ArtworkDurationControlState extends State { e.copy(duration: Duration(milliseconds: duration.inMilliseconds))) .toList(); _canvasDeviceBloc.add(CanvasDeviceUpdateDurationEvent( - controllingDevice, playArtworkWithNewDuration)); + lastSelectedCanvasDevice, playArtworkWithNewDuration)); } @override diff --git a/lib/view/stream_device_view.dart b/lib/view/stream_device_view.dart index 17c0e7595..1f9627eee 100644 --- a/lib/view/stream_device_view.dart +++ b/lib/view/stream_device_view.dart @@ -14,13 +14,16 @@ import 'package:feralfile_app_tv_proto/models/canvas_device.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; class StreamDeviceView extends StatefulWidget { final Function(CanvasDevice device)? onDeviceSelected; + final String? displayKey; const StreamDeviceView({ super.key, this.onDeviceSelected, + this.displayKey, }); @override @@ -48,32 +51,46 @@ class _StreamDeviceViewState extends State { bloc: _canvasDeviceBloc, builder: (context, state) { final devices = state.devices; - final connectedDevice = state.devices - .where((element) => - state.controllingDeviceStatus?.keys - .contains(element.device.id) ?? - false) - .firstOrNull; + final connectedDevice = widget.displayKey == null + ? null + : state.lastSelectedActiveDeviceForKey(widget.displayKey!); return Padding( padding: ResponsiveLayout.pageHorizontalEdgeInsets, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - RichText( - text: TextSpan( - children: [ - TextSpan( - text: 'connect_and_control'.tr(), - style: theme.textTheme.ppMori700White24, + Row( + children: [ + Expanded( + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: 'display'.tr(), + style: theme.textTheme.ppMori700White24, + ), + if (connectedDevice != null) + TextSpan( + text: ' ${connectedDevice.name}', + style: theme.textTheme.ppMori400White24, + ), + ], + ), ), - if (connectedDevice != null) - TextSpan( - text: ' ${connectedDevice.device.name}', - style: theme.textTheme.ppMori400White24, + ), + Padding( + padding: const EdgeInsets.only(left: 10), + child: GestureDetector( + onTap: () => Navigator.pop(context), + child: SvgPicture.asset( + 'assets/images/circle_close.svg', + width: 22, + height: 22, ), - ], - ), + ), + ) + ], ), const SizedBox(height: 40), ListView.builder( @@ -83,7 +100,8 @@ class _StreamDeviceViewState extends State { itemCount: devices.length, itemBuilder: (BuildContext context, int index) { final device = devices[index].device; - final isControlling = state.isDeviceControlling(device); + final isControlling = + device.deviceId == connectedDevice?.deviceId; return Column( children: [ Builder( @@ -91,7 +109,7 @@ class _StreamDeviceViewState extends State { item: OptionItem( title: device.name, onTap: () { - log.info('device selected: ${device.id}'); + log.info('device selected: ${device.deviceId}'); widget.onDeviceSelected?.call(device); }), backgroundColor: connectedDevice == null @@ -99,6 +117,8 @@ class _StreamDeviceViewState extends State { : isControlling ? AppColor.feralFileLightBlue : AppColor.disabledColor, + isControlling: isControlling, + onRotateClicked: () => onRotate(context), ), ), if (index < devices.length - 1) @@ -125,7 +145,7 @@ class _StreamDeviceViewState extends State { ), recognizer: TapGestureRecognizer() ..onTap = () async { - await scanToAddMore(context); + await _scanToAddMore(context); }, ), TextSpan( @@ -174,9 +194,17 @@ class _StreamDeviceViewState extends State { ); } - Future scanToAddMore(BuildContext context) async { + void onRotate(BuildContext context) { + final lastSelectedCanvasDevice = _canvasDeviceBloc.state + .lastSelectedActiveDeviceForKey(widget.displayKey!); + if (lastSelectedCanvasDevice != null) { + _canvasDeviceBloc.add(CanvasDeviceRotateEvent(lastSelectedCanvasDevice)); + } + } + + Future _scanToAddMore(BuildContext context) async { final device = await Navigator.of(context) - .pushNamed(AppRouter.scanQRPage, arguments: ScannerItem.CANVAS_DEVICE); + .pushNamed(AppRouter.scanQRPage, arguments: ScannerItem.CANVAS); log.info('device selected: $device'); _canvasDeviceBloc.add(CanvasDeviceGetDevicesEvent()); } diff --git a/lib/view/touchpad.dart b/lib/view/touchpad.dart index fbb7fe7f7..a73cf958a 100644 --- a/lib/view/touchpad.dart +++ b/lib/view/touchpad.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:after_layout/after_layout.dart'; import 'package:autonomy_flutter/common/injector.dart'; -import 'package:autonomy_flutter/service/canvas_client_service.dart'; +import 'package:autonomy_flutter/service/canvas_client_service_v2.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; @@ -21,7 +21,7 @@ class TouchPad extends StatefulWidget { } class _TouchPadState extends State with AfterLayoutMixin { - final _canvasClient = injector(); + final _canvasClient = injector(); final _touchPadKey = GlobalKey(); Size? _touchpadSize; diff --git a/pubspec.lock b/pubspec.lock index e4ec1ace1..b48e7c45a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -145,54 +145,6 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.2" - bonsoir: - dependency: "direct main" - description: - name: bonsoir - sha256: "9703ca3ce201c7ab6cd278ae5a530a125959687f59c2b97822f88a8db5bef106" - url: "https://pub.dev" - source: hosted - version: "5.1.9" - bonsoir_android: - dependency: transitive - description: - name: bonsoir_android - sha256: "19583ae34a5e5743fa2c16619e4ec699b35ae5e6cece59b99b1cf21c1b4ed618" - url: "https://pub.dev" - source: hosted - version: "5.1.4" - bonsoir_darwin: - dependency: transitive - description: - name: bonsoir_darwin - sha256: "985c4c38b4cbfa57ed5870e724a7e17aa080ee7f49d03b43e6d08781511505c6" - url: "https://pub.dev" - source: hosted - version: "5.1.2" - bonsoir_linux: - dependency: transitive - description: - name: bonsoir_linux - sha256: "65554b20bc169c68c311eb31fab46ccdd8ee3d3dd89a2d57c338f4cbf6ceb00d" - url: "https://pub.dev" - source: hosted - version: "5.1.2" - bonsoir_platform_interface: - dependency: transitive - description: - name: bonsoir_platform_interface - sha256: "4ee898bec0b5a63f04f82b06da9896ae8475f32a33b6fa395bea56399daeb9f0" - url: "https://pub.dev" - source: hosted - version: "5.1.2" - bonsoir_windows: - dependency: transitive - description: - name: bonsoir_windows - sha256: abbc90b73ac39e823b0c127da43b91d8906dcc530fc0cec4e169cf0d8c4404b1 - url: "https://pub.dev" - source: hosted - version: "5.1.4" boolean_selector: dependency: transitive description: @@ -663,8 +615,8 @@ packages: dependency: "direct main" description: path: "." - ref: dfa84af4d0c25196cc256628e20e4bfe1c9a04b5 - resolved-ref: dfa84af4d0c25196cc256628e20e4bfe1c9a04b5 + ref: "18fcb34f2a6966312b2ca0aae858621ab89295f4" + resolved-ref: "18fcb34f2a6966312b2ca0aae858621ab89295f4" url: "https://github.com/autonomy-system/feralfile-app-tv-proto-communication" source: git version: "0.0.1" @@ -717,7 +669,7 @@ packages: source: hosted version: "0.9.3" fixnum: - dependency: "direct main" + dependency: transitive description: name: fixnum sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" diff --git a/pubspec.yaml b/pubspec.yaml index 9317138c5..9a32722e6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -122,7 +122,7 @@ dependencies: feralfile_app_tv_proto: git: url: https://github.com/autonomy-system/feralfile-app-tv-proto-communication - ref: dfa84af4d0c25196cc256628e20e4bfe1c9a04b5 + ref: 18fcb34f2a6966312b2ca0aae858621ab89295f4 flutter_branch_sdk: ^7.0.2 flutter_rating_bar: ^4.0.1 multi_value_listenable_builder: ^0.0.2 @@ -159,9 +159,7 @@ dependencies: image_gallery_saver: ^2.0.3 hand_signature: ^3.0.1 flutter_pdfview: ^1.3.2 - fixnum: ^1.1.0 retry: ^3.1.2 - bonsoir: ^5.1.9 network_info_plus: ^5.0.3 backdrop: ^0.9.1