вроде как починил сферум, УБРАЛ РАМКУ У МЕДИА БЕЗ ТЕКСТА
This commit is contained in:
@@ -1600,11 +1600,11 @@ class _ChatsScreenState extends State<ChatsScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _showSferumWebView(BuildContext context, String url) {
|
void _showSferumWebView(BuildContext context, String url) {
|
||||||
showModalBottomSheet(
|
Navigator.of(context).push(
|
||||||
context: context,
|
MaterialPageRoute(
|
||||||
isScrollControlled: true,
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
builder: (context) => SferumWebViewPanel(url: url),
|
builder: (context) => SferumWebViewPanel(url: url),
|
||||||
|
fullscreenDialog: true,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4039,34 +4039,36 @@ class SferumWebViewPanel extends StatefulWidget {
|
|||||||
|
|
||||||
class _SferumWebViewPanelState extends State<SferumWebViewPanel> {
|
class _SferumWebViewPanelState extends State<SferumWebViewPanel> {
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
|
InAppWebViewController? _webViewController;
|
||||||
|
|
||||||
|
Future<void> _checkCanGoBack() async {
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _goBack() async {
|
||||||
|
if (_webViewController != null && await _webViewController!.canGoBack()) {
|
||||||
|
await _webViewController!.goBack();
|
||||||
|
_checkCanGoBack();
|
||||||
|
} else {
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final colors = Theme.of(context).colorScheme;
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return DraggableScrollableSheet(
|
return Scaffold(
|
||||||
initialChildSize: 0.9,
|
backgroundColor: colors.surface,
|
||||||
minChildSize: 0.5,
|
appBar: AppBar(
|
||||||
maxChildSize: 0.95,
|
backgroundColor: colors.surface,
|
||||||
builder: (context, scrollController) {
|
elevation: 0,
|
||||||
return Container(
|
leading: IconButton(
|
||||||
decoration: BoxDecoration(
|
icon: const Icon(Icons.arrow_back),
|
||||||
color: colors.surface,
|
onPressed: _goBack,
|
||||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
|
||||||
),
|
),
|
||||||
child: Column(
|
title: Row(
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border(
|
|
||||||
bottom: BorderSide(
|
|
||||||
color: colors.outline.withOpacity(0.2),
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
children: [
|
||||||
Image.asset(
|
Image.asset(
|
||||||
'assets/images/spermum.png',
|
'assets/images/spermum.png',
|
||||||
@@ -4081,17 +4083,17 @@ class _SferumWebViewPanelState extends State<SferumWebViewPanel> {
|
|||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
tooltip: 'Закрыть',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
body: Stack(
|
||||||
|
|
||||||
Expanded(
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
children: [
|
||||||
if (!Platform.isLinux)
|
if (!Platform.isLinux)
|
||||||
InAppWebView(
|
InAppWebView(
|
||||||
@@ -4099,28 +4101,184 @@ class _SferumWebViewPanelState extends State<SferumWebViewPanel> {
|
|||||||
initialSettings: InAppWebViewSettings(
|
initialSettings: InAppWebViewSettings(
|
||||||
javaScriptEnabled: true,
|
javaScriptEnabled: true,
|
||||||
transparentBackground: true,
|
transparentBackground: true,
|
||||||
useShouldOverrideUrlLoading: false,
|
useShouldOverrideUrlLoading: true,
|
||||||
useOnLoadResource: false,
|
useOnLoadResource: false,
|
||||||
useOnDownloadStart: false,
|
useOnDownloadStart: false,
|
||||||
cacheEnabled: true,
|
cacheEnabled: true,
|
||||||
|
verticalScrollBarEnabled: true,
|
||||||
|
horizontalScrollBarEnabled: true,
|
||||||
|
supportZoom: false,
|
||||||
|
disableVerticalScroll: false,
|
||||||
|
disableHorizontalScroll: false,
|
||||||
|
allowsInlineMediaPlayback: true,
|
||||||
|
mediaPlaybackRequiresUserGesture: false,
|
||||||
|
allowsBackForwardNavigationGestures: true,
|
||||||
|
useHybridComposition: true,
|
||||||
|
supportMultipleWindows: false,
|
||||||
|
javaScriptCanOpenWindowsAutomatically: false,
|
||||||
),
|
),
|
||||||
onLoadStart: (controller, url) {
|
onWebViewCreated: (controller) {
|
||||||
|
_webViewController = controller;
|
||||||
|
},
|
||||||
|
onCreateWindow: (controller, createWindowAction) async {
|
||||||
|
final uri = createWindowAction.request.url;
|
||||||
|
print('🪟 Попытка открыть новое окно: $uri');
|
||||||
|
if (uri != null) {
|
||||||
|
await controller.loadUrl(urlRequest: URLRequest(url: uri));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
shouldOverrideUrlLoading: (controller, navigationAction) async {
|
||||||
|
final uri = navigationAction.request.url;
|
||||||
|
final navigationType = navigationAction.navigationType;
|
||||||
|
print('🔗 Попытка перехода по ссылке: $uri (тип: $navigationType)');
|
||||||
|
|
||||||
|
if (navigationType == NavigationType.LINK_ACTIVATED) {
|
||||||
|
return NavigationActionPolicy.ALLOW;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NavigationActionPolicy.ALLOW;
|
||||||
|
},
|
||||||
|
onLoadStart: (controller, url) async {
|
||||||
print('🌐 WebView начало загрузки: $url');
|
print('🌐 WebView начало загрузки: $url');
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
});
|
});
|
||||||
|
try {
|
||||||
|
await controller.evaluateJavascript(source: '''
|
||||||
|
// Переопределяем window.open сразу
|
||||||
|
if (window.open.toString().indexOf('native code') === -1) {
|
||||||
|
var originalOpen = window.open;
|
||||||
|
window.open = function(url, name, features) {
|
||||||
|
if (url && typeof url === 'string') {
|
||||||
|
window.location.href = url;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return originalOpen.apply(this, arguments);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
''');
|
||||||
|
} catch (e) {
|
||||||
|
print('⚠️ Ошибка при выполнении JavaScript в onLoadStart: $e');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onLoadStop: (controller, url) {
|
onLoadStop: (controller, url) async {
|
||||||
print('✅ WebView загрузка завершена: $url');
|
print('✅ WebView загрузка завершена: $url');
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
|
_checkCanGoBack();
|
||||||
|
try {
|
||||||
|
await controller.evaluateJavascript(source: '''
|
||||||
|
// Включаем прокрутку
|
||||||
|
document.body.style.overflow = 'auto';
|
||||||
|
document.documentElement.style.overflow = 'auto';
|
||||||
|
document.body.style.webkitOverflowScrolling = 'touch';
|
||||||
|
document.body.style.position = 'relative';
|
||||||
|
document.documentElement.style.position = 'relative';
|
||||||
|
|
||||||
|
// Перехватываем все клики по ссылкам и принудительно открываем в том же окне
|
||||||
|
(function() {
|
||||||
|
// Функция для обработки ссылок
|
||||||
|
function processLink(link) {
|
||||||
|
if (link && link.tagName === 'A') {
|
||||||
|
var href = link.getAttribute('href');
|
||||||
|
if (href && !href.startsWith('javascript:') && !href.startsWith('mailto:')) {
|
||||||
|
// Убираем target="_blank" если есть
|
||||||
|
link.removeAttribute('target');
|
||||||
|
// Добавляем обработчик клика
|
||||||
|
link.addEventListener('click', function(e) {
|
||||||
|
var href = this.getAttribute('href');
|
||||||
|
if (href && !href.startsWith('javascript:') && !href.startsWith('mailto:')) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
window.location.href = href;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обрабатываем все существующие ссылки
|
||||||
|
function processAllLinks() {
|
||||||
|
var links = document.querySelectorAll('a');
|
||||||
|
for (var i = 0; i < links.length; i++) {
|
||||||
|
processLink(links[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обрабатываем ссылки при загрузке
|
||||||
|
processAllLinks();
|
||||||
|
|
||||||
|
// Перехватываем клики на уровне document
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
var target = e.target;
|
||||||
|
// Находим ближайшую ссылку
|
||||||
|
while (target && target.tagName !== 'A' && target !== document.body) {
|
||||||
|
target = target.parentElement;
|
||||||
|
}
|
||||||
|
if (target && target.tagName === 'A') {
|
||||||
|
var href = target.getAttribute('href');
|
||||||
|
if (href && !href.startsWith('javascript:') && !href.startsWith('mailto:')) {
|
||||||
|
// Убираем target если есть
|
||||||
|
target.removeAttribute('target');
|
||||||
|
// Открываем в том же окне
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
window.location.href = href;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
// Отслеживаем динамически добавляемые ссылки
|
||||||
|
var observer = new MutationObserver(function(mutations) {
|
||||||
|
mutations.forEach(function(mutation) {
|
||||||
|
mutation.addedNodes.forEach(function(node) {
|
||||||
|
if (node.nodeType === 1) { // Element node
|
||||||
|
if (node.tagName === 'A') {
|
||||||
|
processLink(node);
|
||||||
|
}
|
||||||
|
// Обрабатываем ссылки внутри добавленного элемента
|
||||||
|
var links = node.querySelectorAll ? node.querySelectorAll('a') : [];
|
||||||
|
for (var i = 0; i < links.length; i++) {
|
||||||
|
processLink(links[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Начинаем наблюдение за изменениями в DOM
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Переопределяем window.open чтобы открывать в том же окне
|
||||||
|
var originalOpen = window.open;
|
||||||
|
window.open = function(url, name, features) {
|
||||||
|
if (url && typeof url === 'string') {
|
||||||
|
window.location.href = url;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return originalOpen.apply(this, arguments);
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
''');
|
||||||
|
} catch (e) {
|
||||||
|
print('⚠️ Ошибка при выполнении JavaScript: $e');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onReceivedError: (controller, request, error) {
|
onReceivedError: (controller, request, error) {
|
||||||
print(
|
print(
|
||||||
'❌ WebView ошибка: ${error.description} (${error.type})',
|
'❌ WebView ошибка: ${error.description} (${error.type})',
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
onConsoleMessage: (controller, consoleMessage) {
|
||||||
|
print('📝 Console: ${consoleMessage.message}');
|
||||||
|
},
|
||||||
),
|
),
|
||||||
if (Platform.isLinux)
|
if (Platform.isLinux)
|
||||||
Container(
|
Container(
|
||||||
@@ -4144,11 +4302,6 @@ class _SferumWebViewPanelState extends State<SferumWebViewPanel> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
import 'package:chewie/chewie.dart';
|
import 'package:chewie/chewie.dart';
|
||||||
|
|
||||||
@@ -17,11 +19,27 @@ class _FullScreenVideoPlayerState extends State<FullScreenVideoPlayer> {
|
|||||||
ChewieController? _chewieController;
|
ChewieController? _chewieController;
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
bool _hasError = false;
|
bool _hasError = false;
|
||||||
|
bool _showControls = true;
|
||||||
|
Timer? _hideControlsTimer;
|
||||||
|
bool _isDragging = false;
|
||||||
|
double _currentVolume = 1.0;
|
||||||
|
double _currentBrightness = 1.0;
|
||||||
|
bool _showVolumeIndicator = false;
|
||||||
|
bool _showBrightnessIndicator = false;
|
||||||
|
double _volumeIndicatorOpacity = 0.0;
|
||||||
|
double _brightnessIndicatorOpacity = 0.0;
|
||||||
|
Timer? _indicatorTimer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_initializePlayer();
|
_initializePlayer();
|
||||||
|
SystemChrome.setPreferredOrientations([
|
||||||
|
DeviceOrientation.landscapeRight,
|
||||||
|
DeviceOrientation.landscapeLeft,
|
||||||
|
DeviceOrientation.portraitUp,
|
||||||
|
]);
|
||||||
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initializePlayer() async {
|
Future<void> _initializePlayer() async {
|
||||||
@@ -39,21 +57,29 @@ class _FullScreenVideoPlayerState extends State<FullScreenVideoPlayer> {
|
|||||||
_chewieController = ChewieController(
|
_chewieController = ChewieController(
|
||||||
videoPlayerController: _videoPlayerController!,
|
videoPlayerController: _videoPlayerController!,
|
||||||
aspectRatio: _videoPlayerController!.value.aspectRatio,
|
aspectRatio: _videoPlayerController!.value.aspectRatio,
|
||||||
autoPlay: true, // Начинаем воспроизведение сразу
|
autoPlay: true,
|
||||||
looping: false, // Не зацикливаем
|
looping: false,
|
||||||
showControls: true, // Показываем стандартные элементы управления Chewie
|
showControls: false, // Используем кастомные элементы управления
|
||||||
materialProgressColors: ChewieProgressColors(
|
materialProgressColors: ChewieProgressColors(
|
||||||
playedColor: Colors.red,
|
playedColor: Colors.blueAccent,
|
||||||
handleColor: Colors.blueAccent,
|
handleColor: Colors.white,
|
||||||
backgroundColor: Colors.grey,
|
backgroundColor: Colors.white.withOpacity(0.3),
|
||||||
bufferedColor: Colors.white,
|
bufferedColor: Colors.white.withOpacity(0.5),
|
||||||
),
|
),
|
||||||
|
allowFullScreen: false,
|
||||||
|
allowMuting: true,
|
||||||
|
allowPlaybackSpeedChanging: true,
|
||||||
|
playbackSpeeds: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 2.0],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Получаем текущую громкость
|
||||||
|
_currentVolume = _videoPlayerController!.value.volume;
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
|
_startHideControlsTimer();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('❌ [FullScreenVideoPlayer] Error initializing Chewie player: $e');
|
print('❌ [FullScreenVideoPlayer] Error initializing Chewie player: $e');
|
||||||
@@ -66,49 +92,572 @@ class _FullScreenVideoPlayerState extends State<FullScreenVideoPlayer> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _startHideControlsTimer() {
|
||||||
|
_hideControlsTimer?.cancel();
|
||||||
|
_hideControlsTimer = Timer(const Duration(seconds: 3), () {
|
||||||
|
if (mounted && !_isDragging) {
|
||||||
|
setState(() {
|
||||||
|
_showControls = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _toggleControls() {
|
||||||
|
setState(() {
|
||||||
|
_showControls = !_showControls;
|
||||||
|
});
|
||||||
|
if (_showControls) {
|
||||||
|
_startHideControlsTimer();
|
||||||
|
} else {
|
||||||
|
_hideControlsTimer?.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _togglePlayPause() {
|
||||||
|
if (_chewieController?.isPlaying ?? false) {
|
||||||
|
_chewieController?.pause();
|
||||||
|
} else {
|
||||||
|
_chewieController?.play();
|
||||||
|
}
|
||||||
|
_toggleControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _displayVolumeIndicator(double volume) {
|
||||||
|
setState(() {
|
||||||
|
_showVolumeIndicator = true;
|
||||||
|
_currentVolume = volume;
|
||||||
|
_volumeIndicatorOpacity = 1.0;
|
||||||
|
});
|
||||||
|
_indicatorTimer?.cancel();
|
||||||
|
_indicatorTimer = Timer(const Duration(milliseconds: 1500), () {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_volumeIndicatorOpacity = 0.0;
|
||||||
|
});
|
||||||
|
Future.delayed(const Duration(milliseconds: 300), () {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_showVolumeIndicator = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _displayBrightnessIndicator(double brightness) {
|
||||||
|
setState(() {
|
||||||
|
_showBrightnessIndicator = true;
|
||||||
|
_currentBrightness = brightness;
|
||||||
|
_brightnessIndicatorOpacity = 1.0;
|
||||||
|
});
|
||||||
|
_indicatorTimer?.cancel();
|
||||||
|
_indicatorTimer = Timer(const Duration(milliseconds: 1500), () {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_brightnessIndicatorOpacity = 0.0;
|
||||||
|
});
|
||||||
|
Future.delayed(const Duration(milliseconds: 300), () {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_showBrightnessIndicator = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatDuration(Duration duration) {
|
||||||
|
String twoDigits(int n) => n.toString().padLeft(2, '0');
|
||||||
|
final hours = duration.inHours;
|
||||||
|
final minutes = duration.inMinutes.remainder(60);
|
||||||
|
final seconds = duration.inSeconds.remainder(60);
|
||||||
|
if (hours > 0) {
|
||||||
|
return '${twoDigits(hours)}:${twoDigits(minutes)}:${twoDigits(seconds)}';
|
||||||
|
}
|
||||||
|
return '${twoDigits(minutes)}:${twoDigits(seconds)}';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_hideControlsTimer?.cancel();
|
||||||
|
_indicatorTimer?.cancel();
|
||||||
_videoPlayerController?.dispose();
|
_videoPlayerController?.dispose();
|
||||||
_chewieController?.dispose();
|
_chewieController?.dispose();
|
||||||
|
SystemChrome.setPreferredOrientations([
|
||||||
|
DeviceOrientation.portraitUp,
|
||||||
|
DeviceOrientation.portraitDown,
|
||||||
|
DeviceOrientation.landscapeLeft,
|
||||||
|
DeviceOrientation.landscapeRight,
|
||||||
|
]);
|
||||||
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.black, // Черный фон для полноэкранного видео
|
|
||||||
appBar: AppBar(
|
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
iconTheme: const IconThemeData(color: Colors.white),
|
body: SafeArea(
|
||||||
title: const Text('Видео', style: TextStyle(color: Colors.white)),
|
child: Stack(
|
||||||
),
|
children: [
|
||||||
body: Center(
|
// Видео плеер
|
||||||
|
Center(
|
||||||
child: _isLoading
|
child: _isLoading
|
||||||
? const CircularProgressIndicator(color: Colors.white)
|
|
||||||
: _hasError
|
|
||||||
? const Column(
|
? const Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.error_outline, color: Colors.red, size: 50),
|
CircularProgressIndicator(color: Colors.white),
|
||||||
SizedBox(height: 10),
|
SizedBox(height: 20),
|
||||||
Text(
|
Text(
|
||||||
'Не удалось загрузить видео.',
|
'Загрузка видео...',
|
||||||
style: TextStyle(color: Colors.white, fontSize: 16),
|
style: TextStyle(color: Colors.white70, fontSize: 14),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: _hasError
|
||||||
|
? Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.error_outline,
|
||||||
|
color: Colors.red, size: 60),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
const Text(
|
||||||
|
'Не удалось загрузить видео',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
const Text(
|
||||||
|
'Проверьте интернет или попробуйте позже',
|
||||||
|
style: TextStyle(color: Colors.white70, fontSize: 14),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_hasError = false;
|
||||||
|
});
|
||||||
|
_initializePlayer();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
label: const Text('Попробовать снова'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.blueAccent,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 24, vertical: 12),
|
||||||
),
|
),
|
||||||
Text(
|
|
||||||
'Проверьте интернет или попробуйте позже.',
|
|
||||||
style: TextStyle(color: Colors.white70, fontSize: 12),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: _chewieController != null &&
|
: _chewieController != null &&
|
||||||
_chewieController!.videoPlayerController.value.isInitialized
|
_chewieController!
|
||||||
? Chewie(controller: _chewieController!)
|
.videoPlayerController.value.isInitialized
|
||||||
: const Text(
|
? GestureDetector(
|
||||||
|
onTap: _toggleControls,
|
||||||
|
onDoubleTap: _togglePlayPause,
|
||||||
|
onVerticalDragUpdate: (details) {
|
||||||
|
if (!_isDragging) {
|
||||||
|
_isDragging = true;
|
||||||
|
setState(() {
|
||||||
|
_showControls = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_hideControlsTimer?.cancel();
|
||||||
|
|
||||||
|
final delta = details.delta.dy;
|
||||||
|
final screenHeight = MediaQuery.of(context).size.height;
|
||||||
|
final change = -delta / screenHeight;
|
||||||
|
|
||||||
|
// Левая сторона экрана - яркость
|
||||||
|
if (details.globalPosition.dx <
|
||||||
|
MediaQuery.of(context).size.width / 2) {
|
||||||
|
final newBrightness = (_currentBrightness + change)
|
||||||
|
.clamp(0.0, 1.0);
|
||||||
|
_currentBrightness = newBrightness;
|
||||||
|
// Здесь можно изменить яркость экрана, но это требует системных разрешений
|
||||||
|
_displayBrightnessIndicator(newBrightness);
|
||||||
|
} else {
|
||||||
|
// Правая сторона экрана - громкость
|
||||||
|
final newVolume = (_currentVolume + change).clamp(0.0, 1.0);
|
||||||
|
_videoPlayerController?.setVolume(newVolume);
|
||||||
|
_currentVolume = newVolume;
|
||||||
|
_displayVolumeIndicator(newVolume);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onVerticalDragEnd: (_) {
|
||||||
|
_isDragging = false;
|
||||||
|
_startHideControlsTimer();
|
||||||
|
},
|
||||||
|
child: Chewie(controller: _chewieController!),
|
||||||
|
)
|
||||||
|
: const Center(
|
||||||
|
child: Text(
|
||||||
'Ошибка плеера',
|
'Ошибка плеера',
|
||||||
style: TextStyle(color: Colors.white),
|
style: TextStyle(color: Colors.white),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Кастомные элементы управления
|
||||||
|
if (!_isLoading && !_hasError && _chewieController != null)
|
||||||
|
AnimatedOpacity(
|
||||||
|
opacity: _showControls ? 1.0 : 0.0,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
child: _buildCustomControls(),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Индикатор громкости
|
||||||
|
if (_showVolumeIndicator)
|
||||||
|
Center(
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
opacity: _volumeIndicatorOpacity,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
child: _buildVolumeIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Индикатор яркости
|
||||||
|
if (_showBrightnessIndicator)
|
||||||
|
Center(
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
opacity: _brightnessIndicatorOpacity,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
child: _buildBrightnessIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Кнопка назад
|
||||||
|
if (_showControls && !_isLoading && !_hasError)
|
||||||
|
Positioned(
|
||||||
|
top: 10,
|
||||||
|
left: 10,
|
||||||
|
child: SafeArea(
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => Navigator.of(context).pop(),
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.5),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.arrow_back,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCustomControls() {
|
||||||
|
if (_chewieController == null) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
final controller = _chewieController!;
|
||||||
|
final videoController = controller.videoPlayerController;
|
||||||
|
final isPlaying = controller.isPlaying;
|
||||||
|
final duration = videoController.value.duration;
|
||||||
|
final position = videoController.value.position;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
Colors.black.withOpacity(0.7),
|
||||||
|
Colors.transparent,
|
||||||
|
Colors.transparent,
|
||||||
|
Colors.black.withOpacity(0.7),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
// Верхняя панель
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Spacer(),
|
||||||
|
// Кнопка скорости воспроизведения
|
||||||
|
_buildSpeedButton(controller),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
// Кнопка полноэкранного режима
|
||||||
|
_buildFullScreenButton(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Центральная кнопка воспроизведения/паузы
|
||||||
|
Center(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: _togglePlayPause,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.5),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
isPlaying ? Icons.pause : Icons.play_arrow,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 48,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Нижняя панель управления
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Прогресс бар
|
||||||
|
VideoProgressIndicator(
|
||||||
|
videoController,
|
||||||
|
allowScrubbing: true,
|
||||||
|
colors: VideoProgressColors(
|
||||||
|
playedColor: Colors.blueAccent,
|
||||||
|
bufferedColor: Colors.white.withOpacity(0.3),
|
||||||
|
backgroundColor: Colors.white.withOpacity(0.2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
// Время и кнопки управления
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
// Кнопка перемотки назад
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.replay_10, color: Colors.white),
|
||||||
|
onPressed: () {
|
||||||
|
final newPosition = position - const Duration(seconds: 10);
|
||||||
|
final clampedPosition = newPosition < Duration.zero
|
||||||
|
? Duration.zero
|
||||||
|
: (newPosition > duration ? duration : newPosition);
|
||||||
|
videoController.seekTo(clampedPosition);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// Время
|
||||||
|
Text(
|
||||||
|
'${_formatDuration(position)} / ${_formatDuration(duration)}',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
// Кнопка перемотки вперед
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.forward_10, color: Colors.white),
|
||||||
|
onPressed: () {
|
||||||
|
final newPosition = position + const Duration(seconds: 10);
|
||||||
|
final clampedPosition = newPosition < Duration.zero
|
||||||
|
? Duration.zero
|
||||||
|
: (newPosition > duration ? duration : newPosition);
|
||||||
|
videoController.seekTo(clampedPosition);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
// Кнопка громкости
|
||||||
|
_buildVolumeButton(controller),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSpeedButton(ChewieController controller) {
|
||||||
|
final speed = controller.videoPlayerController.value.playbackSpeed;
|
||||||
|
return Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
final speeds = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 2.0];
|
||||||
|
final currentIndex = speeds.indexOf(speed);
|
||||||
|
final nextIndex = (currentIndex + 1) % speeds.length;
|
||||||
|
controller.videoPlayerController.setPlaybackSpeed(speeds[nextIndex]);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.5),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'${speed}x',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFullScreenButton() {
|
||||||
|
return Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
// Переключение ориентации
|
||||||
|
if (MediaQuery.of(context).orientation == Orientation.portrait) {
|
||||||
|
SystemChrome.setPreferredOrientations([
|
||||||
|
DeviceOrientation.landscapeLeft,
|
||||||
|
DeviceOrientation.landscapeRight,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
SystemChrome.setPreferredOrientations([
|
||||||
|
DeviceOrientation.portraitUp,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.5),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
MediaQuery.of(context).orientation == Orientation.portrait
|
||||||
|
? Icons.fullscreen
|
||||||
|
: Icons.fullscreen_exit,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildVolumeButton(ChewieController controller) {
|
||||||
|
final isMuted = controller.videoPlayerController.value.volume == 0;
|
||||||
|
return IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
isMuted ? Icons.volume_off : Icons.volume_up,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
if (isMuted) {
|
||||||
|
controller.videoPlayerController.setVolume(_currentVolume > 0 ? _currentVolume : 0.5);
|
||||||
|
} else {
|
||||||
|
controller.videoPlayerController.setVolume(0);
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildVolumeIndicator() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.7),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
_currentVolume == 0
|
||||||
|
? Icons.volume_off
|
||||||
|
: _currentVolume < 0.5
|
||||||
|
? Icons.volume_down
|
||||||
|
: Icons.volume_up,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
SizedBox(
|
||||||
|
width: 200,
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: _currentVolume,
|
||||||
|
backgroundColor: Colors.white.withOpacity(0.3),
|
||||||
|
valueColor: const AlwaysStoppedAnimation<Color>(Colors.blueAccent),
|
||||||
|
minHeight: 4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'${(_currentVolume * 100).toInt()}%',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBrightnessIndicator() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.7),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
_currentBrightness < 0.3
|
||||||
|
? Icons.brightness_2
|
||||||
|
: _currentBrightness < 0.7
|
||||||
|
? Icons.brightness_medium
|
||||||
|
: Icons.brightness_high,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
SizedBox(
|
||||||
|
width: 200,
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: _currentBrightness,
|
||||||
|
backgroundColor: Colors.white.withOpacity(0.3),
|
||||||
|
valueColor: const AlwaysStoppedAnimation<Color>(Colors.amber),
|
||||||
|
minHeight: 4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'${(_currentBrightness * 100).toInt()}%',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -979,6 +979,27 @@ class ChatMessageBubble extends StatelessWidget {
|
|||||||
return _buildVideoCircleOnlyMessage(context);
|
return _buildVideoCircleOnlyMessage(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final isPhotoOnly =
|
||||||
|
message.attaches.isNotEmpty &&
|
||||||
|
message.attaches.every((a) => a['_type'] == 'PHOTO') &&
|
||||||
|
message.text.isEmpty &&
|
||||||
|
!message.isReply &&
|
||||||
|
!message.isForwarded;
|
||||||
|
if (isPhotoOnly) {
|
||||||
|
return _buildPhotoOnlyMessage(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
final isVideoOnly =
|
||||||
|
message.attaches.isNotEmpty &&
|
||||||
|
message.attaches.every((a) => a['_type'] == 'VIDEO') &&
|
||||||
|
message.attaches.every((a) => (a['videoType'] as int?) != 1) &&
|
||||||
|
message.text.isEmpty &&
|
||||||
|
!message.isReply &&
|
||||||
|
!message.isForwarded;
|
||||||
|
if (isVideoOnly) {
|
||||||
|
return _buildVideoOnlyMessage(context);
|
||||||
|
}
|
||||||
|
|
||||||
final hasUnsupportedContent = _hasUnsupportedMessageTypes();
|
final hasUnsupportedContent = _hasUnsupportedMessageTypes();
|
||||||
|
|
||||||
final messageOpacity = themeProvider.messageBubbleOpacity;
|
final messageOpacity = themeProvider.messageBubbleOpacity;
|
||||||
@@ -1519,6 +1540,208 @@ class ChatMessageBubble extends StatelessWidget {
|
|||||||
return videoContent;
|
return videoContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildPhotoOnlyMessage(BuildContext context) {
|
||||||
|
final photos = message.attaches.where((a) => a['_type'] == 'PHOTO').toList();
|
||||||
|
final themeProvider = Provider.of<ThemeProvider>(context);
|
||||||
|
final isUltraOptimized = themeProvider.ultraOptimizeChats;
|
||||||
|
final messageOpacity = themeProvider.messageBubbleOpacity;
|
||||||
|
final bubbleColor = _getBubbleColor(isMe, themeProvider, messageOpacity);
|
||||||
|
final textColor = _getTextColor(
|
||||||
|
isMe,
|
||||||
|
bubbleColor,
|
||||||
|
themeProvider.messageTextOpacity,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
|
||||||
|
final timeColor = Theme.of(context).brightness == Brightness.dark
|
||||||
|
? const Color(0xFF9bb5c7)
|
||||||
|
: const Color(0xFF6b7280);
|
||||||
|
|
||||||
|
Widget photoContent = Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: isMe
|
||||||
|
? CrossAxisAlignment.end
|
||||||
|
: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: isMe
|
||||||
|
? MainAxisAlignment.end
|
||||||
|
: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
if (!isMe && isGroupChat && !isChannel) ...[
|
||||||
|
SizedBox(
|
||||||
|
width: 40,
|
||||||
|
child: isLastInGroup
|
||||||
|
? Transform.translate(
|
||||||
|
offset: Offset(0, avatarVerticalOffset),
|
||||||
|
child: _buildSenderAvatar(),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: isMe
|
||||||
|
? CrossAxisAlignment.end
|
||||||
|
: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildSmartPhotoGroup(context, photos, textColor, isUltraOptimized),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4, right: 6),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_formatMessageTime(context, message.time),
|
||||||
|
style: TextStyle(fontSize: 12, color: timeColor),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (onReaction != null || (isMe && (onEdit != null || onDelete != null))) {
|
||||||
|
photoContent = GestureDetector(
|
||||||
|
onTapDown: (TapDownDetails details) {
|
||||||
|
_showMessageContextMenu(context, details.globalPosition);
|
||||||
|
},
|
||||||
|
child: photoContent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return photoContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildVideoOnlyMessage(BuildContext context) {
|
||||||
|
final videos = message.attaches.where((a) => a['_type'] == 'VIDEO').toList();
|
||||||
|
|
||||||
|
final timeColor = Theme.of(context).brightness == Brightness.dark
|
||||||
|
? const Color(0xFF9bb5c7)
|
||||||
|
: const Color(0xFF6b7280);
|
||||||
|
|
||||||
|
Widget videoContent = Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: isMe
|
||||||
|
? CrossAxisAlignment.end
|
||||||
|
: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
...videos.asMap().entries.map((entry) {
|
||||||
|
final index = entry.key;
|
||||||
|
final video = entry.value;
|
||||||
|
final videoId = video['videoId'] as int?;
|
||||||
|
final videoType = video['videoType'] as int?;
|
||||||
|
final previewData = video['previewData'] as String?;
|
||||||
|
final thumbnailUrl = video['url'] ?? video['baseUrl'] as String?;
|
||||||
|
|
||||||
|
Uint8List? previewBytes;
|
||||||
|
if (previewData != null && previewData.startsWith('data:')) {
|
||||||
|
final idx = previewData.indexOf('base64,');
|
||||||
|
if (idx != -1) {
|
||||||
|
final b64 = previewData.substring(idx + 7);
|
||||||
|
try {
|
||||||
|
previewBytes = base64Decode(b64);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String? highQualityThumbnailUrl;
|
||||||
|
if (thumbnailUrl != null && thumbnailUrl.isNotEmpty) {
|
||||||
|
highQualityThumbnailUrl = thumbnailUrl;
|
||||||
|
if (!thumbnailUrl.contains('?')) {
|
||||||
|
highQualityThumbnailUrl =
|
||||||
|
'$thumbnailUrl?size=medium&quality=high&format=jpeg';
|
||||||
|
} else {
|
||||||
|
highQualityThumbnailUrl =
|
||||||
|
'$thumbnailUrl&size=medium&quality=high&format=jpeg';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: isMe
|
||||||
|
? CrossAxisAlignment.end
|
||||||
|
: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: isMe
|
||||||
|
? MainAxisAlignment.end
|
||||||
|
: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
if (!isMe && isGroupChat && !isChannel && index == 0) ...[
|
||||||
|
SizedBox(
|
||||||
|
width: 40,
|
||||||
|
child: isLastInGroup
|
||||||
|
? Transform.translate(
|
||||||
|
offset: Offset(0, avatarVerticalOffset),
|
||||||
|
child: _buildSenderAvatar(),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: isMe
|
||||||
|
? CrossAxisAlignment.end
|
||||||
|
: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (videoId != null && chatId != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 4.0),
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 300),
|
||||||
|
child: _buildVideoPreview(
|
||||||
|
context: context,
|
||||||
|
videoId: videoId,
|
||||||
|
messageId: message.id,
|
||||||
|
highQualityUrl: highQualityThumbnailUrl,
|
||||||
|
lowQualityBytes: previewBytes,
|
||||||
|
videoType: videoType,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (index == videos.length - 1)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4, right: 6),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_formatMessageTime(context, message.time),
|
||||||
|
style: TextStyle(fontSize: 12, color: timeColor),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (onReaction != null || (isMe && (onEdit != null || onDelete != null))) {
|
||||||
|
videoContent = GestureDetector(
|
||||||
|
onTapDown: (TapDownDetails details) {
|
||||||
|
_showMessageContextMenu(context, details.globalPosition);
|
||||||
|
},
|
||||||
|
child: videoContent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return videoContent;
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildStickerImage(
|
Widget _buildStickerImage(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
Map<String, dynamic> sticker,
|
Map<String, dynamic> sticker,
|
||||||
|
|||||||
@@ -91,9 +91,7 @@ include(flutter/generated_plugins.cmake)
|
|||||||
# By default, "installing" just makes a relocatable bundle in the build
|
# By default, "installing" just makes a relocatable bundle in the build
|
||||||
# directory.
|
# directory.
|
||||||
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
|
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
|
||||||
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "Installation Directory" FORCE)
|
||||||
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# Start with a clean build bundle directory every time.
|
# Start with a clean build bundle directory every time.
|
||||||
install(CODE "
|
install(CODE "
|
||||||
|
|||||||
Reference in New Issue
Block a user