diff --git a/lib/common/injector.dart b/lib/common/injector.dart index 27c65f011..67b8c1547 100644 --- a/lib/common/injector.dart +++ b/lib/common/injector.dart @@ -121,6 +121,7 @@ Future setup() async { migrateV15ToV16, migrateV16ToV17, migrateV17ToV18, + migrateV18ToV19, ]).build(); final cloudDB = await $FloorCloudDatabase @@ -320,8 +321,8 @@ Future setup() async { injector.registerLazySingleton( () => PlayListServiceImp(injector(), injector(), injector(), injector())); - injector.registerLazySingleton( - () => CanvasClientService(injector())); + injector + .registerLazySingleton(() => CanvasClientService()); injector.registerLazySingleton( () => PostcardServiceImpl( diff --git a/lib/database/app_database.dart b/lib/database/app_database.dart index ed0509e5a..6b54c9eee 100644 --- a/lib/database/app_database.dart +++ b/lib/database/app_database.dart @@ -8,7 +8,6 @@ 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'; @@ -25,11 +24,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: 18, entities: [ Identity, DraftCustomerSupport, AnnouncementLocal, - CanvasDevice, Scene, ]) abstract class AppDatabase extends FloorDatabase { @@ -39,16 +37,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 +156,9 @@ 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;'); + log.info('Migrated App database from version 18 to 18'); +}); diff --git a/lib/database/app_database.g.dart b/lib/database/app_database.g.dart index 4a7d5ceb0..542684593 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: 18, onConfigure: (database) async { await database.execute('PRAGMA foreign_keys = ON'); await callback?.onConfigure?.call(database); @@ -98,8 +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`))'); @@ -125,17 +119,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 +421,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/screen/detail/preview/artwork_preview_page.dart b/lib/screen/detail/preview/artwork_preview_page.dart index 66e04d953..6e6a7eac4 100644 --- a/lib/screen/detail/preview/artwork_preview_page.dart +++ b/lib/screen/detail/preview/artwork_preview_page.dart @@ -364,13 +364,6 @@ class _ArtworkPreviewPageState extends State const SizedBox( width: 20, ), - CastButton( - onCastTap: () async => _onCastTap(assetToken), - isCasting: isCasting, - ), - const SizedBox( - width: 20, - ), GestureDetector( onTap: isCasting ? null @@ -387,6 +380,12 @@ class _ArtworkPreviewPageState extends State ), ), ), + const SizedBox( + width: 20, + ), + FFCastButton( + onDeviceSelected: (deviceID) {}, + ), ], ), ), diff --git a/lib/screen/detail/preview/canvas_device_bloc.dart b/lib/screen/detail/preview/canvas_device_bloc.dart index 628b993c5..724fce20f 100644 --- a/lib/screen/detail/preview/canvas_device_bloc.dart +++ b/lib/screen/detail/preview/canvas_device_bloc.dart @@ -136,21 +136,52 @@ class CanvasDeviceBloc extends AuBloc { CanvasDeviceBloc(this._canvasClientService) : super(CanvasDeviceState(devices: [], isLoaded: false)) { on((event, emit) async { - emit(CanvasDeviceState( - devices: state.devices, - sceneId: event.sceneId, - isLoaded: state.devices.isNotEmpty)); - final devices = await _canvasClientService.getConnectingDevices( - doSync: event.syncAll); - emit(state.copyWith( - devices: devices - .map((e) => DeviceState( - device: e, - status: e.isConnecting && e.playingSceneId != null - ? DeviceStatus.playing - : DeviceStatus.connected)) - .toList(), - isLoaded: true)); + // emit(CanvasDeviceState( + // devices: state.devices, + // sceneId: event.sceneId, + // isLoaded: state.devices.isNotEmpty)); + // final devices = await _canvasClientService.getConnectingDevices( + // doSync: event.syncAll); + emit( + state.copyWith( + // devices: devices + // .map((e) => DeviceState( + // device: e, + // status: e.isConnecting && e.playingSceneId != null + // ? DeviceStatus.playing + // : DeviceStatus.connected)) + // .toList(), + devices: [ + DeviceState( + device: CanvasDevice( + id: '0', + ip: '192.168.31.1', + port: 4200, + name: 'LG-423', + isConnecting: true, + playingSceneId: ''), + ), + DeviceState( + device: CanvasDevice( + id: '1', + ip: '192.168.31.2', + port: 4200, + name: "Sean's iPad Pro", + isConnecting: false, + playingSceneId: ''), + ), + DeviceState( + device: CanvasDevice( + id: '2', + ip: '192.168.31.3', + port: 4200, + name: 'LG-424', + isConnecting: false, + playingSceneId: ''), + ), + ], + isLoaded: true), + ); }); on((event, emit) async { diff --git a/lib/screen/exhibition_details/exhibition_detail_page.dart b/lib/screen/exhibition_details/exhibition_detail_page.dart index 12e988da3..aadcc1c32 100644 --- a/lib/screen/exhibition_details/exhibition_detail_page.dart +++ b/lib/screen/exhibition_details/exhibition_detail_page.dart @@ -3,8 +3,6 @@ import 'dart:async'; import 'package:after_layout/after_layout.dart'; import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/model/ff_exhibition.dart'; -import 'package:autonomy_flutter/model/play_control_model.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/exhibition_details/exhibition_detail_bloc.dart'; @@ -12,16 +10,13 @@ import 'package:autonomy_flutter/screen/exhibition_details/exhibition_detail_sta 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/ui_helper.dart'; import 'package:autonomy_flutter/view/back_appbar.dart'; -import 'package:autonomy_flutter/view/canvas_device_view.dart'; import 'package:autonomy_flutter/view/cast_button.dart'; import 'package:autonomy_flutter/view/event_view.dart'; import 'package:autonomy_flutter/view/exhibition_detail_last_page.dart'; import 'package:autonomy_flutter/view/exhibition_detail_preview.dart'; import 'package:autonomy_flutter/view/ff_artwork_preview.dart'; import 'package:autonomy_flutter/view/note_view.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -201,48 +196,45 @@ class _ExhibitionDetailPageState extends State : Padding( padding: const EdgeInsets.only(right: 14, bottom: 10, top: 10), child: FFCastButton( - text: _currentIndex == 0 ? 'stream_to_device'.tr() : null, - onCastTap: () async { - await _onCastTap(buildContext, exhibitionDetail); - }, + onDeviceSelected: (deviceID) async {}, ), ), ); - 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 _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 diff --git a/lib/screen/home/collection_home_page.dart b/lib/screen/home/collection_home_page.dart index e541281d7..09ed09de3 100644 --- a/lib/screen/home/collection_home_page.dart +++ b/lib/screen/home/collection_home_page.dart @@ -37,6 +37,7 @@ 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/header.dart'; import 'package:autonomy_flutter/view/primary_button.dart'; import 'package:autonomy_flutter/view/responsive.dart'; @@ -222,7 +223,12 @@ class CollectionHomePageState extends State final paddingTop = MediaQuery.of(context).viewPadding.top; return Padding( padding: EdgeInsets.only(top: paddingTop), - child: HeaderView(title: 'collection'.tr()), + child: HeaderView( + title: 'collection'.tr(), + action: FFCastButton( + onDeviceSelected: (deviceID) async {}, + ), + ), ); } diff --git a/lib/screen/home/home_navigation_page.dart b/lib/screen/home/home_navigation_page.dart index d4ed59360..b970e9ac9 100644 --- a/lib/screen/home/home_navigation_page.dart +++ b/lib/screen/home/home_navigation_page.dart @@ -15,6 +15,7 @@ import 'package:autonomy_flutter/main.dart'; import 'package:autonomy_flutter/screen/app_router.dart'; import 'package:autonomy_flutter/screen/customer_support/support_thread_page.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/exhibitions/exhibitions_bloc.dart'; import 'package:autonomy_flutter/screen/exhibitions/exhibitions_page.dart'; import 'package:autonomy_flutter/screen/exhibitions/exhibitions_state.dart'; @@ -284,11 +285,17 @@ class HomeNavigationPageState extends State }); _pages = [ - CollectionHomePage(key: _collectionHomePageKey), + BlocProvider( + create: (_) => CanvasDeviceBloc( + injector(), + ), + child: CollectionHomePage(key: _collectionHomePageKey), + ), OrganizeHomePage(key: _organizeHomePageKey), MultiBlocProvider(providers: [ BlocProvider.value( - value: ExhibitionBloc(injector())..add(GetAllExhibitionsEvent())), + value: ExhibitionBloc(injector())..add(GetAllExhibitionsEvent()), + ), ], child: const ExhibitionsPage()), ScanQRPage( key: _scanQRPageKey, diff --git a/lib/screen/scan_qr/scan_qr_page.dart b/lib/screen/scan_qr/scan_qr_page.dart index c9dd46ad3..4221785f2 100644 --- a/lib/screen/scan_qr/scan_qr_page.dart +++ b/lib/screen/scan_qr/scan_qr_page.dart @@ -778,7 +778,7 @@ class QRScanViewState extends State try { final device = CanvasDevice.fromJson(jsonDecode(code)); final canvasClient = injector(); - final result = await canvasClient.connectToDevice(device); + final result = await canvasClient.connectToDevice(device, isLocal: true); if (result) { device.isConnecting = true; } diff --git a/lib/service/canvas_client_service.dart b/lib/service/canvas_client_service.dart index 0b1129c03..bd0ad3432 100644 --- a/lib/service/canvas_client_service.dart +++ b/lib/service/canvas_client_service.dart @@ -1,25 +1,24 @@ 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/model/shared_postcard.dart'; import 'package:autonomy_flutter/screen/detail/preview/canvas_device_bloc.dart'; import 'package:autonomy_flutter/service/account_service.dart'; +import 'package:autonomy_flutter/service/mdns_service.dart'; import 'package:autonomy_flutter/service/navigation_service.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/view/user_agent_utils.dart' as my_device; +import 'package:bonsoir/bonsoir.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; - - CanvasClientService(this._db); +class CanvasClientService extends DiscoverHandler { + CanvasClientService() : super(); final List _viewingDevices = []; late final String _deviceId; @@ -29,6 +28,7 @@ class CanvasClientService { final _connectDevice = Lock(); final AccountService _accountService = injector(); final NavigationService _navigationService = injector(); + final MDnsService _mdnsService = MDnsService(); Offset currentCursorOffset = Offset.zero; @@ -43,6 +43,8 @@ class CanvasClientService { _deviceName = await device.getMachineName() ?? 'Autonomy App'; final account = await _accountService.getDefaultAccount(); _deviceId = await account.getAccountDID(); + _discoverService = DiscoverService(this); + await _discoverService.discover(); _didInitialized = true; } @@ -68,10 +70,12 @@ class CanvasClientService { return CanvasControlClient(channel); } - Future connectToDevice(CanvasDevice device) async => - _connectDevice.synchronized(() async => await _connectToDevice(device)); + Future connectToDevice(CanvasDevice device, + {bool isLocal = false}) async => + _connectDevice + .synchronized(() async => await _connectToDevice(device, isLocal)); - Future _connectToDevice(CanvasDevice device) async { + Future _connectToDevice(CanvasDevice device, bool isLocal) async { final stub = _getStub(device); try { final request = ConnectRequest() @@ -94,7 +98,9 @@ class CanvasClientService { } else { _viewingDevices[index].isConnecting = true; } - await _db.canvasDeviceDao.insertCanvasDevice(device); + if (!isLocal) { + await _db.canvasDeviceDao.insertCanvasDevice(device); + } return true; } else { log.info('CanvasClientService: Failed to connect to device'); @@ -155,17 +161,10 @@ class CanvasClientService { case CanvasServerStatus.connected: device.playingSceneId = status.second; device.isConnecting = true; - unawaited(_db.canvasDeviceDao.updateCanvasDevice(device)); - devicesToAdd.add(device); - break; - case CanvasServerStatus.open: - device.playingSceneId = status.second; device.isConnecting = false; - unawaited(_db.canvasDeviceDao.updateCanvasDevice(device)); devicesToAdd.add(device); break; case CanvasServerStatus.notServing: - unawaited(_updateLocalDisconnectedDevice(device)); break; case CanvasServerStatus.error: break; @@ -179,11 +178,7 @@ class CanvasClientService { 'CanvasClientService sync device available ${_viewingDevices.length}'); } - Future> getAllDevices() async { - final devices = await _db.canvasDeviceDao.getCanvasDevices(); - log.info('CanvasClientService get devices local ${devices.length}'); - return devices; - } + Future> getAllDevices() async => _viewingDevices; Future> getConnectingDevices({bool doSync = false}) async { if (doSync) { @@ -197,12 +192,6 @@ class CanvasClientService { return _viewingDevices; } - Future _updateLocalDisconnectedDevice(CanvasDevice device) async { - final updatedDevice = device.copyWith(isConnecting: false) - ..playingSceneId = null; - await _db.canvasDeviceDao.updateCanvasDevice(updatedDevice); - } - Future castSingleArtwork(CanvasDevice device, String tokenId) async { final stub = _getStub(device); final size = @@ -352,6 +341,36 @@ class CanvasClientService { await stub.setCursorOffset(request); } + + Future> _findRawDevices() async { + final devices = []; + final discoverDevices = await _mdnsService.findCanvas(); + final localDevices = await _db.canvasDeviceDao.getCanvasDevices(); + localDevices.removeWhere((l) => discoverDevices.any((d) => d.ip == l.ip)); + devices + ..addAll(discoverDevices) + ..addAll(localDevices); + return devices; + } + + Future> fetchDevices() 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; + } } enum CanvasServerStatus { diff --git a/lib/service/discover_handler.dart b/lib/service/discover_handler.dart new file mode 100644 index 000000000..b98364f53 --- /dev/null +++ b/lib/service/discover_handler.dart @@ -0,0 +1,7 @@ +import 'package:bonsoir/bonsoir.dart'; + +abstract class DiscoverHandler { + void handleServiceResolved(BonsoirService service); + + void handleServiceLost(BonsoirService service); +} diff --git a/lib/service/discover_service.dart b/lib/service/discover_service.dart new file mode 100644 index 000000000..7b52f1177 --- /dev/null +++ b/lib/service/discover_service.dart @@ -0,0 +1,34 @@ +import 'package:autonomy_flutter/service/discover_handler.dart'; +import 'package:autonomy_flutter/util/log.dart'; +import 'package:bonsoir/bonsoir.dart'; + +class DiscoverService { + final BonsoirDiscovery _discover; + static const String _type = '__feralFileCanvas._tcp'; + final DiscoverHandler _handler; + + DiscoverService(this._handler) : _discover = BonsoirDiscovery(type: _type); + + Future discover() async { + await _discover.ready; + _discover.eventStream!.listen((event) { + if (event.type == BonsoirDiscoveryEventType.discoveryServiceResolved) { + log.info( + '[DiscoverService] Service resolved : ${event.service?.toJson()}'); + if (event.service != null) { + _handler.handleServiceResolved(event.service!); + } + } else if (event.type == BonsoirDiscoveryEventType.discoveryServiceLost) { + log.info('[DiscoverService] Service lost : ${event.service?.toJson()}'); + if (event.service != null) { + _handler.handleServiceLost(event.service!); + } + } + }); + await _discover.start(); + } + + Future stop() async { + await _discover.stop(); + } +} diff --git a/lib/service/mdns_service.dart b/lib/service/mdns_service.dart new file mode 100644 index 000000000..a8567c91c --- /dev/null +++ b/lib/service/mdns_service.dart @@ -0,0 +1,53 @@ +import 'package:autonomy_flutter/util/log.dart'; +import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; +import 'package:multicast_dns/multicast_dns.dart'; + +class MDnsService { + static const String _serviceType = '_feralFileCanvas._tcp'; + final MDnsClient _client; + + MDnsService() : _client = MDnsClient(); + + Future> findCanvas() async { + final devices = []; + await _client.start(); + await _client + .lookup( + ResourceRecordQuery.serverPointer(_serviceType), + ) + .forEach((PtrResourceRecord record) async { + await for (final TxtResourceRecord txt + in _client.lookup( + ResourceRecordQuery.text(record.domainName), + )) { + log.info('Found device: ${txt.text}'); + final name = record.domainName.split('.').first; + final text = txt.text; + final attributes = text.split('\n') + ..removeWhere((element) => !element.contains('=')); + final Map map = {}; + for (final attribute in attributes) { + final parts = attribute.split('='); + map[parts.first] = parts.last; + } + final ip = map['ip']; + final port = map['port']; + final id = map['id']; + if (ip != null && port != null && id != null) { + devices.add(CanvasDevice( + id: id, + ip: ip, + port: int.parse(port), + name: name, + )); + } + } + }); + await stop(); + return devices; + } + + Future stop() async { + _client.stop(); + } +} diff --git a/lib/util/play_control.dart b/lib/util/play_control.dart index 36df78f58..074e2dec0 100644 --- a/lib/util/play_control.dart +++ b/lib/util/play_control.dart @@ -1,183 +1,183 @@ -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'; +// 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; +// 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, - }); +// 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); +// @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( - onCastTap: () => {onCastTap?.call()}, - isCasting: isCasting, - ), - ) - ] - ], - ), - ), - ); - } -} +// 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; +// 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, - }); +// const ControlItem({ +// required this.icon, +// required this.iconFocus, +// super.key, +// this.isActive = false, +// this.onTap, +// }); - @override - State createState() => _ControlItemState(); -} +// @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, - ) - ], - ), - ); - } -} +// 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/ui_helper.dart b/lib/util/ui_helper.dart index 86a41f754..2a1a5162c 100644 --- a/lib/util/ui_helper.dart +++ b/lib/util/ui_helper.dart @@ -46,6 +46,7 @@ import 'package:feralfile_app_theme/extensions/theme_extension/moma_sans.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_vibrate/flutter_vibrate.dart'; import 'package:jiffy/jiffy.dart'; @@ -530,23 +531,19 @@ class UIHelper { : Constants.maxWidthModalTablet), isScrollControlled: true, barrierColor: Colors.black.withOpacity(0.5), - builder: (context) => Container( - color: Colors.transparent, - padding: const EdgeInsets.only(top: 200), - child: ClipPath( - clipper: isRoundCorner ? null : AutonomyTopRightRectangleClipper(), - child: Container( - padding: const EdgeInsets.fromLTRB(0, 20, 0, 40), - decoration: BoxDecoration( - color: backgroundColor ?? theme.auGreyBackground, - borderRadius: isRoundCorner - ? const BorderRadius.only( - topRight: Radius.circular(20), - ) - : null, - ), - child: content, + builder: (context) => ClipPath( + clipper: isRoundCorner ? null : AutonomyTopRightRectangleClipper(), + child: Container( + padding: const EdgeInsets.fromLTRB(0, 20, 0, 40), + decoration: BoxDecoration( + color: backgroundColor ?? theme.auGreyBackground, + borderRadius: isRoundCorner + ? const BorderRadius.only( + topRight: Radius.circular(20), + ) + : null, ), + child: content, ), ), ); diff --git a/lib/view/cast_button.dart b/lib/view/cast_button.dart index 5a6c2933c..c995eae95 100644 --- a/lib/view/cast_button.dart +++ b/lib/view/cast_button.dart @@ -1,36 +1,55 @@ +import 'package:autonomy_flutter/screen/detail/preview/canvas_device_bloc.dart'; +import 'package:autonomy_flutter/util/ui_helper.dart'; +import 'package:autonomy_flutter/view/stream_device_view.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; -class FFCastButton extends StatelessWidget { - final VoidCallback? onCastTap; - final bool isCasting; +class FFCastButton extends StatefulWidget { + final Function(String device)? onDeviceSelected; final String? text; + final String? type; const FFCastButton( - {super.key, this.onCastTap, this.isCasting = false, this.text}); + {this.type = '', super.key, this.onDeviceSelected, this.text}); + + @override + State createState() => _FFCastButtonState(); +} + +class _FFCastButtonState extends State { + late CanvasDeviceBloc _canvasDeviceBloc; + + @override + void initState() { + super.initState(); + _canvasDeviceBloc = context.read(); + } @override Widget build(BuildContext context) { final theme = Theme.of(context); return GestureDetector( - onTap: onCastTap, + onTap: () async => { + await _showStreamAction(context), + }, child: Semantics( label: 'cast_icon', child: DecoratedBox( decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(60), color: AppColor.feralFileLightBlue, ), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 5), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 9), child: Row( children: [ - if (text != null) + if (widget.text != null) Padding( padding: const EdgeInsets.only(right: 10), child: Text( - text!, + widget.text!, style: theme.textTheme.ppMori400Black14.copyWith( color: theme.colorScheme.primary, ), @@ -40,7 +59,9 @@ class FFCastButton extends StatelessWidget { 'assets/images/cast_icon.svg', height: 20, colorFilter: ColorFilter.mode( - theme.colorScheme.primary, BlendMode.srcIn), + theme.colorScheme.primary, + BlendMode.srcIn, + ), ), ], ), @@ -49,25 +70,17 @@ class FFCastButton extends StatelessWidget { ), ); } -} - -class CastButton extends StatelessWidget { - final VoidCallback? onCastTap; - final bool isCasting; - const CastButton({super.key, this.onCastTap, this.isCasting = false}); - - @override - Widget build(BuildContext context) => GestureDetector( - onTap: onCastTap, - child: Semantics( - label: 'cast_icon', - child: SvgPicture.asset( - 'assets/images/cast_icon.svg', - colorFilter: ColorFilter.mode( - isCasting ? AppColor.feralFileHighlight : AppColor.white, - BlendMode.srcIn), - ), + Future _showStreamAction(BuildContext context) async { + await UIHelper.showFlexibleDialog( + context, + BlocProvider.value( + value: _canvasDeviceBloc, + child: StreamDeviceView( + onDeviceSelected: widget.onDeviceSelected, ), - ); + ), + isDismissible: true, + ); + } } diff --git a/lib/view/stream_common_widget.dart b/lib/view/stream_common_widget.dart new file mode 100644 index 000000000..bf1b5c095 --- /dev/null +++ b/lib/view/stream_common_widget.dart @@ -0,0 +1,167 @@ +import 'dart:ui' as ui; + +import 'package:autonomy_flutter/util/ui_helper.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:feralfile_app_theme/feral_file_app_theme.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class StreamDrawerItem extends StatelessWidget { + final OptionItem item; + final Color backgroundColor; + const StreamDrawerItem( + {required this.item, required this.backgroundColor, super.key}); + + @override + Widget build(BuildContext context) => GestureDetector( + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(50), + ), + width: MediaQuery.of(context).size.width, + child: Center( + child: Text( + item.title ?? '', + style: Theme.of(context).textTheme.ppMori400Black14, + ), + ), + ), + onTap: () => item.onTap?.call(), + ); +} + +class PlaylistControl extends StatefulWidget { + const PlaylistControl({super.key}); + + @override + State createState() => _PlaylistControlState(); +} + +class _PlaylistControlState extends State { + final speedValues = [ + '1min', + '2min', + '5min', + '10min', + '15min', + '30min', + '1hr', + '4hr', + '12hr', + '24hr', + ]; + double _currentSliderValue = 0; + @override + Widget build(BuildContext context) => Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: AppColor.primaryBlack, + ), + child: Column( + children: [ + _buildPlayControls(context), + const SizedBox(height: 15), + _buildSpeedControl(context), + ], + )); + + Widget _buildPlayButton({required String icon, required Function() onTap}) => + Expanded( + child: GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: AppColor.auGreyBackground, + ), + child: SvgPicture.asset( + 'assets/images/$icon.svg', + ), + ), + ), + ); + + Widget _buildPlayControls(BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'play_collection'.tr(), + style: Theme.of(context).textTheme.ppMori400White12, + ), + const SizedBox(height: 4), + Row( + children: [ + _buildPlayButton(icon: 'chevron_left_icon', onTap: () => {}), + const SizedBox(width: 15), + _buildPlayButton(icon: 'stream_play_icon', onTap: () => {}), + const SizedBox(width: 15), + _buildPlayButton(icon: 'chevron_right_icon', onTap: () => {}), + ], + ) + ], + ); + + Widget _buildSpeedControl(BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'autoplay_speed'.tr(), + style: Theme.of(context).textTheme.ppMori400White12, + ), + const SizedBox(height: 4), + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: AppColor.auGreyBackground, + ), + child: Row( + children: [ + Text( + '24hr', + style: Theme.of(context).textTheme.ppMori400White12, + ), + const SizedBox(width: 10), + Expanded( + child: SliderTheme( + data: const SliderThemeData( + activeTrackColor: AppColor.white, + inactiveTrackColor: AppColor.white, + trackHeight: 1, + trackShape: RectangularSliderTrackShape(), + thumbColor: AppColor.white, + thumbShape: RoundSliderThumbShape(enabledThumbRadius: 8), + valueIndicatorColor: AppColor.white, + overlayShape: RoundSliderOverlayShape(overlayRadius: 0), + ), + child: Directionality( + textDirection: ui.TextDirection.rtl, + child: Slider( + value: _currentSliderValue, + max: speedValues.length.toDouble() - 1, + divisions: speedValues.length, + label: speedValues[_currentSliderValue.round()], + onChanged: (double value) { + setState(() { + _currentSliderValue = value; + }); + }, + ), + ), + ), + ), + const SizedBox(width: 10), + Text( + '1min', + style: Theme.of(context).textTheme.ppMori400White12, + ), + ], + ), + ) + ], + ); +} diff --git a/lib/view/stream_device_view.dart b/lib/view/stream_device_view.dart new file mode 100644 index 000000000..506d3644c --- /dev/null +++ b/lib/view/stream_device_view.dart @@ -0,0 +1,136 @@ +import 'dart:async'; + +import 'package:autonomy_flutter/screen/detail/preview/canvas_device_bloc.dart'; +import 'package:autonomy_flutter/util/ui_helper.dart'; +import 'package:autonomy_flutter/view/responsive.dart'; +import 'package:autonomy_flutter/view/stream_common_widget.dart'; +import 'package:collection/collection.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:feralfile_app_theme/feral_file_app_theme.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class StreamDeviceView extends StatefulWidget { + final Function(String device)? onDeviceSelected; + const StreamDeviceView({ + super.key, + this.onDeviceSelected, + }); + + @override + State createState() => _StreamDeviceViewState(); +} + +class _StreamDeviceViewState extends State { + late final CanvasDeviceBloc _canvasDeviceBloc; + + @override + void initState() { + super.initState(); + _canvasDeviceBloc = context.read(); + unawaited(_fetchDevice()); + } + + Future _fetchDevice() async { + _canvasDeviceBloc.add(CanvasDeviceGetDevicesEvent('')); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return BlocBuilder( + builder: (context, state) { + final devices = state.devices; + final connectedDevice = devices + .firstWhereOrNull((deviceState) => deviceState.device.isConnecting) + ?.device; + const isPlaylist = true; + return Padding( + padding: ResponsiveLayout.pageHorizontalEdgeInsets, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + children: [ + TextSpan( + text: 'display'.tr(), + style: theme.textTheme.ppMori700White24, + ), + if (connectedDevice != null) + TextSpan( + text: ' ${connectedDevice.name}', + style: theme.textTheme.ppMori400White24, + ), + ], + ), + ), + const SizedBox(height: 40), + if (isPlaylist) ...[ + const PlaylistControl(), + const SizedBox(height: 40), + ], + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + itemCount: devices.length, + itemBuilder: (BuildContext context, int index) { + final device = devices[index].device; + final isConnected = device.id == connectedDevice?.id; + return Column( + children: [ + Builder( + builder: (context) => StreamDrawerItem( + item: OptionItem( + title: device.name, + onTap: () => + {widget.onDeviceSelected?.call(device.id)}, + ), + backgroundColor: connectedDevice == null + ? AppColor.white + : isConnected + ? AppColor.feralFileLightBlue + : AppColor.disabledColor, + ), + ), + if (index < devices.length - 1) + const SizedBox( + height: 15, + ) + ], + ); + }, + ), + if (connectedDevice != null) ...[ + const SizedBox( + height: 40, + ), + GestureDetector( + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(50), + border: Border.all( + color: AppColor.white, + ), + ), + width: MediaQuery.of(context).size.width, + child: Center( + child: Text( + 'disconnect'.tr(), + style: theme.textTheme.ppMori400White14, + ), + ), + ), + onTap: () => {}, + ) + ] + ], + ), + ); + }, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 48a3ee2d8..1b373ab16 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -137,6 +137,54 @@ 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: @@ -598,8 +646,8 @@ packages: dependency: "direct main" description: path: "." - ref: feb58026dc94c46be74eb4407f41e56b775c2127 - resolved-ref: feb58026dc94c46be74eb4407f41e56b775c2127 + ref: "8d791a051a9b38791e82ac1582176d1b4e8e74fa" + resolved-ref: "8d791a051a9b38791e82ac1582176d1b4e8e74fa" url: "https://github.com/bitmark-inc/feralfile-app-client-shared-theme" source: git version: "0.0.1+20" @@ -1850,6 +1898,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.2" + multicast_dns: + dependency: "direct main" + description: + name: multicast_dns + sha256: "316cc47a958d4bd3c67bd238fe8b44fdfb6133bad89cb191c0c3bd3edb14e296" + url: "https://pub.dev" + source: hosted + version: "0.3.2+6" nested: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 554ddeeae..2701d4c55 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -112,7 +112,7 @@ dependencies: feralfile_app_theme: git: url: https://github.com/bitmark-inc/feralfile-app-client-shared-theme - ref: feb58026dc94c46be74eb4407f41e56b775c2127 + ref: 8d791a051a9b38791e82ac1582176d1b4e8e74fa markdown: ^7.1.1 easy_localization: ^3.0.1 rxdart: ^0.27.1 @@ -161,6 +161,7 @@ dependencies: image_gallery_saver: ^2.0.3 hand_signature: ^3.0.1 flutter_pdfview: ^1.3.2 + multicast_dns: ^0.3.2+6 dependency_overrides: intl: 0.18.0