D1APP-99 studio (beta 2)

This commit is contained in:
MohammadTaha Basiri 2022-03-23 13:58:58 +04:30
parent 0738072d5d
commit 5dd6c4205f
27 changed files with 1015 additions and 326 deletions

View File

@ -3,6 +3,7 @@ import 'package:didvan/config/theme_data.dart';
import 'package:didvan/providers/theme_provider.dart';
import 'package:didvan/providers/user_provider.dart';
import 'package:didvan/routes/route_generator.dart';
import 'package:didvan/views/home/studio/studio_details/studio_details_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart';
@ -26,6 +27,9 @@ class Didvan extends StatelessWidget {
ChangeNotifierProvider<ThemeProvider>(
create: (context) => ThemeProvider(),
),
ChangeNotifierProvider<StudioDetailsState>(
create: (context) => StudioDetailsState(),
),
],
child: Consumer<ThemeProvider>(
builder: (context, themeProvider, child) => MaterialApp(

View File

@ -45,7 +45,7 @@ class OverviewData {
createdAt: json['createdAt'],
duration: json['duration'],
type: json['type'] ?? '',
marked: json['marked'] ?? false,
marked: json['marked'] ?? true,
media: json['media'],
categories: json['categories'] != null
? List<CategoryData>.from(

View File

@ -0,0 +1,27 @@
class SliderData {
final int id;
final String title;
final String image;
final String media;
const SliderData({
required this.id,
required this.title,
required this.image,
required this.media,
});
factory SliderData.fromJson(Map<String, dynamic> json) => SliderData(
id: json['id'],
title: json['title'],
image: json['image'],
media: json['media'],
);
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'image': image,
'media': media,
};
}

View File

@ -132,7 +132,7 @@ class UserProvider extends CoreProvier {
final MapEntry? lastChange =
_radarMarkQueue.lastWhereOrNull((item) => item.key == id);
if (lastChange == null) return;
final service = RequestService(RequestHelper.markRadar(id));
final service = RequestService(RequestHelper.mark(id, 'radar'));
if (lastChange.value) {
await service.post();
} else {
@ -148,7 +148,7 @@ class UserProvider extends CoreProvier {
final MapEntry? lastChange =
_studioMarkQueue.lastWhereOrNull((item) => item.key == id);
if (lastChange == null) return;
final service = RequestService(RequestHelper.markStudio(id));
final service = RequestService(RequestHelper.mark(id, 'studio'));
if (lastChange.value) {
await service.post();
} else {
@ -164,7 +164,7 @@ class UserProvider extends CoreProvier {
final MapEntry? lastChange =
_newsMarkQueue.lastWhereOrNull((item) => item.key == id);
if (lastChange == null) return;
final service = RequestService(RequestHelper.markNews(id));
final service = RequestService(RequestHelper.mark(id, 'news'));
if (lastChange.value) {
await service.post();
} else {

View File

@ -25,8 +25,9 @@ import 'package:didvan/views/home/settings/direct_list/direct_list_state.dart';
import 'package:didvan/views/home/settings/general_settings/settings.dart';
import 'package:didvan/views/home/settings/general_settings/settings_state.dart';
import 'package:didvan/views/home/settings/profile/profile.dart';
import 'package:didvan/views/home/studio/studio_details/studio_details.dart';
import 'package:didvan/views/home/studio/studio_details/studio_details_state.dart';
import 'package:didvan/views/home/studio/studio_details/studio_details.mobile.dart'
if (dart.library.io) 'package:didvan/views/home/studio/studio_details/studio_details.mobile.dart'
if (dart.library.html) 'package:didvan/views/home/studio/studio_details/studio_details.web.dart';
import 'package:didvan/views/home/studio/studio_state.dart';
import 'package:didvan/views/splash/splash.dart';
import 'package:didvan/routes/routes.dart';
@ -64,9 +65,6 @@ class RouteGenerator {
ChangeNotifierProvider<StudioState>(
create: (context) => StudioState(),
),
ChangeNotifierProvider<StudioDetailsState>(
create: (context) => StudioDetailsState(),
),
],
child: const Home(),
),
@ -106,11 +104,8 @@ class RouteGenerator {
);
case Routes.studioDetails:
return _createRoute(
ChangeNotifierProvider<StudioDetailsState>.value(
value: (settings.arguments as Map<String, dynamic>)['state'],
child: StudioDetails(
pageData: settings.arguments as Map<String, dynamic>,
),
StudioDetails(
pageData: settings.arguments as Map<String, dynamic>,
),
);
case Routes.directList:
@ -150,14 +145,15 @@ class RouteGenerator {
child: Hashtag(tag: settings.arguments as Tag),
),
);
case Routes.filteredBookmarks:
return _createRoute(
ChangeNotifierProvider<FilteredBookmarksState>(
create: (context) => FilteredBookmarksState(
settings.arguments as String,
(settings.arguments as Map<String, dynamic>)['type'],
),
child: const FilteredBookmarks(),
child: FilteredBookmarks(
onDeleted:
(settings.arguments as Map<String, dynamic>)['onDeleted']),
),
);
default:

View File

@ -54,11 +54,6 @@ class RequestHelper {
MapEntry('tags', _urlListConcatGenerator(ids)),
]);
static String markRadar(int id) => _baseRadarUrl + '/$id/mark';
static String radarComments(int id) => _baseRadarUrl + '/$id/comments';
static String addRadarComment(int id) => _baseRadarUrl + '/$id/comments/add';
static String feedbackRadarComment(int radarId, int id) =>
_baseRadarUrl + '/$radarId/comments/$id/feedback';
static String radarDetails(int id, RadarRequestArgs args) =>
_baseRadarUrl +
'/$id' +
@ -79,11 +74,6 @@ class RequestHelper {
MapEntry('categories', _urlListConcatGenerator(args.categories)),
]);
static String markNews(int id) => _baseNewsUrl + '/$id/mark';
static String newsComments(int id) => _baseNewsUrl + '/$id/comments';
static String addNewsComment(int id) => _baseNewsUrl + '/$id/comments/add';
static String feedbackNewsComment(int radarId, int id) =>
_baseNewsUrl + '/$radarId/comments/$id/feedback';
static String newsDetails(int id, NewsRequestArgs args) =>
_baseNewsUrl +
'/$id' +
@ -102,12 +92,10 @@ class RequestHelper {
MapEntry('search', args.search),
]);
static String markStudio(int id) => _baseStudioUrl + '/$id/mark';
static String studioComments(int id) => _baseStudioUrl + '/$id/comments';
static String addStudioComment(int id) =>
_baseStudioUrl + '/$id/comments/add';
static String feedbackStudioComment(int studioId, int id) =>
_baseStudioUrl + '/$studioId/comments/$id/feedback';
static String sudioSlider(String type) =>
_baseStudioUrl +
'/slider' +
_urlConcatGenerator([MapEntry('type', type)]);
static String studioDetails(int id, StudioRequestArgs args) =>
_baseStudioUrl +
'/$id' +
@ -126,6 +114,14 @@ class RequestHelper {
MapEntry('search', args.search),
]);
static String mark(int id, String type) => baseUrl + '/$type/$id/mark';
static String comments(int id, String type) =>
baseUrl + '/$type/$id/comments';
static String feedback(int id, int commentId, String type) =>
baseUrl + '/$type/$id/comments/$commentId/feedback';
static String addComment(int id, String type) =>
baseUrl + '/$type/$id/comments/add';
static String _urlConcatGenerator(List<MapEntry<String, dynamic>> additions) {
String result = '';
additions.removeWhere(

View File

@ -1,6 +1,7 @@
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/models/view/app_bar_data.dart';
import 'package:didvan/views/home/comments/comments_state.dart';
import 'package:didvan/views/home/comments/widgets/comment_item.dart';
@ -9,6 +10,7 @@ import 'package:didvan/views/widgets/didvan/icon_button.dart';
import 'package:didvan/views/widgets/didvan/scaffold.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/shimmer_placeholder.dart';
import 'package:didvan/views/widgets/state_handlers/empty_state.dart';
import 'package:didvan/views/widgets/state_handlers/sliver_state_handler.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@ -32,7 +34,7 @@ class _CommentsState extends State<Comments> {
void initState() {
final state = context.read<CommentsState>();
state.itemId = widget.pageData['id'];
state.isRadar = widget.pageData['isRadar'];
state.type = widget.pageData['type'];
state.onCommentsChanged = widget.pageData['onCommentsChanged'];
Future.delayed(
Duration.zero,
@ -41,6 +43,8 @@ class _CommentsState extends State<Comments> {
super.initState();
}
bool get _isPage => widget.pageData['isPage'] != false;
@override
Widget build(BuildContext context) {
final bottomViewInset = MediaQuery.of(context).viewInsets.bottom;
@ -56,11 +60,13 @@ class _CommentsState extends State<Comments> {
children: [
DidvanScaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
appBarData: AppBarData(
hasBack: true,
title: 'نظرات',
subtitle: widget.pageData['title'],
),
appBarData: _isPage
? AppBarData(
hasBack: true,
title: 'نظرات',
subtitle: widget.pageData['title'],
)
: null,
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 92),
slivers: [
Consumer<CommentsState>(
@ -71,6 +77,11 @@ class _CommentsState extends State<Comments> {
itemPadding: const EdgeInsets.symmetric(vertical: 16),
childCount: state.comments.length,
placeholder: const _CommentPlaceholder(),
enableEmptyState: state.comments.isEmpty,
emptyState: EmptyState(
asset: Assets.emptyChat,
title: 'اولین نظر را بنویسید...',
),
builder: (context, state, index) => Comment(
focusNode: _focusNode,
comment: state.comments[index],

View File

@ -17,19 +17,17 @@ class CommentsState extends CoreProvier {
bool showReplyBox = false;
late void Function(int count) onCommentsChanged;
int _count = 0;
late String type;
final List<CommentData> comments = [];
final Map<int, MapEntry<bool, bool>> _feedbackQueue = {};
bool isRadar = true;
int itemId = 0;
Future<void> getComments() async {
appState = AppState.busy;
final service = RequestService(
isRadar
? RequestHelper.radarComments(itemId)
: RequestHelper.newsComments(itemId),
RequestHelper.comments(itemId, type),
);
await service.httpGet();
if (service.isSuccess) {
@ -52,13 +50,12 @@ class CommentsState extends CoreProvier {
Future.delayed(const Duration(milliseconds: 500), () async {
if (!_feedbackQueue.containsKey(id)) return;
final service = RequestService(
isRadar
? RequestHelper.feedbackRadarComment(itemId, id)
: RequestHelper.feedbackNewsComment(itemId, id),
body: {
'like': _feedbackQueue[id]!.key,
'dislike': _feedbackQueue[id]!.value,
});
RequestHelper.feedback(itemId, id, type),
body: {
'like': _feedbackQueue[id]!.key,
'dislike': _feedbackQueue[id]!.value,
},
);
await service.put();
_feedbackQueue.remove(id);
});
@ -119,10 +116,9 @@ class CommentsState extends CoreProvier {
update();
body.addAll({'text': text});
final service = RequestService(
isRadar
? RequestHelper.addRadarComment(itemId)
: RequestHelper.addNewsComment(itemId),
body: body);
RequestHelper.addComment(itemId, type),
body: body,
);
await service.post();
if (service.isSuccess) {

View File

@ -131,7 +131,15 @@ class _BookmarksState extends State<Bookmarks> {
void _onCategorySelected(String type) {
FocusScope.of(context).unfocus();
Navigator.of(context).pushNamed(Routes.filteredBookmarks, arguments: type);
Navigator.of(context).pushNamed(Routes.filteredBookmarks, arguments: {
'type': type,
'onDeleted': (int id) {
final state = context.read<BookmarksState>();
state.bookmarks
.removeWhere((element) => element.id == id && element.type == type);
state.update();
},
});
}
void _onChanged(String value) {

View File

@ -1,7 +1,10 @@
import 'package:didvan/models/requests/studio.dart';
import 'package:didvan/models/view/app_bar_data.dart';
import 'package:didvan/views/home/settings/bookmarks/filtered_bookmark/filtered_bookmarks_state.dart';
import 'package:didvan/views/home/widgets/overview/news.dart';
import 'package:didvan/views/home/widgets/overview/podcast.dart';
import 'package:didvan/views/home/widgets/overview/radar.dart';
import 'package:didvan/views/home/widgets/overview/video.dart';
import 'package:didvan/views/widgets/didvan/scaffold.dart';
import 'package:didvan/views/widgets/state_handlers/empty_list.dart';
import 'package:didvan/views/widgets/state_handlers/sliver_state_handler.dart';
@ -9,7 +12,8 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class FilteredBookmarks extends StatefulWidget {
const FilteredBookmarks({Key? key}) : super(key: key);
final void Function(int id)? onDeleted;
const FilteredBookmarks({Key? key, this.onDeleted}) : super(key: key);
@override
_FilteredBookmarksState createState() => _FilteredBookmarksState();
@ -67,8 +71,26 @@ class _FilteredBookmarksState extends State<FilteredBookmarks> {
hasUnmarkConfirmation: true,
);
}
return NewsOverview(
news: state.bookmarks[index],
if (state.type == 'news') {
return NewsOverview(
news: state.bookmarks[index],
onMarkChanged: _onBookmarkChanged,
hasUnmarkConfirmation: true,
);
}
if (state.type == 'podcast') {
return PodcastOverview(
studioRequestArgs:
const StudioRequestArgs(page: 0, type: 'podcast'),
podcast: state.bookmarks[index],
onMarkChanged: _onBookmarkChanged,
hasUnmarkConfirmation: true,
);
}
return VideoOverview(
studioRequestArgs:
const StudioRequestArgs(page: 0, type: 'video'),
video: state.bookmarks[index],
onMarkChanged: _onBookmarkChanged,
hasUnmarkConfirmation: true,
);
@ -85,5 +107,6 @@ class _FilteredBookmarksState extends State<FilteredBookmarks> {
if (value) return;
final state = context.read<FilteredBookmarksState>();
state.onMarkChanged(id, false);
widget.onDeleted?.call(id);
}
}

View File

@ -56,14 +56,12 @@ class FilteredBookmarksState extends CoreProvier {
}
void onMarkChanged(int id, bool value) {
switch (type) {
case 'radar':
UserProvider.changeRadarMark(id, value);
break;
case 'news':
UserProvider.changeNewsMark(id, value);
break;
default:
if (type == 'radar') {
UserProvider.changeRadarMark(id, value);
} else if (type == 'news') {
UserProvider.changeNewsMark(id, value);
} else {
UserProvider.changeStudioMark(id, value);
}
bookmarks.removeWhere((element) => element.id == id);
notifyListeners();

View File

@ -1,6 +1,10 @@
import 'dart:async';
import 'package:didvan/config/design_config.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/requests/studio.dart';
import 'package:didvan/models/view/action_sheet_data.dart';
import 'package:didvan/routes/routes.dart';
import 'package:didvan/utils/action_sheet.dart';
import 'package:didvan/views/home/studio/studio_state.dart';
import 'package:didvan/views/home/studio/widgets/slider.dart';
@ -9,6 +13,7 @@ import 'package:didvan/views/home/widgets/logo_app_bar.dart';
import 'package:didvan/views/home/widgets/overview/podcast.dart';
import 'package:didvan/views/home/widgets/overview/video.dart';
import 'package:didvan/views/home/widgets/search_field.dart';
import 'package:didvan/views/widgets/animated_visibility.dart';
import 'package:didvan/views/widgets/didvan/divider.dart';
import 'package:didvan/views/widgets/didvan/icon_button.dart';
import 'package:didvan/views/widgets/didvan/radial_button.dart';
@ -26,6 +31,7 @@ class Studio extends StatefulWidget {
class _StudioState extends State<Studio> {
final _focusNode = FocusNode();
Timer? _timer;
@override
void initState() {
@ -46,7 +52,10 @@ class _StudioState extends State<Studio> {
EdgeInsets.only(top: MediaQuery.of(context).padding.top),
child: DidvanIconButton(
icon: DidvanIcons.bookmark_regular,
onPressed: () {},
onPressed: () => Navigator.of(context).pushNamed(
Routes.filteredBookmarks,
arguments: context.read<StudioState>().type,
),
),
),
],
@ -59,14 +68,20 @@ class _StudioState extends State<Studio> {
child: Padding(
padding: const EdgeInsets.all(16.0),
child: SearchField(
title: 'جستجو در استودیو',
onChanged: (value) {},
title: 'استودیو',
onChanged: _onChanged,
focusNode: _focusNode,
),
),
),
const SliverToBoxAdapter(
child: StudioSlider(),
SliverToBoxAdapter(
child: Consumer<StudioState>(
builder: (context, state, child) => AnimatedVisibility(
isVisible: !state.searching,
duration: DesignConfig.lowAnimationDuration,
child: const StudioSlider(),
),
),
),
const SliverPadding(
padding: EdgeInsets.symmetric(horizontal: 16),
@ -82,7 +97,13 @@ class _StudioState extends State<Studio> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const ItemTitle(title: 'تازه‌ترین‌ها'),
Consumer<StudioState>(
builder: (context, state, child) => AnimatedVisibility(
isVisible: !state.searching,
duration: DesignConfig.lowAnimationDuration,
child: ItemTitle(title: state.orderString),
),
),
DidvanIconButton(
gestureSize: 36,
icon: DidvanIcons.sort_regular,
@ -108,7 +129,6 @@ class _StudioState extends State<Studio> {
onMarkChanged: state.changeMark,
hasUnmarkConfirmation: false,
video: state.studios[index],
onCommentsChanged: state.onCommentsChanged,
studioRequestArgs: StudioRequestArgs(
page: state.page,
order: state.order,
@ -127,13 +147,25 @@ class _StudioState extends State<Studio> {
),
),
childCount: state.studios.length,
onRetry: () => state.getStudioOverviews(page: 1),
onRetry: () => state.getStudios(page: 1),
),
),
],
);
}
void _onChanged(String value) {
final state = context.read<StudioState>();
if (value.length < 4 && value.isNotEmpty || state.lastSearch == value) {
return;
}
_timer?.cancel();
_timer = Timer(const Duration(seconds: 1), () {
state.search = value;
state.getStudios(page: 1);
});
}
void _showSortDialog() {
final state = context.read<StudioState>();
ActionSheetUtils.showBottomSheet(
@ -142,7 +174,7 @@ class _StudioState extends State<Studio> {
builder: (context, setState) => Column(
children: [
DidvanRadialButton(
title: 'جدیدترین‌ها',
title: 'تازه‌ترین‌ها',
onSelected: () => setState(
() => state.selectedSortTypeIndex = 0,
),
@ -171,7 +203,7 @@ class _StudioState extends State<Studio> {
titleIcon: DidvanIcons.sort_regular,
hasDismissButton: false,
confrimTitle: 'مرتب سازی',
onConfirmed: () => state.getStudioOverviews(page: 1),
onConfirmed: () => state.getStudios(page: 1),
),
);
}

View File

@ -1,13 +1,17 @@
import 'dart:io';
import 'package:didvan/config/design_config.dart';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/models/view/app_bar_data.dart';
import 'package:didvan/views/home/studio/studio_details/studio_details_state.dart';
import 'package:didvan/views/home/studio/studio_details/widgets/studio_details_widget.dart';
import 'package:didvan/views/widgets/didvan/scaffold.dart';
import 'package:didvan/views/widgets/state_handlers/state_handler.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:webview_flutter/webview_flutter.dart';
class StudioDetails extends StatefulWidget {
@ -20,6 +24,7 @@ class StudioDetails extends StatefulWidget {
}
class _StudioDetailsState extends State<StudioDetails> {
final _scrollController = ScrollController();
bool _isFullScreen = false;
bool _isInit = true;
@ -29,14 +34,12 @@ class _StudioDetailsState extends State<StudioDetails> {
@override
void initState() {
final state = context.read<StudioDetailsState>();
Future.delayed(
Duration.zero,
() => state.getStudioDetails(widget.pageData['id']),
);
state.args = widget.pageData['args'];
if (Platform.isAndroid) WebView.platform = AndroidWebView();
if (!kIsWeb && Platform.isAndroid) WebView.platform = AndroidWebView();
super.initState();
}
@ -77,39 +80,44 @@ class _StudioDetailsState extends State<StudioDetails> {
_scaleInPortrait = _dwInPortrait / 576;
_isInit = false;
}
return Consumer<StudioDetailsState>(
builder: (context, state, child) => StateHandler<StudioDetailsState>(
state: state,
onRetry: () => state.getStudioDetails(state.currentStudio.id),
builder: (context, state) => state.studios.isEmpty
? const SizedBox()
: WillPopScope(
onWillPop: () async {
if (_isFullScreen) {
await _changeFullSceen(false);
return false;
}
return true;
},
child: DidvanScaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
padding: EdgeInsets.zero,
appBarData: _isFullScreen
? null
: AppBarData(
isSmall: true,
title: state.currentStudio.title,
),
children: [
SizedBox(
width: ds.width,
height: _isFullScreen ? ds.height : ds.width * 9 / 16,
child: Stack(
children: [
WebView(
allowsInlineMediaPlayback: true,
initialUrl: Uri.dataFromString(
'''
builder: (context, state) {
if (state.studios.isEmpty) {
return const SizedBox();
}
return WillPopScope(
onWillPop: () async {
if (_isFullScreen) {
await _changeFullSceen(false);
return false;
}
return true;
},
child: DidvanScaffold(
scrollController: _scrollController,
backgroundColor: Theme.of(context).colorScheme.surface,
padding: EdgeInsets.zero,
appBarData: _isFullScreen
? null
: AppBarData(
isSmall: true,
title: state.currentStudio.title,
),
children: [
SizedBox(
width: ds.width,
height: _isFullScreen ? ds.height : ds.width * 9 / 16,
child: Stack(
children: [
WebView(
backgroundColor: Theme.of(context).colorScheme.black,
allowsInlineMediaPlayback: true,
initialUrl: Uri.dataFromString(
'''
<html>
<head>
<meta
@ -142,28 +150,38 @@ class _StudioDetailsState extends State<StudioDetails> {
</body>
</html>
''',
mimeType: 'text/html',
).toString(),
javascriptMode: JavascriptMode.unrestricted,
),
Positioned(
right: 42,
bottom: 8,
child: GestureDetector(
onTap: () => _changeFullSceen(!_isFullScreen),
child: Container(
color: Colors.transparent,
width: 24,
height: 30,
),
),
),
],
mimeType: 'text/html',
).toString(),
javascriptMode: JavascriptMode.unrestricted,
),
),
],
Positioned(
right: 42,
bottom: 8,
child: GestureDetector(
onTap: () => _changeFullSceen(!_isFullScreen),
child: Container(
color: Colors.transparent,
width: 24,
height: 30,
),
),
),
],
),
),
),
const SizedBox(height: 20),
StudioDetailsWidget(
onCommentsTabSelected: () => _scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: DesignConfig.lowAnimationDuration,
curve: Curves.easeIn,
),
studio: state.currentStudio,
),
],
),
);
},
),
);
}

View File

@ -0,0 +1,210 @@
import 'dart:io';
import 'dart:ui' as ui;
import 'package:didvan/config/design_config.dart';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/models/view/app_bar_data.dart';
import 'package:didvan/views/home/studio/studio_details/studio_details_state.dart';
import 'package:didvan/views/home/studio/studio_details/widgets/studio_details_widget.dart';
import 'package:didvan/views/widgets/didvan/scaffold.dart';
import 'package:didvan/views/widgets/state_handlers/state_handler.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:universal_html/html.dart' as html;
import 'package:webview_flutter/webview_flutter.dart';
class StudioDetails extends StatefulWidget {
final Map<String, dynamic> pageData;
const StudioDetails({Key? key, required this.pageData}) : super(key: key);
@override
State<StudioDetails> createState() => _StudioDetailsState();
}
class _StudioDetailsState extends State<StudioDetails> {
final _scrollController = ScrollController();
bool _isFullScreen = false;
bool _isInit = true;
double _dwInPortrait = 0;
double _scaleInPortrait = 1;
@override
void initState() {
final state = context.read<StudioDetailsState>();
Future.delayed(
Duration.zero,
() => state.getStudioDetails(widget.pageData['id']),
);
state.args = widget.pageData['args'];
if (!kIsWeb && Platform.isAndroid) WebView.platform = AndroidWebView();
super.initState();
}
Future<void> _changeFullSceen(bool value) async {
if (value) {
await SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: [],
);
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
systemNavigationBarColor: Colors.black,
),
);
await SystemChrome.setPreferredOrientations(
[DeviceOrientation.landscapeLeft],
);
} else {
await SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: [SystemUiOverlay.bottom, SystemUiOverlay.top],
);
await SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp],
);
DesignConfig.updateSystemUiOverlayStyle();
}
setState(() {
_isFullScreen = value;
});
}
@override
Widget build(BuildContext context) {
final ds = MediaQuery.of(context).size;
if (_isInit) {
_dwInPortrait = MediaQuery.of(context).size.width;
_scaleInPortrait = _dwInPortrait / 576;
_isInit = false;
}
return Consumer<StudioDetailsState>(
builder: (context, state, child) => StateHandler<StudioDetailsState>(
state: state,
onRetry: () => state.getStudioDetails(state.currentStudio.id),
builder: (context, state) {
if (state.studios.isEmpty) {
return const SizedBox();
}
if (kIsWeb) {
// ignore: undefined_prefixed_name
ui.platformViewRegistry.registerViewFactory(
"video",
(int viewId) => html.IFrameElement()
..allowFullscreen = true
..src = Uri.dataFromString(
'<style>*{padding: 0 ; margin: 0; background: black;}</style>' +
state.currentStudio.media,
mimeType: 'text/html',
).toString()
..style.border = 'none',
);
}
return WillPopScope(
onWillPop: () async {
if (_isFullScreen) {
await _changeFullSceen(false);
return false;
}
return true;
},
child: DidvanScaffold(
scrollController: _scrollController,
padding: EdgeInsets.zero,
appBarData: _isFullScreen
? null
: AppBarData(
isSmall: true,
title: state.currentStudio.title,
),
children: [
if (kIsWeb)
const AspectRatio(
aspectRatio: 16 / 9,
child: HtmlElementView(viewType: 'video'),
),
if (!kIsWeb)
SizedBox(
width: ds.width,
height: _isFullScreen ? ds.height : ds.width * 9 / 16,
child: Stack(
children: [
WebView(
backgroundColor: Theme.of(context).colorScheme.black,
allowsInlineMediaPlayback: true,
initialUrl: Uri.dataFromString(
'''
<html>
<head>
<meta
name="viewport"
content="width=device-width, initial-scale=$_scaleInPortrait"
/>
<style>
* {
padding: 0;
margin: 0;
overflow: hidden;
}
iframe {
max-height: 100vh;
}
.r1_iframe_embed {
height: ${MediaQuery.of(context).size.width / _scaleInPortrait}px !important;
padding-top: 0 !important;
}
@media(max-width:580px){
.r1_iframe_embed {
height: ${_dwInPortrait * 9 / 16 / _scaleInPortrait}px !important;
padding-top: 0 !important;
}
}
</style>
</head>
<body>
${state.currentStudio.media}
</body>
</html>
''',
mimeType: 'text/html',
).toString(),
javascriptMode: JavascriptMode.unrestricted,
),
if (!kIsWeb)
Positioned(
right: 42,
bottom: 8,
child: GestureDetector(
onTap: () => _changeFullSceen(!_isFullScreen),
child: Container(
color: Colors.transparent,
width: 24,
height: 30,
),
),
),
],
),
),
const SizedBox(height: 20),
StudioDetailsWidget(
onCommentsTabSelected: () => _scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: DesignConfig.lowAnimationDuration,
curve: Curves.easeIn,
),
studio: state.currentStudio,
),
],
),
);
},
),
);
}
}

View File

@ -17,6 +17,7 @@ class StudioDetailsState extends CoreProvier {
int _selectedDetailsIndex = 0;
bool isFetchingNewItem = false;
final List<int> relatedQueue = [];
bool currentTypeIsVideo = true;
int _currentIndex = 0;
int get currentIndex => _currentIndex;
@ -24,6 +25,9 @@ class StudioDetailsState extends CoreProvier {
int get selectedDetailsIndex => _selectedDetailsIndex;
set selectedDetailsIndex(int value) {
_selectedDetailsIndex = value;
if (value == 2) {
getRelatedContents();
}
notifyListeners();
}
@ -35,12 +39,16 @@ class StudioDetailsState extends CoreProvier {
}
}
Future<void> getStudioDetails(int id,
{bool? isForward, StudioRequestArgs? args}) async {
Future<void> getStudioDetails(
int id, {
bool? isForward,
StudioRequestArgs? args,
}) async {
if (args != null) {
this.args = args;
}
if (isForward == null) {
_selectedDetailsIndex = 0;
appState = AppState.busy;
} else {
isFetchingNewItem = true;
@ -49,22 +57,16 @@ class StudioDetailsState extends CoreProvier {
final service = RequestService(RequestHelper.studioDetails(id, this.args));
await service.httpGet();
if (service.isSuccess) {
studios.clear();
final result = service.result;
final studio = StudioDetailsData.fromJson(result['studio']);
await _handlePodcastPlayback(studio);
if (this.args.page == 0) {
studios.add(studio);
initialIndex = 0;
appState = AppState.idle;
return;
}
if (this.args.type == 'podcast') {
MediaService.currentPodcast = studio;
MediaService.podcastPlaylistArgs = args;
await MediaService.handleAudioPlayback(
audioSource: studio.media,
isVoiceMessage: false,
);
}
StudioDetailsData? prevStudio;
if (result['prevStudio'].isNotEmpty) {
@ -108,13 +110,24 @@ class StudioDetailsState extends CoreProvier {
}
}
Future<void> _handlePodcastPlayback(StudioDetailsData studio) async {
if (args.type == 'podcast') {
MediaService.currentPodcast = studio;
MediaService.podcastPlaylistArgs = args;
await MediaService.handleAudioPlayback(
audioSource: studio.media,
isVoiceMessage: false,
);
}
}
Future<void> getRelatedContents() async {
if (currentStudio.relatedContents.isNotEmpty) return;
relatedQueue.add(currentStudio.id);
final service = RequestService(RequestHelper.tag(
ids: currentStudio.tags.map((tag) => tag.id).toList(),
itemId: currentStudio.id,
type: 'studio',
type: currentStudio.media.contains('iframe') ? 'video' : 'podcast',
));
await service.httpGet();
if (service.isSuccess) {

View File

@ -1,97 +0,0 @@
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/views/home/studio/studio_details/studio_details_state.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/state_handlers/state_handler.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class StudioDetailsWidget extends StatelessWidget {
const StudioDetailsWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Consumer<StudioDetailsState>(
builder: (context, state, child) => StateHandler<StudioDetailsState>(
onRetry: () {},
state: state,
builder: (context, state) => Container(
color: Theme.of(context).colorScheme.surface,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_TabItem(
icon: DidvanIcons.description_solid,
title: 'توضیحات',
onTap: () => state.selectedDetailsIndex = 0,
isSelected: state.selectedDetailsIndex == 0,
),
_TabItem(
icon: DidvanIcons.chats_solid,
title: 'نظرات',
onTap: () => state.selectedDetailsIndex = 1,
isSelected: state.selectedDetailsIndex == 1,
),
_TabItem(
icon: DidvanIcons.puzzle_solid,
title: 'مطالب مرتبط',
onTap: () => state.selectedDetailsIndex = 2,
isSelected: state.selectedDetailsIndex == 2,
),
],
),
const SizedBox(height: 16),
],
),
),
),
);
}
}
class _TabItem extends StatelessWidget {
final IconData icon;
final String title;
final VoidCallback onTap;
final bool isSelected;
const _TabItem({
Key? key,
required this.icon,
required this.title,
required this.onTap,
required this.isSelected,
}) : super(key: key);
Color? _color(context) =>
isSelected ? Theme.of(context).colorScheme.focusedBorder : null;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
color: Colors.transparent,
child: Column(
children: [
Icon(
icon,
color: _color(context),
),
Container(
width: 64,
height: 1,
color: _color(context),
),
DidvanText(
title,
color: _color(context),
style: Theme.of(context).textTheme.caption,
)
],
),
),
);
}
}

View File

@ -0,0 +1,272 @@
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/studio_details_data.dart';
import 'package:didvan/views/home/comments/comments.dart';
import 'package:didvan/views/home/comments/comments_state.dart';
import 'package:didvan/views/home/studio/studio_details/studio_details_state.dart';
import 'package:didvan/views/home/widgets/overview/multitype.dart';
import 'package:didvan/views/home/widgets/tag_item.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:didvan/views/widgets/state_handlers/state_handler.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class StudioDetailsWidget extends StatelessWidget {
final StudioDetailsData studio;
final VoidCallback onCommentsTabSelected;
const StudioDetailsWidget({
Key? key,
required this.studio,
required this.onCommentsTabSelected,
}) : super(key: key);
bool get _isVideo => studio.media.contains('ifram');
@override
Widget build(BuildContext context) {
final ds = MediaQuery.of(context).size;
return Consumer<StudioDetailsState>(
builder: (context, state, child) => Container(
color: Theme.of(context).colorScheme.surface,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
const SizedBox(),
_TabItem(
icon: DidvanIcons.description_solid,
title: 'توضیحات',
onTap: () => state.selectedDetailsIndex = 0,
isSelected: state.selectedDetailsIndex == 0,
isVideo: _isVideo,
),
_TabItem(
icon: DidvanIcons.chats_solid,
title: 'نظرات',
onTap: () {
state.selectedDetailsIndex = 1;
onCommentsTabSelected();
},
isSelected: state.selectedDetailsIndex == 1,
isVideo: _isVideo,
),
_TabItem(
icon: DidvanIcons.puzzle_solid,
title: 'مطالب مرتبط',
onTap: () => state.selectedDetailsIndex = 2,
isSelected: state.selectedDetailsIndex == 2,
isVideo: _isVideo,
),
const SizedBox(),
],
),
const SizedBox(height: 24),
StateHandler<StudioDetailsState>(
onRetry: () {},
state: state,
builder: (context, state) {
if (state.selectedDetailsIndex == 0) {
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
DidvanText(state.currentStudio.description),
if (studio.tags.isNotEmpty) const SizedBox(height: 20),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (var i = 0; i < studio.tags.length; i++)
TagItem(tag: studio.tags[i]),
],
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const SizedBox(),
if (state.studios.length != state.currentIndex + 1)
_StudioPreview(
isNext: true,
studio: state.studios[state.currentIndex + 1]!,
),
if (state.currentIndex != 0)
_StudioPreview(
isNext: false,
studio: state.studios[state.currentIndex - 1]!,
),
const SizedBox(),
],
)
],
),
);
}
if (state.selectedDetailsIndex == 1) {
return ChangeNotifierProvider<CommentsState>(
create: (context) => CommentsState(),
child: SizedBox(
height: ds.height - 180,
child: Comments(
pageData: {
'id': studio.id,
'type': 'studio',
'title': studio.title,
'onCommentsChanged': state.onCommentsChanged,
'isPage': false,
},
),
),
);
}
return Column(
children: [
if (studio.relatedContents.isEmpty)
for (var i = 0; i < 3; i++)
Padding(
padding: const EdgeInsets.only(
bottom: 8,
left: 16,
right: 16,
),
child: MultitypeOverview.placeholder,
),
for (var i = 0; i < studio.relatedContents.length; i++)
Padding(
padding: const EdgeInsets.only(
bottom: 8,
left: 16,
right: 16,
),
child: MultitypeOverview(
item: studio.relatedContents[i],
onMarkChanged: (id, value) {},
),
),
],
);
},
),
],
),
),
);
}
}
class _TabItem extends StatelessWidget {
final IconData icon;
final String title;
final VoidCallback onTap;
final bool isSelected;
final bool isVideo;
const _TabItem({
Key? key,
required this.icon,
required this.title,
required this.onTap,
required this.isSelected,
required this.isVideo,
}) : super(key: key);
Color? _color(context) {
if (isSelected) {
if (isVideo) {
return Theme.of(context).colorScheme.secondary;
}
return Theme.of(context).colorScheme.focusedBorder;
}
return Theme.of(context).colorScheme.border;
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
color: Colors.transparent,
child: Column(
children: [
Icon(
icon,
color: _color(context),
),
if (isSelected) const SizedBox(height: 8),
if (isSelected)
Container(
width: 64,
height: 1,
color: _color(context),
),
if (isSelected)
DidvanText(
title,
color: _color(context),
style: Theme.of(context).textTheme.caption,
)
],
),
),
);
}
}
class _StudioPreview extends StatelessWidget {
final bool isNext;
final StudioDetailsData studio;
const _StudioPreview({Key? key, required this.isNext, required this.studio})
: super(key: key);
String get _previewTitle {
if (studio.media.contains('iframe')) {
return 'ویدئو ${isNext ? 'بعدی' : 'قبلی'} ';
}
return 'پادکست ${isNext ? 'بعدی' : 'قبلی'} ';
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
final state = context.read<StudioDetailsState>();
state.getStudioDetails(studio.id, args: state.args);
},
child: Container(
width: 88,
color: Colors.transparent,
child: Column(
children: [
SkeletonImage(
imageUrl: studio.image,
aspectRatio: 1 / 1,
),
const SizedBox(height: 8),
Icon(
isNext
? DidvanIcons.angle_right_regular
: DidvanIcons.angle_left_regular,
),
const SizedBox(height: 8),
DidvanText(
_previewTitle,
style: Theme.of(context).textTheme.caption,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
DidvanText(
studio.title,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.overline,
color: Theme.of(context).colorScheme.caption,
),
],
),
),
);
}
}

View File

@ -1,6 +1,7 @@
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/overview_data.dart';
import 'package:didvan/models/requests/studio.dart';
import 'package:didvan/models/slider_data.dart';
import 'package:didvan/providers/core_provider.dart';
import 'package:didvan/providers/user_provider.dart';
import 'package:didvan/services/network/request.dart';
@ -8,9 +9,10 @@ import 'package:didvan/services/network/request_helper.dart';
class StudioState extends CoreProvier {
final List<OverviewData> studios = [];
final List<SliderData> sliders = [];
String? search;
String? lastSearch;
String search = '';
String lastSearch = '';
int page = 1;
int lastPage = 1;
@ -20,21 +22,13 @@ class StudioState extends CoreProvier {
bool get videosSelected => _videosSelected;
bool get searching => search.isNotEmpty;
set videosSelected(bool value) {
if (_videosSelected == value) return;
_videosSelected = value;
studios.clear();
getStudioOverviews(page: page);
}
void init() {
search = '';
lastSearch = '';
_videosSelected = true;
selectedSortTypeIndex = 0;
Future.delayed(Duration.zero, () {
getStudioOverviews(page: 1);
});
_getSliders();
getStudios(page: page);
}
String get order {
@ -43,13 +37,46 @@ class StudioState extends CoreProvier {
return 'comment';
}
String get orderString {
if (selectedSortTypeIndex == 0) return 'تازه‌ترین‌ها';
if (selectedSortTypeIndex == 1) return 'پربازدیدترین‌ها';
return 'پربحث‌نرین‌ها';
}
String get type {
if (videosSelected) return 'video';
return 'podcast';
}
Future<void> getStudioOverviews({required int page}) async {
void init() {
search = '';
lastSearch = '';
_videosSelected = true;
selectedSortTypeIndex = 0;
Future.delayed(Duration.zero, () {
_getSliders();
getStudios(page: 1);
});
}
Future<void> _getSliders() async {
final service = RequestService(
RequestHelper.sudioSlider(type),
);
await service.httpGet();
if (service.isSuccess) {
sliders.clear();
final sliderItems = service.result['studios'];
for (var i = 0; i < sliderItems.length; i++) {
sliders.add(SliderData.fromJson(sliderItems[i]));
}
}
notifyListeners();
}
Future<void> getStudios({required int page}) async {
this.page = page;
lastSearch = search;
if (page == 1) {
appState = AppState.busy;
}

View File

@ -1,28 +1,92 @@
import 'package:carousel_slider/carousel_slider.dart';
import 'package:didvan/config/design_config.dart';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/models/enums.dart';
import 'package:didvan/views/home/studio/studio_state.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/shimmer_placeholder.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class StudioSlider extends StatelessWidget {
class StudioSlider extends StatefulWidget {
const StudioSlider({Key? key}) : super(key: key);
@override
State<StudioSlider> createState() => _StudioSliderState();
}
class _StudioSliderState extends State<StudioSlider> {
int selectedIndex = 0;
@override
Widget build(BuildContext context) {
final state = context.watch<StudioState>();
return Column(
children: [
CarouselSlider(
items: [
Image.network('https://wallpapercave.com/wp/wp10731650.jpg'),
Image.network('https://wallpapercave.com/wp/wp10731650.jpg'),
Image.network('https://wallpapercave.com/wp/wp10731650.jpg'),
Image.network('https://wallpapercave.com/wp/wp10731650.jpg'),
for (var i = 0; i < state.sliders.length; i++)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: state.appState == AppState.busy
? const ShimmerPlaceholder()
: Stack(
children: [
SkeletonImage(
borderRadius: DesignConfig.mediumBorderRadius,
imageUrl: state.sliders[i].image,
width: double.infinity,
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 4,
horizontal: 8,
),
decoration: BoxDecoration(
color: (state.videosSelected
? Theme.of(context)
.colorScheme
.secondaryDisabled
: Theme.of(context).colorScheme.focused)
.withOpacity(0.9),
borderRadius: const BorderRadius.vertical(
bottom: Radius.circular(10),
),
),
child: DidvanText(
state.sliders[i].title,
color: Theme.of(context).colorScheme.title,
style: Theme.of(context).textTheme.caption,
),
),
),
],
),
),
],
options: CarouselOptions(
onPageChanged: (index, reason) => setState(
() => selectedIndex = index,
),
viewportFraction: 0.94,
aspectRatio: 16 / 9,
autoPlay: true,
),
),
Row(),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var i = 0; i < state.sliders.length; i++)
_SliderIndicator(isCurrentIndex: selectedIndex == i),
],
),
const SizedBox(height: 16),
],
);
}
@ -35,9 +99,11 @@ class _SliderIndicator extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
return AnimatedContainer(
duration: DesignConfig.lowAnimationDuration,
height: 8,
width: 8,
margin: const EdgeInsets.only(left: 4),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.focusedBorder,

View File

@ -1,8 +1,11 @@
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/models/studio_details_data.dart';
import 'package:didvan/services/media/media.dart';
import 'package:didvan/views/home/studio/studio_state.dart';
import 'package:didvan/views/home/widgets/audio/audio_slider.dart';
import 'package:didvan/views/home/widgets/bookmark_button.dart';
import 'package:didvan/views/widgets/didvan/icon_button.dart';
@ -10,6 +13,7 @@ import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/ink_wrapper.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class AudioPlayerWidget extends StatelessWidget {
final StudioDetailsData podcast;
@ -77,8 +81,13 @@ class AudioPlayerWidget extends StatelessWidget {
const DidvanText('30', isEnglishFont: true),
],
),
_PlayPouseAnimatedIcon(
audioSource: podcast.media,
StreamBuilder<bool>(
stream: MediaService.audioPlayer.playingStream,
builder: (context, snapshot) {
return _PlayPouseAnimatedIcon(
audioSource: podcast.media,
);
},
),
Column(
children: [
@ -88,8 +97,10 @@ class AudioPlayerWidget extends StatelessWidget {
onPressed: () {
MediaService.audioPlayer.seek(
Duration(
seconds:
MediaService.audioPlayer.position.inSeconds - 10,
seconds: max(
0,
MediaService.audioPlayer.position.inSeconds - 10,
),
),
);
},
@ -100,7 +111,8 @@ class AudioPlayerWidget extends StatelessWidget {
BookmarkButton(
gestureSize: 48,
value: podcast.marked,
onMarkChanged: (value) {},
onMarkChanged: (value) =>
context.read<StudioState>().changeMark(podcast.id, value),
),
],
),
@ -123,6 +135,12 @@ class __PlayPouseAnimatedIconState extends State<_PlayPouseAnimatedIcon>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
@override
void didUpdateWidget(covariant _PlayPouseAnimatedIcon oldWidget) {
_handleAnimation();
super.didUpdateWidget(oldWidget);
}
@override
void initState() {
super.initState();
@ -130,8 +148,13 @@ class __PlayPouseAnimatedIconState extends State<_PlayPouseAnimatedIcon>
vsync: this,
duration: DesignConfig.lowAnimationDuration,
);
}
void _handleAnimation() {
if (MediaService.audioPlayer.playing) {
_animationController.forward();
} else {
_animationController.reverse();
}
}
@ -144,11 +167,7 @@ class __PlayPouseAnimatedIconState extends State<_PlayPouseAnimatedIcon>
audioSource: widget.audioSource,
isVoiceMessage: false,
);
if (MediaService.audioPlayer.playing) {
_animationController.forward();
} else {
_animationController.reverse();
}
_handleAnimation();
},
child: Container(
padding: const EdgeInsets.all(8),

View File

@ -1,9 +1,11 @@
import 'package:didvan/config/design_config.dart';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/enums.dart';
import 'package:didvan/services/media/media.dart';
import 'package:didvan/views/home/studio/studio_details/studio_details_state.dart';
import 'package:didvan/views/home/studio/studio_details/widgets/studio_details.dart';
import 'package:didvan/views/home/studio/studio_details/widgets/studio_details_widget.dart';
import 'package:didvan/views/home/studio/studio_state.dart';
import 'package:didvan/views/home/widgets/audio/audio_player_widget.dart';
import 'package:didvan/views/home/widgets/audio/audio_slider.dart';
import 'package:didvan/views/widgets/didvan/icon_button.dart';
@ -179,46 +181,65 @@ class DidvanBNB extends StatelessWidget {
final sheetKey = GlobalKey<ExpandableBottomSheetState>();
bool isExpanded = false;
final detailsState = context.read<StudioDetailsState>();
final state = context.read<StudioState>();
showModalBottomSheet(
backgroundColor: Colors.transparent,
context: context,
isScrollControlled: true,
builder: (context) => ChangeNotifierProvider<StudioDetailsState>.value(
value: detailsState,
child: ExpandableBottomSheet(
key: sheetKey,
background: const SizedBox(),
persistentHeader: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
AudioPlayerWidget(
podcast: MediaService.currentPodcast!,
),
Container(
width: MediaQuery.of(context).size.width,
builder: (context) => ChangeNotifierProvider<StudioState>.value(
value: state,
child: Consumer<StudioDetailsState>(
builder: (context, state, child) => ExpandableBottomSheet(
key: sheetKey,
background: Align(
alignment: Alignment.bottomCenter,
child: Container(
height: MediaQuery.of(context).size.height * 0.7,
color: Theme.of(context).colorScheme.surface,
child: Column(
children: [
DidvanIconButton(
size: 32,
icon: DidvanIcons.angle_down_regular,
onPressed: () {
if (!isExpanded) {
sheetKey.currentState?.expand();
isExpanded = true;
return;
}
isExpanded = false;
sheetKey.currentState?.contract();
},
),
const SizedBox(height: 16),
],
),
),
],
),
persistentHeader: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
AudioPlayerWidget(
podcast: MediaService.currentPodcast!,
),
Container(
width: MediaQuery.of(context).size.width,
color: Theme.of(context).colorScheme.surface,
child: Column(
children: [
DidvanIconButton(
size: 32,
icon: DidvanIcons.angle_down_regular,
onPressed: () {
if (!isExpanded) {
sheetKey.currentState?.expand();
isExpanded = true;
return;
}
isExpanded = false;
sheetKey.currentState?.contract();
},
),
const SizedBox(height: 16),
],
),
),
],
),
expandableContent: state.appState == AppState.busy
? const SizedBox()
: StudioDetailsWidget(
studio: detailsState.currentStudio,
onCommentsTabSelected: () {
Future.delayed(
const Duration(milliseconds: 100),
sheetKey.currentState?.expand,
);
},
),
),
expandableContent: const StudioDetailsWidget(),
),
),
);

View File

@ -130,7 +130,7 @@ class _FloatingNavigationBarState extends State<FloatingNavigationBar> {
Routes.comments,
arguments: {
'id': widget.item.id,
'isRadar': widget.isRadar,
'type': widget.isRadar ? 'radar' : 'news',
'title': widget.item.title,
'onCommentsChanged': widget.onCommentsChanged,
},

View File

@ -3,13 +3,16 @@ import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/overview_data.dart';
import 'package:didvan/models/requests/news.dart';
import 'package:didvan/models/requests/radar.dart';
import 'package:didvan/models/requests/studio.dart';
import 'package:didvan/routes/routes.dart';
import 'package:didvan/views/home/studio/studio_details/studio_details_state.dart';
import 'package:didvan/views/widgets/didvan/card.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/shimmer_placeholder.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:flutter/material.dart';
import 'package:persian_number_utility/persian_number_utility.dart';
import 'package:provider/provider.dart';
class MultitypeOverview extends StatelessWidget {
final OverviewData item;
@ -23,20 +26,60 @@ class MultitypeOverview extends StatelessWidget {
this.hasUnmarkConfirmation = false,
}) : super(key: key);
get _targetPageArgs {
if (item.type == 'radar') {
return const RadarRequestArgs(page: 0);
}
if (item.type == 'news') {
return const NewsRequestArgs(page: 0);
}
return StudioRequestArgs(page: 0, type: item.type);
}
String get _targetPageRouteName {
if (item.type == 'radar') {
return Routes.radarDetails;
}
if (item.type == 'news') {
return Routes.newsDetails;
}
return Routes.studioDetails;
}
IconData get _icon {
if (item.type == 'radar') {
return DidvanIcons.radar_light;
}
if (item.type == 'news') {
return DidvanIcons.news_light;
}
if (item.type == 'video') {
return DidvanIcons.video_light;
}
return DidvanIcons.podcast_light;
}
@override
Widget build(BuildContext context) {
return DidvanCard(
onTap: () => Navigator.of(context).pushNamed(
item.type == 'radar' ? Routes.radarDetails : Routes.newsDetails,
arguments: {
'onMarkChanged': onMarkChanged,
'id': item.id,
'args': item.type == 'radar'
? const RadarRequestArgs(page: 0)
: const NewsRequestArgs(page: 0),
'hasUnmarkConfirmation': hasUnmarkConfirmation,
},
),
onTap: () {
if (item.type == 'podcast') {
context.read<StudioDetailsState>().getStudioDetails(
item.id,
args: StudioRequestArgs(page: 0, type: item.type),
);
return;
}
Navigator.of(context).pushNamed(
_targetPageRouteName,
arguments: {
'onMarkChanged': onMarkChanged,
'id': item.id,
'args': _targetPageArgs,
'hasUnmarkConfirmation': hasUnmarkConfirmation,
},
);
},
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -52,9 +95,7 @@ class MultitypeOverview extends StatelessWidget {
),
),
child: Icon(
item.type == 'radar'
? DidvanIcons.radar_light
: DidvanIcons.news_light,
_icon,
color: Theme.of(context).colorScheme.white,
size: 18,
),

View File

@ -19,11 +19,13 @@ class PodcastOverview extends StatelessWidget {
final OverviewData podcast;
final void Function(int id, bool value) onMarkChanged;
final StudioRequestArgs? studioRequestArgs;
final bool hasUnmarkConfirmation;
const PodcastOverview({
Key? key,
required this.podcast,
required this.onMarkChanged,
this.studioRequestArgs,
this.hasUnmarkConfirmation = false,
}) : super(key: key);
@override
@ -82,6 +84,7 @@ class PodcastOverview extends StatelessWidget {
),
const SizedBox(width: 16),
BookmarkButton(
askForConfirmation: hasUnmarkConfirmation,
gestureSize: 24,
value: podcast.marked,
onMarkChanged: (value) => onMarkChanged(podcast.id, value),

View File

@ -117,7 +117,7 @@ class RadarOverview extends StatelessWidget {
onPressed: () => Navigator.of(context).pushNamed(
Routes.comments,
arguments: {
'isRadar': true,
'type': 'radar',
'title': radar.title,
'id': radar.id,
'onCommentsChanged': (count) =>

View File

@ -4,7 +4,6 @@ import 'package:didvan/models/overview_data.dart';
import 'package:didvan/models/requests/studio.dart';
import 'package:didvan/routes/routes.dart';
import 'package:didvan/utils/date_time.dart';
import 'package:didvan/views/home/studio/studio_details/studio_details_state.dart';
import 'package:didvan/views/home/widgets/bookmark_button.dart';
import 'package:didvan/views/home/widgets/duration_widget.dart';
import 'package:didvan/views/widgets/didvan/card.dart';
@ -14,18 +13,15 @@ import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/shimmer_placeholder.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class VideoOverview extends StatelessWidget {
final OverviewData video;
final void Function(int id, int count) onCommentsChanged;
final void Function(int id, bool value) onMarkChanged;
final bool hasUnmarkConfirmation;
final StudioRequestArgs? studioRequestArgs;
const VideoOverview({
Key? key,
required this.video,
required this.onCommentsChanged,
required this.onMarkChanged,
required this.hasUnmarkConfirmation,
this.studioRequestArgs,
@ -38,12 +34,10 @@ class VideoOverview extends StatelessWidget {
Routes.studioDetails,
arguments: {
'onMarkChanged': onMarkChanged,
'onCommentsChanged': onCommentsChanged,
'id': video.id,
'args': studioRequestArgs,
'hasUnmarkConfirmation': hasUnmarkConfirmation,
'isVideo': true,
'state': context.read<StudioDetailsState>(),
},
),
child: Row(
@ -115,6 +109,7 @@ class VideoOverview extends StatelessWidget {
gestureSize: 24,
value: video.marked,
onMarkChanged: (value) => onMarkChanged(video.id, value),
askForConfirmation: hasUnmarkConfirmation,
),
],
),

View File

@ -9,6 +9,7 @@ class DidvanScaffold extends StatefulWidget {
final EdgeInsets padding;
final Color? backgroundColor;
final bool reverse;
final ScrollController? scrollController;
const DidvanScaffold({
Key? key,
@ -18,6 +19,7 @@ class DidvanScaffold extends StatefulWidget {
this.padding = const EdgeInsets.symmetric(horizontal: 16),
this.backgroundColor,
this.reverse = false,
this.scrollController,
}) : super(key: key);
@override
@ -25,7 +27,13 @@ class DidvanScaffold extends StatefulWidget {
}
class _DidvanScaffoldState extends State<DidvanScaffold> {
final _scrollController = ScrollController();
late final ScrollController _scrollController;
@override
void initState() {
_scrollController = widget.scrollController ?? ScrollController();
super.initState();
}
@override
Widget build(BuildContext context) {
@ -33,7 +41,9 @@ class _DidvanScaffoldState extends State<DidvanScaffold> {
return Scaffold(
backgroundColor: widget.backgroundColor,
body: Padding(
padding: EdgeInsets.only(top: statusBarHeight),
padding: widget.appBarData == null
? EdgeInsets.zero
: EdgeInsets.only(top: statusBarHeight),
child: Stack(
children: [
CustomScrollView(