D1APP-99 studio updates

This commit is contained in:
MohammadTaha Basiri 2022-03-16 11:08:05 +03:30
parent 9c096bcd0c
commit ddc707bbe9
20 changed files with 870 additions and 123 deletions

View File

@ -6,6 +6,7 @@ class OverviewData {
final String image; final String image;
final String description; final String description;
final int? timeToRead; final int? timeToRead;
final int? duration;
final String? reference; final String? reference;
final bool forManagers; final bool forManagers;
final String createdAt; final String createdAt;
@ -24,6 +25,7 @@ class OverviewData {
required this.marked, required this.marked,
required this.comments, required this.comments,
required this.forManagers, required this.forManagers,
this.duration,
this.timeToRead, this.timeToRead,
this.reference, this.reference,
this.categories, this.categories,
@ -39,11 +41,16 @@ class OverviewData {
forManagers: json['forManagers'] ?? false, forManagers: json['forManagers'] ?? false,
comments: json['comments'] ?? 0, comments: json['comments'] ?? 0,
createdAt: json['createdAt'], createdAt: json['createdAt'],
duration: json['duration'],
type: json['type'] ?? '', type: json['type'] ?? '',
marked: json['marked'] ?? false, marked: json['marked'] ?? false,
categories: (json['categories'] as List<dynamic>?) categories: json['categories'] != null
?.map((e) => CategoryData.fromJson(e as Map<String, dynamic>)) ? List<CategoryData>.from(
.toList(), json['categories'].map(
(e) => CategoryData.fromJson(e),
),
)
: null,
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {

View File

@ -1,37 +0,0 @@
class StudioData {
final int id;
final String title;
final String image;
final String duration;
final String createdAt;
bool marked;
StudioData({
required this.id,
required this.title,
required this.image,
required this.duration,
required this.createdAt,
required this.marked,
});
factory StudioData.fromJson(Map<String, dynamic> json) {
return StudioData(
id: json['id'],
title: json['title'],
image: json['image'],
duration: json['duration'],
createdAt: json['createdAt'],
marked: json['marked'],
);
}
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'image': image,
'duration': duration,
'createdAt': createdAt,
'marked': marked,
};
}

View File

@ -0,0 +1,61 @@
import 'package:didvan/models/overview_data.dart';
import 'package:didvan/models/tag.dart';
class StudioDetailsData {
final int id;
final int duration;
final String title;
final String description;
final String image;
final String media;
final String createdAt;
final int order;
bool marked;
int comments;
final List<Tag> tags;
final List<OverviewData> relatedContents = [];
StudioDetailsData({
required this.id,
required this.duration,
required this.title,
required this.description,
required this.image,
required this.media,
required this.createdAt,
required this.order,
required this.marked,
required this.comments,
required this.tags,
});
factory StudioDetailsData.fromJson(Map<String, dynamic> json) {
return StudioDetailsData(
id: json['id'],
duration: json['duration'],
title: json['title'],
description: json['description'],
image: json['image'],
media: json['media'],
createdAt: json['createdAt'],
order: json['order'],
marked: json['marked'],
comments: json['comments'],
tags: List<Tag>.from(json['tags'].map((e) => Tag.fromJson(e))),
);
}
Map<String, dynamic> toJson() => {
'id': id,
'duration': duration,
'title': title,
'description': description,
'image': image,
'media': media,
'createdAt': createdAt,
'order': order,
'marked': marked,
'comments': comments,
'tags': tags.map((e) => e.toJson()).toList(),
};
}

View File

@ -5,6 +5,13 @@ class AppBarData {
final String? subtitle; final String? subtitle;
final bool hasBack; final bool hasBack;
final Widget? trailing; final Widget? trailing;
final bool isSmall;
AppBarData({this.title, this.subtitle, this.hasBack = false, this.trailing}); AppBarData({
this.title,
this.subtitle,
this.hasBack = false,
this.trailing,
this.isSmall = false,
});
} }

View File

@ -14,6 +14,7 @@ class UserProvider extends CoreProvier {
static final List<MapEntry> _radarMarkQueue = []; static final List<MapEntry> _radarMarkQueue = [];
static final List<MapEntry> _newsMarkQueue = []; static final List<MapEntry> _newsMarkQueue = [];
static final List<MapEntry> _studioMarkQueue = [];
Future<String?> setAndGetToken({String? newToken}) async { Future<String?> setAndGetToken({String? newToken}) async {
if (newToken == null) { if (newToken == null) {
@ -141,6 +142,22 @@ class UserProvider extends CoreProvier {
}); });
} }
static Future<void> changeStudioMark(int id, bool value) async {
_studioMarkQueue.add(MapEntry(id, value));
Future.delayed(const Duration(milliseconds: 500), () async {
final MapEntry? lastChange =
_studioMarkQueue.lastWhereOrNull((item) => item.key == id);
if (lastChange == null) return;
final service = RequestService(RequestHelper.markStudio(id));
if (lastChange.value) {
await service.post();
} else {
await service.delete();
}
_studioMarkQueue.removeWhere((element) => element.key == id);
});
}
static Future<void> changeNewsMark(int id, bool value) async { static Future<void> changeNewsMark(int id, bool value) async {
_newsMarkQueue.add(MapEntry(id, value)); _newsMarkQueue.add(MapEntry(id, value));
Future.delayed(const Duration(milliseconds: 500), () async { Future.delayed(const Duration(milliseconds: 500), () async {

View File

@ -25,6 +25,8 @@ 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.dart';
import 'package:didvan/views/home/settings/general_settings/settings_state.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/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_state.dart'; import 'package:didvan/views/home/studio/studio_state.dart';
import 'package:didvan/views/splash/splash.dart'; import 'package:didvan/views/splash/splash.dart';
import 'package:didvan/routes/routes.dart'; import 'package:didvan/routes/routes.dart';
@ -99,6 +101,15 @@ class RouteGenerator {
), ),
), ),
); );
case Routes.studioDetails:
return _createRoute(
ChangeNotifierProvider<StudioDetailsState>(
create: (context) => StudioDetailsState(),
child: StudioDetails(
pageData: settings.arguments as Map<String, dynamic>,
),
),
);
case Routes.directList: case Routes.directList:
return _createRoute( return _createRoute(
ChangeNotifierProvider<DirectListState>( ChangeNotifierProvider<DirectListState>(

View File

@ -8,6 +8,7 @@ class Routes {
static const String generalSettings = '/general-settings'; static const String generalSettings = '/general-settings';
static const String radarDetails = '/radar-details'; static const String radarDetails = '/radar-details';
static const String newsDetails = '/news-details'; static const String newsDetails = '/news-details';
static const String studioDetails = '/studio-details';
static const String directList = '/direct-list'; static const String directList = '/direct-list';
static const String direct = '/direct'; static const String direct = '/direct';
static const String comments = '/comments'; static const String comments = '/comments';

View File

@ -19,8 +19,19 @@ class RequestHelper {
static const String checkUsername = _baseUserUrl + '/CheckUsername'; static const String checkUsername = _baseUserUrl + '/CheckUsername';
static const String updateProfile = _baseUserUrl + '/profile/edit'; static const String updateProfile = _baseUserUrl + '/profile/edit';
static const String otp = _baseUserUrl + '/otp'; static const String otp = _baseUserUrl + '/otp';
static String bookmarks({String? type}) => static String bookmarks({
_baseUserUrl + '/marked/${type ?? ''}'; required int page,
String? search,
String? type,
String? studioType,
}) =>
_baseUserUrl +
'/marked/${type ?? ''}' +
_urlConcatGenerator([
MapEntry('page', page),
MapEntry('type', studioType),
MapEntry('search', search),
]);
static const String directTypes = baseUrl + '/direct/types'; static const String directTypes = baseUrl + '/direct/types';
static String direct(int id) => _baseDirectUrl + '/$id'; static String direct(int id) => _baseDirectUrl + '/$id';
@ -36,10 +47,10 @@ class RequestHelper {
baseUrl + baseUrl +
'/tag' + '/tag' +
_urlConcatGenerator([ _urlConcatGenerator([
MapEntry('page', page?.toString()), MapEntry('page', page),
MapEntry('limit', limit?.toString() ?? '3'), MapEntry('limit', limit ?? '3'),
MapEntry('type', type), MapEntry('type', type),
MapEntry('id', itemId?.toString() ?? '1'), MapEntry('id', itemId ?? '1'),
MapEntry('tags', _urlListConcatGenerator(ids)), MapEntry('tags', _urlListConcatGenerator(ids)),
]); ]);
@ -52,7 +63,7 @@ class RequestHelper {
_baseRadarUrl + _baseRadarUrl +
'/$id' + '/$id' +
_urlConcatGenerator([ _urlConcatGenerator([
MapEntry('page', args.page.toString()), MapEntry('page', args.page),
MapEntry('start', args.startDate), MapEntry('start', args.startDate),
MapEntry('end', args.endDate), MapEntry('end', args.endDate),
MapEntry('search', args.search), MapEntry('search', args.search),
@ -61,7 +72,7 @@ class RequestHelper {
static String radarOverviews({required RadarRequestArgs args}) => static String radarOverviews({required RadarRequestArgs args}) =>
_baseRadarUrl + _baseRadarUrl +
_urlConcatGenerator([ _urlConcatGenerator([
MapEntry('page', args.page.toString()), MapEntry('page', args.page),
MapEntry('start', args.startDate), MapEntry('start', args.startDate),
MapEntry('end', args.endDate), MapEntry('end', args.endDate),
MapEntry('search', args.search), MapEntry('search', args.search),
@ -77,7 +88,7 @@ class RequestHelper {
_baseNewsUrl + _baseNewsUrl +
'/$id' + '/$id' +
_urlConcatGenerator([ _urlConcatGenerator([
MapEntry('page', args.page.toString()), MapEntry('page', args.page),
MapEntry('start', args.startDate), MapEntry('start', args.startDate),
MapEntry('end', args.endDate), MapEntry('end', args.endDate),
MapEntry('search', args.search), MapEntry('search', args.search),
@ -85,7 +96,7 @@ class RequestHelper {
static String newsOverviews({required NewsRequestArgs args}) => static String newsOverviews({required NewsRequestArgs args}) =>
_baseNewsUrl + _baseNewsUrl +
_urlConcatGenerator([ _urlConcatGenerator([
MapEntry('page', args.page.toString()), MapEntry('page', args.page),
MapEntry('start', args.startDate), MapEntry('start', args.startDate),
MapEntry('end', args.endDate), MapEntry('end', args.endDate),
MapEntry('search', args.search), MapEntry('search', args.search),
@ -101,27 +112,29 @@ class RequestHelper {
_baseStudioUrl + _baseStudioUrl +
'/$id' + '/$id' +
_urlConcatGenerator([ _urlConcatGenerator([
MapEntry('page', args.page.toString()), MapEntry('page', args.page),
MapEntry('type', args.type), MapEntry('type', args.type),
MapEntry('order', args.order), MapEntry('order', args.order),
MapEntry('search', args.search), MapEntry('search', args.search),
]); ]);
static String studioOverviews({required StudioRequestArgs args}) => static String studioOverviews({required StudioRequestArgs args}) =>
_baseNewsUrl + _baseStudioUrl +
_urlConcatGenerator([ _urlConcatGenerator([
MapEntry('page', args.page.toString()), MapEntry('page', args.page),
MapEntry('type', args.type), MapEntry('type', args.type),
MapEntry('order', args.order), MapEntry('order', args.order),
MapEntry('search', args.search), MapEntry('search', args.search),
]); ]);
static String _urlConcatGenerator(List<MapEntry<String, String?>> additions) { static String _urlConcatGenerator(List<MapEntry<String, dynamic>> additions) {
String result = ''; String result = '';
additions.removeWhere((element) => element.value == null); additions.removeWhere(
(element) => element.value == null || element.value.toString().isEmpty,
);
if (additions.isNotEmpty) { if (additions.isNotEmpty) {
result += '?'; result += '?';
for (var i = 0; i < additions.length; i++) { for (var i = 0; i < additions.length; i++) {
result += (additions[i].key + '=' + additions[i].value!); result += (additions[i].key + '=' + additions[i].value!.toString());
if (i != additions.length - 1) { if (i != additions.length - 1) {
result += '&'; result += '&';
} }

View File

@ -1,11 +1,14 @@
import 'package:didvan/constants/app_icons.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/models/view/action_sheet_data.dart';
import 'package:didvan/utils/action_sheet.dart'; import 'package:didvan/utils/action_sheet.dart';
import 'package:didvan/views/home/studio/studio_state.dart'; import 'package:didvan/views/home/studio/studio_state.dart';
import 'package:didvan/views/home/studio/widgets/slider.dart'; import 'package:didvan/views/home/studio/widgets/slider.dart';
import 'package:didvan/views/home/studio/widgets/tab_bar.dart'; import 'package:didvan/views/home/studio/widgets/tab_bar.dart';
import 'package:didvan/views/home/widgets/logo_app_bar.dart'; import 'package:didvan/views/home/widgets/logo_app_bar.dart';
import 'package:didvan/views/home/widgets/podcast_overview.dart';
import 'package:didvan/views/home/widgets/search_field.dart'; import 'package:didvan/views/home/widgets/search_field.dart';
import 'package:didvan/views/home/widgets/video_overview.dart';
import 'package:didvan/views/widgets/didvan/divider.dart'; import 'package:didvan/views/widgets/didvan/divider.dart';
import 'package:didvan/views/widgets/didvan/icon_button.dart'; import 'package:didvan/views/widgets/didvan/icon_button.dart';
import 'package:didvan/views/widgets/didvan/radial_button.dart'; import 'package:didvan/views/widgets/didvan/radial_button.dart';
@ -24,6 +27,12 @@ class Studio extends StatefulWidget {
class _StudioState extends State<Studio> { class _StudioState extends State<Studio> {
final FocusNode _focusNode = FocusNode(); final FocusNode _focusNode = FocusNode();
@override
void initState() {
context.read<StudioState>().init();
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CustomScrollView( return CustomScrollView(
@ -86,9 +95,41 @@ class _StudioState extends State<Studio> {
Consumer<StudioState>( Consumer<StudioState>(
builder: (context, state, child) => SliverStateHandler<StudioState>( builder: (context, state, child) => SliverStateHandler<StudioState>(
state: state, state: state,
builder: (context, state, index) => Container(), itemPadding: const EdgeInsets.only(
bottom: 8,
left: 16,
right: 16,
),
placeholder: state.videosSelected
? VideoOverview.placeHolder
: PodcastOverview.placeholder,
builder: (context, state, index) => state.videosSelected
? VideoOverview(
onMarkChanged: state.changeMark,
hasUnmarkConfirmation: false,
video: state.studios[index],
onCommentsChanged: state.onCommentsChanged,
studioRequestArgs: StudioRequestArgs(
page: state.page,
order: state.order,
search: state.search,
type: state.type,
),
)
: PodcastOverview(
podcast: state.studios[index],
onMarkChanged: state.changeMark,
hasUnmarkConfirmation: false,
onCommentsChanged: state.onCommentsChanged,
studioRequestArgs: StudioRequestArgs(
page: state.page,
order: state.order,
search: state.search,
type: state.type,
),
),
childCount: state.studios.length, childCount: state.studios.length,
onRetry: () {}, onRetry: () => state.getStudioOverviews(page: 1),
), ),
), ),
], ],
@ -132,7 +173,7 @@ class _StudioState extends State<Studio> {
titleIcon: DidvanIcons.sort_regular, titleIcon: DidvanIcons.sort_regular,
hasDismissButton: false, hasDismissButton: false,
confrimTitle: 'مرتب سازی', confrimTitle: 'مرتب سازی',
onConfirmed: () {}, onConfirmed: () => state.getStudioOverviews(page: 1),
), ),
); );
} }

View File

@ -0,0 +1,210 @@
import 'dart:io';
import 'package:didvan/config/design_config.dart';
import 'package:didvan/models/studio_details_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/widgets/audio_slider.dart';
import 'package:didvan/views/widgets/didvan/scaffold.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:flutter/services.dart';
import 'package:provider/provider.dart';
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> {
bool _isFullScreen = false;
bool _isInit = true;
double _dwInPortrait = 0;
double _scaleInPortrait = 1;
bool get _isVideo => widget.pageData['isVideo'];
@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();
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) => state.studios.isEmpty
? const SizedBox()
: WillPopScope(
onWillPop: () async {
if (_isFullScreen) {
await _changeFullSceen(false);
return false;
}
return true;
},
child: DidvanScaffold(
appBarData: _isFullScreen
? null
: AppBarData(
isSmall: true,
title: state.currentStudio.title,
),
children: [
if (_isVideo)
SizedBox(
width: ds.width,
height: _isFullScreen ? ds.height : ds.width * 9 / 16,
child: Stack(
children: [
WebView(
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,
),
Positioned(
right: 42,
bottom: 8,
child: GestureDetector(
onTap: () => _changeFullSceen(!_isFullScreen),
child: Container(
color: Colors.transparent,
width: 24,
height: 30,
),
),
),
],
),
),
if (!_isVideo)
AudioPlayerWidget(podcast: state.currentStudio),
],
),
),
),
);
}
}
class AudioPlayerWidget extends StatelessWidget {
final StudioDetailsData podcast;
const AudioPlayerWidget({Key? key, required this.podcast}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Hero(
tag: podcast.media,
child: SkeletonImage(
imageUrl: podcast.image,
aspectRatio: 1 / 1,
),
),
),
const SizedBox(height: 16),
DidvanText(
podcast.title,
style: Theme.of(context).textTheme.bodyText1,
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: AudioSlider(
tag: podcast.media,
showTimer: true,
),
),
],
);
}
}

View File

@ -0,0 +1,141 @@
import 'dart:async';
import 'dart:math';
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/overview_data.dart';
import 'package:didvan/models/requests/studio.dart';
import 'package:didvan/models/studio_details_data.dart';
import 'package:didvan/providers/core_provider.dart';
import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart';
class StudioDetailsState extends CoreProvier {
final List<StudioDetailsData?> studios = [];
late Timer _trackingTimer;
int _trackingTimerCounter = 0;
late final int initialIndex;
late final StudioRequestArgs args;
bool isFetchingNewItem = false;
final List<int> relatedQueue = [];
int _currentIndex = 0;
int get currentIndex => _currentIndex;
StudioDetailsData get currentStudio {
try {
return studios[_currentIndex]!;
} catch (e) {
return studios[_currentIndex + 1]!;
}
}
Future<void> getStudioDetails(int id, {bool? isForward}) async {
if (isForward == null) {
appState = AppState.busy;
} else {
isFetchingNewItem = true;
notifyListeners();
}
final service = RequestService(RequestHelper.studioDetails(id, args));
await service.httpGet();
_handleTracking(sendRequest: isForward != null);
if (service.isSuccess) {
final result = service.result;
final studio = StudioDetailsData.fromJson(result['studio']);
if (args.page == 0) {
studios.add(studio);
initialIndex = 0;
appState = AppState.idle;
return;
}
StudioDetailsData? prevStudio;
if (result['prevStudio'].isNotEmpty) {
prevStudio = StudioDetailsData.fromJson(result['prevStudio']);
}
StudioDetailsData? nextStudio;
if (result['nextStudio'].isNotEmpty) {
nextStudio = StudioDetailsData.fromJson(result['nextStudio']);
}
if (isForward == null) {
studios
.addAll(List.generate(max(studio.order - 2, 0), (index) => null));
if (prevStudio != null) {
studios.add(prevStudio);
}
studios.add(studio);
if (nextStudio != null) {
studios.add(nextStudio);
}
_currentIndex = initialIndex = studio.order - 1;
} else if (isForward) {
if (!exists(nextStudio) && nextStudio != null) {
studios.add(nextStudio);
}
_currentIndex++;
} else if (!isForward) {
if (!exists(prevStudio) && prevStudio != null) {
studios[_currentIndex - 2] = prevStudio;
}
_currentIndex--;
}
isFetchingNewItem = false;
appState = AppState.idle;
return;
}
//why? total page state shouldn't die!
if (isForward == null) {
appState = AppState.failed;
}
}
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',
));
await service.httpGet();
if (service.isSuccess) {
final relateds = service.result['contents'];
for (var i = 0; i < relateds.length; i++) {
studios
.where((element) => element != null)
.firstWhere((element) => element!.id == currentStudio.id)!
.relatedContents
.add(OverviewData.fromJson(relateds[i]));
}
notifyListeners();
}
}
bool exists(StudioDetailsData? studio) =>
studios.any((r) => studio != null && r != null && r.id == studio.id);
void onCommentsChanged(int count) {
studios.firstWhere((studio) => studio?.id == currentStudio.id)!.comments =
count;
notifyListeners();
}
Future<void> _handleTracking({bool sendRequest = true}) async {
if (!sendRequest) {
_trackingTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
_trackingTimerCounter++;
});
return;
}
//send request
_trackingTimerCounter = 0;
}
@override
void dispose() {
_trackingTimer.cancel();
super.dispose();
}
}

View File

@ -1,12 +1,13 @@
import 'package:didvan/models/enums.dart'; import 'package:didvan/models/enums.dart';
import 'package:didvan/models/overview_data.dart';
import 'package:didvan/models/requests/studio.dart'; import 'package:didvan/models/requests/studio.dart';
import 'package:didvan/models/srudio_data.dart';
import 'package:didvan/providers/core_provider.dart'; import 'package:didvan/providers/core_provider.dart';
import 'package:didvan/providers/user_provider.dart';
import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart'; import 'package:didvan/services/network/request_helper.dart';
class StudioState extends CoreProvier { class StudioState extends CoreProvier {
final List<StudioData> studios = []; final List<OverviewData> studios = [];
String? search; String? search;
String? lastSearch; String? lastSearch;
@ -22,52 +23,71 @@ class StudioState extends CoreProvier {
set videosSelected(bool value) { set videosSelected(bool value) {
if (_videosSelected == value) return; if (_videosSelected == value) return;
_videosSelected = value; _videosSelected = value;
studios.clear();
getStudioOverviews(page: page); getStudioOverviews(page: page);
} }
void init() { void init() {
search = ''; search = '';
lastSearch = ''; lastSearch = '';
videosSelected = true; _videosSelected = true;
selectedSortTypeIndex = 0; selectedSortTypeIndex = 0;
Future.delayed(Duration.zero, () { Future.delayed(Duration.zero, () {
getStudioOverviews(page: 1); getStudioOverviews(page: 1);
}); });
} }
String get _order { String get order {
if (selectedSortTypeIndex == 0) return 'date'; if (selectedSortTypeIndex == 0) return 'date';
if (selectedSortTypeIndex == 1) return 'view'; if (selectedSortTypeIndex == 1) return 'view';
return 'comment'; return 'comment';
} }
String get type {
if (videosSelected) return 'video';
return 'podcast';
}
Future<void> getStudioOverviews({required int page}) async { Future<void> getStudioOverviews({required int page}) async {
this.page = page; this.page = page;
if (page == 1) { if (page == 1) {
appState = AppState.busy; appState = AppState.busy;
} }
final service = RequestService( final service = RequestService(
RequestHelper.studioOverviews( RequestHelper.studioOverviews(
args: StudioRequestArgs( args: StudioRequestArgs(
page: page, page: page,
type: videosSelected ? 'video' : 'podcast', type: type,
search: search, search: search,
order: _order, order: order,
), ),
), ),
); );
await service.httpGet(); await service.httpGet();
if (service.isSuccess) { if (service.isSuccess) {
if (page == 1) {
studios.clear();
}
lastPage = service.result['lastPage']; lastPage = service.result['lastPage'];
final studioItems = service.result['studios']; final studioItems = service.result['studios'];
for (var i = 0; i < studioItems.length; i++) { for (var i = 0; i < studioItems.length; i++) {
studios.add(StudioData.fromJson(studioItems[i])); studios.add(OverviewData.fromJson(studioItems[i]));
} }
appState = AppState.idle; appState = AppState.idle;
return; return;
} }
appState = AppState.failed; appState = AppState.failed;
} }
Future<void> changeMark(int id, bool value) async {
studios.firstWhere((element) => element.id == id).marked = value;
notifyListeners();
UserProvider.changeStudioMark(id, value);
}
void onCommentsChanged(int id, int count) {
studios.firstWhere((radar) => radar.id == id).comments = count;
notifyListeners();
}
} }

View File

@ -1,10 +1,13 @@
import 'package:audio_video_progress_bar/audio_video_progress_bar.dart'; import 'package:audio_video_progress_bar/audio_video_progress_bar.dart';
import 'package:didvan/config/design_config.dart';
import 'package:didvan/services/media/media.dart'; import 'package:didvan/services/media/media.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class AudioSlider extends StatelessWidget { class AudioSlider extends StatelessWidget {
final String tag; final String tag;
const AudioSlider({Key? key, required this.tag}) : super(key: key); final bool showTimer;
const AudioSlider({Key? key, required this.tag, this.showTimer = false})
: super(key: key);
bool get _isPlaying => MediaService.audioPlayerTag == tag; bool get _isPlaying => MediaService.audioPlayerTag == tag;
@ -24,7 +27,14 @@ class AudioSlider extends StatelessWidget {
_isPlaying ? MediaService.audioPlayer.bufferedPosition : null, _isPlaying ? MediaService.audioPlayer.bufferedPosition : null,
thumbRadius: 6, thumbRadius: 6,
barHeight: 3, barHeight: 3,
timeLabelTextStyle: const TextStyle(fontSize: 0), timeLabelTextStyle: TextStyle(
fontSize: showTimer ? null : 0,
height: showTimer ? null : 0,
fontFamily: DesignConfig.fontFamily.replaceAll(
'-FA',
'',
),
),
onSeek: (value) => _onSeek(value.inMilliseconds), onSeek: (value) => _onSeek(value.inMilliseconds),
); );
}, },

View File

@ -0,0 +1,47 @@
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/utils/date_time.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:flutter/material.dart';
class DurationWidget extends StatelessWidget {
final int duration;
const DurationWidget({Key? key, required this.duration}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.focusedBorder,
),
borderRadius: BorderRadius.circular(5),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
DidvanIcons.timer_regular,
size: 16,
color: Theme.of(context).colorScheme.focusedBorder,
),
const SizedBox(width: 4),
DidvanText(
DateTimeUtils.normalizeTimeDuration(
Duration(seconds: duration),
),
isEnglishFont: true,
color: Theme.of(context).colorScheme.focusedBorder,
),
const SizedBox(width: 4),
Icon(
DidvanIcons.play_circle_regular,
size: 16,
color: Theme.of(context).colorScheme.focusedBorder,
),
],
),
);
}
}

View File

@ -1,38 +1,46 @@
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/overview_data.dart'; import 'package:didvan/models/overview_data.dart';
import 'package:didvan/models/requests/news.dart'; import 'package:didvan/models/requests/studio.dart';
import 'package:didvan/routes/routes.dart'; import 'package:didvan/routes/routes.dart';
import 'package:didvan/utils/date_time.dart'; import 'package:didvan/utils/date_time.dart';
import 'package:didvan/views/home/widgets/bookmark_button.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'; import 'package:didvan/views/widgets/didvan/card.dart';
import 'package:didvan/views/widgets/didvan/divider.dart'; import 'package:didvan/views/widgets/didvan/divider.dart';
import 'package:didvan/views/widgets/didvan/icon_button.dart';
import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/shimmer_placeholder.dart'; import 'package:didvan/views/widgets/shimmer_placeholder.dart';
import 'package:didvan/views/widgets/skeleton_image.dart'; import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class PodcastOverview extends StatelessWidget { class PodcastOverview extends StatelessWidget {
final OverviewData news; final OverviewData podcast;
final NewsRequestArgs? newsRequestArgs; final void Function(int id, int count) onCommentsChanged;
final void Function(int id, bool value) onMarkChanged; final void Function(int id, bool value) onMarkChanged;
final bool hasUnmarkConfirmation; final bool hasUnmarkConfirmation;
final StudioRequestArgs? studioRequestArgs;
const PodcastOverview({ const PodcastOverview({
Key? key, Key? key,
required this.news, required this.podcast,
required this.onCommentsChanged,
required this.onMarkChanged, required this.onMarkChanged,
this.newsRequestArgs, required this.hasUnmarkConfirmation,
this.hasUnmarkConfirmation = false, this.studioRequestArgs,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DidvanCard( return DidvanCard(
onTap: () => Navigator.of(context).pushNamed( onTap: () => Navigator.of(context).pushNamed(
Routes.newsDetails, Routes.studioDetails,
arguments: { arguments: {
'onMarkChanged': onMarkChanged, 'onMarkChanged': onMarkChanged,
'id': news.id, 'onCommentsChanged': onCommentsChanged,
'args': newsRequestArgs, 'id': podcast.id,
'args': studioRequestArgs,
'hasUnmarkConfirmation': hasUnmarkConfirmation, 'hasUnmarkConfirmation': hasUnmarkConfirmation,
'isVideo': false,
}, },
), ),
child: Column( child: Column(
@ -41,47 +49,50 @@ class PodcastOverview extends StatelessWidget {
Row( Row(
children: [ children: [
SkeletonImage( SkeletonImage(
imageUrl: news.image, imageUrl: podcast.image,
width: 64, width: 64,
height: 64, height: 64,
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: SizedBox( child: Column(
height: 64, crossAxisAlignment: CrossAxisAlignment.start,
child: DidvanText( children: [
news.title, DidvanText(
style: Theme.of(context).textTheme.bodyText1, podcast.title,
), style: Theme.of(context).textTheme.bodyText1,
),
const SizedBox(height: 4),
DidvanText(
DateTimeUtils.momentGenerator(podcast.createdAt),
style: Theme.of(context).textTheme.overline,
color: Theme.of(context).colorScheme.caption,
),
],
), ),
), ),
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
DidvanText( DidvanText(
news.description, podcast.description,
maxLines: 3, maxLines: 2,
overflow: TextOverflow.ellipsis,
), ),
const DidvanDivider(verticalPadding: 8), const DidvanDivider(verticalPadding: 8),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Row( DurationWidget(duration: podcast.duration!),
children: [ const Spacer(),
DidvanText( DidvanIconButton(
news.reference!, gestureSize: 28,
style: Theme.of(context).textTheme.caption, icon: DidvanIcons.download_regular,
), onPressed: () {},
DidvanText(
' - ' + DateTimeUtils.momentGenerator(news.createdAt),
style: Theme.of(context).textTheme.caption,
),
],
), ),
const SizedBox(width: 16),
BookmarkButton( BookmarkButton(
value: news.marked, value: podcast.marked,
onMarkChanged: (value) => onMarkChanged(news.id, value), onMarkChanged: (value) => onMarkChanged(podcast.id, value),
askForConfirmation: hasUnmarkConfirmation,
), ),
], ],
), ),
@ -95,7 +106,7 @@ class PodcastOverview extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
const ShimmerPlaceholder(height: 64, width: 64), const ShimmerPlaceholder(height: 64, width: 64),
const SizedBox(width: 8), const SizedBox(width: 8),
@ -109,7 +120,7 @@ class PodcastOverview extends StatelessWidget {
), ),
], ],
), ),
const SizedBox(height: 12), const SizedBox(height: 16),
const ShimmerPlaceholder( const ShimmerPlaceholder(
height: 16, height: 16,
width: double.infinity, width: double.infinity,
@ -117,19 +128,21 @@ class PodcastOverview extends StatelessWidget {
const SizedBox(height: 8), const SizedBox(height: 8),
const ShimmerPlaceholder( const ShimmerPlaceholder(
height: 16, height: 16,
width: double.infinity, width: 200,
),
const SizedBox(height: 8),
const ShimmerPlaceholder(
height: 16,
width: 100,
), ),
const SizedBox(height: 4),
const DidvanDivider(verticalPadding: 8), const DidvanDivider(verticalPadding: 8),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
children: const [ ShimmerPlaceholder(
ShimmerPlaceholder(height: 12, width: 150), height: 36,
ShimmerPlaceholder(height: 24, width: 24), width: 92,
borderRadius: BorderRadius.circular(5),
),
const Spacer(),
const ShimmerPlaceholder(width: 24, height: 24),
const SizedBox(width: 16),
const ShimmerPlaceholder(width: 24, height: 24),
], ],
), ),
], ],

View File

@ -0,0 +1,157 @@
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart';
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/widgets/bookmark_button.dart';
import 'package:didvan/views/home/widgets/duration_widget.dart';
import 'package:didvan/views/widgets/didvan/card.dart';
import 'package:didvan/views/widgets/didvan/divider.dart';
import 'package:didvan/views/widgets/didvan/icon_button.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';
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,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return DidvanCard(
onTap: () => Navigator.of(context).pushNamed(
Routes.studioDetails,
arguments: {
'onMarkChanged': onMarkChanged,
'onCommentsChanged': onCommentsChanged,
'id': video.id,
'args': studioRequestArgs,
'hasUnmarkConfirmation': hasUnmarkConfirmation,
'isVideo': true,
},
),
child: Row(
children: [
Stack(
children: [
SkeletonImage(
imageUrl: video.image,
height: 108,
width: 108,
),
Positioned.fill(
child: Center(
child: Container(
height: 28,
width: 28,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context)
.colorScheme
.secondary
.withOpacity(0.7),
),
child: Icon(
DidvanIcons.play_solid,
color: Theme.of(context).colorScheme.white,
),
),
),
),
],
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DidvanText(
video.title,
style: Theme.of(context).textTheme.bodyText1,
),
const SizedBox(height: 4),
Row(
children: [
const Icon(
DidvanIcons.calendar_day_regular,
size: 16,
),
const SizedBox(width: 4),
DidvanText(
DateTimeUtils.momentGenerator(video.createdAt),
style: Theme.of(context).textTheme.overline,
color: Theme.of(context).colorScheme.caption,
),
],
),
const DidvanDivider(verticalPadding: 8),
Row(
children: [
DurationWidget(duration: video.duration!),
const Spacer(),
DidvanIconButton(
gestureSize: 28,
icon: DidvanIcons.download_regular,
onPressed: () {},
),
const SizedBox(width: 16),
BookmarkButton(
value: video.marked,
onMarkChanged: (value) => onMarkChanged(video.id, value),
),
],
),
],
),
),
],
),
);
}
static Widget get placeHolder => DidvanCard(
child: Row(
children: [
const ShimmerPlaceholder(height: 108, width: 108),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ShimmerPlaceholder(height: 20),
const SizedBox(height: 8),
const ShimmerPlaceholder(width: 100, height: 16),
const DidvanDivider(verticalPadding: 10),
Row(
children: [
ShimmerPlaceholder(
height: 36,
width: 92,
borderRadius: BorderRadius.circular(5),
),
const Spacer(),
const ShimmerPlaceholder(width: 24, height: 24),
const SizedBox(width: 16),
const ShimmerPlaceholder(width: 24, height: 24),
],
),
],
),
),
],
),
);
}

View File

@ -18,11 +18,10 @@ class DidvanAppBar extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
height: kToolbarHeight + MediaQuery.of(context).padding.top, height: appBarData.isSmall ? 56 : 72,
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.only(right: 4, left: 20), padding: const EdgeInsets.only(right: 4, left: 20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: backgroundColor,
border: hasBorder border: hasBorder
? Border( ? Border(
bottom: BorderSide( bottom: BorderSide(

View File

@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
class DidvanScaffold extends StatefulWidget { class DidvanScaffold extends StatefulWidget {
final List<Widget>? slivers; final List<Widget>? slivers;
final List<Widget>? children; final List<Widget>? children;
final AppBarData appBarData; final AppBarData? appBarData;
final EdgeInsets padding; final EdgeInsets padding;
final Color? backgroundColor; final Color? backgroundColor;
final bool reverse; final bool reverse;
@ -40,14 +40,14 @@ class _DidvanScaffoldState extends State<DidvanScaffold> {
controller: _scrollController, controller: _scrollController,
reverse: widget.reverse, reverse: widget.reverse,
slivers: [ slivers: [
if (!widget.reverse) if (!widget.reverse && widget.appBarData != null)
SliverAppBar( SliverAppBar(
toolbarHeight: kToolbarHeight, toolbarHeight: widget.appBarData!.isSmall ? 56 : 72,
backgroundColor: widget.backgroundColor ?? backgroundColor: widget.backgroundColor ??
Theme.of(context).colorScheme.background, Theme.of(context).colorScheme.background,
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
pinned: true, pinned: true,
flexibleSpace: DidvanAppBar(appBarData: widget.appBarData), flexibleSpace: DidvanAppBar(appBarData: widget.appBarData!),
), ),
if (!widget.reverse) if (!widget.reverse)
const SliverToBoxAdapter( const SliverToBoxAdapter(
@ -79,9 +79,9 @@ class _DidvanScaffoldState extends State<DidvanScaffold> {
), ),
], ],
), ),
if (widget.reverse) if (widget.reverse && widget.appBarData != null)
_AppBar( _AppBar(
appBarData: widget.appBarData, appBarData: widget.appBarData!,
scrollController: _scrollController, scrollController: _scrollController,
), ),
], ],

View File

@ -566,7 +566,7 @@ packages:
name: plugin_platform_interface name: plugin_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.2" version: "2.1.2"
process: process:
dependency: transitive dependency: transitive
description: description:
@ -782,6 +782,34 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.1" version: "2.1.1"
webview_flutter:
dependency: "direct main"
description:
name: webview_flutter
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
webview_flutter_android:
dependency: transitive
description:
name: webview_flutter_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.8.3"
webview_flutter_platform_interface:
dependency: transitive
description:
name: webview_flutter_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.1"
webview_flutter_wkwebview:
dependency: transitive
description:
name: webview_flutter_wkwebview
url: "https://pub.dartlang.org"
source: hosted
version: "2.7.1"
win32: win32:
dependency: transitive dependency: transitive
description: description:

View File

@ -62,6 +62,7 @@ dependencies:
image_cropper: ^1.5.0 image_cropper: ^1.5.0
firebase_messaging: ^11.2.8 firebase_messaging: ^11.2.8
firebase_core: ^1.13.1 firebase_core: ^1.13.1
webview_flutter: ^3.0.1
dev_dependencies: dev_dependencies: