feat(ui): adaptive 3-panel layout, chat panel switching, audio metering fix
- Add responsive breakpoints (compact <600, medium 600-1023, expanded >=1024) - Add ViewportInfo InheritedWidget for layout-aware descendants - Add inline ChatPanel (380dp right column) for expanded desktop layout - Add channel right-click context menu with Chat option for in-place switching - Add per-target draft persistence via restoredDraft/onDraftChanged callbacks - Fix header chat button to switch to current voice channel when panel open - Fix close = dismiss (preserves last target and draft for reopen) - Add unread dot indicator on channel tiles when chat is closed - Fix audio regression: decimate dBFS computation to every 3rd callback (~31 Hz) to avoid buffer underruns on macOS CoreAudio real-time thread - Add tools/build-macos.sh release build script (7-step process) - Add chat panel switching implementation plan and 3-panel design spec Tests: 183 passed, 2 skipped. Flutter analyze clean.
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Canonical layout breakpoints for Chanora.
|
||||
//
|
||||
// Aligned with Material 3 adaptive layout guidance:
|
||||
// compact < 600dp — phone, narrow tablet
|
||||
// medium 600–1023 — tablet portrait, small desktop window
|
||||
// expanded ≥ 1024dp — desktop, tablet landscape
|
||||
//
|
||||
// 1024dp was chosen as the expanded threshold based on production app
|
||||
// research: Discord (member list at 1024px), Mattermost (RHS docked at
|
||||
// ≥ 1024px), and Rocket.Chat (contextual bar persistent at lg/1024px).
|
||||
|
||||
/// Canonical breakpoint thresholds in logical pixels.
|
||||
///
|
||||
/// Use these instead of hardcoded pixel values in layout decisions.
|
||||
/// Migrate existing `_wideBreakpoint` / `_chatMobileBreakpoint` references
|
||||
/// to these named constants.
|
||||
class ChanoraBreakpoints {
|
||||
ChanoraBreakpoints._();
|
||||
|
||||
/// Width at which the layout switches from compact to medium.
|
||||
/// Below this: single-column mobile layout.
|
||||
/// At/above: two-panel side-by-side layout.
|
||||
static const double medium = 600;
|
||||
|
||||
/// Width at which the layout switches from medium to expanded.
|
||||
/// Below this: chat opens as a pushed route.
|
||||
/// At/above: three-panel layout with inline chat panel.
|
||||
static const double expanded = 1024;
|
||||
|
||||
// Panel sizing constants.
|
||||
|
||||
/// Fixed width of the left voice/control panel.
|
||||
static const double voicePanelWidth = 320;
|
||||
|
||||
/// Fixed width of the right chat panel (expanded layout only).
|
||||
static const double chatPanelWidth = 380;
|
||||
|
||||
/// Horizontal gap between panels.
|
||||
static const double panelGap = 12;
|
||||
|
||||
/// Desktop snackbar width cap (used when width ≥ [medium]).
|
||||
static const double snackBarDesktopCap = 560;
|
||||
|
||||
/// Connect form action buttons switch from row to column below this width.
|
||||
static const double connectActionsStackMaxWidth = 400;
|
||||
|
||||
/// Modal bottom sheet max height as fraction of screen height.
|
||||
static const double modalSheetHeightFraction = 0.72;
|
||||
}
|
||||
|
||||
/// Semantic layout class derived from viewport width.
|
||||
enum LayoutClass {
|
||||
/// < 600dp — single-column mobile layout.
|
||||
compact,
|
||||
|
||||
/// 600–1023dp — two-panel side-by-side layout.
|
||||
medium,
|
||||
|
||||
/// ≥ 1024dp — three-panel layout with inline chat.
|
||||
expanded,
|
||||
}
|
||||
|
||||
/// Computes the current [LayoutClass] from viewport [width].
|
||||
LayoutClass layoutClassFromWidth(double width) {
|
||||
if (width >= ChanoraBreakpoints.expanded) return LayoutClass.expanded;
|
||||
if (width >= ChanoraBreakpoints.medium) return LayoutClass.medium;
|
||||
return LayoutClass.compact;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Viewport info inherited widget for Chanora.
|
||||
//
|
||||
// Computes [LayoutClass] once per frame from the current [MediaQuery] size
|
||||
// and provides it to the entire widget subtree. Downstream widgets read
|
||||
// `ViewportInfo.of(context)` instead of calling `LayoutBuilder` or
|
||||
// `MediaQuery.sizeOf` directly for layout-class decisions.
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'breakpoints.dart';
|
||||
|
||||
/// Inherited widget that exposes the current layout class and viewport
|
||||
/// dimensions to the entire subtree.
|
||||
///
|
||||
/// Insert this once near the top of the widget tree (inside the Scaffold
|
||||
/// body or equivalent). All descendants can then read
|
||||
/// `ViewportInfo.of(context)` to determine their layout behaviour.
|
||||
class ViewportInfo extends InheritedWidget {
|
||||
/// Creates a [ViewportInfo].
|
||||
const ViewportInfo({
|
||||
super.key,
|
||||
required this.layoutClass,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
/// Current layout class derived from viewport width.
|
||||
final LayoutClass layoutClass;
|
||||
|
||||
/// Current viewport width in logical pixels.
|
||||
final double width;
|
||||
|
||||
/// Current viewport height in logical pixels.
|
||||
final double height;
|
||||
|
||||
/// Returns the nearest [ViewportInfo] in the widget tree.
|
||||
///
|
||||
/// Asserts that a [ViewportInfo] ancestor exists.
|
||||
static ViewportInfo of(BuildContext context) {
|
||||
final info = context.dependOnInheritedWidgetOfExactType<ViewportInfo>();
|
||||
assert(info != null, 'No ViewportInfo found in widget tree');
|
||||
return info!;
|
||||
}
|
||||
|
||||
/// Whether the current layout is compact (< 600dp).
|
||||
bool get isCompact => layoutClass == LayoutClass.compact;
|
||||
|
||||
/// Whether the current layout is medium (600–1023dp).
|
||||
bool get isMedium => layoutClass == LayoutClass.medium;
|
||||
|
||||
/// Whether the current layout is expanded (≥ 1024dp).
|
||||
bool get isExpanded => layoutClass == LayoutClass.expanded;
|
||||
|
||||
/// Whether the layout has room for at least two panels (medium or expanded).
|
||||
bool get isWide => !isCompact;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(ViewportInfo old) =>
|
||||
layoutClass != old.layoutClass ||
|
||||
width != old.width ||
|
||||
height != old.height;
|
||||
}
|
||||
Reference in New Issue
Block a user