fix(studio): sidebar — fixed-height header rows, no more Y jump
Some checks failed
Security / Security check (push) Failing after 1s
Some checks failed
Security / Security check (push) Failing after 1s
The earlier "single AnimationController for width + opacity"
commit (0.51.5) made the *visual* expand smooth but the
underlying layout still jumped because the rows themselves
changed height between states:
collapsed expanded
───────── ─────────
triangle (36 px) triangle + 2-line title (44 px)
dot (10 px) dot + multi-line pill with
optional "Start hub" button
(60-80 px when disconnected)
letter (16 px) letter + accented chip (28 px)
So everything BELOW the header — destinations list, footer —
slid down ~50 px every time the rail opened. That's the
"Versatz" Stefan kept seeing. Fading the labels in didn't
help; the row geometry was the wrong source of truth.
Structural fix: every header row is now a SizedBox with a
fixed pixel height. Collapsed-state and expanded-state
content both fit inside the same height:
_brandRowH = 48 // FaiDeltaMark (36 px) centered
_connRowH = 44 // single conceptual block, two single-
// line texts, no multi-line pill
_channelRowH = 28 // chip + single-line channel name
_rowGap = 8
The total header block is therefore a constant pixel sum.
Items below it (the destinations ListView, the footer) sit
at the same Y in both states by construction — not by lucky
math, not by animation tricks. The width animation only
moves the rail's RIGHT edge; the left + top + bottom edges
of every row are immovable.
To make the connection row fit in 44 px we drop the inline
"Start hub" tonal button. The same affordance is preserved
by making the whole row tap-handled when the daemon is
unreachable: tap the red dot (or anywhere on the row) to
fire `fai daemon start`. The tooltip is updated to spell
this out ("Disconnected · tap to start · …").
The channel row is now ALWAYS reserved (28 px placeholder
when no channel is active) so that flipping the operator
config from local→dev at runtime doesn't shift the
destinations list either. The placeholder is invisible.
Side-effects:
- _ConnectionPill is removed (nothing references it).
- New _BrandLabel + _ConnectionLabel widgets, both
trivially Column(MainAxisAlignment.center, …) so their
content sits visually centered inside the fixed row.
Version 0.51.6 → 0.51.7.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
ba1a1a4f06
commit
dbcff6f7a6
2 changed files with 178 additions and 172 deletions
324
lib/main.dart
324
lib/main.dart
|
|
@ -27,7 +27,7 @@ import 'widgets/widgets.dart';
|
||||||
/// Studio's own build version. Bump on every UI commit so the
|
/// Studio's own build version. Bump on every UI commit so the
|
||||||
/// running app self-identifies — visible in the sidebar header
|
/// running app self-identifies — visible in the sidebar header
|
||||||
/// and quick-glance proof that you're seeing the current build.
|
/// and quick-glance proof that you're seeing the current build.
|
||||||
const String kStudioVersion = '0.51.6';
|
const String kStudioVersion = '0.51.7';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
@ -461,11 +461,18 @@ class _SidebarState extends State<_Sidebar>
|
||||||
// mirrors NavigationRail's spec.
|
// mirrors NavigationRail's spec.
|
||||||
static const double _collapsedWidth = 72;
|
static const double _collapsedWidth = 72;
|
||||||
static const double _expandedWidth = 220;
|
static const double _expandedWidth = 220;
|
||||||
// Single source of truth for the entire rail's expand
|
// Fixed pixel heights for every header row so the layout
|
||||||
// animation. width and every label's opacity are derived
|
// is the SAME total height in collapsed and expanded
|
||||||
// from `_ctrl.value` — no second timer, no conditional
|
// states. Items below the header (destinations list,
|
||||||
// widget-tree mutation, so width and content move in
|
// footer) sit at a mathematically constant Y — they
|
||||||
// lockstep instead of one snapping while the other glides.
|
// cannot move when the rail expands because nothing
|
||||||
|
// above them grows. This is the structural fix; the
|
||||||
|
// earlier opacity-tied animation only made the broken
|
||||||
|
// height transition smoother to the eye.
|
||||||
|
static const double _brandRowH = 48;
|
||||||
|
static const double _connRowH = 44;
|
||||||
|
static const double _channelRowH = 28;
|
||||||
|
static const double _rowGap = 8;
|
||||||
late final AnimationController _ctrl;
|
late final AnimationController _ctrl;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -484,80 +491,7 @@ class _SidebarState extends State<_Sidebar>
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Future<void> _startDaemon(BuildContext context) async {
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
final mode = widget.connected == true ? FaiDeltaMode.live : FaiDeltaMode.idle;
|
|
||||||
return MouseRegion(
|
|
||||||
onEnter: (_) => _ctrl.forward(),
|
|
||||||
onExit: (_) => _ctrl.reverse(),
|
|
||||||
child: AnimatedBuilder(
|
|
||||||
animation: _ctrl,
|
|
||||||
builder: (context, _) {
|
|
||||||
// eased between 0 (fully collapsed) and 1 (fully
|
|
||||||
// expanded). Opacity uses the same curve, so a label
|
|
||||||
// that starts fading in is at the same progress
|
|
||||||
// fraction as the rail's geometry — visually they
|
|
||||||
// travel together.
|
|
||||||
final t = Curves.easeInOutCubic.transform(_ctrl.value);
|
|
||||||
final width = _collapsedWidth + (_expandedWidth - _collapsedWidth) * t;
|
|
||||||
// Hold pointer hits on labels until the rail is at
|
|
||||||
// least half-open; otherwise the operator might tap
|
|
||||||
// through an almost-invisible button on the way to
|
|
||||||
// the icon they actually want.
|
|
||||||
final labelsInteractive = t > 0.5;
|
|
||||||
return Container(
|
|
||||||
width: width,
|
|
||||||
color: theme.colorScheme.surfaceContainerLow,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: FaiSpace.xl),
|
|
||||||
child: ClipRect(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_sidebarRow(
|
|
||||||
leading: FaiDeltaMark(
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
mode: mode,
|
|
||||||
),
|
|
||||||
label: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
'F∆I Studio',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 2),
|
|
||||||
Text(
|
|
||||||
'v$kStudioVersion',
|
|
||||||
style: FaiTheme.mono(
|
|
||||||
size: 10,
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
t: t,
|
|
||||||
labelsInteractive: labelsInteractive,
|
|
||||||
),
|
|
||||||
const SizedBox(height: FaiSpace.md),
|
|
||||||
_sidebarRow(
|
|
||||||
leading: Tooltip(
|
|
||||||
message: widget.connected == true
|
|
||||||
? 'Connected · ${widget.endpointLabel}'
|
|
||||||
: widget.connected == false
|
|
||||||
? 'Disconnected · ${widget.endpointLabel}'
|
|
||||||
: 'Connecting · ${widget.endpointLabel}',
|
|
||||||
child: _CollapsedConnectionDot(
|
|
||||||
connected: widget.connected,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
label: _ConnectionPill(
|
|
||||||
connected: widget.connected,
|
|
||||||
label: widget.endpointLabel,
|
|
||||||
onStartRequested: () async {
|
|
||||||
final r = await SystemActions.faiDaemon(['start']);
|
final r = await SystemActions.faiDaemon(['start']);
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
|
@ -569,26 +503,98 @@ class _SidebarState extends State<_Sidebar>
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final mode = widget.connected == true ? FaiDeltaMode.live : FaiDeltaMode.idle;
|
||||||
|
final hasChannel =
|
||||||
|
widget.activeChannel != null && widget.activeChannel!.isNotEmpty;
|
||||||
|
return MouseRegion(
|
||||||
|
onEnter: (_) => _ctrl.forward(),
|
||||||
|
onExit: (_) => _ctrl.reverse(),
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: _ctrl,
|
||||||
|
builder: (context, _) {
|
||||||
|
final t = Curves.easeInOutCubic.transform(_ctrl.value);
|
||||||
|
final width = _collapsedWidth + (_expandedWidth - _collapsedWidth) * t;
|
||||||
|
final labelsInteractive = t > 0.5;
|
||||||
|
return Container(
|
||||||
|
width: width,
|
||||||
|
color: theme.colorScheme.surfaceContainerLow,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: FaiSpace.xl),
|
||||||
|
child: ClipRect(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Brand row — fixed 48px regardless of state.
|
||||||
|
SizedBox(
|
||||||
|
height: _brandRowH,
|
||||||
|
child: _sidebarRow(
|
||||||
|
leading: FaiDeltaMark(
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
mode: mode,
|
||||||
),
|
),
|
||||||
|
label: _BrandLabel(),
|
||||||
t: t,
|
t: t,
|
||||||
labelsInteractive: labelsInteractive,
|
labelsInteractive: labelsInteractive,
|
||||||
),
|
),
|
||||||
if (widget.activeChannel != null && widget.activeChannel!.isNotEmpty) ...[
|
),
|
||||||
const SizedBox(height: FaiSpace.sm),
|
const SizedBox(height: _rowGap),
|
||||||
_sidebarRow(
|
// Connection row — fixed 44px. Tapping the row
|
||||||
|
// (anywhere in either icon col or label) when
|
||||||
|
// disconnected fires the daemon-start action,
|
||||||
|
// so we can drop the inline "Start hub" button
|
||||||
|
// that used to make this row's height balloon.
|
||||||
|
SizedBox(
|
||||||
|
height: _connRowH,
|
||||||
|
child: _sidebarRow(
|
||||||
|
leading: Tooltip(
|
||||||
|
message: widget.connected == true
|
||||||
|
? 'Connected · ${widget.endpointLabel}'
|
||||||
|
: widget.connected == false
|
||||||
|
? 'Disconnected · tap to start · ${widget.endpointLabel}'
|
||||||
|
: 'Connecting · ${widget.endpointLabel}',
|
||||||
|
child: _CollapsedConnectionDot(
|
||||||
|
connected: widget.connected,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
label: _ConnectionLabel(
|
||||||
|
connected: widget.connected,
|
||||||
|
endpoint: widget.endpointLabel,
|
||||||
|
),
|
||||||
|
t: t,
|
||||||
|
labelsInteractive: labelsInteractive,
|
||||||
|
onTap: widget.connected == false
|
||||||
|
? () => _startDaemon(context)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: _rowGap),
|
||||||
|
// Channel row — fixed 28px. Always reserved
|
||||||
|
// (with empty content when there's no channel
|
||||||
|
// selected) so toggling channels at runtime
|
||||||
|
// doesn't shift the destinations list below.
|
||||||
|
SizedBox(
|
||||||
|
height: _channelRowH,
|
||||||
|
child: hasChannel
|
||||||
|
? _sidebarRow(
|
||||||
leading: Tooltip(
|
leading: Tooltip(
|
||||||
message: widget.activeChannel!,
|
message: widget.activeChannel!,
|
||||||
child: _CollapsedChannelChip(
|
child: _CollapsedChannelChip(
|
||||||
channel: widget.activeChannel!,
|
channel: widget.activeChannel!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
label: _ChannelPill(channel: widget.activeChannel!),
|
label: _ChannelPill(
|
||||||
|
channel: widget.activeChannel!,
|
||||||
|
),
|
||||||
t: t,
|
t: t,
|
||||||
labelsInteractive: labelsInteractive,
|
labelsInteractive: labelsInteractive,
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
],
|
const SizedBox(height: FaiSpace.md),
|
||||||
const SizedBox(height: FaiSpace.xxl),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
|
|
@ -636,8 +642,9 @@ class _SidebarState extends State<_Sidebar>
|
||||||
required Widget label,
|
required Widget label,
|
||||||
required double t,
|
required double t,
|
||||||
required bool labelsInteractive,
|
required bool labelsInteractive,
|
||||||
|
VoidCallback? onTap,
|
||||||
}) {
|
}) {
|
||||||
return Row(
|
final body = Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
|
|
@ -647,7 +654,11 @@ class _SidebarState extends State<_Sidebar>
|
||||||
Expanded(
|
Expanded(
|
||||||
child: t > 0
|
child: t > 0
|
||||||
? IgnorePointer(
|
? IgnorePointer(
|
||||||
ignoring: !labelsInteractive,
|
// The whole row is interactive when onTap is set
|
||||||
|
// (we don't want the label sub-widgets eating
|
||||||
|
// the tap), so ignore pointer events on the
|
||||||
|
// label sub-tree even when the rail is expanded.
|
||||||
|
ignoring: onTap != null || !labelsInteractive,
|
||||||
child: Opacity(
|
child: Opacity(
|
||||||
opacity: t,
|
opacity: t,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|
@ -660,6 +671,15 @@ class _SidebarState extends State<_Sidebar>
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
if (onTap == null) return body;
|
||||||
|
return MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: onTap,
|
||||||
|
child: body,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _openSettings(BuildContext context) async {
|
Future<void> _openSettings(BuildContext context) async {
|
||||||
|
|
@ -753,7 +773,7 @@ class _ChannelPill extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mini-version of [_ConnectionPill] used when the sidebar is
|
/// Mini-version of [_ConnectionLabel] used when the sidebar is
|
||||||
/// collapsed. A 10px dot in the same green/red/amber tonality
|
/// collapsed. A 10px dot in the same green/red/amber tonality
|
||||||
/// the FaiDeltaMark uses for live / idle / unknown.
|
/// the FaiDeltaMark uses for live / idle / unknown.
|
||||||
class _CollapsedConnectionDot extends StatelessWidget {
|
class _CollapsedConnectionDot extends StatelessWidget {
|
||||||
|
|
@ -838,99 +858,85 @@ class _CollapsedChannelChip extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ConnectionPill extends StatelessWidget {
|
/// Two-line brand label shown in the rail's expanded state.
|
||||||
final bool? connected;
|
/// Fits inside a 48-px fixed-height row by design: 16-px title
|
||||||
final String label;
|
/// + 2-px gap + 10-px monospaced version stamp = 28-px content
|
||||||
/// Triggered by the inline "Start hub" button when the
|
/// with 10 px of vertical breathing room top + bottom.
|
||||||
/// daemon is unreachable. Runs `fai daemon start` server-side
|
class _BrandLabel extends StatelessWidget {
|
||||||
/// (well, locally — Studio shells out). The shell-level
|
@override
|
||||||
/// health poll picks the daemon up on its next tick.
|
Widget build(BuildContext context) {
|
||||||
final Future<void> Function() onStartRequested;
|
final theme = Theme.of(context);
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'F∆I Studio',
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
'v$kStudioVersion',
|
||||||
|
style: FaiTheme.mono(size: 10, color: theme.colorScheme.primary),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const _ConnectionPill({
|
/// Single-conceptual-block connection label shown in the
|
||||||
required this.connected,
|
/// rail's expanded state. Two single-line rows: a caption
|
||||||
required this.label,
|
/// (`connected` / `tap to start` / `connecting…`) and the
|
||||||
required this.onStartRequested,
|
/// endpoint label below in mono. Fits inside the 44-px fixed
|
||||||
});
|
/// connection-row height — see `_connRowH` in `_SidebarState`.
|
||||||
|
/// The old inline "Start hub" button is gone; the parent row
|
||||||
|
/// is tappable when `connected == false` and fires the same
|
||||||
|
/// daemon-start action.
|
||||||
|
class _ConnectionLabel extends StatelessWidget {
|
||||||
|
final bool? connected;
|
||||||
|
final String endpoint;
|
||||||
|
|
||||||
|
const _ConnectionLabel({required this.connected, required this.endpoint});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final dotColor = connected == true
|
final captionColor = connected == false
|
||||||
? FaiColors.success
|
? theme.colorScheme.error
|
||||||
: connected == false
|
: theme.colorScheme.onSurface;
|
||||||
? FaiColors.danger
|
|
||||||
: FaiColors.muted;
|
|
||||||
final caption = connected == true
|
final caption = connected == true
|
||||||
? 'connected'
|
? 'connected'
|
||||||
: connected == false
|
: connected == false
|
||||||
? 'unreachable'
|
? 'tap to start'
|
||||||
: 'connecting…';
|
: 'connecting…';
|
||||||
return Container(
|
return Column(
|
||||||
padding: const EdgeInsets.symmetric(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
horizontal: FaiSpace.sm,
|
|
||||||
vertical: 6,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.colorScheme.surfaceContainerHigh,
|
|
||||||
borderRadius: BorderRadius.circular(FaiRadius.sm),
|
|
||||||
border: Border.all(color: theme.colorScheme.outlineVariant),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
FaiStatusDot(color: dotColor, pulsing: connected == true),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Text(
|
Text(
|
||||||
caption,
|
caption,
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
maxLines: 1,
|
||||||
color: dotColor == FaiColors.danger
|
overflow: TextOverflow.ellipsis,
|
||||||
? theme.colorScheme.error
|
style: theme.textTheme.labelMedium?.copyWith(
|
||||||
: theme.colorScheme.onSurface,
|
color: captionColor,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
label,
|
endpoint,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
style: FaiTheme.mono(
|
style: FaiTheme.mono(
|
||||||
size: 10,
|
size: 10,
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Inline "Start hub" affordance when the daemon
|
|
||||||
// isn't there. Resolves the chicken-and-egg for
|
|
||||||
// Windows operators who can't run `fai daemon start`
|
|
||||||
// from a shell.
|
|
||||||
if (connected == false) ...[
|
|
||||||
const SizedBox(height: FaiSpace.sm),
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: FilledButton.tonalIcon(
|
|
||||||
onPressed: onStartRequested,
|
|
||||||
icon: const Icon(Icons.play_arrow, size: 14),
|
|
||||||
label: const Text(
|
|
||||||
'Start hub',
|
|
||||||
style: TextStyle(fontSize: 11),
|
|
||||||
),
|
|
||||||
style: FilledButton.styleFrom(
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: FaiSpace.sm,
|
|
||||||
vertical: 4,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
name: fai_studio
|
name: fai_studio
|
||||||
description: "F∆I Studio — desktop GUI for the F∆I hub"
|
description: "F∆I Studio — desktop GUI for the F∆I hub"
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 0.51.6
|
version: 0.51.7
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.0-200.1.beta
|
sdk: ^3.11.0-200.1.beta
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue