Skip to content

Commit

Permalink
Refactored entrybutton storage
Browse files Browse the repository at this point in the history
  • Loading branch information
hufman committed Aug 25, 2023
1 parent a6ab731 commit ca95409
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 138 deletions.
159 changes: 22 additions & 137 deletions app/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import 'dart:developer';
import 'package:flutter/material.dart';
import 'dart:async';

import "package:collection/collection.dart";
import 'package:flutter/services.dart';
import 'package:headunit/pigeon.dart';
import 'package:headunit_example/rhmi.dart';
import 'package:image/image.dart' as ImageManips;
import 'package:headunit_example/rhmi_widgets.dart';

void main() {
runApp(const MyApp());
Expand All @@ -25,6 +26,7 @@ class _MyAppState extends State<MyApp> implements HeadunitApi {

final amApps = <String, AMAppInfo>{};
final rhmiApps = <String, RHMIApp>{};
final entryButtonsByCategory = <String, List<RHMIEntryButtonClickable>>{};

@override
void initState() {
Expand Down Expand Up @@ -58,6 +60,8 @@ class _MyAppState extends State<MyApp> implements HeadunitApi {

@override
Widget build(BuildContext context) {
final categories = entryButtonsByCategory.keys.sortedBy((element) => element);

return MaterialApp(
darkTheme: ThemeData(
brightness: Brightness.dark,
Expand All @@ -71,32 +75,39 @@ class _MyAppState extends State<MyApp> implements HeadunitApi {
Center(
child: Text('Running on: $_platformVersion\n'),
),
...amApps.entries.map((e) => AMAppInfoWidget(
server: _serverPlugin,
appInfo: e.value
)),
... rhmiApps.values.map((app) => RHMIAppEntrybuttonWidget(
server: _serverPlugin,
app: app,
entryButton: app.description.entryButtons.values.first
))
...categories.map((e) => RHMISectionWidget(name: e, buttons: entryButtonsByCategory[e]!))
],
)
),
);
}

void updateEntryButtons() {
final entryButtonsByCategory = amApps.values.map((e) => RHMIEntryButtonClickable.wrapAMAppInfo(_serverPlugin, e)).groupListsBy((e) => e.category);
for (final app in rhmiApps.values) {
for (final entry in app.description.entryButtons.entries) {
if (!entryButtonsByCategory.containsKey(entry.key)) {
entryButtonsByCategory[entry.key] = [];
}
entryButtonsByCategory[entry.key]?.add(RHMIEntryButtonClickable.wrapRhmiEntryButton(_serverPlugin, app, entry.value, entry.key));
}
}
this.entryButtonsByCategory.clear();
this.entryButtonsByCategory.addAll(entryButtonsByCategory);
}
@override
void amRegisterApp(AMAppInfo appInfo) {
setState(() {
amApps[appInfo.appId] = appInfo;
updateEntryButtons();
});
}

@override
void amUnregisterApp(String appId) {
setState(() {
amApps.remove(appId);
updateEntryButtons();
});
}

Expand All @@ -107,6 +118,7 @@ class _MyAppState extends State<MyApp> implements HeadunitApi {
if (description != null) {
setState(() {
rhmiApps[appInfo.appId] = RHMIApp.loadResources(appInfo.appId, appInfo.resources);
updateEntryButtons();
});
}
}
Expand Down Expand Up @@ -134,130 +146,3 @@ class _MyAppState extends State<MyApp> implements HeadunitApi {
});
}
}

class AMAppInfoWidget extends StatelessWidget {
const AMAppInfoWidget({
super.key,
required this.server,
required this.appInfo,
});
final ServerApi server;
final AMAppInfo appInfo;

@override
Widget build(BuildContext context) {
return Row(
children: [
TextButton.icon(
onPressed: () {
server.amTrigger(appInfo.appId);
},
icon: TransparentIcon(
iconData: appInfo.iconData,
darkMode: MediaQuery.of(context).platformBrightness == Brightness.dark,
width: 48,
height: 48,
),
label: Text(appInfo.name),
)
],
);
}
}

class RHMIAppEntrybuttonWidget extends StatelessWidget {
const RHMIAppEntrybuttonWidget({
super.key,
required this.server,
required this.app,
required this.entryButton,
});
final ServerApi server;
final RHMIApp app;
final RHMIComponent entryButton;

@override
Widget build(BuildContext context) {
final textId = entryButton.models['model']?.properties['textId'];
log("Loaded textId $textId for entryButton for ${app.appId}");
final String? name = app.texts['en-US']?[textId];
log("Loaded name $name for entryButton for ${app.appId} from ${app.texts['en-US']}");
final imageId = entryButton.models['imageModel']?.properties['imageId'];
log("Loaded imageId $imageId for entryButton for ${app.appId}");
final Uint8List? imageData = app.images[imageId];
return Row(
children: [
TextButton.icon(
onPressed: () {
},
icon: imageData != null ? TransparentIcon(
iconData: imageData,
darkMode: MediaQuery.of(context).platformBrightness == Brightness.dark,
width: 48,
height: 48,
) : const SizedBox(width: 48, height: 48),
label: Text(name ?? ""),
)
],
);
}
}

