From 0a6ff4586d810c67f5c7fa2486822a20b1fa4789 Mon Sep 17 00:00:00 2001 From: estevez-dev Date: Mon, 9 Sep 2019 12:25:13 +0300 Subject: [PATCH] Share media url to HA CLient to play on media_player --- .../hassclient/MainActivity.java | 3 +- lib/cards/card_widget.dart | 2 +- lib/const.dart | 1 + lib/entity_collection.class.dart | 11 + .../default_entity_container.dart | 53 +++-- lib/home_assistant.class.dart | 7 +- lib/main.dart | 4 + lib/pages/main.page.dart | 20 +- lib/pages/play_media.page.dart | 225 ++++++++++++++++++ lib/panels/panel_class.dart | 1 - lib/panels/widgets/link_to_web_config.dart | 2 +- pubspec.lock | 9 + pubspec.yaml | 3 + 13 files changed, 311 insertions(+), 30 deletions(-) create mode 100644 lib/pages/play_media.page.dart diff --git a/android/app/src/main/java/com/keyboardcrumbs/hassclient/MainActivity.java b/android/app/src/main/java/com/keyboardcrumbs/hassclient/MainActivity.java index e8d670c..558fc7e 100644 --- a/android/app/src/main/java/com/keyboardcrumbs/hassclient/MainActivity.java +++ b/android/app/src/main/java/com/keyboardcrumbs/hassclient/MainActivity.java @@ -3,8 +3,9 @@ package com.keyboardcrumbs.hassclient; import android.os.Bundle; import io.flutter.app.FlutterActivity; import io.flutter.plugins.GeneratedPluginRegistrant; +import io.flutter.plugins.share.FlutterShareReceiverActivity; -public class MainActivity extends FlutterActivity { +public class MainActivity extends FlutterShareReceiverActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); diff --git a/lib/cards/card_widget.dart b/lib/cards/card_widget.dart index 74b4053..df09baa 100644 --- a/lib/cards/card_widget.dart +++ b/lib/cards/card_widget.dart @@ -253,7 +253,7 @@ class CardWidget extends StatelessWidget { return Wrap( //spacing: 5.0, //alignment: WrapAlignment.spaceEvenly, - runSpacing: Sizes.rowPadding*2, + runSpacing: Sizes.doubleRowPadding, children: buttons, ); } diff --git a/lib/const.dart b/lib/const.dart index 4906313..6e7c99e 100644 --- a/lib/const.dart +++ b/lib/const.dart @@ -112,4 +112,5 @@ class Sizes { static const largeFontSize = 24.0; static const inputWidth = 160.0; static const rowPadding = 10.0; + static const doubleRowPadding = rowPadding*2; } \ No newline at end of file diff --git a/lib/entity_collection.class.dart b/lib/entity_collection.class.dart index af3fe8a..2ca6a56 100644 --- a/lib/entity_collection.class.dart +++ b/lib/entity_collection.class.dart @@ -149,6 +149,17 @@ class EntityCollection { return _allEntities[entityId] != null; } + List getByDomains(List domains) { + List result = []; + _allEntities.forEach((id, entity) { + if (domains.contains(entity.domain)) { + Logger.d("getByDomain: ${entity.isHidden}"); + result.add(entity); + } + }); + return result; + } + List filterEntitiesForDefaultView() { List result = []; List groups = []; diff --git a/lib/entity_widgets/default_entity_container.dart b/lib/entity_widgets/default_entity_container.dart index 84f4b6e..9ae5b0b 100644 --- a/lib/entity_widgets/default_entity_container.dart +++ b/lib/entity_widgets/default_entity_container.dart @@ -34,32 +34,37 @@ class DefaultEntityContainer extends StatelessWidget { ], ); } - return InkWell( - onLongPress: () { - if (entityModel.handleTap) { - entityModel.entityWrapper.handleHold(); - } - }, - onTap: () { - if (entityModel.handleTap) { - entityModel.entityWrapper.handleTap(); - } - }, - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - EntityIcon(), + Widget result = Row( + mainAxisSize: MainAxisSize.max, + children: [ + EntityIcon(), - Flexible( - fit: FlexFit.tight, - flex: 3, - child: EntityName( - padding: EdgeInsets.fromLTRB(10.0, 2.0, 10.0, 2.0), - ), + Flexible( + fit: FlexFit.tight, + flex: 3, + child: EntityName( + padding: EdgeInsets.fromLTRB(10.0, 2.0, 10.0, 2.0), ), - state - ], - ), + ), + state + ], ); + if (entityModel.handleTap) { + return InkWell( + onLongPress: () { + if (entityModel.handleTap) { + entityModel.entityWrapper.handleHold(); + } + }, + onTap: () { + if (entityModel.handleTap) { + entityModel.entityWrapper.handleTap(); + } + }, + child: result, + ); + } else { + return result; + } } } \ No newline at end of file diff --git a/lib/home_assistant.class.dart b/lib/home_assistant.class.dart index 1bf2689..b0b80a9 100644 --- a/lib/home_assistant.class.dart +++ b/lib/home_assistant.class.dart @@ -11,6 +11,7 @@ class HomeAssistant { EntityCollection entities; HomeAssistantUI ui; Map _instanceConfig = {}; + Map services; String _userName; HSVColor savedColor; @@ -115,7 +116,11 @@ class HomeAssistant { } Future _getServices() async { - await ConnectionManager().sendSocketMessage(type: "get_services").then((data) => Logger.d("Services received")).catchError((e) { + await ConnectionManager().sendSocketMessage(type: "get_services").then((data) { + Logger.d("Got ${data.length} services"); + Logger.d("Media extractor: ${data["media_extractor"]}"); + services = data; + }).catchError((e) { Logger.w("Can't get services: ${e}"); }); } diff --git a/lib/main.dart b/lib/main.dart index 171877f..70f150f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -23,6 +23,8 @@ import 'package:device_info/device_info.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:in_app_purchase/in_app_purchase.dart'; import 'plugins/circular_slider/single_circular_slider.dart'; +import 'package:share/receive_share_state.dart'; +import 'package:share/share.dart'; import 'utils/logger.dart'; @@ -121,6 +123,7 @@ part 'types/ha_error.dart'; part 'types/event_bus_events.dart'; part 'cards/widgets/gauge_card_body.dart'; part 'cards/widgets/light_card_body.dart'; +part 'pages/play_media.page.dart'; EventBus eventBus = new EventBus(); @@ -167,6 +170,7 @@ class HAClientApp extends StatelessWidget { "/": (context) => MainPage(title: 'HA Client'), "/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"), "/putchase": (context) => PurchasePage(title: "Support app development"), + "/play-media": (context) => PlayMediaPage(mediaUrl: "${(ModalRoute.of(context).settings.arguments as Map)['url']}",), "/log-view": (context) => LogViewPage(title: "Log"), "/login": (context) => WebviewScaffold( url: "${ConnectionManager().oauthUrl}", diff --git a/lib/pages/main.page.dart b/lib/pages/main.page.dart index 48c3d3d..197ad16 100644 --- a/lib/pages/main.page.dart +++ b/lib/pages/main.page.dart @@ -9,7 +9,7 @@ class MainPage extends StatefulWidget { _MainPageState createState() => new _MainPageState(); } -class _MainPageState extends State with WidgetsBindingObserver, TickerProviderStateMixin { +class _MainPageState extends ReceiveShareState with WidgetsBindingObserver, TickerProviderStateMixin { StreamSubscription> _subscription; StreamSubscription _stateSubscription; @@ -25,6 +25,7 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker int _previousViewCount; bool _showLoginButton = false; bool _preventAppRefresh = false; + String _savedSharedText; @override void initState() { @@ -34,6 +35,7 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker _handlePurchaseUpdates(purchases); }); super.initState(); + enableShareReceiving(); WidgetsBinding.instance.addObserver(this); _firebaseMessaging.configure( @@ -76,6 +78,12 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker } + @override void receiveShare(Share shared) { + if (shared.mimeType == ShareType.TYPE_PLAIN_TEXT) { + _savedSharedText = shared.text; + } + } + Future onSelectNotification(String payload) async { if (payload != null) { Logger.d('Notification clicked: ' + payload); @@ -121,6 +129,11 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker } _fetchData() async { + if (_savedSharedText != null && !HomeAssistant().isNoEntities) { + Logger.d("Got shared text: $_savedSharedText"); + Navigator.pushNamed(context, "/play-media", arguments: {"url": _savedSharedText}); + _savedSharedText = null; + } await HomeAssistant().fetchData().then((_) { _hideBottomBar(); int currentViewCount = HomeAssistant().ui?.views?.length ?? 0; @@ -659,6 +672,11 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker primary: true, title: Text(HomeAssistant().locationName ?? ""), actions: [ + IconButton( + icon: Icon(MaterialDesignIcons.getIconDataFromIconName( + "mdi:television"), color: Colors.white,), + onPressed: () => Navigator.pushNamed(context, "/play-media", arguments: {"url": ""}) + ), IconButton( icon: Icon(MaterialDesignIcons.getIconDataFromIconName( "mdi:dots-vertical"), color: Colors.white,), diff --git a/lib/pages/play_media.page.dart b/lib/pages/play_media.page.dart new file mode 100644 index 0000000..d3b1543 --- /dev/null +++ b/lib/pages/play_media.page.dart @@ -0,0 +1,225 @@ +part of '../main.dart'; + +class PlayMediaPage extends StatefulWidget { + + final String mediaUrl; + + PlayMediaPage({Key key, this.mediaUrl}) : super(key: key); + + @override + _PlayMediaPageState createState() => new _PlayMediaPageState(); +} + +class _PlayMediaPageState extends State { + + bool _loaded = false; + String _error = ""; + String _validationMessage = ""; + List _players; + String _mediaUrl; + String _contentType; + bool _useMediaExtractor = false; + bool _isMediaExtractorExist = false; + StreamSubscription _stateSubscription; + StreamSubscription _refreshDataSubscription; + final List _contentTypes = ["movie", "video", "music", "image", "image/jpg", "playlist"]; + + @override + void initState() { + super.initState(); + _mediaUrl = widget.mediaUrl; + _contentType = _contentTypes[0]; + _stateSubscription = eventBus.on().listen((event) { + if (event.entityId.contains("media_player")) { + Logger.d("State change event handled by play media page: ${event.entityId}"); + setState(() {}); + } + }); + _refreshDataSubscription = eventBus.on().listen((event) { + _loadMediaEntities(); + }); + _loadMediaEntities(); + } + + _loadMediaEntities() async { + if (HomeAssistant().isNoEntities) { + setState(() { + _loaded = false; + }); + } else { + _isMediaExtractorExist = HomeAssistant().services.containsKey("media_extractor"); + //_useMediaExtractor = _isMediaExtractorExist; + _players = HomeAssistant().entities.getByDomains(["media_player"]); + setState(() { + if (_players.isNotEmpty) { + _loaded = true; + } else { + _loaded = false; + _error = "Looks like you don't have any media player"; + } + }); + } + } + + void _playMedia(Entity entity) { + if (_mediaUrl == null || _mediaUrl.isEmpty) { + setState(() { + _validationMessage = "Media url must be specified"; + }); + } else { + String serviceDomain; + if (_useMediaExtractor) { + serviceDomain = "media_extractor"; + } else { + serviceDomain = "media_player"; + } + Navigator.pop(context); + ConnectionManager().callService( + domain: serviceDomain, + entityId: entity.entityId, + service: "play_media", + additionalServiceData: { + "media_content_id": _mediaUrl, + "media_content_type": _contentType + } + ); + eventBus.fire(ShowEntityPageEvent(entity)); + } + } + + + @override + Widget build(BuildContext context) { + Widget body; + if (!_loaded) { + body = _error.isEmpty ? PageLoadingIndicator() : PageLoadingError(errorText: _error); + } else { + List children = []; + children.add(CardHeader(name: "Media:")); + children.add( + TextField( + maxLines: 5, + minLines: 1, + decoration: InputDecoration( + labelText: "Media url" + ), + controller: TextEditingController.fromValue(TextEditingValue(text: _mediaUrl)), + onChanged: (value) { + _mediaUrl = value; + } + ), + ); + if (_validationMessage.isNotEmpty) { + children.add(Text( + "$_validationMessage", + style: TextStyle(color: Colors.red) + )); + } + children.addAll([ + Container(height: Sizes.rowPadding,), + DropdownButton( + value: _contentType, + isExpanded: true, + items: _contentTypes.map((String value) { + return new DropdownMenuItem( + value: value, + child: new Text(value), + ); + }).toList(), + onChanged: (value) { + setState(() { + _contentType = value; + }); + }, + ) + ] + ); + if (_isMediaExtractorExist) { + children.addAll([ + Row( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: Text("Use media extractor"), + ), + Switch( + value: _useMediaExtractor, + onChanged: (value) => setState((){_useMediaExtractor = value;}), + ), + ], + ), + Container( + height: Sizes.rowPadding, + ) + ] + ); + } else { + children.addAll([ + Row( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: Text("You can use media extractor here"), + ), + GestureDetector( + onTap: () { + Launcher.launchURLInCustomTab( + context: context, + url: "https://www.home-assistant.io/components/media_extractor/" + ); + }, + child: Text( + "How?", + style: TextStyle( + color: Colors.blue, + decoration: TextDecoration.underline + ), + ), + ), + ], + ), + Container( + height: Sizes.doubleRowPadding, + ) + ] + ); + } + children.add(CardHeader(name: "Play on:")); + children.addAll( + _players.map((player) => InkWell( + child: EntityModel( + entityWrapper: EntityWrapper(entity: player), + handleTap: false, + child: Padding( + padding: EdgeInsets.only(bottom: Sizes.doubleRowPadding), + child: DefaultEntityContainer(state: player._buildStatePart(context)), + ) + ), + onTap: () => _playMedia(player), + )) + ); + body = ListView( + padding: EdgeInsets.all(Sizes.leftWidgetPadding), + scrollDirection: Axis.vertical, + children: children + ); + } + return new Scaffold( + appBar: new AppBar( + leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){ + Navigator.pop(context); + }), + title: new Text("Play media"), + ), + body: body, + ); + } + + @override + void dispose(){ + _stateSubscription?.cancel(); + _refreshDataSubscription?.cancel(); + super.dispose(); + } + +} \ No newline at end of file diff --git a/lib/panels/panel_class.dart b/lib/panels/panel_class.dart index 1bc0f07..2be9775 100644 --- a/lib/panels/panel_class.dart +++ b/lib/panels/panel_class.dart @@ -23,7 +23,6 @@ class Panel { if (icon == null || !icon.startsWith("mdi:")) { icon = Panel.iconsByComponent[type]; } - Logger.d("New panel '$title'. type=$type, icon=$icon, urlPath=$urlPath"); isHidden = (type == 'lovelace' || type == 'kiosk' || type == 'states' || type == 'profile' || type == 'developer-tools'); isWebView = (type != 'config'); } diff --git a/lib/panels/widgets/link_to_web_config.dart b/lib/panels/widgets/link_to_web_config.dart index 4c7b98c..501f1b8 100644 --- a/lib/panels/widgets/link_to_web_config.dart +++ b/lib/panels/widgets/link_to_web_config.dart @@ -17,7 +17,7 @@ class LinkToWebConfig extends StatelessWidget { textAlign: TextAlign.left, overflow: TextOverflow.ellipsis, style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize)), - subtitle: Text("Tap to opne web version"), + subtitle: Text("Tap to open web version"), onTap: () { Launcher.launchAuthenticatedWebView(context: context, url: this.url, title: this.name); }, diff --git a/pubspec.lock b/pubspec.lock index 98da7b0..a90ac41 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -284,6 +284,15 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.3" + share: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "1f8b139ca0bd35b643ef4f5ccce3a1b09931f16a" + url: "https://github.com/d-silveira/flutter-share.git" + source: git + version: "0.6.0" shared_preferences: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index b9b1c96..4cbbbde 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,6 +26,9 @@ dependencies: flutter_secure_storage: ^3.2.1+1 device_info: ^0.4.0+2 flutter_local_notifications: ^0.8.2 + share: + git: + url: https://github.com/d-silveira/flutter-share.git dev_dependencies: flutter_test: