Compare commits
17 Commits
rc/1.1.0-b
...
rc/1.1.0-c
Author | SHA1 | Date | |
---|---|---|---|
be49180205 | |||
c4a0b16553 | |||
caacd5e9f4 | |||
5fa28abb6c | |||
e0a28c0b59 | |||
096e714a04 | |||
78893ea01f | |||
90efb29be5 | |||
fca323c56b | |||
e5fe6af5f3 | |||
f0090d522d | |||
edbfd8359b | |||
2702bb254a | |||
ca7b6ed550 | |||
fb00b5d9ff | |||
7ffba397ce | |||
1080076e3b |
@ -1,5 +1,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.keyboardcrumbs.hassclient">
|
||||
package="com.keyboardcrumbs.hassclient"
|
||||
android:installLocation="auto">
|
||||
|
||||
<uses-feature android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
|
@ -16,6 +16,8 @@ import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
import com.google.android.gms.tasks.OnCompleteListener;
|
||||
import com.google.android.gms.tasks.Task;
|
||||
import com.google.android.gms.common.GoogleApiAvailability;
|
||||
import com.google.android.gms.common.ConnectionResult;
|
||||
import com.google.firebase.iid.FirebaseInstanceId;
|
||||
import com.google.firebase.iid.InstanceIdResult;
|
||||
import com.google.firebase.messaging.FirebaseMessaging;
|
||||
@ -31,13 +33,14 @@ public class MainActivity extends FlutterActivity {
|
||||
new MethodChannel.MethodCallHandler() {
|
||||
@Override
|
||||
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
|
||||
Context context = getActivity();
|
||||
if (call.method.equals("getFCMToken")) {
|
||||
FirebaseInstanceId.getInstance().getInstanceId()
|
||||
if (checkPlayServices()) {
|
||||
FirebaseInstanceId.getInstance().getInstanceId()
|
||||
.addOnCompleteListener(new OnCompleteListener<InstanceIdResult>() {
|
||||
@Override
|
||||
public void onComplete(@NonNull Task<InstanceIdResult> task) {
|
||||
if (task.isSuccessful()) {
|
||||
Context context = getActivity();
|
||||
String token = task.getResult().getToken();
|
||||
UpdateTokenTask updateTokenTask = new UpdateTokenTask(context);
|
||||
updateTokenTask.execute(token);
|
||||
@ -47,12 +50,19 @@ public class MainActivity extends FlutterActivity {
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
result.error("google_play_service_error", "Google Play Services unavailable", null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private boolean checkPlayServices() {
|
||||
return (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
@ -38,6 +38,15 @@ class CardData {
|
||||
case CardType.LIGHT:
|
||||
return LightCardData(rawData);
|
||||
break;
|
||||
case CardType.PICTURE_ELEMENTS:
|
||||
//TODO temporary solution
|
||||
if (rawData.containsKey('camera_image')) {
|
||||
rawData['entity'] = rawData['camera_image'];
|
||||
return ButtonCardData(rawData);
|
||||
} else {
|
||||
return CardData(null);
|
||||
}
|
||||
break;
|
||||
case CardType.ENTITY_BUTTON:
|
||||
case CardType.BUTTON:
|
||||
case CardType.PICTURE_ENTITY:
|
||||
|
@ -16,7 +16,8 @@ import 'package:http/http.dart' as http;
|
||||
import 'package:charts_flutter/flutter.dart' as charts;
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:device_info/device_info.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'plugins/dynamic_multi_column_layout.dart';
|
||||
@ -184,7 +185,13 @@ void main() async {
|
||||
};
|
||||
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
AppSettings().loadAppTheme();
|
||||
await AppSettings().loadStartupSettings();
|
||||
await Hive.initFlutter();
|
||||
if (AppSettings().displayMode == DisplayMode.fullscreen) {
|
||||
SystemChrome.setEnabledSystemUIOverlays([]);
|
||||
} else {
|
||||
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
|
||||
}
|
||||
|
||||
runZoned(() {
|
||||
runApp(new HAClientApp(
|
||||
|
@ -1,7 +1,13 @@
|
||||
part of '../main.dart';
|
||||
|
||||
enum DisplayMode {normal, fullscreen}
|
||||
|
||||
class AppSettings {
|
||||
|
||||
static const DEFAULT_HIVE_BOX = 'defaultSettingsBox';
|
||||
|
||||
static const AUTH_TOKEN_KEY = 'llt';
|
||||
|
||||
static final AppSettings _instance = AppSettings._internal();
|
||||
|
||||
factory AppSettings() {
|
||||
@ -22,6 +28,7 @@ class AppSettings {
|
||||
String webhookId;
|
||||
double haVersion;
|
||||
bool scrollBadges;
|
||||
DisplayMode displayMode;
|
||||
AppTheme appTheme;
|
||||
final int defaultLocationUpdateIntervalMinutes = 20;
|
||||
Duration locationUpdateInterval;
|
||||
@ -30,13 +37,15 @@ class AppSettings {
|
||||
bool get isAuthenticated => longLivedToken != null;
|
||||
bool get isTempAuthenticated => tempToken != null;
|
||||
|
||||
loadAppTheme() async {
|
||||
loadStartupSettings() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
appTheme = AppTheme.values[prefs.getInt('app-theme') ?? AppTheme.defaultTheme.index];
|
||||
displayMode = DisplayMode.values[prefs.getInt('display-mode') ?? DisplayMode.normal.index];
|
||||
}
|
||||
|
||||
Future load(bool full) async {
|
||||
if (full) {
|
||||
await Hive.openBox(DEFAULT_HIVE_BOX);
|
||||
Logger.d('Loading settings...');
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
_domain = prefs.getString('hassio-domain');
|
||||
@ -52,18 +61,11 @@ class AppSettings {
|
||||
locationUpdateInterval = Duration(minutes: prefs.getInt("location-interval") ??
|
||||
defaultLocationUpdateIntervalMinutes);
|
||||
locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
|
||||
Logger.d('Done. $_domain:$_port');
|
||||
try {
|
||||
final storage = new FlutterSecureStorage();
|
||||
longLivedToken = await storage.read(key: "hacl_llt");
|
||||
Logger.d("Long-lived token read successful");
|
||||
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
|
||||
'https://ha-client.app')}&redirect_uri=${Uri
|
||||
.encodeComponent(
|
||||
'https://ha-client.app/service/auth_callback.html')}";
|
||||
} catch (e, stacktrace) {
|
||||
Logger.e("Error reading secure storage: $e", stacktrace: stacktrace);
|
||||
}
|
||||
longLivedToken = Hive.box(DEFAULT_HIVE_BOX).get(AUTH_TOKEN_KEY);
|
||||
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
|
||||
'https://ha-client.app')}&redirect_uri=${Uri
|
||||
.encodeComponent(
|
||||
'https://ha-client.app/service/auth_callback.html')}";
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,25 +105,13 @@ class AppSettings {
|
||||
Future clearTokens() async {
|
||||
longLivedToken = null;
|
||||
tempToken = null;
|
||||
try {
|
||||
final storage = new FlutterSecureStorage();
|
||||
await storage.delete(key: "hacl_llt");
|
||||
} catch(e, stacktrace) {
|
||||
Logger.e("Error clearing tokens: $e", stacktrace: stacktrace);
|
||||
}
|
||||
Hive.box(DEFAULT_HIVE_BOX).delete(AUTH_TOKEN_KEY);
|
||||
}
|
||||
|
||||
Future saveLongLivedToken(token) async {
|
||||
void saveLongLivedToken(token) {
|
||||
longLivedToken = token;
|
||||
tempToken = null;
|
||||
try {
|
||||
final storage = new FlutterSecureStorage();
|
||||
await storage.write(key: "hacl_llt", value: "$longLivedToken");
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
prefs.setBool("oauth-used", true);
|
||||
} catch(e, stacktrace) {
|
||||
Logger.e("Error saving long-lived token: $e", stacktrace: stacktrace);
|
||||
}
|
||||
Hive.box(DEFAULT_HIVE_BOX).put(AUTH_TOKEN_KEY, longLivedToken);
|
||||
}
|
||||
|
||||
bool isNotConfigured() {
|
||||
|
@ -254,6 +254,7 @@ class ConnectionManager {
|
||||
sendSocketMessage(type: "auth/long_lived_access_token", additionalData: {"client_name": "HA Client app ${DateTime.now().millisecondsSinceEpoch}", "lifespan": 365}).then((data) {
|
||||
Logger.d("Got long-lived token.");
|
||||
AppSettings().saveLongLivedToken(data);
|
||||
completer.complete();
|
||||
}).catchError((e) {
|
||||
completer.completeError(HACException("Authentication error: $e", actions: [HAErrorAction.reload(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
|
||||
});
|
||||
|
@ -139,7 +139,7 @@ class MobileAppIntegrationManager {
|
||||
positiveText: "Report issue",
|
||||
negativeText: "Close",
|
||||
onPositive: () {
|
||||
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new");
|
||||
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new/choose");
|
||||
}
|
||||
)
|
||||
));
|
||||
|
@ -1,18 +1,37 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class FullScreenPage extends StatelessWidget {
|
||||
class FullScreenPage extends StatefulWidget {
|
||||
|
||||
final Widget child;
|
||||
|
||||
const FullScreenPage({Key key, this.child}) : super(key: key);
|
||||
|
||||
@override
|
||||
_FullScreenPageState createState() => _FullScreenPageState();
|
||||
}
|
||||
|
||||
class _FullScreenPageState extends State<FullScreenPage> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
SystemChrome.setEnabledSystemUIOverlays([]);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
child: Center(
|
||||
child: this.child,
|
||||
child: this.widget.child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
}
|
@ -301,7 +301,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
title: Text("Report an issue"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new");
|
||||
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new/choose");
|
||||
},
|
||||
),
|
||||
Divider(),
|
||||
@ -484,7 +484,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
floating: true,
|
||||
pinned: true,
|
||||
snap: false,
|
||||
primary: true,
|
||||
primary: AppSettings().displayMode == DisplayMode.normal,
|
||||
title: Text(HomeAssistant().locationName ?? ""),
|
||||
actions: <Widget>[
|
||||
PopupMenuButton(
|
||||
@ -493,6 +493,15 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
child: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
"mdi:dots-vertical"), color: Theme.of(context).primaryIconTheme.color)
|
||||
),
|
||||
onSelected: (String val) {
|
||||
if (val == "reload") {
|
||||
_quickLoad();
|
||||
} else if (val == "logout") {
|
||||
HomeAssistant().logout().then((_) {
|
||||
_quickLoad();
|
||||
});
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) {
|
||||
List<PopupMenuEntry<String>> result = [
|
||||
PopupMenuItem<String>(
|
||||
@ -654,6 +663,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
Hive.close();
|
||||
//final flutterWebviewPlugin = new FlutterWebviewPlugin();
|
||||
//flutterWebviewPlugin.dispose();
|
||||
_viewsTabController?.dispose();
|
||||
|
@ -31,7 +31,7 @@ class _PurchasePageState extends State<PurchasePage> {
|
||||
} else {
|
||||
const Set<String> _kIds = {'one_time_support','just_few_bucks_per_year', 'app_fan_support_per_year', 'grateful_user_support_per_year'};
|
||||
final ProductDetailsResponse response = await InAppPurchaseConnection.instance.queryProductDetails(_kIds);
|
||||
if (!response.notFoundIDs.isEmpty) {
|
||||
if (response.notFoundIDs.isNotEmpty) {
|
||||
Logger.d("Products not found: ${response.notFoundIDs}");
|
||||
}
|
||||
_products = response.productDetails;
|
||||
@ -90,22 +90,6 @@ class _PurchasePageState extends State<PurchasePage> {
|
||||
} else {
|
||||
body = _buildProducts();
|
||||
}
|
||||
body.add(
|
||||
Card(
|
||||
child: Container(
|
||||
height: 80,
|
||||
child: InkWell(
|
||||
child: Image.network('https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif'),
|
||||
onTap: () {
|
||||
Launcher.launchURLInCustomTab(
|
||||
context: context,
|
||||
url: 'https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=ARWGETZD2D83Q&source=url'
|
||||
);
|
||||
},
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
return new Scaffold(
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||
|
@ -23,7 +23,7 @@ class _AppSettingsPageState extends State<AppSettingsPage> {
|
||||
|
||||
Widget _buildMenuItem(BuildContext context, IconData icon,String title, AppSettingsSection section) {
|
||||
return ListTile(
|
||||
title: Text(title, style: Theme.of(context).textTheme.subhead),
|
||||
title: Text(title),
|
||||
leading: Icon(icon),
|
||||
trailing: Icon(Icons.keyboard_arrow_right),
|
||||
onTap: () {
|
||||
|
@ -13,6 +13,7 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
|
||||
|
||||
AppTheme _currentTheme;
|
||||
bool _scrollBadges = false;
|
||||
DisplayMode _displayMode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -25,7 +26,8 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
|
||||
await prefs.reload();
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
setState(() {
|
||||
_currentTheme = AppTheme.values[prefs.getInt("app-theme") ?? AppTheme.defaultTheme.index];
|
||||
_currentTheme = AppTheme.values[prefs.getInt('app-theme') ?? AppTheme.defaultTheme.index];
|
||||
_displayMode = DisplayMode.values[prefs.getInt('display-mode') ?? DisplayMode.normal.index];
|
||||
_scrollBadges = prefs.getBool('scroll-badges') ?? true;
|
||||
});
|
||||
});
|
||||
@ -42,18 +44,34 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
|
||||
});
|
||||
}
|
||||
|
||||
Future _saveOther() async {
|
||||
Future _saveBadgesSettings() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
AppSettings().scrollBadges = _scrollBadges;
|
||||
await prefs.setBool('scroll-badges', _scrollBadges);
|
||||
}
|
||||
|
||||
Future _saveDisplayMode(DisplayMode mode) async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
AppSettings().displayMode = mode;
|
||||
await prefs.setInt('display-mode', mode.index);
|
||||
if (mode == DisplayMode.fullscreen) {
|
||||
SystemChrome.setEnabledSystemUIOverlays([]);
|
||||
} else {
|
||||
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
|
||||
}
|
||||
}
|
||||
|
||||
Map appThemeName = {
|
||||
AppTheme.defaultTheme: 'Default',
|
||||
AppTheme.haTheme: 'Home Assistant theme',
|
||||
AppTheme.darkTheme: 'Dark theme'
|
||||
};
|
||||
|
||||
Map DisplayModeName = {
|
||||
DisplayMode.normal: 'Normal',
|
||||
DisplayMode.fullscreen: 'Fullscreen'
|
||||
};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
@ -93,7 +111,28 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
|
||||
setState(() {
|
||||
_scrollBadges = val;
|
||||
});
|
||||
_saveOther();
|
||||
_saveBadgesSettings();
|
||||
},
|
||||
),
|
||||
Container(height: Sizes.doubleRowPadding),
|
||||
Text("Display mode:", style: Theme.of(context).textTheme.body2),
|
||||
Container(height: Sizes.rowPadding),
|
||||
DropdownButton<DisplayMode>(
|
||||
value: _displayMode,
|
||||
iconSize: 30.0,
|
||||
isExpanded: true,
|
||||
style: Theme.of(context).textTheme.title,
|
||||
items: DisplayMode.values.map((value) {
|
||||
return new DropdownMenuItem<DisplayMode>(
|
||||
value: value,
|
||||
child: Text('${DisplayModeName[value]}'),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (DisplayMode val) {
|
||||
setState(() {
|
||||
_displayMode = val;
|
||||
});
|
||||
_saveDisplayMode(val);
|
||||
},
|
||||
),
|
||||
]
|
||||
|
@ -73,11 +73,9 @@ class TokenLoginPopup extends Popup {
|
||||
padding: EdgeInsets.all(20),
|
||||
child: TextFormField(
|
||||
onSaved: (newValue) {
|
||||
final storage = new FlutterSecureStorage();
|
||||
storage.write(key: "hacl_llt", value: newValue.trim()).then((_) {
|
||||
Navigator.of(context).pop();
|
||||
eventBus.fire(SettingsChangedEvent(true));
|
||||
});
|
||||
Hive.box(AppSettings.DEFAULT_HIVE_BOX).put(AppSettings.AUTH_TOKEN_KEY, newValue.trim());
|
||||
Navigator.of(context).pop();
|
||||
eventBus.fire(SettingsChangedEvent(true));
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Please enter long-lived token',
|
||||
|
@ -1,7 +1,7 @@
|
||||
name: hass_client
|
||||
description: Home Assistant Android Client
|
||||
|
||||
version: 0.0.0+1151
|
||||
version: 1.1.0+1154
|
||||
|
||||
|
||||
environment:
|
||||
@ -23,7 +23,8 @@ dependencies:
|
||||
flutter_custom_tabs: ^0.6.0
|
||||
flutter_webview_plugin: ^0.3.10+1
|
||||
webview_flutter: ^0.3.19+7
|
||||
flutter_secure_storage: ^3.3.3
|
||||
hive: ^1.4.1+1
|
||||
hive_flutter: ^0.3.0+2
|
||||
device_info: ^0.4.1+4
|
||||
geolocator: ^5.3.1
|
||||
workmanager: ^0.2.2
|
||||
|
Reference in New Issue
Block a user