diff --git a/apps/chanora_flutter/ios/Runner/Info.plist b/apps/chanora_flutter/ios/Runner/Info.plist index 2437290..885f9d5 100644 --- a/apps/chanora_flutter/ios/Runner/Info.plist +++ b/apps/chanora_flutter/ios/Runner/Info.plist @@ -7,7 +7,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Chanora Flutter + Chanora CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -15,7 +15,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - chanora_flutter + Chanora CFBundlePackageType APPL CFBundleShortVersionString diff --git a/apps/chanora_flutter/lib/main.dart b/apps/chanora_flutter/lib/main.dart index 667f343..5243be7 100644 --- a/apps/chanora_flutter/lib/main.dart +++ b/apps/chanora_flutter/lib/main.dart @@ -27,6 +27,11 @@ import 'widgets/voice_bar.dart'; import 'widgets/voice_compact.dart'; import 'widgets/voice_settings.dart'; +bool get _isMacOS => !kIsWeb && Platform.isMacOS; + +/// Top padding for macOS to clear traffic-light buttons. +const double _macOSTrafficLightPad = 56.0; + /// True when the host is a touch-only mobile platform without a /// hardware keyboard. Mirrors the helpers in widgets/voice_bar.dart /// and widgets/voice_settings.dart so the AppBar + narrow-mode @@ -1007,280 +1012,249 @@ class _BetaHomeState extends State<_BetaHome> { } } + final headerActions = [ + if (_phase == _Phase.connected && + _inChannel && + MediaQuery.of(context).size.width < 840.0) ...[ + IconButton( + tooltip: l10n.voiceHardMuteLabel, + icon: Icon(_hardMute ? Icons.mic_off : Icons.mic), + isSelected: _hardMute, + selectedIcon: const Icon(Icons.mic_off), + onPressed: _onToggleHardMute, + ), + IconButton( + tooltip: l10n.voiceOutputMuteLabel, + icon: Icon(_outputMuted ? Icons.headset_off : Icons.headset), + isSelected: _outputMuted, + selectedIcon: const Icon(Icons.headset_off), + onPressed: _toggleOutputMute, + ), + ], + IconButton( + tooltip: l10n.aboutAction, + icon: const Icon(Icons.info_outline), + onPressed: () => _onShowAbout(context), + ), + if (_phase == _Phase.connected) ...[ + IconButton( + tooltip: l10n.disconnectAction, + icon: const Icon(Icons.logout), + onPressed: _onDisconnect, + ), + ], + ]; + + final headerTitle = _AppBarTitle( + phase: _phase, + inChannel: _inChannel, + channelName: _currentVoiceChannelName(), + ); + + final bodyContent = LayoutBuilder( + builder: (ctx, bodyConstraints) { + const wideBreakpoint = 840.0; + final isWideSnapshot = + bodyConstraints.maxWidth >= wideBreakpoint && + _phase == _Phase.connected && + _snapshot != null; + final banner = Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: theme.colorScheme.tertiaryContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + l10n.homeNotProductionReadyBanner, + style: TextStyle(color: theme.colorScheme.onTertiaryContainer), + ), + ); + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!isWideSnapshot) ...[banner, const SizedBox(height: 12)], + Text(statusText(), style: theme.textTheme.titleMedium), + if (_lostReason != null || _reconnectAttempt != null) ...[ + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: theme.colorScheme.errorContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: theme.colorScheme.onErrorContainer, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Text( + _reconnectAttempt != null + ? l10n.statusReconnecting( + _reconnectAttempt!, + _reconnectDelay ?? 0, + ) + : l10n.statusConnectionLost(_lostReason ?? ''), + style: TextStyle( + color: theme.colorScheme.onErrorContainer, + ), + ), + ), + ], + ), + ), + ], + const SizedBox(height: 12), + if (_phase == _Phase.idle) ...[ + Expanded( + child: SingleChildScrollView( + keyboardDismissBehavior: + ScrollViewKeyboardDismissBehavior.onDrag, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _ConnectForm( + hostCtl: _hostCtl, + nickCtl: _nickCtl, + passwordCtl: _passwordCtl, + onConnect: () => _onConnect(), + onAddBookmark: _onAddCurrentBookmark, + ), + const SizedBox(height: 16), + _BookmarkList( + bookmarks: _bookmarks, + onConnect: _onUseBookmark, + onDelete: _onDeleteBookmark, + ), + ], + ), + ), + ), + ] else if (_phase == _Phase.connecting) ...[ + const Center( + child: Padding( + padding: EdgeInsets.all(32), + child: CircularProgressIndicator(), + ), + ), + ] else if (_phase == _Phase.connected && _snapshot != null) ...[ + Expanded( + child: LayoutBuilder( + builder: (ctx, constraints) { + final voiceBar = VoiceBar( + inChannel: _inChannel, + transmitMode: _transmitMode, + hardMute: _hardMute, + outputMuted: _outputMuted, + releaseTailMs: _releaseTailMs, + channelName: _currentVoiceChannelName(), + audioStats: _audioStats, + pttLevel: _pttLevel, + pttBackendId: _pttBackendId, + pttBoundInputClass: _pttBoundInputClass, + pttBoundKeyLabel: _pttBoundKeyLabel, + onToggleMute: _onToggleHardMute, + onToggleOutputMute: _toggleOutputMute, + onConfigure: _onOpenVoiceSettings, + onPttHeldChanged: _onOnscreenPttHeldChanged, + ); + final snapshotView = _SnapshotView( + snapshot: _snapshot!, + currentVoiceChannelId: _currentVoiceChannelId, + onJoinChannel: _onJoinChannel, + onLeaveVoice: _onLeaveVoice, + ); + const voiceBarWidthWide = 320.0; + if (constraints.maxWidth >= wideBreakpoint) { + return Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox( + width: voiceBarWidthWide, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + banner, + const SizedBox(height: 12), + voiceBar, + ], + ), + ), + const SizedBox(width: 12), + Expanded(child: snapshotView), + ], + ); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded(child: snapshotView), + const SizedBox(height: 8), + VoiceStatusChip( + transmitMode: _transmitMode, + releaseTailMs: _releaseTailMs, + pttBoundKeyLabel: _pttBoundKeyLabel, + audioStats: _audioStats, + isTouchOnly: _isTouchOnlyPttHost, + onTap: () => _onOpenVoiceDetailsSheet(), + ), + if (_inChannel && + _transmitMode == + rust.BridgeTransmitMode.ptt) ...[ + const SizedBox(height: 8), + VoicePttButton( + active: _audioStats?.pttActive ?? false, + onHeldChanged: _onOnscreenPttHeldChanged, + ), + ], + ], + ); + }, + ), + ), + ], + ], + ); + }, + ); + + if (_isMacOS) { + return Scaffold( + body: Padding( + padding: const EdgeInsets.only(top: _macOSTrafficLightPad), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Expanded(child: headerTitle), + ...headerActions, + ], + ), + ), + Expanded(child: Padding(padding: const EdgeInsets.all(16), child: bodyContent)), + ], + ), + ), + ); + } + return Scaffold( appBar: AppBar( - title: _AppBarTitle( - phase: _phase, - inChannel: _inChannel, - channelName: _currentVoiceChannelName(), - ), - actions: [ - // Narrow-mode AppBar gains mic-mute + headset-mute icons - // when the user is in a voice channel, so the on-screen - // body can dedicate ~85% of its height to the channel tree - // + the bottom-anchored PTT button. Wide mode keeps these - // controls inside the VoiceBar widget (left column) so the - // signed-off rc.8 wide-layout doesn't shift. - // - // The gear icon (Icons.tune) that used to live here was - // **removed** in the rc.8 follow-up: it duplicated the - // "Adjust mode & release tail" button now inside the modal - // sheet, and the user reported the duplication. The modal - // is reached by tapping the status chip above the PTT - // button — there is now exactly one entry point. - if (_phase == _Phase.connected && - _inChannel && - MediaQuery.of(context).size.width < 840.0) ...[ - IconButton( - tooltip: l10n.voiceHardMuteLabel, - icon: Icon(_hardMute ? Icons.mic_off : Icons.mic), - isSelected: _hardMute, - selectedIcon: const Icon(Icons.mic_off), - onPressed: _onToggleHardMute, - ), - IconButton( - tooltip: l10n.voiceOutputMuteLabel, - icon: Icon(_outputMuted ? Icons.headset_off : Icons.headset), - isSelected: _outputMuted, - selectedIcon: const Icon(Icons.headset_off), - onPressed: _toggleOutputMute, - ), - ], - IconButton( - tooltip: l10n.aboutAction, - icon: const Icon(Icons.info_outline), - onPressed: () => _onShowAbout(context), - ), - if (_phase == _Phase.connected) ...[ - // The previous Refresh action (Icons.refresh + _onRefresh) - // was removed in v1.0.0-rc.8 — the snapshot stream the - // bridge pushes via BridgeEvent::SnapshotChanged keeps the - // tree current automatically, and a manual refresh was a - // no-op from the user's perspective. - IconButton( - tooltip: l10n.disconnectAction, - icon: const Icon(Icons.logout), - onPressed: _onDisconnect, - ), - ], - ], + title: headerTitle, + actions: headerActions, ), body: Padding( padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (ctx, bodyConstraints) { - // Shared breakpoint with the inner snapshot LayoutBuilder - // below — when the UI is wide enough to split into two - // columns AND we are showing the snapshot, the - // not-production-ready banner is moved into the left - // column above the Voice Bar so it doesn't span the - // wider channel-tree area. In every other state (narrow, - // or idle / connecting at any width) the banner stays - // pinned to the top of the body. - const wideBreakpoint = 840.0; - final isWideSnapshot = - bodyConstraints.maxWidth >= wideBreakpoint && - _phase == _Phase.connected && - _snapshot != null; - final banner = Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: theme.colorScheme.tertiaryContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Text( - l10n.homeNotProductionReadyBanner, - style: TextStyle(color: theme.colorScheme.onTertiaryContainer), - ), - ); - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (!isWideSnapshot) ...[banner, const SizedBox(height: 12)], - Text(statusText(), style: theme.textTheme.titleMedium), - if (_lostReason != null || _reconnectAttempt != null) ...[ - const SizedBox(height: 8), - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: theme.colorScheme.errorContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Row( - children: [ - SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator( - strokeWidth: 2, - color: theme.colorScheme.onErrorContainer, - ), - ), - const SizedBox(width: 10), - Expanded( - child: Text( - _reconnectAttempt != null - ? l10n.statusReconnecting( - _reconnectAttempt!, - _reconnectDelay ?? 0, - ) - : l10n.statusConnectionLost(_lostReason ?? ''), - style: TextStyle( - color: theme.colorScheme.onErrorContainer, - ), - ), - ), - ], - ), - ), - ], - const SizedBox(height: 12), - if (_phase == _Phase.idle) ...[ - Expanded( - child: SingleChildScrollView( - // Dismiss keyboard when the user drags away from - // a focused field — friendlier mobile UX than - // forcing them to tap outside. - keyboardDismissBehavior: - ScrollViewKeyboardDismissBehavior.onDrag, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _ConnectForm( - hostCtl: _hostCtl, - nickCtl: _nickCtl, - passwordCtl: _passwordCtl, - onConnect: () => _onConnect(), - onAddBookmark: _onAddCurrentBookmark, - ), - const SizedBox(height: 16), - _BookmarkList( - bookmarks: _bookmarks, - onConnect: _onUseBookmark, - onDelete: _onDeleteBookmark, - ), - ], - ), - ), - ), - ] else if (_phase == _Phase.connecting) ...[ - const Center( - child: Padding( - padding: EdgeInsets.all(32), - child: CircularProgressIndicator(), - ), - ), - ] else if (_phase == _Phase.connected && _snapshot != null) ...[ - Expanded( - child: LayoutBuilder( - builder: (ctx, constraints) { - final voiceBar = VoiceBar( - inChannel: _inChannel, - transmitMode: _transmitMode, - hardMute: _hardMute, - outputMuted: _outputMuted, - releaseTailMs: _releaseTailMs, - channelName: _currentVoiceChannelName(), - audioStats: _audioStats, - pttLevel: _pttLevel, - pttBackendId: _pttBackendId, - pttBoundInputClass: _pttBoundInputClass, - pttBoundKeyLabel: _pttBoundKeyLabel, - onToggleMute: _onToggleHardMute, - onToggleOutputMute: _toggleOutputMute, - onConfigure: _onOpenVoiceSettings, - onPttHeldChanged: _onOnscreenPttHeldChanged, - ); - final snapshotView = _SnapshotView( - snapshot: _snapshot!, - currentVoiceChannelId: _currentVoiceChannelId, - onJoinChannel: _onJoinChannel, - onLeaveVoice: _onLeaveVoice, - ); - // Responsive: at <840 dp use a stacked layout - // (Voice Bar on top, channel tree below). At - // ≥840 dp use a side-by-side layout with the - // Voice Bar pinned to 320 dp on the left and - // the channel tree expanding on the right. - // 840 dp matches Material's tablet / desktop - // breakpoint. - const wideBreakpoint = 840.0; - const voiceBarWidthWide = 320.0; - if (constraints.maxWidth >= wideBreakpoint) { - return Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SizedBox( - width: voiceBarWidthWide, - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - banner, - const SizedBox(height: 12), - voiceBar, - ], - ), - ), - const SizedBox(width: 12), - Expanded(child: snapshotView), - ], - ); - } - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // In narrow / one-column layout the layout - // is: - // - // [ channel tree (Expanded) ] - // [ status chip (2-line live readout) ] - // [ PTT button (mobile-only, - // PTT-mode-only) ] - // - // The chip is a tap target that opens - // `showVoiceDetailsSheet` with the mic - // level meter + TX/RX counts + capability - // badge. Mode + bind key + release-tail - // live in `VoiceSettingsDialog` - // (gear icon in AppBar). - // - // Wide mode (Row branch above) is - // unchanged from rc.8. - Expanded(child: snapshotView), - const SizedBox(height: 8), - VoiceStatusChip( - transmitMode: _transmitMode, - releaseTailMs: _releaseTailMs, - pttBoundKeyLabel: _pttBoundKeyLabel, - audioStats: _audioStats, - isTouchOnly: _isTouchOnlyPttHost, - onTap: () => _onOpenVoiceDetailsSheet(), - ), - // PTT button only when PTT mode is active - // AND the user is in a voice channel. In - // Continuous mode there is nothing to - // hold; the chip alone surfaces the - // "Mic on / off" state. - if (_inChannel && - _transmitMode == - rust.BridgeTransmitMode.ptt) ...[ - const SizedBox(height: 8), - VoicePttButton( - active: _audioStats?.pttActive ?? false, - onHeldChanged: _onOnscreenPttHeldChanged, - ), - ], - ], - ); - }, - ), - ), - ], - ], - ); - }, - ), + child: bodyContent, ), ); } diff --git a/apps/chanora_flutter/macos/Runner/Info.plist b/apps/chanora_flutter/macos/Runner/Info.plist index ae18827..2f2d7f5 100644 --- a/apps/chanora_flutter/macos/Runner/Info.plist +++ b/apps/chanora_flutter/macos/Runner/Info.plist @@ -13,7 +13,9 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - $(PRODUCT_NAME) + Chanora + CFBundleDisplayName + Chanora CFBundlePackageType APPL CFBundleShortVersionString diff --git a/apps/chanora_flutter/macos/Runner/MainFlutterWindow.swift b/apps/chanora_flutter/macos/Runner/MainFlutterWindow.swift index 3cc05eb..c21442f 100644 --- a/apps/chanora_flutter/macos/Runner/MainFlutterWindow.swift +++ b/apps/chanora_flutter/macos/Runner/MainFlutterWindow.swift @@ -8,8 +8,13 @@ class MainFlutterWindow: NSWindow { self.contentViewController = flutterViewController self.setFrame(windowFrame, display: true) + self.titlebarAppearsTransparent = true + self.titleVisibility = .hidden + self.styleMask.insert(.fullSizeContentView) + self.isMovableByWindowBackground = true + RegisterGeneratedPlugins(registry: flutterViewController) super.awakeFromNib() } -} +} \ No newline at end of file diff --git a/tools/build-linux.sh b/tools/build-linux.sh new file mode 100755 index 0000000..25b92e5 --- /dev/null +++ b/tools/build-linux.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +# Chanora Linux build helper. +# +# Run this on a Linux host (Ubuntu 22.04+ / Debian bookworm+ / Fedora 38+) that +# has already completed the prerequisites in §4 of docs/release/linux-build.md: +# - Rust stable with x86_64-unknown-linux-gnu target +# - Flutter 3.41.9+ stable on PATH +# - CMake 3.13+, pkg-config, GTK3 dev headers, clang +# - System libopus dev package (libopus-dev / opus-devel) +# - flutter_rust_bridge_codegen 2.12.0 (only if regenerating bindings) +# +# Usage (from the repo root ~/chanora): +# ./tools/build-linux.sh +# ./tools/build-linux.sh --no-rust # skip Rust rebuild (cached) +# ./tools/build-linux.sh --regenerate-bindings # rerun FRB codegen first +# ./tools/build-linux.sh --version v0.2.0-beta.1 +# +# Produces: +# target/x86_64-unknown-linux-gnu/release/libchanora_bridge.so +# apps/chanora_flutter/build/linux/x64/release/bundle/chanora_flutter +# chanora--linux-x64.tar.gz (release bundle) + +set -euo pipefail + +VERSION="v0.2.0-beta.1" +SKIP_RUST=0 +REGEN=0 +TARGET="x86_64-unknown-linux-gnu" + +while [[ $# -gt 0 ]]; do + case "$1" in + --version) VERSION="$2"; shift 2;; + --no-rust) SKIP_RUST=1; shift;; + --regenerate-bindings) REGEN=1; shift;; + --target) TARGET="$2"; shift 2;; + *) echo "unknown arg: $1" >&2; exit 2;; + esac +done + +cd "$(dirname "$0")/.." +REPO_ROOT="$(pwd)" + +bar() { printf '%s\n' '========================================================================'; } + +bar +echo "Chanora Linux build" +echo "Repo root : $REPO_ROOT" +echo "Version : $VERSION" +echo "Target : $TARGET" +echo "Build Rust: $([ "$SKIP_RUST" = 1 ] && echo skip || echo yes)" +bar + +# ---------- 1. Verify toolchain ---------- +echo "[1/6] Verify toolchain" +for cmd in cargo rustc flutter cmake pkg-config clang gcc; do + if ! command -v "$cmd" >/dev/null; then + echo " ERROR: '$cmd' not on PATH. See docs/release/linux-build.md." >&2 + exit 1 + fi + printf " %-12s : %s\n" "$cmd" "$($cmd --version 2>&1 | head -1)" +done + +# Verify GTK3 dev headers +if ! pkg-config --exists gtk+-3.0 2>/dev/null; then + echo " ERROR: gtk+-3.0 dev headers not found. Install libgtk-3-dev (Debian/Ubuntu) or gtk3-devel (Fedora)." >&2 + exit 1 +fi + +# Verify libopus dev headers (needed by audiopus_sys to link) +if ! pkg-config --exists opus 2>/dev/null; then + echo " ERROR: opus dev headers not found. Install libopus-dev (Debian/Ubuntu) or opus-devel (Fedora)." >&2 + exit 1 +fi + +# Verify Rust target +if ! rustup target list --installed | grep -q "^$TARGET$"; then + echo " Adding rustup target $TARGET..." + rustup target add "$TARGET" +fi + +# ---------- 2. Add Linux platform if missing ---------- +FLUTTER_APP="$REPO_ROOT/apps/chanora_flutter" +if [[ ! -d "$FLUTTER_APP/linux" ]]; then + echo "[2/6] Add Linux platform to apps/chanora_flutter" + (cd "$FLUTTER_APP" && flutter create --platforms=linux .) +else + echo "[2/6] Linux platform already present in apps/chanora_flutter" +fi + +# ---------- 3. (Optional) regenerate FRB bindings ---------- +if [[ $REGEN -eq 1 ]]; then + echo "[3/6] Regenerate flutter_rust_bridge bindings" + if ! command -v flutter_rust_bridge_codegen >/dev/null; then + echo " flutter_rust_bridge_codegen not on PATH; install it first." >&2 + exit 1 + fi + flutter_rust_bridge_codegen generate +else + echo "[3/6] Skipping FRB codegen (use --regenerate-bindings if api.rs changed)" +fi + +# ---------- 4. Build the Rust cdylib ---------- +BRIDGE_SO="$REPO_ROOT/target/$TARGET/release/libchanora_bridge.so" + +if [[ $SKIP_RUST -eq 1 ]]; then + echo "[4/6] Skipping Rust build (cached)" + if [[ ! -f "$BRIDGE_SO" ]]; then + echo " ERROR: --no-rust but $BRIDGE_SO not found. Run without --no-rust first." >&2 + exit 1 + fi +else + echo "[4/6] cargo build --release -p chanora_bridge --target $TARGET" + export CMAKE_POLICY_VERSION_MINIMUM=3.5 + export LIBOPUS_STATIC=0 + cargo build --release --target "$TARGET" -p chanora_bridge +fi + +echo " Bridge .so: $BRIDGE_SO" +if [[ ! -f "$BRIDGE_SO" ]]; then + echo " ERROR: $BRIDGE_SO not found after build." >&2 + exit 1 +fi + +# ---------- 5. flutter build linux --release ---------- +echo "[5/6] flutter build linux --release" +cd "$FLUTTER_APP" +flutter pub get +flutter build linux --release + +BUNDLE_DIR="$FLUTTER_APP/build/linux/x64/release/bundle" +EXE="$BUNDLE_DIR/chanora_flutter" +if [[ ! -f "$EXE" ]]; then + echo " ERROR: $EXE not found after flutter build." >&2 + exit 1 +fi + +# Copy the bridge .so into the bundle's lib/ directory so dart:ffi finds it. +mkdir -p "$BUNDLE_DIR/lib" +cp "$BRIDGE_SO" "$BUNDLE_DIR/lib/" +echo " Bundle dir : $BUNDLE_DIR" +echo " EXE : $EXE" +echo " Bridge .so : $BUNDLE_DIR/lib/libchanora_bridge.so" + +# ---------- 6. Package the bundle ---------- +echo "[6/6] Package release tarball" +cd "$REPO_ROOT" +TAR_NAME="chanora-$VERSION-linux-x64.tar.gz" +TAR_PATH="$REPO_ROOT/$TAR_NAME" +rm -f "$TAR_PATH" + +# Bundle everything in the bundle directory +tar czf "$TAR_PATH" -C "$FLUTTER_APP/build/linux/x64/release/bundle" . +echo " Tarball : $TAR_PATH" +echo " Size : $(du -h "$TAR_PATH" | cut -f1)" + +bar +echo "Linux build complete." +bar +echo "Artefacts:" +echo " Rust lib:" +echo " target/$TARGET/release/libchanora_bridge.so" +echo " Bundle dir:" +echo " $BUNDLE_DIR/" +echo " Tarball:" +echo " $TAR_PATH" \ No newline at end of file