class TransparentIcon extends StatelessWidget {
const TransparentIcon({
super.key,
required this.iconData,
required this.darkMode,
required this.width,
required this.height,
});
final Uint8List iconData;
final bool darkMode;
final int width;
final int height;

Future<Image> filter(Uint8List iconData) async {
// https://stackoverflow.com/q/71817119/169035
final image = ImageManips.decodeImage(iconData);
if (image != null && image.channels == ImageManips.Channels.rgb) {
image.channels = ImageManips.Channels.rgba;
final pixels = image.getBytes(format: ImageManips.Format.rgba);
for (var i = 0; i < pixels.lengthInBytes; i += 4) {
if (pixels[i+0] < 3 && pixels[i+1] < 3 && pixels[i+2] < 3) {
pixels[i+3] = 0; // full transparent
}
if (!darkMode) {
// switch from dark mode to light mode
pixels[i + 0] = 255 - pixels[i + 0];
pixels[i + 1] = 255 - pixels[i + 1];
pixels[i + 2] = 255 - pixels[i + 2];
// TODO maybe UI tinting support?
}
}
return Image.memory(
ImageManips.encodePng(image) as Uint8List,
width: width.toDouble(),
height: height.toDouble(),
);
} else {
return Image.memory(
iconData,
width: width.toDouble(),
height: height.toDouble(),
);
}
}

@override
Widget build(BuildContext context) {
return FutureBuilder(
future: filter(iconData),
builder: (_, AsyncSnapshot<Image> parsedImage) {
return parsedImage.data != null ? parsedImage.data! : SizedBox(
height: height.toDouble(),
width: width.toDouble(),
);
}
);
}
}
145 changes: 145 additions & 0 deletions app/lib/rhmi_widgets.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import 'dart:developer';
import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'package:headunit/pigeon.dart';
import 'package:image/image.dart' as image_manips;

import 'rhmi.dart';

class RHMIEntryButtonClickable {
RHMIEntryButtonClickable(this.name, this.iconData, this.category, this.onClick);

final String name;
final Uint8List? iconData;
final String category;
final void Function() onClick;

static RHMIEntryButtonClickable wrapAMAppInfo(ServerApi server, AMAppInfo appInfo) {
return RHMIEntryButtonClickable(appInfo.name, appInfo.iconData, appInfo.category, () =>
server.amTrigger(appInfo.appId)
);
}
static RHMIEntryButtonClickable wrapRhmiEntryButton(ServerApi server, RHMIApp app, RHMIComponent entryButton, String category) {
final textId = entryButton.models['model']?.properties['textId'];
log("Loaded textId $textId for entryButton for ${app.appId}");
final String name = app.texts['en-US']?[textId] ?? "";
log("Loaded name $name for entryButton for ${app.appId} from ${app.texts['en-US']}");
final imageId = entryButton.models['imageModel']?.properties['imageId'];
log("Loaded imageId $imageId for entryButton for ${app.appId}");
final Uint8List? iconData = app.images[imageId];

return RHMIEntryButtonClickable(name, iconData, category, () => {
// TODO server.rhmiCallback
});
}
}

class RHMISectionWidget extends StatelessWidget {
const RHMISectionWidget({
super.key,
required this.name,
required this.buttons,
});
final String name;
final List<RHMIEntryButtonClickable> buttons;

@override
Widget build(BuildContext context) {

return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(name),
...buttons.map((e) => RHMIEntryButtonWidget(entryButton: e))
]
);
}

}

class RHMIEntryButtonWidget extends StatelessWidget {
const RHMIEntryButtonWidget({
super.key,
required this.entryButton,
});
final RHMIEntryButtonClickable entryButton;

@override
Widget build(BuildContext context) {
final iconData = entryButton.iconData;
return Row(
children: [
TextButton.icon(
onPressed: entryButton.onClick,
icon: iconData != null ? TransparentIcon(
iconData: iconData,
darkMode: MediaQuery.of(context).platformBrightness == Brightness.dark,
width: 48,
height: 48,
) : const SizedBox(width: 48, height: 48),
label: Text(entryButton.name),
)
],
);
}
}

class TransparentIcon extends StatelessWidget {
const TransparentIcon({
super.key,
required this.iconData,
required this.darkMode,
required this.width,
required this.height,
});
final Uint8List iconData;
final bool darkMode;
final int width;
final int height;

Future<Image> filter(Uint8List iconData) async {
// https://stackoverflow.com/q/71817119/169035
final image = image_manips.decodeImage(iconData);
if (image != null && image.channels == image_manips.Channels.rgb) {
image.channels = image_manips.Channels.rgba;
final pixels = image.getBytes(format: image_manips.Format.rgba);
for (var i = 0; i < pixels.lengthInBytes; i += 4) {
if (pixels[i+0] < 3 && pixels[i+1] < 3 && pixels[i+2] < 3) {
pixels[i+3] = 0; // full transparent
}
if (!darkMode) {
// switch from dark mode to light mode
pixels[i + 0] = 255 - pixels[i + 0];
pixels[i + 1] = 255 - pixels[i + 1];
pixels[i + 2] = 255 - pixels[i + 2];
// TODO maybe UI tinting support?
}
}
return Image.memory(
image_manips.encodePng(image) as Uint8List,
width: width.toDouble(),
height: height.toDouble(),
);
} else {
return Image.memory(
iconData,
width: width.toDouble(),
height: height.toDouble(),
);
}
}

@override
Widget build(BuildContext context) {
return FutureBuilder(
future: filter(iconData),
builder: (_, AsyncSnapshot<Image> parsedImage) {
return parsedImage.data != null ? parsedImage.data! : SizedBox(
height: height.toDouble(),
width: width.toDouble(),
);
}
);
}
}
2 changes: 1 addition & 1 deletion app/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ packages:
source: hosted
version: "1.1.1"
collection:
dependency: transitive
dependency: "direct main"
description:
name: collection
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
Expand Down
1 change: 1 addition & 0 deletions app/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ environment:
dependencies:
flutter:
sdk: flutter
collection: ^1.17.1
image: ^3.0.6

headunit:
Expand Down

0 comments on commit ca95409

Please sign in to comment.