Merge branch 'content-html' into 'master'

refactor: Improve handling of API content in material feature

See merge request profile-image/kedaireka/polinema-adapative-learning/mobile-adaptive-learning!2
This commit is contained in:
Naresh Pratista 2024-11-05 06:51:06 +00:00
commit 6e91ce7898
14 changed files with 442 additions and 61 deletions

View File

@ -1,2 +1 @@
const String baseUrl =
'https://3be6-2001-448a-50a0-58e3-1c9f-405a-b360-22ed.ngrok-free.app/';
const String baseUrl = 'https://c62a-139-255-101-170.ngrok-free.app/';

View File

@ -1,5 +1,4 @@
import 'package:english_learning/core/services/dio_client.dart';
import 'package:english_learning/core/services/constants.dart';
import 'package:english_learning/core/services/repositories/student_learning_repository.dart';
import 'package:english_learning/core/widgets/global_button.dart';
import 'package:english_learning/features/auth/provider/user_provider.dart';
@ -10,6 +9,7 @@ import 'package:english_learning/features/learning/modules/material/widgets/imag
import 'package:english_learning/features/learning/modules/material/widgets/video_player_widget.dart';
import 'package:flutter/material.dart';
import 'package:english_learning/core/utils/styles/theme.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:provider/provider.dart';
class MaterialScreen extends StatefulWidget {
@ -80,13 +80,8 @@ class _MaterialScreenState extends State<MaterialScreen>
final result =
await _repository.createStudentLearning(widget.levelId, token);
print('Student Learning created: ${result['message']}');
// Navigate to ExerciseScreen
_navigateToExercise(result['payload']['ID_STUDENT_LEARNING']);
} catch (e) {
// Show error message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e')),
);
@ -152,31 +147,33 @@ class _MaterialScreenState extends State<MaterialScreen>
child: Column(
children: [
const SizedBox(height: 22),
Text(
level.content,
style: AppTextStyles.blackTextStyle.copyWith(
fontSize: 14,
Html(
data: level.content,
style: {
'body': Style(
fontSize: FontSize(14),
fontWeight: FontWeight.w500,
),
},
extensions: [
ImageExtension(
builder: (context) => ImageWidget(
imageFileName: context.attributes['src'] ?? '',
),
const SizedBox(height: 24),
if (level.image != null)
ImageWidget(
imageFileName: level.image!,
baseUrl: '${baseUrl}uploads/level/image/',
),
const SizedBox(height: 24),
if (level.audio != null)
AudioPlayerWidget(
AudioExtension(
builder: (context) => AudioPlayerWidget(
key: _audioPlayerKey,
audioFileName: level.audio!,
baseUrl: '${baseUrl}uploads/level/audio/',
audioFileName: context.attributes['src'] ?? '',
),
const SizedBox(height: 24),
if (level.video != null)
VideoPlayerWidget(
),
VideoExtension(
builder: (context) => VideoPlayerWidget(
key: _videoPlayerKey,
videoUrl: level.video!,
videoUrl: context.attributes['src'] ?? '',
),
),
],
),
const SizedBox(height: 32),
if (!widget.isReview)
@ -209,3 +206,49 @@ class _MaterialScreenState extends State<MaterialScreen>
});
}
}
class ImageExtension extends HtmlExtension {
final Widget Function(ExtensionContext) builder;
ImageExtension({required this.builder});
@override
Set<String> get supportedTags => {'img'};
@override
InlineSpan build(ExtensionContext context) {
context.attributes.remove('width');
context.attributes.remove('height');
return WidgetSpan(child: builder(context));
}
}
class AudioExtension extends HtmlExtension {
final Widget Function(ExtensionContext) builder;
AudioExtension({required this.builder});
@override
Set<String> get supportedTags => {'audio'};
@override
InlineSpan build(ExtensionContext context) =>
WidgetSpan(child: builder(context));
}
class VideoExtension extends HtmlExtension {
final Widget Function(ExtensionContext) builder;
VideoExtension({required this.builder});
@override
Set<String> get supportedTags => {'iframe'};
@override
InlineSpan build(ExtensionContext context) {
context.attributes.remove('width');
context.attributes.remove('height');
return WidgetSpan(child: builder(context));
}
}

View File

@ -2,13 +2,13 @@ import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart';
class AudioPlayerWidget extends StatefulWidget {
final String? audioFileName;
final String baseUrl;
final String audioFileName;
final String? baseUrl;
const AudioPlayerWidget({
super.key,
required this.audioFileName,
required this.baseUrl,
this.baseUrl,
});
@override
@ -34,8 +34,10 @@ class AudioPlayerWidgetState extends State<AudioPlayerWidget> {
}
void _setupAudioPlayer() {
if (widget.audioFileName != null && widget.audioFileName!.isNotEmpty) {
String fullAudioUrl = '${widget.baseUrl}${widget.audioFileName}';
if (widget.audioFileName.isNotEmpty) {
String fullAudioUrl = widget.audioFileName.startsWith('http')
? widget.audioFileName
: '${widget.baseUrl ?? ''}${widget.audioFileName}';
_audioPlayer.setSource(UrlSource(fullAudioUrl)).then((_) {
setState(() {

View File

@ -3,17 +3,19 @@ import 'package:flutter/material.dart';
class ImageWidget extends StatelessWidget {
final String imageFileName;
final String baseUrl;
final String? baseUrl;
const ImageWidget({
super.key,
required this.imageFileName,
required this.baseUrl,
this.baseUrl,
});
@override
Widget build(BuildContext context) {
String fullImageUrl = '$baseUrl$imageFileName';
String fullImageUrl = imageFileName.startsWith('http')
? imageFileName
: '${baseUrl ?? ''}$imageFileName';
return Container(
width: double.infinity,

View File

@ -132,18 +132,11 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
baseUrl: '$baseUrl/uploads/avatar/',
onImageSelected: _pickImage,
selectedImage: userProvider.selectedImage,
showCameraIcon: true,
),
),
],
),
const SizedBox(height: 8),
Text(
'Change Avatar',
style: AppTextStyles.blueTextStyle.copyWith(
fontSize: 18,
fontWeight: FontWeight.w900,
),
),
const SizedBox(height: 39),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),

View File

@ -1,5 +1,4 @@
import 'dart:io';
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/material.dart';
import 'package:english_learning/core/utils/styles/theme.dart';
@ -10,6 +9,7 @@ class UserAvatar extends StatelessWidget {
final String baseUrl;
final Function()? onImageSelected;
final File? selectedImage;
final bool showCameraIcon;
const UserAvatar({
super.key,
@ -18,17 +18,87 @@ class UserAvatar extends StatelessWidget {
required this.baseUrl,
this.onImageSelected,
this.selectedImage,
this.showCameraIcon = false,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onImageSelected,
return Stack(
children: [
// Avatar with detail view
GestureDetector(
onTap: () {
showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
child: Container(
width: double.infinity,
height: 400,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: ClipRRect(
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12),
),
child: _getDetailAvatarContent(),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: TextButton(
style: TextButton.styleFrom(
foregroundColor: AppColors.primaryColor,
),
onPressed: () => Navigator.pop(context),
child: const Text('Close'),
),
),
],
),
),
);
},
);
},
child: CircleAvatar(
radius: radius,
backgroundColor: AppColors.primaryColor,
child: _getAvatarContent(),
),
),
// Camera icon
if (showCameraIcon)
Positioned(
right: 0,
bottom: 0,
child: GestureDetector(
onTap: onImageSelected,
child: Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: AppColors.primaryColor,
shape: BoxShape.circle,
border: Border.all(
color: Colors.white,
width: 2,
),
),
child: Icon(
BootstrapIcons.camera,
color: AppColors.whiteColor,
size: radius * 0.3,
),
),
),
),
],
);
}
@ -60,6 +130,37 @@ class UserAvatar extends StatelessWidget {
}
}
Widget _getDetailAvatarContent() {
if (selectedImage != null) {
return Image.file(
selectedImage!,
fit: BoxFit.cover,
);
} else if (pictureUrl != null && pictureUrl!.isNotEmpty) {
return Image.network(
'$baseUrl$pictureUrl',
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Center(
child: Icon(
BootstrapIcons.person,
color: AppColors.primaryColor,
size: 120,
),
);
},
);
} else {
return Center(
child: Icon(
BootstrapIcons.person,
color: AppColors.primaryColor,
size: 120,
),
);
}
}
Widget _buildDefaultIcon() {
return Icon(
BootstrapIcons.person,

View File

@ -9,6 +9,7 @@
#include <audioplayers_linux/audioplayers_linux_plugin.h>
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
@ -20,4 +21,7 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
}

View File

@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_linux
file_selector_linux
flutter_secure_storage_linux
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@ -5,26 +5,32 @@
import FlutterMacOS
import Foundation
import audio_session
import audioplayers_darwin
import file_selector_macos
import flutter_inappwebview_macos
import flutter_secure_storage_macos
import just_audio
import package_info_plus
import path_provider_foundation
import shared_preferences_foundation
import sqflite
import url_launcher_macos
import video_player_avfoundation
import wakelock_plus
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
}

View File

@ -17,6 +17,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.11.0"
audio_session:
dependency: transitive
description:
name: audio_session
sha256: "343e83bc7809fbda2591a49e525d6b63213ade10c76f15813be9aed6657b3261"
url: "https://pub.dev"
source: hosted
version: "0.1.21"
audioplayers:
dependency: "direct main"
description:
@ -137,6 +145,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.1"
chewie:
dependency: transitive
description:
name: chewie
sha256: "335df378c025588aef400c704bd71f0daea479d4cd57c471c88c056c1144e7cd"
url: "https://pub.dev"
source: hosted
version: "1.8.5"
clock:
dependency: transitive
description:
@ -173,10 +189,10 @@ packages:
dependency: transitive
description:
name: csslib
sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb"
sha256: "831883fb353c8bdc1d71979e5b342c7d88acfbc643113c14ae51e2442ea0f20f"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
version: "0.17.3"
cupertino_icons:
dependency: "direct main"
description:
@ -294,8 +310,16 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.4.1"
flutter_html:
dependency: "direct main"
description:
name: flutter_html
sha256: "02ad69e813ecfc0728a455e4bf892b9379983e050722b1dce00192ee2e41d1ee"
url: "https://pub.dev"
source: hosted
version: "3.0.0-beta.2"
flutter_inappwebview:
dependency: transitive
dependency: "direct main"
description:
name: flutter_inappwebview
sha256: "3e9a443a18ecef966fb930c3a76ca5ab6a7aafc0c7b5e14a4a850cf107b09959"
@ -432,6 +456,70 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_widget_from_html:
dependency: "direct main"
description:
name: flutter_widget_from_html
sha256: "9e2a6201c4d2eb910b6b3ebb2a9f5c490fc61c9a1aa35eafdde38f0fc659cf4c"
url: "https://pub.dev"
source: hosted
version: "0.15.2"
flutter_widget_from_html_core:
dependency: transitive
description:
name: flutter_widget_from_html_core
sha256: b1048fd119a14762e2361bd057da608148a895477846d6149109b2151d2f7abf
url: "https://pub.dev"
source: hosted
version: "0.15.2"
fwfh_cached_network_image:
dependency: transitive
description:
name: fwfh_cached_network_image
sha256: "8e44226801bfba27930673953afce8af44da7e92573be93f60385d9865a089dd"
url: "https://pub.dev"
source: hosted
version: "0.14.3"
fwfh_chewie:
dependency: transitive
description:
name: fwfh_chewie
sha256: "37bde9cedfb6dc5546176f7f0c56af1e814966cb33ec58f16c9565ed93ccb704"
url: "https://pub.dev"
source: hosted
version: "0.14.8"
fwfh_just_audio:
dependency: transitive
description:
name: fwfh_just_audio
sha256: "38dc2c55803bd3cef33042c473e0c40b891ad4548078424641a32032f6a1245f"
url: "https://pub.dev"
source: hosted
version: "0.15.2"
fwfh_svg:
dependency: transitive
description:
name: fwfh_svg
sha256: "550b1014d12b5528d8bdb6e3b44b58721f3fb1f65d7a852d1623a817008bdfc4"
url: "https://pub.dev"
source: hosted
version: "0.8.3"
fwfh_url_launcher:
dependency: transitive
description:
name: fwfh_url_launcher
sha256: b9f5d55a5ae2c2c07243ba33f7ba49ac9544bdb2f4c16d8139df9ccbebe3449c
url: "https://pub.dev"
source: hosted
version: "0.9.1"
fwfh_webview:
dependency: transitive
description:
name: fwfh_webview
sha256: f67890bc0d6278da98bd197469ae9511c859f7db327e92299fe0ea0cf46c4057
url: "https://pub.dev"
source: hosted
version: "0.15.2"
google_fonts:
dependency: "direct main"
description:
@ -552,6 +640,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.7"
just_audio:
dependency: transitive
description:
name: just_audio
sha256: a49e7120b95600bd357f37a2bb04cd1e88252f7cdea8f3368803779b925b1049
url: "https://pub.dev"
source: hosted
version: "0.9.42"
just_audio_platform_interface:
dependency: transitive
description:
name: just_audio_platform_interface
sha256: "0243828cce503c8366cc2090cefb2b3c871aa8ed2f520670d76fd47aa1ab2790"
url: "https://pub.dev"
source: hosted
version: "4.3.0"
just_audio_web:
dependency: transitive
description:
name: just_audio_web
sha256: "9a98035b8b24b40749507687520ec5ab404e291d2b0937823ff45d92cb18d448"
url: "https://pub.dev"
source: hosted
version: "0.4.13"
jwt_decoder:
dependency: "direct main"
description:
@ -592,6 +704,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.0"
list_counter:
dependency: transitive
description:
name: list_counter
sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237
url: "https://pub.dev"
source: hosted
version: "1.0.2"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
@ -933,6 +1061,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.2"
url_launcher:
dependency: transitive
description:
name: url_launcher
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
url: "https://pub.dev"
source: hosted
version: "6.3.1"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: f0c73347dfcfa5b3db8bc06e1502668265d39c08f310c29bff4e28eea9699f79
url: "https://pub.dev"
source: hosted
version: "6.3.9"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e
url: "https://pub.dev"
source: hosted
version: "6.3.1"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af
url: "https://pub.dev"
source: hosted
version: "3.2.0"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e"
url: "https://pub.dev"
source: hosted
version: "2.3.3"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4"
url: "https://pub.dev"
source: hosted
version: "3.1.3"
uuid:
dependency: transitive
description:
@ -1045,6 +1237,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
webview_flutter:
dependency: transitive
description:
name: webview_flutter
sha256: "6869c8786d179f929144b4a1f86e09ac0eddfe475984951ea6c634774c16b522"
url: "https://pub.dev"
source: hosted
version: "4.8.0"
webview_flutter_android:
dependency: transitive
description:
name: webview_flutter_android
sha256: ed021f27ae621bc97a6019fb601ab16331a3db4bf8afa305e9f6689bdb3edced
url: "https://pub.dev"
source: hosted
version: "3.16.8"
webview_flutter_platform_interface:
dependency: transitive
description:
name: webview_flutter_platform_interface
sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d
url: "https://pub.dev"
source: hosted
version: "2.10.0"
webview_flutter_wkwebview:
dependency: transitive
description:
name: webview_flutter_wkwebview
sha256: "9c62cc46fa4f2d41e10ab81014c1de470a6c6f26051a2de32111b2ee55287feb"
url: "https://pub.dev"
source: hosted
version: "3.14.0"
win32:
dependency: transitive
description:

View File

@ -37,8 +37,11 @@ dependencies:
flick_video_player: ^0.9.0
flutter:
sdk: flutter
flutter_html: ^3.0.0-beta.2
flutter_inappwebview: ^6.0.0
flutter_secure_storage: ^9.2.2
flutter_svg: ^2.0.10+1
flutter_widget_from_html: ^0.15.2
google_fonts: ^6.2.1
google_nav_bar: ^5.0.6
image_picker: ^1.1.2

View File

@ -9,6 +9,7 @@
#include <audioplayers_windows/audioplayers_windows_plugin.h>
#include <file_selector_windows/file_selector_windows.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
AudioplayersWindowsPluginRegisterWithRegistrar(
@ -17,4 +18,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}

View File

@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_windows
file_selector_windows
flutter_secure_storage_windows
url_launcher_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST