diff --git a/obs_flutter/obs/obs_demo/README.md b/obs_flutter/obs/obs_demo/README.md index 8cfa551..8041cdc 100644 --- a/obs_flutter/obs/obs_demo/README.md +++ b/obs_flutter/obs/obs_demo/README.md @@ -14,3 +14,17 @@ A few resources to get you started if this is your first Flutter project: For help getting started with Flutter development, view the [online documentation](https://docs.flutter.dev/), which offers tutorials, samples, guidance on mobile development, and a full API reference. + +- Clone the repo from the github into your system +- Packages used + - http: ^0.13.3 + - path_provider: ^2.0.2 + - audioplayers: ^4.1.0 + - record: ^4.4.4 + - permission_handler: ^11.3.1 + - audio_waveforms: ^1.0.5 +- how to add the package + - flutter pub add package_name +- If already have android studio than through vs code you can ctr+hipt+p +- Launch emulater than select the emmulater +- Than run the cammond `flutter run` diff --git a/obs_flutter/obs/obs_demo/android/app/build.gradle b/obs_flutter/obs/obs_demo/android/app/build.gradle index c230f7c..2117066 100644 --- a/obs_flutter/obs/obs_demo/android/app/build.gradle +++ b/obs_flutter/obs/obs_demo/android/app/build.gradle @@ -38,11 +38,17 @@ android { applicationId = "com.example.obs_demo" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdk = flutter.minSdkVersion - targetSdk = flutter.targetSdkVersion + minSdk =24 + targetSdk = 31 versionCode = flutterVersionCode.toInteger() versionName = flutterVersionName } + configurations.all { + resolutionStrategy { + force "org.jetbrains.kotlin:kotlin-stdlib:1.8.20" + force "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.20" + } + } buildTypes { release { diff --git a/obs_flutter/obs/obs_demo/android/app/src/main/AndroidManifest.xml b/obs_flutter/obs/obs_demo/android/app/src/main/AndroidManifest.xml index ceb8ef8..116442b 100644 --- a/obs_flutter/obs/obs_demo/android/app/src/main/AndroidManifest.xml +++ b/obs_flutter/obs/obs_demo/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,9 @@ + + + + + { final jsonData = await file.readAsString(); final data = jsonDecode(jsonData) as Map; _controller.text = data['story'][0]['text']; - data['story'][0]['isEmpty'] = false; return data; } on FileSystemException { return {}; @@ -315,7 +314,6 @@ class _EditorTextLayoutState extends State { void saveData(String value) async { story['story'][paraIndex]['text'] = value; - story['story'][paraIndex]['isEmpty'] = value.isEmpty; writeJsonToFile(story); widget.onUpdateTextAvailability(value.isNotEmpty); print('Data saved: $value'); diff --git a/obs_flutter/obs/obs_demo/lib/main.dart b/obs_flutter/obs/obs_demo/lib/main.dart index 8c5ac97..0b4f168 100644 --- a/obs_flutter/obs/obs_demo/lib/main.dart +++ b/obs_flutter/obs/obs_demo/lib/main.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; import 'package:obs_demo/CreateUserPage.dart'; -import 'package:obs_demo/screen/bottomNavi.dart'; -import 'package:obs_demo/screen/dashboard.dart'; void main() { runApp(const MyApp()); diff --git a/obs_flutter/obs/obs_demo/lib/screen/audio_record_context.dart b/obs_flutter/obs/obs_demo/lib/screen/audio_record_context.dart new file mode 100644 index 0000000..72af963 --- /dev/null +++ b/obs_flutter/obs/obs_demo/lib/screen/audio_record_context.dart @@ -0,0 +1,521 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:audio_waveforms/audio_waveforms.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:obs_demo/utils/chat_bubble.dart'; + +class AudioRecordContext extends StatefulWidget { + const AudioRecordContext({super.key, required this.rowIndex}); + final int rowIndex; + + @override + _AudioRecordContextState createState() => _AudioRecordContextState(); +} + +class _AudioRecordContextState extends State { + //global variables + late final RecorderController recorderController; + late final PlayerController playerController; + String? path; + bool isRecording = false; + bool isPaused = false; + bool isRecordingCompleted = false; + bool isLoading = true; + bool isPlaying = false; + late Directory appDirectory; + String _textFieldValue = ""; + + List> storyDatas = []; + Map story = {}; + FocusNode _focusNode = FocusNode(); + late int storyIndex; + int paraIndex = 0; + final TextEditingController _controller = TextEditingController(); +// this function for fetching the story data from the json + Future fetchStoryText() async { + final jsonString = await rootBundle.loadString('assets/OBSTextData.json'); + setState(() { + storyDatas = json.decode(jsonString).cast>(); + storyIndex = widget.rowIndex; + }); + } + + @override + void initState() { + _getDir(); + fetchStoryText(); + fetchJson(); + _initialiseControllers(); + super.initState(); + } + +//initiaize the directory + void _getDir() async { + appDirectory = await getApplicationDocumentsDirectory(); + String appDocPath = appDirectory.path; + path = '$appDocPath/${storyIndex}.${paraIndex}.wav'; + isLoading = false; + setState(() {}); + } + +//initiaize controllerss value + void _initialiseControllers() { + recorderController = RecorderController() + ..androidEncoder = AndroidEncoder.aac + ..androidOutputFormat = AndroidOutputFormat.mpeg4 + ..iosEncoder = IosEncoder.kAudioFormatMPEG4AAC + ..sampleRate = 24; + recorderController.bitRate = 48000; + + playerController = PlayerController(); + } + +// for deleting the recording from the device path and also from the json + Future deleteRecording(filepath) async { + final file = File(filepath!); + try { + if (await file.exists()) { + await file.delete(); + story['story'][paraIndex].remove('audio'); + writeJsonToFile(story); + + print('Recording deleted'); + print(path!); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Recording deleted successfully')), + ); + setState(() { + isRecordingCompleted = false; + }); + } else { + print('File does not exist'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('File does not exist')), + ); + } + } catch (e) { + print('Error deleting file: $e'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error deleting file')), + ); + } + } + +//fetching the json data also writing the data + Future fetchJson() async { + Map data = await readJsonToFile(); + if (data.isEmpty) { + final obsJson = await rootBundle.loadString('assets/OBSData.json'); + var obsData = json.decode(obsJson).cast>(); + writeJsonToFile(obsData[storyIndex]); + setState(() { + story = obsData[storyIndex]; + }); + } else { + setState(() { + story = data; + }); + } + } + +//reading the json file + Future> readJsonToFile() async { + final directory = await getApplicationDocumentsDirectory(); + final file = File('${directory.path}/${storyIndex}.json'); + // Replace with your desired filename + try { + final jsonData = await file.readAsString(); + final data = jsonDecode(jsonData) as Map; + _controller.text = data['story'][0]['text']; + return data; + } on FileSystemException { + return {}; + } catch (e) { + print("Error reading JSON file: $e"); + rethrow; + } + } + +//writting into the json file + Future writeJsonToFile(Map data) async { + final directory = await getApplicationDocumentsDirectory(); + final file = File('${directory.path}/${storyIndex}.json'); + final jsonData = jsonEncode(data); + await file.writeAsString(jsonData); + } + + @override + void dispose() { + recorderController.dispose(); + playerController.dispose(); + super.dispose(); + } + +//playing the audio through the path + void _playAudio(audioPath) { + // Ensure to provide the correct path to your audio file + playerController.preparePlayer(path: audioPath); + + setState(() { + isPlaying = true; + }); + playerController.startPlayer( + finishMode: FinishMode.pause, + ); + setState(() { + isPlaying = false; + }); + } + + @override + Widget build(BuildContext context) { + String text = + storyDatas[storyIndex]['story'][paraIndex]['url'].split('/').last; + return Container( + child: Column( + children: [ + Container( + width: double.infinity, + height: 200, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/images/$text'), + fit: BoxFit.cover, + ), + ), + child: Container( + width: double.infinity, + height: 200, + padding: const EdgeInsets.all(8), + color: const Color(0xF0FDFDFF).withOpacity(0.9), + child: Column( + children: [ + Text( + storyDatas[storyIndex]['story'][paraIndex]['text'], + style: const TextStyle( + fontSize: 14.5, + fontWeight: FontWeight.bold, + ), + ), + isPlaying + ? IconButton( + onPressed: () => {}, + // _playAudio(story['story'][paraIndex]['audio']), + icon: const Icon( + Icons.stop, + color: Colors.black, + ), + ) + : IconButton( + onPressed: () => + _playAudio(story['story'][paraIndex]['audio']), + icon: const Icon( + Icons.play_arrow, + color: Colors.black, + ), + ), + ], + )), + ), + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + storyIndex != 0 + ? IconButton( + icon: const Icon(Icons.skip_previous), + iconSize: 35, + onPressed: () { + setState(() { + storyIndex = + storyIndex > 0 ? storyIndex - 1 : 0; + paraIndex = 0; + }); + _getDir(); + fetchJson(); + }, + ) + : IconButton( + icon: Icon(Icons.skip_previous), + iconSize: 35, + color: Color.fromARGB(66, 168, 163, 163) + .withOpacity(0.5), + onPressed: () {}, + ), + paraIndex != 0 + ? IconButton( + icon: Icon(Icons.arrow_left_sharp), + iconSize: 35, + onPressed: () { + setState(() { + paraIndex = paraIndex > 0 ? paraIndex - 1 : 0; + }); + _getDir(); + _controller.text = + story['story'][paraIndex]['text']; + }, + ) + : IconButton( + icon: Icon(Icons.arrow_left_sharp), + iconSize: 35, + color: Color.fromARGB(66, 168, 163, 163) + .withOpacity(0.5), + onPressed: () {}, + ), + Text(storyDatas[storyIndex]['storyId'].toString()), + const Text(":"), + Text(storyDatas[storyIndex]['story'][paraIndex]['id'] + .toString()), + paraIndex != storyDatas[storyIndex]['story'].length - 1 + ? IconButton( + icon: Icon(Icons.arrow_right_sharp), + iconSize: 35, + onPressed: () { + setState(() { + paraIndex = paraIndex + 1; + }); + _getDir(); + _controller.text = + story?['story']?[paraIndex]['text']; + }, + ) + : IconButton( + icon: Icon(Icons.arrow_right_sharp), + iconSize: 35, + color: Colors.black26.withOpacity(0.5), + onPressed: () {}, + ), + storyIndex != storyDatas.length - 1 + ? IconButton( + icon: Icon(Icons.skip_next), + iconSize: 35, + onPressed: () { + setState(() { + storyIndex = storyIndex + 1; + paraIndex = 0; + }); + _getDir(); + fetchJson(); + }, + ) + : IconButton( + icon: Icon(Icons.skip_next), + iconSize: 35, + color: Color.fromARGB(66, 168, 163, 163) + .withOpacity(0.5), + onPressed: () {}, + ), + ], + ), + Padding( + padding: const EdgeInsets.all(4.0), + child: SizedBox( + height: 200, + child: Container( + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: + Colors.grey.withOpacity(0.5), // Shadow color + spreadRadius: 1, + blurRadius: 7, + offset: + Offset(0, 3), // Changes position of shadow + ), + ], + color: Colors + .white, // Background color for the text field + borderRadius: + BorderRadius.circular(5), // Rounded corners + ), + child: Theme( + data: Theme.of(context).copyWith( + textSelectionTheme: TextSelectionThemeData( + selectionColor: + Colors.grey, // Color of the selected text + cursorColor: Colors + .grey, // Color of the caret (text cursor) + selectionHandleColor: + Colors.grey, // Color of the selection handles + ), + ), + child: TextField( + controller: _controller, + focusNode: _focusNode, + onChanged: (value) { + setState(() { + _textFieldValue = value; + }); + }, + enabled: false, + decoration: InputDecoration( + labelText: (_focusNode.hasFocus || + _textFieldValue.isNotEmpty) + ? null + : 'Start translating story', + labelStyle: TextStyle( + color: Colors + .grey), // Optional: changes label color to grey + + floatingLabelBehavior: + FloatingLabelBehavior.always, + border: InputBorder + .none, // Removes the default border + contentPadding: EdgeInsets.symmetric( + vertical: 10.0, + horizontal: 10.0), // Adjust padding as needed + ), + maxLines: + 30, // Increases the height to accommodate up to 30 lines + ), + ), + ), + ), + ), + //for audio waves + if (story['story'][paraIndex]['audio'] != null) + WaveBubble( + path: story['story'][paraIndex]['audio'], + isSender: true, + appDirectory: appDirectory, + deleteRecording: deleteRecording), + if (!isRecording && + story['story'][paraIndex]['audio'] == null) + WaveBubble( + path: '', + isSender: false, + appDirectory: appDirectory, + deleteRecording: deleteRecording), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: isRecording + ? AudioWaveforms( + enableGesture: true, + size: Size( + MediaQuery.of(context).size.width / 2, + 50, + ), + recorderController: recorderController, + waveStyle: const WaveStyle( + waveColor: Colors.white, + extendWaveform: true, + showMiddleLine: false, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.0), + color: const Color(0xFF1E1B26), + ), + ) + : const Text(""), + ), + //start and stop recording + if (story['story'][paraIndex]['audio'] == null) + Center( + child: IconButton( + onPressed: () => + _startOrStopRecording(storyIndex, paraIndex), + icon: Icon(isRecording ? Icons.stop : Icons.mic), + color: Colors.black, + iconSize: 28, + ), + ), + //pause reording + if (isRecording && !isPaused) + IconButton( + onPressed: _pauseRecording, + icon: const Icon( + Icons.pause, + color: Colors.black, + ), + ), + //resume recording + if (isPaused) + IconButton( + onPressed: _resumeRecording, + icon: const Icon( + Icons.play_arrow, + color: Colors.black, + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ); + } + +// this function work for start and stop recording + void _startOrStopRecording(storyNumber, paraNumber) async { + isLoading = false; + try { + if (isRecording) { + print(path); + path = await recorderController.stop(); + setState(() { + isPaused = false; + }); + recorderController.reset(); + + if (path != "") { + isRecordingCompleted = true; + debugPrint(path); + } + story['story'][paraNumber]['audio'] = path; + writeJsonToFile(story); + } else { + await recorderController.record(path: path); // Path is optional + setState(() { + isRecordingCompleted = false; + }); + } + } catch (e) { + debugPrint(e.toString()); + } finally { + setState(() { + isRecording = !isRecording; + }); + } + } + +// this function work for paue recording + void _pauseRecording() async { + try { + await recorderController.pause(); + setState(() { + isPaused = true; + }); + } catch (e) { + debugPrint(e.toString()); + } + } +// this function work for resume recording + + void _resumeRecording() async { + try { + await recorderController.record(); + setState(() { + isPaused = false; + }); + } catch (e) { + debugPrint(e.toString()); + } + } + +//refresh the waves + void _refreshWave() { + if (isRecording) recorderController.refresh(); + } +} diff --git a/obs_flutter/obs/obs_demo/lib/screen/audio_recorder.dart b/obs_flutter/obs/obs_demo/lib/screen/audio_recorder.dart new file mode 100644 index 0000000..5d9c155 --- /dev/null +++ b/obs_flutter/obs/obs_demo/lib/screen/audio_recorder.dart @@ -0,0 +1,284 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:audioplayers/audioplayers.dart'; +import 'package:record/record.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class AudioRecorder extends StatefulWidget { + @override + _AudioRecorderState createState() => _AudioRecorderState(); +} + +class _AudioRecorderState extends State { + late Record audioRecord; + late AudioPlayer audioPlayer; + String _audioFilePath = ''; + bool _isRecording = false; + bool _hasRecording = false; + Duration elapsedTime = const Duration(seconds: 0); + Duration maxDuration = const Duration(seconds: 150); + + @override + void initState() { + audioRecord = Record(); + audioPlayer = AudioPlayer(); + _requestPermissions(); + + super.initState(); + } + + void dispose() { + audioRecord.dispose(); + audioPlayer.dispose(); + super.dispose(); + } + + Future _requestPermissions() async { + await Permission.microphone.request(); + await Permission.storage.request(); + } + + Future startRecording() async { + try { + if (await audioRecord.hasPermission()) { + await audioRecord.start(samplingRate: 21, bitRate: 48000); + setState(() { + _isRecording = true; + _hasRecording = false; + }); + } + } catch (e) { + print(e); + } + } + + Future stopRecording() async { + try { + String? path = await audioRecord.stop(); + setState(() { + _isRecording = false; + _audioFilePath = path!; + _hasRecording = true; + }); + } catch (e) { + print(e); + } + } + + Future playRecording() async { + try { + Source urlSource = UrlSource(_audioFilePath); + await audioPlayer.play(urlSource); + } catch (e) { + print(e); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Modern Audio Recorder'), + elevation: 0, + ), + body: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + _isRecording ? 'Recording...' : 'Press the button to record', + style: TextStyle(fontSize: 20), + ), + SizedBox(height: 10), + ElevatedButton( + onPressed: _isRecording ? stopRecording : startRecording, + child: Text(_isRecording ? 'Stop Recording' : 'Start Recording'), + ), + SizedBox(height: 20), + if (_hasRecording) + ElevatedButton( + onPressed: playRecording, + child: Text('Play Recording'), + ), + PlayerWidget(player: audioPlayer) + ], + ), + ), + ); + } +} + +class PlayerWidget extends StatefulWidget { + final AudioPlayer player; + + const PlayerWidget({ + required this.player, + super.key, + }); + + @override + State createState() { + return _PlayerWidgetState(); + } +} + +class _PlayerWidgetState extends State { + PlayerState? _playerState; + Duration? _duration; + Duration? _position; + + StreamSubscription? _durationSubscription; + StreamSubscription? _positionSubscription; + StreamSubscription? _playerCompleteSubscription; + StreamSubscription? _playerStateChangeSubscription; + + bool get _isPlaying => _playerState == PlayerState.playing; + + bool get _isPaused => _playerState == PlayerState.paused; + + String get _durationText => _duration?.toString().split('.').first ?? ''; + + String get _positionText => _position?.toString().split('.').first ?? ''; + + AudioPlayer get player => widget.player; + + @override + void initState() { + super.initState(); + // Use initial values from player + _playerState = player.state; + player.getDuration().then( + (value) => setState(() { + _duration = value; + }), + ); + player.getCurrentPosition().then( + (value) => setState(() { + _position = value; + }), + ); + _initStreams(); + } + + @override + void setState(VoidCallback fn) { + // Subscriptions only can be closed asynchronously, + // therefore events can occur after widget has been disposed. + if (mounted) { + super.setState(fn); + } + } + + @override + void dispose() { + _durationSubscription?.cancel(); + _positionSubscription?.cancel(); + _playerCompleteSubscription?.cancel(); + _playerStateChangeSubscription?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final color = Theme.of(context).primaryColor; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + key: const Key('play_button'), + onPressed: _isPlaying ? null : _play, + iconSize: 48.0, + icon: const Icon(Icons.play_arrow), + color: color, + ), + IconButton( + key: const Key('pause_button'), + onPressed: _isPlaying ? _pause : null, + iconSize: 48.0, + icon: const Icon(Icons.pause), + color: color, + ), + IconButton( + key: const Key('stop_button'), + onPressed: _isPlaying || _isPaused ? _stop : null, + iconSize: 48.0, + icon: const Icon(Icons.stop), + color: color, + ), + ], + ), + Slider( + onChanged: (value) { + final duration = _duration; + if (duration == null) { + return; + } + final position = value * duration.inMilliseconds; + player.seek(Duration(milliseconds: position.round())); + }, + value: (_position != null && + _duration != null && + _position!.inMilliseconds > 0 && + _position!.inMilliseconds < _duration!.inMilliseconds) + ? _position!.inMilliseconds / _duration!.inMilliseconds + : 0.0, + ), + Text( + _position != null + ? '$_positionText / $_durationText' + : _duration != null + ? _durationText + : '', + style: const TextStyle(fontSize: 16.0), + ), + ], + ); + } + + void _initStreams() { + _durationSubscription = player.onDurationChanged.listen((duration) { + setState(() => _duration = duration); + }); + + _positionSubscription = player.onPositionChanged.listen( + (p) => setState(() => _position = p), + ); + + _playerCompleteSubscription = player.onPlayerComplete.listen((event) { + setState(() { + _playerState = PlayerState.stopped; + _position = Duration.zero; + }); + }); + + _playerStateChangeSubscription = + player.onPlayerStateChanged.listen((state) { + setState(() { + _playerState = state; + }); + }); + } + + Future _play() async { + await player.resume(); + setState(() => _playerState = PlayerState.playing); + } + + Future _pause() async { + await player.pause(); + setState(() => _playerState = PlayerState.paused); + } + + Future _stop() async { + await player.stop(); + setState(() { + _playerState = PlayerState.stopped; + _position = Duration.zero; + }); + } +} diff --git a/obs_flutter/obs/obs_demo/lib/screen/bottomNavi.dart b/obs_flutter/obs/obs_demo/lib/screen/bottomNavi.dart index 68d569f..44c07a9 100644 --- a/obs_flutter/obs/obs_demo/lib/screen/bottomNavi.dart +++ b/obs_flutter/obs/obs_demo/lib/screen/bottomNavi.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:obs_demo/editortext.dart'; +import 'package:obs_demo/screen/audio_record_context.dart'; import 'package:obs_demo/screen/dashboard.dart'; import 'package:obs_demo/user_profile.dart'; @@ -64,6 +65,8 @@ class _BottomNavigationBarExampleState // 'Audio', // style: optionStyle, // ), + //call the audio recorder here + AudioRecordContext(rowIndex: 0), Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -130,10 +133,11 @@ class _BottomNavigationBarExampleState icon: Icon(Icons.create), label: '', ), - // BottomNavigationBarItem( - // icon: Icon(Icons.audiotrack_outlined), - // label: 'Audio', - // ), + //added the icon for audio recorder + BottomNavigationBarItem( + icon: Icon(Icons.volume_up), + label: '', + ), BottomNavigationBarItem( icon: Icon(Icons.menu), label: '', diff --git a/obs_flutter/obs/obs_demo/lib/screen/story_para.dart b/obs_flutter/obs/obs_demo/lib/screen/story_para.dart new file mode 100644 index 0000000..265a16f --- /dev/null +++ b/obs_flutter/obs/obs_demo/lib/screen/story_para.dart @@ -0,0 +1,149 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class StoryPara extends StatefulWidget { + const StoryPara({super.key, required this.rowIndex}); + final int rowIndex; + + @override + _StoryParaState createState() => _StoryParaState(); +} + +class _StoryParaState extends State { + List> storyDatas = []; + Map story = {}; + late int storyIndex; + int paraIndex = 0; + final TextEditingController _controller = TextEditingController(); + + Future fetchStoryText() async { + final jsonString = await rootBundle.loadString('assets/OBSTextData.json'); + setState(() { + storyDatas = json.decode(jsonString).cast>(); + storyIndex = widget.rowIndex; + }); + } + + @override + void initState() { + fetchStoryText(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + String text = + storyDatas[storyIndex]['story'][paraIndex]['url'].split('/').last; + return SingleChildScrollView( + child: Column( + children: [ + Container( + width: double.infinity, + height: 200, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/images/$text'), + fit: BoxFit.cover, + ), + ), + child: Container( + width: double.infinity, + height: 200, + padding: const EdgeInsets.all(8), + color: const Color(0xF0FDFDFF).withOpacity(0.9), + child: Text( + storyDatas[storyIndex]['story'][paraIndex]['text'], + style: const TextStyle( + fontSize: 14.5, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + storyIndex != 0 + ? IconButton( + icon: const Icon(Icons.skip_previous), + iconSize: 35, + onPressed: () { + setState(() { + storyIndex = storyIndex > 0 ? storyIndex - 1 : 0; + paraIndex = 0; + }); + }, + ) + : IconButton( + icon: Icon(Icons.skip_previous), + iconSize: 35, + color: Color.fromARGB(66, 168, 163, 163).withOpacity(0.5), + onPressed: () {}, + ), + paraIndex != 0 + ? IconButton( + icon: Icon(Icons.arrow_left_sharp), + iconSize: 35, + onPressed: () { + setState(() { + paraIndex = paraIndex > 0 ? paraIndex - 1 : 0; + }); + _controller.text = story['story'][paraIndex]['text']; + }, + ) + : IconButton( + icon: Icon(Icons.arrow_left_sharp), + iconSize: 35, + color: Color.fromARGB(66, 168, 163, 163).withOpacity(0.5), + onPressed: () {}, + ), + Text(storyDatas[storyIndex]['storyId'].toString()), + const Text(":"), + Text(storyDatas[storyIndex]['story'][paraIndex]['id'].toString()), + paraIndex != storyDatas[storyIndex]['story'].length - 1 + ? IconButton( + icon: Icon(Icons.arrow_right_sharp), + iconSize: 35, + onPressed: () { + setState(() { + paraIndex = paraIndex + 1; + }); + _controller.text = story?['story']?[paraIndex]['text']; + }, + ) + : IconButton( + icon: Icon(Icons.arrow_right_sharp), + iconSize: 35, + color: Colors.black26.withOpacity(0.5), + onPressed: () {}, + ), + storyIndex != storyDatas.length - 1 + ? IconButton( + icon: Icon(Icons.skip_next), + iconSize: 35, + onPressed: () { + setState(() { + storyIndex = storyIndex + 1; + paraIndex = 0; + }); + }, + ) + : IconButton( + icon: Icon(Icons.skip_next), + iconSize: 35, + color: Color.fromARGB(66, 168, 163, 163).withOpacity(0.5), + onPressed: () {}, + ), + ], + ), + // AudioRecordContext( + // story_number: storyIndex, + // para_number: paraIndex, + // ) + ], + ), + ); + } +} diff --git a/obs_flutter/obs/obs_demo/lib/utils/chat_bubble.dart b/obs_flutter/obs/obs_demo/lib/utils/chat_bubble.dart new file mode 100644 index 0000000..f99ee5c --- /dev/null +++ b/obs_flutter/obs/obs_demo/lib/utils/chat_bubble.dart @@ -0,0 +1,173 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:audio_waveforms/audio_waveforms.dart'; +import 'package:flutter/widgets.dart'; + +class WaveBubble extends StatefulWidget { + final bool isSender; + final int? index; + final String? path; + final double? width; + final Directory appDirectory; + final Function deleteRecording; + + const WaveBubble( + {Key? key, + required this.appDirectory, + this.width, + this.index, + this.isSender = false, + this.path, + required this.deleteRecording}) + : super(key: key); + + @override + State createState() => _WaveBubbleState(); +} + +class _WaveBubbleState extends State { + File? file; + + late PlayerController controller; + late StreamSubscription playerStateSubscription; + + final playerWaveStyle = const PlayerWaveStyle( + fixedWaveColor: Colors.white54, + liveWaveColor: Colors.white, + spacing: 6, + ); + + @override + void initState() { + super.initState(); + controller = PlayerController(); + _preparePlayer(); + playerStateSubscription = controller.onPlayerStateChanged.listen((_) { + setState(() {}); + }); + } + + void _preparePlayer() async { + // Opening file from assets folder + if (widget.index != null) { + file = File('${widget.appDirectory.path}/audio${widget.index}.mp3'); + await file?.writeAsBytes( + (await rootBundle.load('assets/audios/audio${widget.index}.mp3')) + .buffer + .asUint8List()); + } + if (widget.index == null && widget.path == null && file?.path == null) { + return; + } + // Prepare player with extracting waveform if index is even. + controller.preparePlayer( + path: widget.path ?? file!.path, + shouldExtractWaveform: widget.index?.isEven ?? true, + ); + // Extracting waveform separately if index is odd. + if (widget.index?.isOdd ?? false) { + controller + .extractWaveformData( + path: widget.path ?? file!.path, + noOfSamples: + playerWaveStyle.getSamplesForWidth(widget.width ?? 200), + ) + .then((waveformData) => debugPrint(waveformData.toString())); + } + } + + @override + void dispose() { + playerStateSubscription.cancel(); + controller.dispose(); + super.dispose(); + } + + @override + void didUpdateWidget(covariant WaveBubble oldWidget) { + if (oldWidget.path != widget.path) { + // Path has changed, update controller + _preparePlayer(); + } + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + return widget.path != null || file?.path != null + ? Column( + children: [ + Align( + alignment: + widget.isSender ? Alignment.center : Alignment.centerLeft, + child: Container( + width: double.infinity, + padding: EdgeInsets.only( + bottom: 6, + right: widget.isSender ? 0 : 10, + top: 6, + ), + margin: + const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: widget.isSender + ? const Color(0xFF276bfd) + : const Color(0xFF343145), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + AudioFileWaveforms( + key: UniqueKey(), // Ensure a unique key is provided + size: Size(MediaQuery.of(context).size.width / 2, 70), + playerController: controller, + waveformType: widget.index?.isOdd ?? false + ? WaveformType.fitWidth + : WaveformType.long, + playerWaveStyle: playerWaveStyle, + ), + if (widget.isSender) const SizedBox(width: 10), + ], + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (widget.isSender) + if (!controller.playerState.isStopped) + IconButton( + onPressed: () async { + controller.playerState.isPlaying + ? await controller.pausePlayer() + : await controller.startPlayer( + finishMode: FinishMode.pause, + ); + }, + icon: Icon( + controller.playerState.isPlaying + ? Icons.stop + : Icons.play_arrow, + ), + color: Colors.black, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + ), + if (widget.path != null && widget.path != "") + IconButton( + onPressed: () => widget.deleteRecording(widget.path), + icon: const Icon( + Icons.delete, + color: Colors.black, + ), + ), + ], + ), + ], + ) + : const SizedBox.shrink(); + } +} diff --git a/obs_flutter/obs/obs_demo/pubspec.yaml b/obs_flutter/obs/obs_demo/pubspec.yaml index 58a9112..b5099e0 100644 --- a/obs_flutter/obs/obs_demo/pubspec.yaml +++ b/obs_flutter/obs/obs_demo/pubspec.yaml @@ -37,6 +37,12 @@ dependencies: http: ^0.13.3 path_provider: ^2.0.2 + audioplayers: ^4.1.0 + record: ^4.4.4 + permission_handler: ^11.3.1 + audio_waveforms: ^1.0.5 + file_picker: ^5.0.0 + flutter_ffmpeg: ^0.4.2 dev_dependencies: flutter_test: