жесты
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
class AppSwipeBackDesktop {
|
||||||
|
static const prefKey = 'dev_swipe_back_desktop';
|
||||||
|
static const bool defaultValue = false;
|
||||||
|
|
||||||
|
static final ValueNotifier<bool> current = ValueNotifier(defaultValue);
|
||||||
|
|
||||||
|
static Future<bool> load() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
return prefs.getBool(prefKey) ?? defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> save(bool value) async {
|
||||||
|
current.value = value;
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setBool(prefKey, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import 'chat_screen.dart';
|
|||||||
import 'create_group_flow.dart';
|
import 'create_group_flow.dart';
|
||||||
import '../../widgets/adaptive_shell.dart';
|
import '../../widgets/adaptive_shell.dart';
|
||||||
import '../../widgets/custom_notification.dart';
|
import '../../widgets/custom_notification.dart';
|
||||||
|
import '../../widgets/swipe_route.dart';
|
||||||
|
|
||||||
import '../calls/calls_tab.dart';
|
import '../calls/calls_tab.dart';
|
||||||
import '../contacts/contacts_tab.dart';
|
import '../contacts/contacts_tab.dart';
|
||||||
@@ -2072,15 +2073,13 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
chatType: chatType,
|
chatType: chatType,
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
Navigator.push(
|
pushSwipeable(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
(context) => ChatScreen(
|
||||||
builder: (context) => ChatScreen(
|
chatId: int.parse(id),
|
||||||
chatId: int.parse(id),
|
name: name,
|
||||||
name: name,
|
imageUrl: imageUrl,
|
||||||
imageUrl: imageUrl,
|
chatType: chatType,
|
||||||
chatType: chatType,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,10 +21,12 @@ import '../../../core/storage/app_database.dart';
|
|||||||
import '../../../core/utils/haptics.dart';
|
import '../../../core/utils/haptics.dart';
|
||||||
import '../../../core/config/app_cache_extent.dart';
|
import '../../../core/config/app_cache_extent.dart';
|
||||||
import '../../../core/config/app_message_actions_style.dart';
|
import '../../../core/config/app_message_actions_style.dart';
|
||||||
|
import '../../../core/config/app_swipe_back_desktop.dart';
|
||||||
import '../../../models/attachment.dart';
|
import '../../../models/attachment.dart';
|
||||||
import '../../widgets/message_bubble.dart';
|
import '../../widgets/message_bubble.dart';
|
||||||
import '../../widgets/message_actions_overlay.dart';
|
import '../../widgets/message_actions_overlay.dart';
|
||||||
import '../../widgets/attachment_panel.dart';
|
import '../../widgets/attachment_panel.dart';
|
||||||
|
import '../../widgets/swipe_to_pop.dart';
|
||||||
|
|
||||||
class _UploadStatus {
|
class _UploadStatus {
|
||||||
final bool active;
|
final bool active;
|
||||||
@@ -791,10 +793,17 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final cs = Theme.of(context).colorScheme;
|
final cs = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
// TODO: Локализация
|
// TODO: Локализация
|
||||||
// TODO: Cклонения
|
// TODO: Cклонения
|
||||||
return Scaffold(
|
return ValueListenableBuilder<bool>(
|
||||||
|
valueListenable: AppSwipeBackDesktop.current,
|
||||||
|
builder: (context, desktopSwipe, child) => SwipeToPop(
|
||||||
|
enabled: widget.embedded && desktopSwipe,
|
||||||
|
onPop: widget.onClose,
|
||||||
|
child: child!,
|
||||||
|
),
|
||||||
|
child: Scaffold(
|
||||||
backgroundColor: cs.surface,
|
backgroundColor: cs.surface,
|
||||||
appBar: PreferredSize(
|
appBar: PreferredSize(
|
||||||
preferredSize: Size.fromHeight(kToolbarHeight),
|
preferredSize: Size.fromHeight(kToolbarHeight),
|
||||||
@@ -942,6 +951,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
_buildInputArea(context),
|
_buildInputArea(context),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import '../../../backend/modules/contacts.dart';
|
|||||||
import '../../../core/storage/token_storage.dart';
|
import '../../../core/storage/token_storage.dart';
|
||||||
import '../../../main.dart';
|
import '../../../main.dart';
|
||||||
import '../../widgets/custom_notification.dart';
|
import '../../widgets/custom_notification.dart';
|
||||||
|
import '../../widgets/swipe_route.dart';
|
||||||
import 'chat_screen.dart';
|
import 'chat_screen.dart';
|
||||||
|
|
||||||
const int _maxAvatarBytes = 8 * 1024 * 1024;
|
const int _maxAvatarBytes = 8 * 1024 * 1024;
|
||||||
@@ -146,14 +147,13 @@ class _CreateGroupFlowState extends State<_CreateGroupFlow> {
|
|||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
navigator.pop();
|
navigator.pop();
|
||||||
navigator.push(
|
pushSwipeable(
|
||||||
MaterialPageRoute(
|
context,
|
||||||
builder: (_) => ChatScreen(
|
(_) => ChatScreen(
|
||||||
chatId: chat.id,
|
chatId: chat.id,
|
||||||
name: chat.title ?? title,
|
name: chat.title ?? title,
|
||||||
imageUrl: chat.iconUrl ?? '',
|
imageUrl: chat.iconUrl ?? '',
|
||||||
chatType: chat.type,
|
chatType: chat.type,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import '../../../core/storage/app_database.dart';
|
|||||||
import '../../../core/storage/token_storage.dart';
|
import '../../../core/storage/token_storage.dart';
|
||||||
import '../../../main.dart';
|
import '../../../main.dart';
|
||||||
import '../../widgets/custom_notification.dart';
|
import '../../widgets/custom_notification.dart';
|
||||||
|
import '../../widgets/swipe_route.dart';
|
||||||
import '../chats/chat_screen.dart';
|
import '../chats/chat_screen.dart';
|
||||||
|
|
||||||
class ContactProfileScreen extends StatefulWidget {
|
class ContactProfileScreen extends StatefulWidget {
|
||||||
@@ -170,15 +171,13 @@ class _ContactProfileScreenState extends State<ContactProfileScreen> {
|
|||||||
);
|
);
|
||||||
final chatId = existing ?? (accountId ^ widget.contactId);
|
final chatId = existing ?? (accountId ^ widget.contactId);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
Navigator.push(
|
pushSwipeable(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
(_) => ChatScreen(
|
||||||
builder: (_) => ChatScreen(
|
chatId: chatId,
|
||||||
chatId: chatId,
|
name: _displayName(),
|
||||||
name: _displayName(),
|
imageUrl: _avatarUrl() ?? '',
|
||||||
imageUrl: _avatarUrl() ?? '',
|
chatType: 'DIALOG',
|
||||||
chatType: 'DIALOG',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import '../../../backend/modules/chats.dart';
|
import '../../../backend/modules/chats.dart';
|
||||||
|
import '../../../core/config/app_swipe_back_desktop.dart';
|
||||||
import '../../../core/protocol/opcode_map.dart';
|
import '../../../core/protocol/opcode_map.dart';
|
||||||
import '../../../core/protocol/packet.dart';
|
import '../../../core/protocol/packet.dart';
|
||||||
import '../../../core/utils/logger.dart';
|
import '../../../core/utils/logger.dart';
|
||||||
@@ -332,6 +333,70 @@ class _DebugMenuScreenState extends State<DebugMenuScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||||
|
child: ValueListenableBuilder<bool>(
|
||||||
|
valueListenable: AppSwipeBackDesktop.current,
|
||||||
|
builder: (context, swipeOn, _) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cs.surfaceContainerHigh,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 20,
|
||||||
|
vertical: 17,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Symbols.swipe_right,
|
||||||
|
color: cs.onSurfaceVariant,
|
||||||
|
size: 22,
|
||||||
|
weight: 400,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Свайп-назад в десктоп-режиме',
|
||||||
|
style: TextStyle(
|
||||||
|
color: cs.onSurface,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
'Включает жест «провести от левого края, чтобы '
|
||||||
|
'закрыть» внутри встроенной панели чата на '
|
||||||
|
'десктопе — для тестирования курсором',
|
||||||
|
style: TextStyle(
|
||||||
|
color: cs.onSurfaceVariant,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Switch(
|
||||||
|
value: swipeOn,
|
||||||
|
onChanged: (v) {
|
||||||
|
AppSwipeBackDesktop.save(v);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
|
||||||
|
class SwipeRoute<T> extends CupertinoPageRoute<T> {
|
||||||
|
SwipeRoute({
|
||||||
|
required super.builder,
|
||||||
|
super.settings,
|
||||||
|
super.maintainState,
|
||||||
|
super.fullscreenDialog,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<T?> pushSwipeable<T>(
|
||||||
|
BuildContext context,
|
||||||
|
WidgetBuilder builder, {
|
||||||
|
RouteSettings? settings,
|
||||||
|
}) {
|
||||||
|
return Navigator.of(context).push<T>(
|
||||||
|
SwipeRoute<T>(builder: builder, settings: settings),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class SwipeToPop extends StatefulWidget {
|
||||||
|
final Widget child;
|
||||||
|
final double edgeWidth;
|
||||||
|
final double popThreshold;
|
||||||
|
final double velocityThreshold;
|
||||||
|
final bool fullWidth;
|
||||||
|
final bool enabled;
|
||||||
|
final VoidCallback? onPop;
|
||||||
|
|
||||||
|
const SwipeToPop({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
this.edgeWidth = 28,
|
||||||
|
this.popThreshold = 0.35,
|
||||||
|
this.velocityThreshold = 700,
|
||||||
|
this.fullWidth = false,
|
||||||
|
this.enabled = true,
|
||||||
|
this.onPop,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SwipeToPop> createState() => _SwipeToPopState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SwipeToPopState extends State<SwipeToPop>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 240),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get _canPop {
|
||||||
|
if (!widget.enabled) return false;
|
||||||
|
if (widget.onPop != null) return true;
|
||||||
|
final route = ModalRoute.of(context);
|
||||||
|
if (route == null) return false;
|
||||||
|
return route.canPop;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onDragStart(DragStartDetails _) {
|
||||||
|
_controller.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onDragUpdate(DragUpdateDetails d, double width) {
|
||||||
|
if (width <= 0) return;
|
||||||
|
final next = (_controller.value + d.delta.dx / width).clamp(0.0, 1.0);
|
||||||
|
_controller.value = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onDragEnd(DragEndDetails d, double width) async {
|
||||||
|
final velocity = d.primaryVelocity ?? 0;
|
||||||
|
final pastThreshold = _controller.value > widget.popThreshold ||
|
||||||
|
velocity > widget.velocityThreshold;
|
||||||
|
if (pastThreshold) {
|
||||||
|
final remaining = 1.0 - _controller.value;
|
||||||
|
_controller.duration = Duration(
|
||||||
|
milliseconds: (remaining * 220).clamp(80, 220).round(),
|
||||||
|
);
|
||||||
|
await _controller.animateTo(1.0, curve: Curves.easeOutCubic);
|
||||||
|
if (!mounted) return;
|
||||||
|
if (widget.onPop != null) {
|
||||||
|
widget.onPop!();
|
||||||
|
} else {
|
||||||
|
Navigator.of(context).maybePop();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_controller.duration = const Duration(milliseconds: 200);
|
||||||
|
await _controller.animateBack(0.0, curve: Curves.easeOutCubic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onDragCancel() {
|
||||||
|
_controller.duration = const Duration(milliseconds: 200);
|
||||||
|
_controller.animateBack(0.0, curve: Curves.easeOutCubic);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (!_canPop) return widget.child;
|
||||||
|
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final width = constraints.maxWidth;
|
||||||
|
final gestureChild = GestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
dragStartBehavior: DragStartBehavior.down,
|
||||||
|
onHorizontalDragStart: _onDragStart,
|
||||||
|
onHorizontalDragUpdate: (d) => _onDragUpdate(d, width),
|
||||||
|
onHorizontalDragEnd: (d) => _onDragEnd(d, width),
|
||||||
|
onHorizontalDragCancel: _onDragCancel,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: _controller,
|
||||||
|
builder: (context, child) {
|
||||||
|
final t = _controller.value;
|
||||||
|
return Transform.translate(
|
||||||
|
offset: Offset(t * width, 0),
|
||||||
|
child: Opacity(
|
||||||
|
opacity: (1.0 - t * 0.35).clamp(0.0, 1.0),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.fullWidth)
|
||||||
|
Positioned.fill(child: gestureChild)
|
||||||
|
else
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: widget.edgeWidth,
|
||||||
|
child: gestureChild,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import 'core/config/app_bubble_shape.dart';
|
|||||||
import 'core/config/app_cache_extent.dart';
|
import 'core/config/app_cache_extent.dart';
|
||||||
import 'core/config/app_fonts.dart';
|
import 'core/config/app_fonts.dart';
|
||||||
import 'core/config/app_message_actions_style.dart';
|
import 'core/config/app_message_actions_style.dart';
|
||||||
|
import 'core/config/app_swipe_back_desktop.dart';
|
||||||
import 'core/config/app_theme_mode.dart';
|
import 'core/config/app_theme_mode.dart';
|
||||||
import 'core/config/app_theme_schedule.dart';
|
import 'core/config/app_theme_schedule.dart';
|
||||||
import 'backend/modules/account.dart';
|
import 'backend/modules/account.dart';
|
||||||
@@ -92,6 +93,7 @@ void main() async {
|
|||||||
AppAmoled.current.value = await AppAmoled.load();
|
AppAmoled.current.value = await AppAmoled.load();
|
||||||
AppThemeSchedule.current.value = await AppThemeSchedule.load();
|
AppThemeSchedule.current.value = await AppThemeSchedule.load();
|
||||||
AppMessageActionsStyle.current.value = await AppMessageActionsStyle.load();
|
AppMessageActionsStyle.current.value = await AppMessageActionsStyle.load();
|
||||||
|
AppSwipeBackDesktop.current.value = await AppSwipeBackDesktop.load();
|
||||||
runApp(
|
runApp(
|
||||||
KometApp(
|
KometApp(
|
||||||
initialLocale: initialLocale,
|
initialLocale: initialLocale,
|
||||||
|
|||||||
Reference in New Issue
Block a user