Merge branch 'linux-fixes'
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../player_native.dart';
|
||||
|
||||
/// Linux implementation of [Player].
|
||||
@@ -9,19 +7,6 @@ import '../player_native.dart';
|
||||
/// the Flutter view using a GtkOverlay, with transparent regions
|
||||
/// in the Flutter UI allowing the video to show through.
|
||||
class PlayerLinux extends PlayerNative {
|
||||
static const _methodChannel = MethodChannel('com.plezy/mpv_player');
|
||||
|
||||
@override
|
||||
int? get textureId => null; // Uses GtkGLArea, not Flutter texture
|
||||
|
||||
/// Sets the visibility of the video controls overlay.
|
||||
///
|
||||
/// On Linux, due to Flutter's lack of transparency support in GtkOverlay,
|
||||
/// we hide the video layer when controls are visible and show it when
|
||||
/// controls are hidden. This provides a workaround for the transparency
|
||||
/// limitation.
|
||||
@override
|
||||
Future<void> setControlsVisible(bool visible) async {
|
||||
await _methodChannel.invokeMethod('setControlsVisible', {'visible': visible});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,13 +163,6 @@ abstract class Player {
|
||||
/// Returns true if the operation was successful.
|
||||
Future<bool> setVisible(bool visible);
|
||||
|
||||
/// Notify the player about controls visibility.
|
||||
///
|
||||
/// On Linux, due to Flutter's lack of transparency support in GtkOverlay,
|
||||
/// the video layer is hidden when controls are visible and shown when
|
||||
/// controls are hidden. On other platforms, this is a no-op.
|
||||
Future<void> setControlsVisible(bool visible);
|
||||
|
||||
/// Update the video frame/surface dimensions.
|
||||
///
|
||||
/// On iOS/macOS, this updates the Metal layer's frame to match the current
|
||||
|
||||
@@ -570,11 +570,6 @@ class PlayerNative implements Player {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setControlsVisible(bool visible) async {
|
||||
// No-op on most platforms. Override on Linux for transparency workaround.
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateFrame() async {
|
||||
_checkDisposed();
|
||||
|
||||
@@ -1049,11 +1049,6 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
if (completed && _nextEpisode != null && !_showPlayNextDialog && !_completionTriggered) {
|
||||
_completionTriggered = true;
|
||||
|
||||
// On Linux, show the Flutter layer so the overlay is visible
|
||||
if (Platform.isLinux) {
|
||||
player?.setControlsVisible(true);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_showPlayNextDialog = true;
|
||||
_autoPlayCountdown = 5;
|
||||
|
||||
@@ -131,7 +131,6 @@ class PlexVideoControls extends StatefulWidget {
|
||||
|
||||
class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListener, WidgetsBindingObserver {
|
||||
bool _showControls = true;
|
||||
bool _controlsFullyHidden = false; // For Linux: true after fade-out completes
|
||||
List<PlexChapter> _chapters = [];
|
||||
bool _chaptersLoaded = false;
|
||||
Timer? _hideTimer;
|
||||
@@ -173,9 +172,6 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
StreamSubscription<bool>? _playingSubscription;
|
||||
// Completed subscription to show controls when video ends
|
||||
StreamSubscription<bool>? _completedSubscription;
|
||||
// Window resize pause state
|
||||
Timer? _resizeDebounceTimer;
|
||||
bool _wasPlayingBeforeResize = false;
|
||||
// Auto-skip state
|
||||
bool _autoSkipIntro = true;
|
||||
bool _autoSkipCredits = true;
|
||||
@@ -304,10 +300,8 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
// Show controls when video completes (for play next dialog etc.)
|
||||
setState(() {
|
||||
_showControls = true;
|
||||
_controlsFullyHidden = false;
|
||||
});
|
||||
_hideTimer?.cancel();
|
||||
_showLinuxControls();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -445,7 +439,6 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
widget.hasFirstFrame?.removeListener(_onFirstFrameReady);
|
||||
_hideTimer?.cancel();
|
||||
_feedbackTimer?.cancel();
|
||||
_resizeDebounceTimer?.cancel();
|
||||
_autoSkipTimer?.cancel();
|
||||
_singleTapTimer?.cancel();
|
||||
_seekThrottle.cancel();
|
||||
@@ -513,22 +506,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
|
||||
@override
|
||||
void onWindowResize() {
|
||||
// Pause video while resizing to prevent lag
|
||||
if (_resizeDebounceTimer == null && widget.player.state.playing) {
|
||||
_wasPlayingBeforeResize = true;
|
||||
widget.player.pause();
|
||||
}
|
||||
|
||||
// Reset debounce timer - resume when resizing stops
|
||||
_resizeDebounceTimer?.cancel();
|
||||
final slowDuration = tokens(context).slow;
|
||||
_resizeDebounceTimer = Timer(slowDuration, () {
|
||||
if (_wasPlayingBeforeResize && mounted) {
|
||||
widget.player.play();
|
||||
}
|
||||
_wasPlayingBeforeResize = false;
|
||||
_resizeDebounceTimer = null;
|
||||
});
|
||||
// Lag during resize is now handled in native code (glViewport + resize signal handler)
|
||||
}
|
||||
|
||||
void _startHideTimer() {
|
||||
@@ -551,7 +529,6 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
if (Platform.isMacOS) {
|
||||
_updateTrafficLightVisibility();
|
||||
}
|
||||
_hideLinuxControlsAfterAnimation();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -564,35 +541,12 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
}
|
||||
}
|
||||
|
||||
/// Show controls immediately on Linux
|
||||
void _showLinuxControls() {
|
||||
if (Platform.isLinux) {
|
||||
widget.player.setControlsVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
/// Hide controls on Linux after animation completes (250ms delay)
|
||||
void _hideLinuxControlsAfterAnimation() {
|
||||
if (Platform.isLinux) {
|
||||
Future.delayed(const Duration(milliseconds: 250), () {
|
||||
if (mounted && !_showControls) {
|
||||
setState(() {
|
||||
_controlsFullyHidden = true;
|
||||
});
|
||||
widget.player.setControlsVisible(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Show controls in response to pointer activity (mouse/trackpad movement).
|
||||
void _showControlsFromPointerActivity() {
|
||||
if (!_showControls) {
|
||||
setState(() {
|
||||
_showControls = true;
|
||||
_controlsFullyHidden = false;
|
||||
});
|
||||
_showLinuxControls();
|
||||
// On macOS, keep window controls in sync with the overlay
|
||||
if (Platform.isMacOS) {
|
||||
_updateTrafficLightVisibility();
|
||||
@@ -606,17 +560,11 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
void _toggleControls() {
|
||||
setState(() {
|
||||
_showControls = !_showControls;
|
||||
if (_showControls) {
|
||||
_controlsFullyHidden = false;
|
||||
_showLinuxControls();
|
||||
}
|
||||
});
|
||||
if (_showControls) {
|
||||
_startHideTimer();
|
||||
// Cancel auto-skip when user manually shows controls
|
||||
_cancelAutoSkipTimer();
|
||||
} else {
|
||||
_hideLinuxControlsAfterAnimation();
|
||||
}
|
||||
|
||||
// On macOS, hide/show traffic lights with controls
|
||||
@@ -1195,9 +1143,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
if (!_showControls) {
|
||||
setState(() {
|
||||
_showControls = true;
|
||||
_controlsFullyHidden = false;
|
||||
});
|
||||
_showLinuxControls();
|
||||
if (Platform.isMacOS) {
|
||||
_updateTrafficLightVisibility();
|
||||
}
|
||||
@@ -1231,7 +1177,6 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
if (Platform.isMacOS) {
|
||||
_updateTrafficLightVisibility();
|
||||
}
|
||||
_hideLinuxControlsAfterAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1330,6 +1275,9 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
onHover: (_) => _showControlsFromPointerActivity(),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Linux keep-alive: 1px widget that continuously repaints to prevent
|
||||
// Flutter animations from freezing when GTK's frame clock goes idle
|
||||
if (Platform.isLinux) const Positioned(top: 0, left: 0, child: _LinuxKeepAlive()),
|
||||
// Invisible tap detector that always covers the full area
|
||||
// Also handles long-press for 2x speed
|
||||
Positioned.fill(
|
||||
@@ -1423,21 +1371,18 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
},
|
||||
),
|
||||
),
|
||||
// Custom controls overlay - use AnimatedOpacity to keep widget tree alive
|
||||
// On Linux, use Offstage after fade completes to fully hide
|
||||
// Custom controls overlay
|
||||
// Positioned AFTER double-tap zones so controls receive taps first
|
||||
Positioned.fill(
|
||||
child: Offstage(
|
||||
offstage: Platform.isLinux && _controlsFullyHidden,
|
||||
child: IgnorePointer(
|
||||
ignoring: !_showControls,
|
||||
child: FocusScope(
|
||||
// Prevent focus from entering controls when hidden
|
||||
canRequestFocus: _showControls,
|
||||
child: AnimatedOpacity(
|
||||
opacity: _showControls ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: LayoutBuilder(
|
||||
child: IgnorePointer(
|
||||
ignoring: !_showControls,
|
||||
child: FocusScope(
|
||||
// Prevent focus from entering controls when hidden
|
||||
canRequestFocus: _showControls,
|
||||
child: AnimatedOpacity(
|
||||
opacity: _showControls ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return GestureDetector(
|
||||
onTapUp: (details) => _handleControlsOverlayTap(details, constraints),
|
||||
@@ -1551,7 +1496,6 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Visual feedback overlay for double-tap
|
||||
if (isMobile && _showDoubleTapFeedback)
|
||||
Positioned.fill(
|
||||
@@ -1721,3 +1665,48 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A 1x1 pixel widget that continuously repaints to keep Flutter's frame clock active on Linux.
|
||||
/// This prevents animations from freezing when GTK's frame clock goes idle.
|
||||
class _LinuxKeepAlive extends StatefulWidget {
|
||||
const _LinuxKeepAlive();
|
||||
|
||||
@override
|
||||
State<_LinuxKeepAlive> createState() => _LinuxKeepAliveState();
|
||||
}
|
||||
|
||||
class _LinuxKeepAliveState extends State<_LinuxKeepAlive> {
|
||||
Timer? _timer;
|
||||
int _tick = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Repaint every 100ms to keep Flutter's frame scheduler active
|
||||
_timer = Timer.periodic(const Duration(milliseconds: 100), (_) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_tick++;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Use _tick to force rebuild, render a 1x1 transparent pixel
|
||||
return SizedBox(
|
||||
width: 1,
|
||||
height: 1,
|
||||
child: ColoredBox(
|
||||
color: Color.fromARGB(_tick % 2, 0, 0, 0), // Alternate 0/1 alpha
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +135,10 @@ bool MpvPlayer::Initialize(GtkGLArea* gl_area) {
|
||||
}
|
||||
|
||||
void MpvPlayer::Dispose() {
|
||||
// Lock mutex to prevent race with OnMpvWakeup callbacks.
|
||||
// This ensures no new event processing starts during dispose.
|
||||
std::lock_guard<std::mutex> lock(callback_mutex_);
|
||||
|
||||
// Guard against multiple dispose calls (double-free protection)
|
||||
if (disposed_.exchange(true)) {
|
||||
return; // Already disposed
|
||||
@@ -282,15 +286,18 @@ void MpvPlayer::RequestRedraw() {
|
||||
void MpvPlayer::OnMpvWakeup(void* ctx) {
|
||||
auto* player = static_cast<MpvPlayer*>(ctx);
|
||||
|
||||
// Don't schedule if already disposed
|
||||
// Don't schedule if already disposed (atomic check for early exit)
|
||||
if (player->disposed_) return;
|
||||
|
||||
// Schedule event processing on the main thread.
|
||||
g_idle_add(
|
||||
[](gpointer data) -> gboolean {
|
||||
auto* player = static_cast<MpvPlayer*>(data);
|
||||
// Check disposed again when callback runs
|
||||
if (!player->disposed_) {
|
||||
|
||||
// Check disposed - atomic ensures we see updated value.
|
||||
// Don't lock mutex here - ProcessEvents() calls SendPropertyChange/SendEvent
|
||||
// which lock callback_mutex_, causing deadlock if we hold it here.
|
||||
if (!player->disposed_ && player->mpv_) {
|
||||
player->ProcessEvents();
|
||||
}
|
||||
return G_SOURCE_REMOVE;
|
||||
|
||||
@@ -31,6 +31,7 @@ static gboolean on_gl_render(GtkGLArea* area,
|
||||
gpointer user_data);
|
||||
static void on_gl_realize(GtkGLArea* area, gpointer user_data);
|
||||
static void on_gl_unrealize(GtkGLArea* area, gpointer user_data);
|
||||
static void on_gl_resize(GtkGLArea* area, gint width, gint height, gpointer user_data);
|
||||
|
||||
static void mpv_plugin_dispose(GObject* object) {
|
||||
MpvPlugin* self = MPV_PLUGIN(object);
|
||||
@@ -103,6 +104,7 @@ MpvPlugin* mpv_plugin_new(FlPluginRegistrar* registrar,
|
||||
g_signal_connect(gl_area, "render", G_CALLBACK(on_gl_render), self);
|
||||
g_signal_connect(gl_area, "realize", G_CALLBACK(on_gl_realize), self);
|
||||
g_signal_connect(gl_area, "unrealize", G_CALLBACK(on_gl_unrealize), self);
|
||||
g_signal_connect(gl_area, "resize", G_CALLBACK(on_gl_resize), self);
|
||||
|
||||
// Set up auto-render to false - we control when to render.
|
||||
gtk_gl_area_set_auto_render(gl_area, FALSE);
|
||||
@@ -132,6 +134,17 @@ static gboolean on_gl_render(GtkGLArea* area,
|
||||
(void)context;
|
||||
MpvPlugin* self = MPV_PLUGIN(user_data);
|
||||
|
||||
// Ensure GL context is current before any GL operations.
|
||||
// Critical during fullscreen transitions and workspace switches (issue #202).
|
||||
gtk_gl_area_make_current(area);
|
||||
|
||||
// Check for GL context errors (can happen during window state changes)
|
||||
GError* error = gtk_gl_area_get_error(area);
|
||||
if (error != nullptr) {
|
||||
g_warning("MPV Plugin: GL context error in render: %s", error->message);
|
||||
return FALSE; // Signal failure to GTK
|
||||
}
|
||||
|
||||
if (!self->player || !self->player->IsInitialized() || !self->visible) {
|
||||
// Clear to transparent when not showing video.
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
@@ -152,10 +165,41 @@ static gboolean on_gl_render(GtkGLArea* area,
|
||||
GLint fbo = 0;
|
||||
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &fbo);
|
||||
|
||||
// Render the video frame.
|
||||
// Save GL state before MPV render (MPV modifies these and doesn't restore them)
|
||||
// This prevents GL state pollution that corrupts Flutter's rendering.
|
||||
GLint prev_viewport[4];
|
||||
GLint prev_scissor_box[4];
|
||||
GLboolean prev_blend, prev_scissor_test;
|
||||
GLint prev_blend_src, prev_blend_dst;
|
||||
|
||||
glGetIntegerv(GL_VIEWPORT, prev_viewport);
|
||||
glGetIntegerv(GL_SCISSOR_BOX, prev_scissor_box);
|
||||
glGetBooleanv(GL_BLEND, &prev_blend);
|
||||
glGetBooleanv(GL_SCISSOR_TEST, &prev_scissor_test);
|
||||
glGetIntegerv(GL_BLEND_SRC_ALPHA, &prev_blend_src);
|
||||
glGetIntegerv(GL_BLEND_DST_ALPHA, &prev_blend_dst);
|
||||
|
||||
// Set viewport and render the video frame.
|
||||
glViewport(0, 0, width, height);
|
||||
self->player->Render(width, height, fbo);
|
||||
self->player->ClearRedrawFlag();
|
||||
|
||||
// Restore GL state after MPV render to prevent Flutter corruption.
|
||||
glViewport(prev_viewport[0], prev_viewport[1], prev_viewport[2], prev_viewport[3]);
|
||||
glScissor(prev_scissor_box[0], prev_scissor_box[1], prev_scissor_box[2], prev_scissor_box[3]);
|
||||
if (prev_blend) {
|
||||
glEnable(GL_BLEND);
|
||||
} else {
|
||||
glDisable(GL_BLEND);
|
||||
}
|
||||
if (prev_scissor_test) {
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
} else {
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
}
|
||||
glBlendFunc(prev_blend_src, prev_blend_dst);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -184,6 +228,13 @@ static void on_gl_unrealize(GtkGLArea* area, gpointer user_data) {
|
||||
|
||||
gtk_gl_area_make_current(area);
|
||||
|
||||
// Check if context is valid before disposing GL resources (issue #202)
|
||||
GError* error = gtk_gl_area_get_error(area);
|
||||
if (error != nullptr) {
|
||||
g_warning("MPV Plugin: GL context error in unrealize: %s", error->message);
|
||||
}
|
||||
|
||||
// Always try to dispose - Dispose() handles its own safety checks
|
||||
if (self->player) {
|
||||
self->player->Dispose();
|
||||
}
|
||||
@@ -191,6 +242,21 @@ static void on_gl_unrealize(GtkGLArea* area, gpointer user_data) {
|
||||
g_message("MPV Plugin: GL area unrealized");
|
||||
}
|
||||
|
||||
/// GtkGLArea resize callback.
|
||||
static void on_gl_resize(GtkGLArea* area,
|
||||
gint width,
|
||||
gint height,
|
||||
gpointer user_data) {
|
||||
MpvPlugin* self = MPV_PLUGIN(user_data);
|
||||
(void)width;
|
||||
(void)height;
|
||||
|
||||
// Force a redraw when size changes to prevent lag during resize.
|
||||
if (self->visible && self->player && self->player->IsInitialized()) {
|
||||
gtk_gl_area_queue_render(area);
|
||||
}
|
||||
}
|
||||
|
||||
/// Method call handler.
|
||||
static void mpv_plugin_handle_method_call(FlMethodChannel* channel,
|
||||
FlMethodCall* method_call,
|
||||
@@ -252,9 +318,7 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel,
|
||||
self->initialized = FALSE;
|
||||
self->visible = FALSE;
|
||||
gtk_widget_set_visible(GTK_WIDGET(self->gl_area), FALSE);
|
||||
// Restore Flutter view opacity to 1.0 (may have been set to 0 by setControlsVisible)
|
||||
if (self->flutter_view != nullptr) {
|
||||
gtk_widget_set_opacity(self->flutter_view, 1.0);
|
||||
// Force Flutter view to redraw
|
||||
gtk_widget_queue_draw(self->flutter_view);
|
||||
}
|
||||
@@ -367,29 +431,6 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel,
|
||||
gtk_gl_area_queue_render(self->gl_area);
|
||||
}
|
||||
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||
}
|
||||
} else if (strcmp(method, "setControlsVisible") == 0) {
|
||||
// Set Flutter view opacity when controls are hidden/shown.
|
||||
// This is a workaround for Flutter's lack of transparency support on Linux.
|
||||
// When controls are hidden, setting opacity to 0 shows only the video
|
||||
// while keeping the widget interactive for mouse events.
|
||||
FlValue* controls_visible_value = fl_value_lookup_string(args, "visible");
|
||||
|
||||
if (controls_visible_value == nullptr ||
|
||||
fl_value_get_type(controls_visible_value) != FL_VALUE_TYPE_BOOL) {
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
|
||||
"INVALID_ARGS", "Missing 'visible'", nullptr));
|
||||
} else {
|
||||
gboolean controls_visible = fl_value_get_bool(controls_visible_value);
|
||||
|
||||
// When controls are hidden, set Flutter view opacity to 0.
|
||||
// When controls are visible, set opacity to 1.
|
||||
// Using opacity keeps the widget interactive for mouse events.
|
||||
if (self->flutter_view != nullptr) {
|
||||
gtk_widget_set_opacity(self->flutter_view, controls_visible ? 1.0 : 0.0);
|
||||
}
|
||||
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||
}
|
||||
} else if (strcmp(method, "isInitialized") == 0) {
|
||||
|
||||
@@ -109,8 +109,9 @@ static void my_application_activate(GApplication* application) {
|
||||
gtk_widget_set_hexpand(GTK_WIDGET(self->flutter_view), TRUE);
|
||||
gtk_widget_set_vexpand(GTK_WIDGET(self->flutter_view), TRUE);
|
||||
|
||||
// Enable transparency for the Flutter view.
|
||||
gtk_widget_set_app_paintable(GTK_WIDGET(self->flutter_view), TRUE);
|
||||
// Set up RGBA visual for transparency support.
|
||||
// Note: app_paintable is only set on the window (line 73), not on child widgets,
|
||||
// to avoid redundant composition passes.
|
||||
setup_rgba_visual(GTK_WIDGET(self->flutter_view));
|
||||
|
||||
// Enable transparent background for the Flutter view.
|
||||
|
||||
Reference in New Issue
Block a user