D1APP-31 message box (full audio support)
This commit is contained in:
parent
98e1e50891
commit
0ec05f1764
|
|
@ -82,8 +82,8 @@ class DarkThemeConfig {
|
||||||
colorScheme: _colorScheme,
|
colorScheme: _colorScheme,
|
||||||
fontFamily: 'Dana-FA',
|
fontFamily: 'Dana-FA',
|
||||||
textTheme: _textTheme,
|
textTheme: _textTheme,
|
||||||
iconTheme: const IconThemeData(
|
iconTheme: IconThemeData(
|
||||||
color: text,
|
color: _colorScheme.focusedBorder,
|
||||||
),
|
),
|
||||||
cardColor: _colorScheme.surface,
|
cardColor: _colorScheme.surface,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ class Assets {
|
||||||
static const String _baseImagesPath = _basePath + '/images';
|
static const String _baseImagesPath = _basePath + '/images';
|
||||||
static const String _baseThemesPath = _basePath + '/images/themes';
|
static const String _baseThemesPath = _basePath + '/images/themes';
|
||||||
static const String _baseAnimationsPath = _basePath + '/animations';
|
static const String _baseAnimationsPath = _basePath + '/animations';
|
||||||
static const String _baseRecordsPath = _basePath + '/records';
|
static const String _baseRecordsPath = _basePath + '/images/records';
|
||||||
|
|
||||||
static String get verticalLogoWithText =>
|
static String get verticalLogoWithText =>
|
||||||
_baseImagesPath + '/logos/logo-vertical-$_themeSuffix.svg';
|
_baseImagesPath + '/logos/logo-vertical-$_themeSuffix.svg';
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,7 @@
|
||||||
import 'package:just_audio/just_audio.dart';
|
import 'package:didvan/pages/home/chat/widgets/message_box.dart';
|
||||||
import 'package:record/record.dart';
|
|
||||||
import 'package:universal_html/js.dart' as js;
|
|
||||||
import 'package:didvan/config/theme_data.dart';
|
|
||||||
import 'package:didvan/constants/app_icons.dart';
|
|
||||||
import 'package:didvan/models/view/app_bar_data.dart';
|
import 'package:didvan/models/view/app_bar_data.dart';
|
||||||
import 'package:didvan/widgets/didvan/scaffold.dart';
|
import 'package:didvan/widgets/didvan/scaffold.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_vibrate/flutter_vibrate.dart';
|
|
||||||
|
|
||||||
class Chat extends StatelessWidget {
|
class Chat extends StatelessWidget {
|
||||||
const Chat({Key? key}) : super(key: key);
|
const Chat({Key? key}) : super(key: key);
|
||||||
|
|
@ -32,101 +26,13 @@ class Chat extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 0,
|
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||||
right: 0,
|
right: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
child: Container(
|
child: const MessageBox(),
|
||||||
height: 56,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border(
|
|
||||||
top: BorderSide(
|
|
||||||
color: Theme.of(context).colorScheme.cardBorder,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const _VoiceRecorderButton(),
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: InputBorder.none,
|
|
||||||
hintText: 'بنویسید یا پیام صوتی بگذارید...',
|
|
||||||
hintStyle: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.caption!
|
|
||||||
.copyWith(
|
|
||||||
color: Theme.of(context).colorScheme.disabledText,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onChanged: (value) {},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _VoiceRecorderButton extends StatefulWidget {
|
|
||||||
const _VoiceRecorderButton({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_VoiceRecorderButtonState createState() => _VoiceRecorderButtonState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _VoiceRecorderButtonState extends State<_VoiceRecorderButton> {
|
|
||||||
final _recorder = Record();
|
|
||||||
final _player = AudioPlayer();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
_recorder.hasPermission();
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return GestureDetector(
|
|
||||||
onLongPressStart: (details) async {
|
|
||||||
if (!kIsWeb) {
|
|
||||||
Vibrate.feedback(FeedbackType.medium);
|
|
||||||
}
|
|
||||||
await _recorder.start();
|
|
||||||
},
|
|
||||||
onLongPressEnd: (details) async {
|
|
||||||
final path = await _recorder.stop();
|
|
||||||
if (kIsWeb) {
|
|
||||||
await _player.setUrl(path!);
|
|
||||||
} else {
|
|
||||||
await _player.setFilePath(path!);
|
|
||||||
}
|
|
||||||
await _player.play();
|
|
||||||
if (kIsWeb) {
|
|
||||||
js.context.callMethod('playAudio', ['/dash.mp3']);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
color: Colors.transparent,
|
|
||||||
height: double.infinity,
|
|
||||||
width: 52,
|
|
||||||
child: Icon(
|
|
||||||
DidvanIcons.mic_solid,
|
|
||||||
color: Theme.of(context).colorScheme.focusedBorder,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_recorder.dispose();
|
|
||||||
_player.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,52 @@
|
||||||
import 'package:didvan/providers/core_provider.dart';
|
import 'dart:io';
|
||||||
|
|
||||||
class ChatState extends CoreProvier {}
|
import 'package:didvan/providers/core_provider.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter_vibrate/flutter_vibrate.dart';
|
||||||
|
import 'package:record/record.dart';
|
||||||
|
|
||||||
|
class ChatState extends CoreProvier {
|
||||||
|
final _recorder = Record();
|
||||||
|
|
||||||
|
File? recordedFile;
|
||||||
|
|
||||||
|
bool isRecording = false;
|
||||||
|
|
||||||
|
void deleteRecordedFile() {
|
||||||
|
recordedFile!.delete();
|
||||||
|
recordedFile = null;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> startRecording() async {
|
||||||
|
await _recorder.hasPermission();
|
||||||
|
if (!kIsWeb) {
|
||||||
|
Vibrate.feedback(FeedbackType.medium);
|
||||||
|
}
|
||||||
|
isRecording = true;
|
||||||
|
_recorder.start();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> stopRecording(bool sendImidiately) async {
|
||||||
|
final path = await _recorder.stop();
|
||||||
|
isRecording = false;
|
||||||
|
if (path == null) {
|
||||||
|
notifyListeners();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (kIsWeb) {
|
||||||
|
final uri = Uri.file(path);
|
||||||
|
recordedFile = File.fromUri(uri);
|
||||||
|
} else {
|
||||||
|
recordedFile = File(path);
|
||||||
|
}
|
||||||
|
if (sendImidiately) {
|
||||||
|
await sendMessage();
|
||||||
|
} else {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> sendMessage() async {}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
import 'package:didvan/config/theme_data.dart';
|
||||||
|
import 'package:didvan/constants/app_icons.dart';
|
||||||
|
import 'package:didvan/pages/home/chat/chat_state.dart';
|
||||||
|
import 'package:didvan/widgets/audio_visualizer.dart';
|
||||||
|
import 'package:didvan/widgets/didvan/icon_button.dart';
|
||||||
|
import 'package:didvan/widgets/didvan/text.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class MessageBox extends StatelessWidget {
|
||||||
|
const MessageBox({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
height: 56,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
top: BorderSide(
|
||||||
|
color: Theme.of(context).colorScheme.cardBorder,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
),
|
||||||
|
child: Consumer<ChatState>(
|
||||||
|
builder: (context, state, child) {
|
||||||
|
if (state.isRecording) {
|
||||||
|
return const _Recording();
|
||||||
|
} else if (!state.isRecording && state.recordedFile != null) {
|
||||||
|
return const _RecordChecking();
|
||||||
|
}
|
||||||
|
return const _Typing();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Typing extends StatelessWidget {
|
||||||
|
const _Typing({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final state = context.read<ChatState>();
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
DidvanIconButton(
|
||||||
|
icon: DidvanIcons.mic_solid,
|
||||||
|
onPressed: state.startRecording,
|
||||||
|
size: 32,
|
||||||
|
color: Theme.of(context).colorScheme.focusedBorder,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
textInputAction: TextInputAction.send,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: InputBorder.none,
|
||||||
|
hintText: 'بنویسید یا پیام صوتی بگذارید...',
|
||||||
|
hintStyle: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.caption!
|
||||||
|
.copyWith(color: Theme.of(context).colorScheme.disabledText),
|
||||||
|
),
|
||||||
|
onChanged: (value) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Recording extends StatelessWidget {
|
||||||
|
const _Recording({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final state = context.read<ChatState>();
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => state.stopRecording(true),
|
||||||
|
child: Container(
|
||||||
|
color: Colors.transparent,
|
||||||
|
height: double.infinity,
|
||||||
|
width: 52,
|
||||||
|
child: Icon(
|
||||||
|
DidvanIcons.send_solid,
|
||||||
|
color: Theme.of(context).colorScheme.focusedBorder,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: DidvanText(
|
||||||
|
'در حال ضبط صدا ...',
|
||||||
|
style: Theme.of(context).textTheme.caption,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DidvanIconButton(
|
||||||
|
icon: DidvanIcons.stop_circle_solid,
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
onPressed: () => state.stopRecording(false),
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RecordChecking extends StatelessWidget {
|
||||||
|
const _RecordChecking({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final state = context.read<ChatState>();
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
DidvanIconButton(
|
||||||
|
icon: DidvanIcons.send_solid,
|
||||||
|
onPressed: () => state.stopRecording(true),
|
||||||
|
color: Theme.of(context).colorScheme.focusedBorder,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: AudioVisualizer(
|
||||||
|
audioFile: state.recordedFile!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DidvanIconButton(
|
||||||
|
icon: DidvanIcons.trash_solid,
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
onPressed: state.deleteRecordedFile,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,9 +2,12 @@ import 'package:didvan/models/settings_data.dart';
|
||||||
import 'package:didvan/services/storage/storage.dart';
|
import 'package:didvan/services/storage/storage.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
class AppInitializer {
|
class AppInitializer {
|
||||||
static Future<void> setupServices() async {
|
static Future<void> setupServices() async {
|
||||||
|
StorageService.appDocsDir = (await getApplicationDocumentsDirectory()).path;
|
||||||
|
StorageService.appTempsDir = (await getTemporaryDirectory()).path;
|
||||||
await Hive.initFlutter();
|
await Hive.initFlutter();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,4 +5,21 @@ class DateTimeUtils {
|
||||||
hour: int.parse(input.split(':')[0]),
|
hour: int.parse(input.split(':')[0]),
|
||||||
minute: int.parse(input.split(':')[1]),
|
minute: int.parse(input.split(':')[1]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static String normalizeTimeDuration(Duration input) {
|
||||||
|
String minute;
|
||||||
|
String second;
|
||||||
|
if (input.inMinutes < 10) {
|
||||||
|
minute = '0${input.inMinutes}';
|
||||||
|
} else {
|
||||||
|
minute = input.inMinutes.toString();
|
||||||
|
}
|
||||||
|
int realSeconds = input.inSeconds % 60;
|
||||||
|
if (realSeconds < 10) {
|
||||||
|
second = '0$realSeconds';
|
||||||
|
} else {
|
||||||
|
second = (realSeconds).toString();
|
||||||
|
}
|
||||||
|
return '$minute:$second';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,314 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:didvan/config/design_config.dart';
|
||||||
|
import 'package:didvan/config/theme_data.dart';
|
||||||
|
import 'package:didvan/constants/app_icons.dart';
|
||||||
|
import 'package:didvan/constants/assets.dart';
|
||||||
|
import 'package:didvan/services/storage/storage.dart';
|
||||||
|
import 'package:didvan/utils/date_time.dart';
|
||||||
|
import 'package:didvan/widgets/didvan/icon_button.dart';
|
||||||
|
import 'package:didvan/widgets/didvan/text.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
import 'package:just_audio/just_audio.dart';
|
||||||
|
import 'package:just_waveform/just_waveform.dart';
|
||||||
|
|
||||||
|
class AudioVisualizer extends StatefulWidget {
|
||||||
|
final File audioFile;
|
||||||
|
|
||||||
|
const AudioVisualizer({
|
||||||
|
Key? key,
|
||||||
|
required this.audioFile,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AudioVisualizer> createState() => _AudioVisualizerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AudioVisualizerState extends State<AudioVisualizer> {
|
||||||
|
final AudioPlayer _audioPlayer = AudioPlayer();
|
||||||
|
|
||||||
|
Stream<WaveformProgress>? waveDataStream;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
if (!kIsWeb) {
|
||||||
|
waveDataStream = JustWaveform.extract(
|
||||||
|
audioInFile: widget.audioFile,
|
||||||
|
waveOutFile: File(StorageService.appTempsDir + '/rec-wave.wave'),
|
||||||
|
zoom: const WaveformZoom.pixelsPerSecond(100),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_setupAudioPlayer();
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).brightness == Brightness.dark
|
||||||
|
? Theme.of(context).colorScheme.black
|
||||||
|
: Theme.of(context).colorScheme.background,
|
||||||
|
borderRadius: DesignConfig.mediumBorderRadius,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
StreamBuilder<Duration>(
|
||||||
|
stream: _audioPlayer.positionStream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
String text = '';
|
||||||
|
if (_audioPlayer.duration == null) {
|
||||||
|
Future.delayed(Duration.zero, () {
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (snapshot.data == null || snapshot.data == Duration.zero) {
|
||||||
|
text = DateTimeUtils.normalizeTimeDuration(
|
||||||
|
_audioPlayer.duration ?? Duration.zero);
|
||||||
|
} else {
|
||||||
|
text = DateTimeUtils.normalizeTimeDuration(snapshot.data!);
|
||||||
|
}
|
||||||
|
return DidvanText(
|
||||||
|
text,
|
||||||
|
color: Theme.of(context).colorScheme.focusedBorder,
|
||||||
|
isEnglishFont: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
if (kIsWeb) {
|
||||||
|
return SvgPicture.asset(Assets.record);
|
||||||
|
}
|
||||||
|
return StreamBuilder<WaveformProgress>(
|
||||||
|
stream: waveDataStream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.data == null) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
if (snapshot.data!.waveform == null) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
final waveform = snapshot.data!.waveform!;
|
||||||
|
return GestureDetector(
|
||||||
|
onHorizontalDragUpdate: _changePosition,
|
||||||
|
onTapDown: _changePosition,
|
||||||
|
child: SizedBox(
|
||||||
|
height: double.infinity,
|
||||||
|
width: double.infinity,
|
||||||
|
child: _AudioWaveformWidget(
|
||||||
|
waveform: waveform,
|
||||||
|
audioPlayer: _audioPlayer,
|
||||||
|
start: Duration.zero,
|
||||||
|
scale: 2,
|
||||||
|
strokeWidth: 3,
|
||||||
|
duration: waveform.duration,
|
||||||
|
waveColor:
|
||||||
|
Theme.of(context).colorScheme.focusedBorder,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
StreamBuilder<bool>(
|
||||||
|
stream: _audioPlayer.playingStream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
return DidvanIconButton(
|
||||||
|
icon: snapshot.data == true
|
||||||
|
? DidvanIcons.pause_circle_solid
|
||||||
|
: DidvanIcons.play_circle_solid,
|
||||||
|
color: Theme.of(context).colorScheme.focusedBorder,
|
||||||
|
onPressed: _playAndPouse,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _changePosition(details) {
|
||||||
|
double posper =
|
||||||
|
details.localPosition.dx / (MediaQuery.of(context).size.width - 200);
|
||||||
|
if (posper >= 1 || posper < 0) return;
|
||||||
|
final position = _audioPlayer.duration!.inMilliseconds;
|
||||||
|
_audioPlayer.seek(
|
||||||
|
Duration(milliseconds: (posper * position).toInt()),
|
||||||
|
);
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _setupAudioPlayer() async {
|
||||||
|
if (kIsWeb) {
|
||||||
|
await _audioPlayer.setUrl(
|
||||||
|
widget.audioFile.uri.path.replaceAll('%3A', ':'),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await _audioPlayer.setFilePath(widget.audioFile.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _playAndPouse() async {
|
||||||
|
if (_audioPlayer.playing) {
|
||||||
|
_audioPlayer.pause();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _audioPlayer.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_audioPlayer.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AudioWaveformWidget extends StatefulWidget {
|
||||||
|
final Color waveColor;
|
||||||
|
final double scale;
|
||||||
|
final double strokeWidth;
|
||||||
|
final double pixelsPerStep;
|
||||||
|
final Waveform waveform;
|
||||||
|
final Duration start;
|
||||||
|
final Duration duration;
|
||||||
|
final AudioPlayer audioPlayer;
|
||||||
|
|
||||||
|
const _AudioWaveformWidget({
|
||||||
|
Key? key,
|
||||||
|
required this.waveform,
|
||||||
|
required this.start,
|
||||||
|
required this.duration,
|
||||||
|
required this.audioPlayer,
|
||||||
|
this.waveColor = Colors.blue,
|
||||||
|
this.scale = 1.0,
|
||||||
|
this.strokeWidth = 5.0,
|
||||||
|
this.pixelsPerStep = 8.0,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
__AudioWaveformWidgetState createState() => __AudioWaveformWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class __AudioWaveformWidgetState extends State<_AudioWaveformWidget>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
double progress = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
widget.audioPlayer.positionStream.listen((event) {
|
||||||
|
if (widget.audioPlayer.duration == null) return;
|
||||||
|
setState(() {
|
||||||
|
progress = event.inMilliseconds /
|
||||||
|
widget.audioPlayer.duration!.inMilliseconds *
|
||||||
|
100;
|
||||||
|
if (progress >= 100) {
|
||||||
|
progress = 0;
|
||||||
|
widget.audioPlayer.stop();
|
||||||
|
widget.audioPlayer.seek(Duration.zero);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ClipRect(
|
||||||
|
child: CustomPaint(
|
||||||
|
painter: _AudioWaveformPainter(
|
||||||
|
waveColor: widget.waveColor,
|
||||||
|
waveform: widget.waveform,
|
||||||
|
start: widget.start,
|
||||||
|
duration: widget.duration,
|
||||||
|
scale: widget.scale,
|
||||||
|
strokeWidth: widget.strokeWidth,
|
||||||
|
pixelsPerStep: widget.pixelsPerStep,
|
||||||
|
progressPercentage: progress,
|
||||||
|
progressColor: Theme.of(context).colorScheme.focusedBorder,
|
||||||
|
color: Theme.of(context).colorScheme.border,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AudioWaveformPainter extends CustomPainter {
|
||||||
|
final double scale;
|
||||||
|
final double strokeWidth;
|
||||||
|
final double pixelsPerStep;
|
||||||
|
final Waveform waveform;
|
||||||
|
final Duration start;
|
||||||
|
final Duration duration;
|
||||||
|
final double progressPercentage;
|
||||||
|
final Color progressColor;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
_AudioWaveformPainter({
|
||||||
|
required this.waveform,
|
||||||
|
required this.start,
|
||||||
|
required this.duration,
|
||||||
|
required this.progressPercentage,
|
||||||
|
required this.color,
|
||||||
|
required this.progressColor,
|
||||||
|
Color waveColor = Colors.blue,
|
||||||
|
this.scale = 1.0,
|
||||||
|
this.strokeWidth = 5.0,
|
||||||
|
this.pixelsPerStep = 8.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
if (duration == Duration.zero) return;
|
||||||
|
double width = size.width;
|
||||||
|
double height = size.height;
|
||||||
|
|
||||||
|
final waveformPixelsPerWindow = waveform.positionToPixel(duration).toInt();
|
||||||
|
final waveformPixelsPerDevicePixel = waveformPixelsPerWindow / width;
|
||||||
|
final waveformPixelsPerStep = waveformPixelsPerDevicePixel * pixelsPerStep;
|
||||||
|
final sampleOffset = waveform.positionToPixel(start);
|
||||||
|
final sampleStart = -sampleOffset % waveformPixelsPerStep;
|
||||||
|
final totalLength = waveformPixelsPerWindow;
|
||||||
|
final wavePaintB = Paint()
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = strokeWidth
|
||||||
|
..strokeCap = StrokeCap.round
|
||||||
|
..color = progressColor;
|
||||||
|
final wavePaintA = Paint()
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = strokeWidth
|
||||||
|
..strokeCap = StrokeCap.round
|
||||||
|
..color = color;
|
||||||
|
for (var i = sampleStart.toDouble();
|
||||||
|
i <= waveformPixelsPerWindow + 1.0;
|
||||||
|
i += waveformPixelsPerStep) {
|
||||||
|
final sampleIdx = (sampleOffset + i).toInt();
|
||||||
|
final x = i / waveformPixelsPerDevicePixel;
|
||||||
|
final minY = normalise(waveform.getPixelMin(sampleIdx), height);
|
||||||
|
final maxY = normalise(waveform.getPixelMax(sampleIdx), height);
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(x + strokeWidth / 2, max(strokeWidth * 0.75, minY)),
|
||||||
|
Offset(x + strokeWidth / 2, min(height - strokeWidth * 0.75, maxY)),
|
||||||
|
i / totalLength < progressPercentage / 100 ? wavePaintB : wavePaintA,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant _AudioWaveformPainter oldDelegate) {
|
||||||
|
return oldDelegate.progressPercentage != progressPercentage;
|
||||||
|
}
|
||||||
|
|
||||||
|
double normalise(int s, double height) {
|
||||||
|
final y = 32768 + (scale * s).clamp(-32768.0, 32767.0).toDouble();
|
||||||
|
return height - 1 - y * height / 65536;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
import 'package:didvan/widgets/ink_wrapper.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class DidvanIconButton extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final Color? color;
|
||||||
|
final double? size;
|
||||||
|
final double? gestureSize;
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
const DidvanIconButton({
|
||||||
|
Key? key,
|
||||||
|
required this.icon,
|
||||||
|
required this.onPressed,
|
||||||
|
this.color,
|
||||||
|
this.size,
|
||||||
|
this.gestureSize,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return InkWrapper(
|
||||||
|
onPressed: onPressed,
|
||||||
|
borderRadius: BorderRadius.circular(200),
|
||||||
|
child: SizedBox(
|
||||||
|
height: gestureSize ?? 48,
|
||||||
|
width: gestureSize ?? 48,
|
||||||
|
child: Icon(
|
||||||
|
icon,
|
||||||
|
size: size,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -301,6 +301,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.2"
|
version: "0.4.2"
|
||||||
|
just_waveform:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: just_waveform
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.1"
|
||||||
lints:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ dependencies:
|
||||||
record: ^3.0.2
|
record: ^3.0.2
|
||||||
just_audio: ^0.9.18
|
just_audio: ^0.9.18
|
||||||
record_web: ^0.2.1
|
record_web: ^0.2.1
|
||||||
|
just_waveform: ^0.0.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue