feat: multi-platform bug fixes, Android audio path, and build tooling
Flutter UI fixes: - Fix stale channel badge/speaker when moved by others (derive current channel from ownClientId instead of optimistic local state) - Fix Linux PTT via focused fallback key handler - Distinguish ServerQuery clients with terminal icon in client list - Reduce duplicate current-channel badge display - Prevent PTT key-bind save from permanently closing voice settings - Fix Linux GTK reopen-after-close (quit app on window destroy) - Fix focused PTT: consume key events, release held keys on disconnect/leave-channel/mode/backend changes, suppress stale errors Flutter Rust bridge: - Thread is_server_query flag through protocol→bridge→Dart - Add own_client_id to BridgeSnapshot DTO - Add log_file_path_str() for platform log path queries Rust protocol: - Add ServerQuery test coverage (query_client_type_maps_to_server_query_flag) - Split reqwest TLS: native-tls for desktop/iOS, rustls for Android Rust audio: - Upgrade cpal 0.16→0.17.3 with API adjustments (SampleRate, description()) - Suppress Android-only dead-code warnings (open_log_file, keyring_account) Android build tooling: - tools/build-opus-android.sh: NDK auto-discovery, correct CMake Android variables (ANDROID_ABI, ANDROID_PLATFORM), portable baseline - tools/build-android-rust.sh: build+copy Rust cdylib for arm64-v8a, armeabi-v7a, x86_64 into android/app/src/main/jniLibs/ - Add jniLibs/ to .gitignore Rust bridge: - Guard open_log_file() on non-Android (Android uses logcat)
This commit is contained in:
@@ -12,3 +12,4 @@ GeneratedPluginRegistrant.java
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
/app/src/main/jniLibs/
|
||||
|
||||
+126
-140
@@ -32,6 +32,48 @@ bool get _isMacOS => !kIsWeb && Platform.isMacOS;
|
||||
/// Top padding for macOS to clear traffic-light buttons.
|
||||
const double _macOSTrafficLightPad = 56.0;
|
||||
|
||||
/// Translate a [LogicalKeyboardKey] into the platform-neutral label
|
||||
/// stored by the PTT binding flow.
|
||||
String? _pttDisplayLabelForKey(LogicalKeyboardKey k) {
|
||||
if (k == LogicalKeyboardKey.space) return 'Space';
|
||||
if (k == LogicalKeyboardKey.enter || k == LogicalKeyboardKey.numpadEnter) {
|
||||
return 'Enter';
|
||||
}
|
||||
if (k == LogicalKeyboardKey.tab) return 'Tab';
|
||||
if (k == LogicalKeyboardKey.escape) return 'Escape';
|
||||
if (k == LogicalKeyboardKey.backspace) return 'Backspace';
|
||||
if (k == LogicalKeyboardKey.delete) return 'Delete';
|
||||
if (k == LogicalKeyboardKey.insert) return 'Insert';
|
||||
if (k == LogicalKeyboardKey.home) return 'Home';
|
||||
if (k == LogicalKeyboardKey.end) return 'End';
|
||||
if (k == LogicalKeyboardKey.pageUp) return 'Page Up';
|
||||
if (k == LogicalKeyboardKey.pageDown) return 'Page Down';
|
||||
if (k == LogicalKeyboardKey.arrowUp) return 'Arrow Up';
|
||||
if (k == LogicalKeyboardKey.arrowDown) return 'Arrow Down';
|
||||
if (k == LogicalKeyboardKey.arrowLeft) return 'Arrow Left';
|
||||
if (k == LogicalKeyboardKey.arrowRight) return 'Arrow Right';
|
||||
if (k == LogicalKeyboardKey.shift ||
|
||||
k == LogicalKeyboardKey.shiftLeft ||
|
||||
k == LogicalKeyboardKey.shiftRight ||
|
||||
k == LogicalKeyboardKey.control ||
|
||||
k == LogicalKeyboardKey.controlLeft ||
|
||||
k == LogicalKeyboardKey.controlRight ||
|
||||
k == LogicalKeyboardKey.alt ||
|
||||
k == LogicalKeyboardKey.altLeft ||
|
||||
k == LogicalKeyboardKey.altRight ||
|
||||
k == LogicalKeyboardKey.meta ||
|
||||
k == LogicalKeyboardKey.metaLeft ||
|
||||
k == LogicalKeyboardKey.metaRight ||
|
||||
k == LogicalKeyboardKey.capsLock ||
|
||||
k == LogicalKeyboardKey.numLock ||
|
||||
k == LogicalKeyboardKey.scrollLock) {
|
||||
return null;
|
||||
}
|
||||
final fallback = k.keyLabel.trim();
|
||||
if (fallback.isEmpty) return null;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -241,17 +283,50 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
// only the platform-neutral label that already crossed into
|
||||
// the Rust side.
|
||||
String _pttBoundKeyLabel = '';
|
||||
final Set<LogicalKeyboardKey> _focusedPttHeldKeys = <LogicalKeyboardKey>{};
|
||||
|
||||
List<rust.BridgeBookmark> _bookmarks = const [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
HardwareKeyboard.instance.addHandler(_handleFocusedPttKey);
|
||||
_eventsSub = rust.eventsStream().listen(_onEvent);
|
||||
unawaited(_reloadBookmarks());
|
||||
unawaited(_hydratePttBinding());
|
||||
}
|
||||
|
||||
bool _handleFocusedPttKey(KeyEvent event) {
|
||||
final label = _pttDisplayLabelForKey(event.logicalKey);
|
||||
final isBoundKey = _pttBoundKeyLabel.isNotEmpty && label == _pttBoundKeyLabel;
|
||||
if (event is KeyUpEvent && _focusedPttHeldKeys.contains(event.logicalKey)) {
|
||||
if (_focusedPttHeldKeys.remove(event.logicalKey)) {
|
||||
unawaited(_setPtt(false));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (_pttBackendId != 'focused' ||
|
||||
_phase != _Phase.connected ||
|
||||
!_inChannel ||
|
||||
_transmitMode != rust.BridgeTransmitMode.ptt ||
|
||||
!isBoundKey) {
|
||||
return false;
|
||||
}
|
||||
if (event is KeyDownEvent) {
|
||||
if (_focusedPttHeldKeys.add(event.logicalKey)) {
|
||||
unawaited(_setPtt(true));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _releaseFocusedPttIfHeld() {
|
||||
if (_focusedPttHeldKeys.isEmpty) return;
|
||||
_focusedPttHeldKeys.clear();
|
||||
unawaited(_setPtt(false, reportError: false));
|
||||
}
|
||||
|
||||
/// Hydrate the bound-key display state from the bridge so the
|
||||
/// Voice Bar shows the user's persisted hotkey label immediately
|
||||
/// at launch — without having to wait for them to open the
|
||||
@@ -326,6 +401,9 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
_pttBackendId = backendId;
|
||||
_pttBoundInputClass = boundInputClass;
|
||||
});
|
||||
if (backendId != 'focused') {
|
||||
_releaseFocusedPttIfHeld();
|
||||
}
|
||||
case rust.BridgeEvent_VoiceState(
|
||||
:final inChannel,
|
||||
:final transmitMode,
|
||||
@@ -341,9 +419,15 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
});
|
||||
if (inChannel) {
|
||||
_ensureStatsTimer();
|
||||
unawaited(_onRefresh());
|
||||
} else {
|
||||
_statsTimer?.cancel();
|
||||
_statsTimer = null;
|
||||
_releaseFocusedPttIfHeld();
|
||||
setState(() => _currentVoiceChannelId = null);
|
||||
}
|
||||
if (transmitMode != rust.BridgeTransmitMode.ptt) {
|
||||
_releaseFocusedPttIfHeld();
|
||||
}
|
||||
case rust.BridgeEvent_InterruptionState(
|
||||
:final began,
|
||||
@@ -403,6 +487,7 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
HardwareKeyboard.instance.removeHandler(_handleFocusedPttKey);
|
||||
_eventsSub?.cancel();
|
||||
_statsTimer?.cancel();
|
||||
_hostCtl.dispose();
|
||||
@@ -421,6 +506,7 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
_error = null;
|
||||
_snapshot = null;
|
||||
});
|
||||
_releaseFocusedPttIfHeld();
|
||||
try {
|
||||
final snap = await rust.connect(
|
||||
host: (host ?? _hostCtl.text).trim(),
|
||||
@@ -430,7 +516,7 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_phase = _Phase.connected;
|
||||
_snapshot = snap;
|
||||
_applySnapshot(snap);
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
@@ -483,11 +569,11 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
}
|
||||
|
||||
// ignore: unused_element
|
||||
Future<void> _setPtt(bool active) async {
|
||||
Future<void> _setPtt(bool active, {bool reportError = true}) async {
|
||||
try {
|
||||
await rust.setPtt(active: active);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
if (!mounted || !reportError) return;
|
||||
setState(() => _error = e.toString());
|
||||
}
|
||||
}
|
||||
@@ -719,6 +805,9 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
}
|
||||
if (result.bindKeyRequested && mounted) {
|
||||
await _onConfigurePtt(context);
|
||||
if (mounted) {
|
||||
unawaited(_onOpenVoiceSettings());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -741,7 +830,7 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
try {
|
||||
final snap = await rust.snapshot();
|
||||
if (!mounted) return;
|
||||
setState(() => _snapshot = snap);
|
||||
setState(() => _applySnapshot(snap));
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() => _error = e.toString());
|
||||
@@ -766,6 +855,7 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
_inChannel = false;
|
||||
_currentVoiceChannelId = null;
|
||||
});
|
||||
_releaseFocusedPttIfHeld();
|
||||
}
|
||||
|
||||
String _currentVoiceChannelName() {
|
||||
@@ -778,6 +868,21 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
return '';
|
||||
}
|
||||
|
||||
void _applySnapshot(rust.BridgeSnapshot snap) {
|
||||
_snapshot = snap;
|
||||
if (!_inChannel) {
|
||||
_currentVoiceChannelId = null;
|
||||
return;
|
||||
}
|
||||
for (final client in snap.clients) {
|
||||
if (client.id == snap.ownClientId) {
|
||||
_currentVoiceChannelId = client.channel;
|
||||
return;
|
||||
}
|
||||
}
|
||||
_currentVoiceChannelId = null;
|
||||
}
|
||||
|
||||
Future<void> _onShowDiagnostics(BuildContext context) async {
|
||||
final l10n = AppL10n.of(context);
|
||||
final text = rust.exportDiagnostics();
|
||||
@@ -1045,11 +1150,7 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
],
|
||||
];
|
||||
|
||||
final headerTitle = _AppBarTitle(
|
||||
phase: _phase,
|
||||
inChannel: _inChannel,
|
||||
channelName: _currentVoiceChannelName(),
|
||||
);
|
||||
const headerTitle = _AppBarTitle();
|
||||
|
||||
final bodyContent = LayoutBuilder(
|
||||
builder: (ctx, bodyConstraints) {
|
||||
@@ -1260,81 +1361,16 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
}
|
||||
}
|
||||
|
||||
/// AppBar title that shows just the app name when idle / connecting,
|
||||
/// and 'app name · #channel' (channel as an outlined chip-style
|
||||
/// pill) when the user is in a voice channel. The chip is read-
|
||||
/// only — tapping does nothing because the source of truth for
|
||||
/// "current channel" is the channel tree below. Long names ellipsize.
|
||||
/// App header title. The current channel is highlighted only in the
|
||||
/// channel tree and Voice Bar to avoid repeating the same badge in
|
||||
/// multiple places.
|
||||
class _AppBarTitle extends StatelessWidget {
|
||||
const _AppBarTitle({
|
||||
required this.phase,
|
||||
required this.inChannel,
|
||||
required this.channelName,
|
||||
});
|
||||
|
||||
final _Phase phase;
|
||||
final bool inChannel;
|
||||
final String channelName;
|
||||
const _AppBarTitle();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppL10n.of(context);
|
||||
final theme = Theme.of(context);
|
||||
if (phase != _Phase.connected || !inChannel || channelName.isEmpty) {
|
||||
return Text(l10n.appTitle);
|
||||
}
|
||||
// When in a voice channel on a narrow phone width, the AppBar
|
||||
// is already crowded with mic / headset / settings / about /
|
||||
// diagnostics / disconnect icons (5-6 action buttons). Keeping
|
||||
// the "Chanora" app-name label in the title here causes the
|
||||
// Row to overflow at typical iPhone widths and Flutter renders
|
||||
// its yellow-and-black "OVERFLOWED BY X PIXELS" debug strip
|
||||
// next to the title — the user reported seeing "RFLOWED BY"
|
||||
// there. Drop the app-name label on narrow widths and let the
|
||||
// channel pill be the only title content; the user knows
|
||||
// they're in Chanora because they just opened it. On wide
|
||||
// widths (>= 840 dp, tablet/desktop) restore the app name
|
||||
// because there's plenty of room.
|
||||
final isNarrow = MediaQuery.of(context).size.width < 840.0;
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!isNarrow) ...[Text(l10n.appTitle), const SizedBox(width: 12)],
|
||||
Flexible(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.tag,
|
||||
size: 14,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
channelName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: false,
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
return Text(l10n.appTitle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1995,8 +2031,16 @@ class _SnapshotView extends StatelessWidget {
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
visualDensity: VisualDensity.compact,
|
||||
leading: const Icon(Icons.person, size: 18),
|
||||
title: Text(cl.name),
|
||||
leading: Icon(
|
||||
cl.isServerQuery ? Icons.terminal : Icons.person,
|
||||
size: 18,
|
||||
),
|
||||
title: Text(
|
||||
cl.name,
|
||||
style: cl.isServerQuery
|
||||
? TextStyle(color: theme.colorScheme.onSurfaceVariant)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -2049,7 +2093,7 @@ class _PttBindingCaptureDialogState extends State<_PttBindingCaptureDialog> {
|
||||
|
||||
KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) {
|
||||
if (event is! KeyDownEvent) return KeyEventResult.ignored;
|
||||
final label = _displayLabelForKey(event.logicalKey);
|
||||
final label = _pttDisplayLabelForKey(event.logicalKey);
|
||||
if (label == null) return KeyEventResult.ignored;
|
||||
setState(() {
|
||||
_captured = label;
|
||||
@@ -2058,64 +2102,6 @@ class _PttBindingCaptureDialogState extends State<_PttBindingCaptureDialog> {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
/// Translate a [LogicalKeyboardKey] into the platform-neutral
|
||||
/// label string the bridge expects (matching the entries in
|
||||
/// `crates/chanora_audio/src/ptt_backends/windows_keymap.rs`).
|
||||
///
|
||||
/// `LogicalKeyboardKey.keyLabel` returns `" "` for Space, empty
|
||||
/// for pure modifiers (shift / ctrl / alt / meta), and localised
|
||||
/// strings for some special keys; we normalise to the canonical
|
||||
/// English label so the badge displays something readable AND so
|
||||
/// the Windows backend's keymap can resolve to a `VK_*`. Pure
|
||||
/// modifier keys are intentionally rejected — chording into the
|
||||
/// real binding (e.g. Ctrl+Shift+M) is supported by ignoring the
|
||||
/// individual modifier-down events.
|
||||
String? _displayLabelForKey(LogicalKeyboardKey k) {
|
||||
// Whitespace / common control keys whose keyLabel is unhelpful.
|
||||
if (k == LogicalKeyboardKey.space) return 'Space';
|
||||
if (k == LogicalKeyboardKey.enter || k == LogicalKeyboardKey.numpadEnter) {
|
||||
return 'Enter';
|
||||
}
|
||||
if (k == LogicalKeyboardKey.tab) return 'Tab';
|
||||
if (k == LogicalKeyboardKey.escape) return 'Escape';
|
||||
if (k == LogicalKeyboardKey.backspace) return 'Backspace';
|
||||
if (k == LogicalKeyboardKey.delete) return 'Delete';
|
||||
if (k == LogicalKeyboardKey.insert) return 'Insert';
|
||||
if (k == LogicalKeyboardKey.home) return 'Home';
|
||||
if (k == LogicalKeyboardKey.end) return 'End';
|
||||
if (k == LogicalKeyboardKey.pageUp) return 'Page Up';
|
||||
if (k == LogicalKeyboardKey.pageDown) return 'Page Down';
|
||||
if (k == LogicalKeyboardKey.arrowUp) return 'Arrow Up';
|
||||
if (k == LogicalKeyboardKey.arrowDown) return 'Arrow Down';
|
||||
if (k == LogicalKeyboardKey.arrowLeft) return 'Arrow Left';
|
||||
if (k == LogicalKeyboardKey.arrowRight) return 'Arrow Right';
|
||||
// Pure modifier keys are not bindable on their own (user can
|
||||
// still chord by pressing a non-modifier while holding them).
|
||||
if (k == LogicalKeyboardKey.shift ||
|
||||
k == LogicalKeyboardKey.shiftLeft ||
|
||||
k == LogicalKeyboardKey.shiftRight ||
|
||||
k == LogicalKeyboardKey.control ||
|
||||
k == LogicalKeyboardKey.controlLeft ||
|
||||
k == LogicalKeyboardKey.controlRight ||
|
||||
k == LogicalKeyboardKey.alt ||
|
||||
k == LogicalKeyboardKey.altLeft ||
|
||||
k == LogicalKeyboardKey.altRight ||
|
||||
k == LogicalKeyboardKey.meta ||
|
||||
k == LogicalKeyboardKey.metaLeft ||
|
||||
k == LogicalKeyboardKey.metaRight ||
|
||||
k == LogicalKeyboardKey.capsLock ||
|
||||
k == LogicalKeyboardKey.numLock ||
|
||||
k == LogicalKeyboardKey.scrollLock) {
|
||||
return null;
|
||||
}
|
||||
// Fall back to keyLabel for letters, digits, function keys,
|
||||
// numpad digits, and punctuation. Trim whitespace as a final
|
||||
// belt-and-braces guard.
|
||||
final fallback = k.keyLabel.trim();
|
||||
if (fallback.isEmpty) return null;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
void _captureMouseSideButton(int button) {
|
||||
setState(() {
|
||||
_captured = 'mouse-side-button:$button';
|
||||
|
||||
@@ -338,14 +338,19 @@ class BridgeClient {
|
||||
/// Nickname.
|
||||
final String name;
|
||||
|
||||
/// True for TeamSpeak ServerQuery clients.
|
||||
final bool isServerQuery;
|
||||
|
||||
const BridgeClient({
|
||||
required this.id,
|
||||
required this.channel,
|
||||
required this.name,
|
||||
required this.isServerQuery,
|
||||
});
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode ^ channel.hashCode ^ name.hashCode;
|
||||
int get hashCode =>
|
||||
id.hashCode ^ channel.hashCode ^ name.hashCode ^ isServerQuery.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
@@ -354,7 +359,8 @@ class BridgeClient {
|
||||
runtimeType == other.runtimeType &&
|
||||
id == other.id &&
|
||||
channel == other.channel &&
|
||||
name == other.name;
|
||||
name == other.name &&
|
||||
isServerQuery == other.isServerQuery;
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
||||
@@ -1157,12 +1157,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
BridgeClient dco_decode_bridge_client(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
final arr = raw as List<dynamic>;
|
||||
if (arr.length != 3)
|
||||
throw Exception('unexpected arr length: expect 3 but see ${arr.length}');
|
||||
if (arr.length != 4)
|
||||
throw Exception('unexpected arr length: expect 4 but see ${arr.length}');
|
||||
return BridgeClient(
|
||||
id: dco_decode_u_64(arr[0]),
|
||||
channel: dco_decode_u_64(arr[1]),
|
||||
name: dco_decode_String(arr[2]),
|
||||
isServerQuery: dco_decode_bool(arr[3]),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1454,7 +1455,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
var var_id = sse_decode_u_64(deserializer);
|
||||
var var_channel = sse_decode_u_64(deserializer);
|
||||
var var_name = sse_decode_String(deserializer);
|
||||
return BridgeClient(id: var_id, channel: var_channel, name: var_name);
|
||||
var var_isServerQuery = sse_decode_bool(deserializer);
|
||||
return BridgeClient(
|
||||
id: var_id,
|
||||
channel: var_channel,
|
||||
name: var_name,
|
||||
isServerQuery: var_isServerQuery,
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
@@ -1799,6 +1806,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
sse_encode_u_64(self.id, serializer);
|
||||
sse_encode_u_64(self.channel, serializer);
|
||||
sse_encode_String(self.name, serializer);
|
||||
sse_encode_bool(self.isServerQuery, serializer);
|
||||
}
|
||||
|
||||
@protected
|
||||
|
||||
@@ -19,6 +19,10 @@ static void first_frame_cb(MyApplication* self, FlView* view) {
|
||||
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
|
||||
}
|
||||
|
||||
static void window_destroy_cb(GtkWidget* widget, gpointer user_data) {
|
||||
g_application_quit(G_APPLICATION(user_data));
|
||||
}
|
||||
|
||||
// Implements GApplication::activate.
|
||||
static void my_application_activate(GApplication* application) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
@@ -45,13 +49,15 @@ static void my_application_activate(GApplication* application) {
|
||||
if (use_header_bar) {
|
||||
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||
gtk_header_bar_set_title(header_bar, "chanora_flutter");
|
||||
gtk_header_bar_set_title(header_bar, "Chanora");
|
||||
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||
} else {
|
||||
gtk_window_set_title(window, "chanora_flutter");
|
||||
gtk_window_set_title(window, "Chanora");
|
||||
}
|
||||
|
||||
g_signal_connect(window, "destroy", G_CALLBACK(window_destroy_cb), application);
|
||||
|
||||
gtk_window_set_default_size(window, 1280, 720);
|
||||
|
||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||
|
||||
Reference in New Issue
Block a user