design updates + bug fixes

This commit is contained in:
MohammadTaha Basiri 2022-03-29 11:58:12 +04:30
parent 9b221c4463
commit 1d401b3ba0
23 changed files with 472 additions and 390 deletions

View File

@ -3,7 +3,7 @@ import 'package:didvan/utils/action_sheet.dart';
import 'package:flutter/cupertino.dart';
class CoreProvier with ChangeNotifier {
AppState _appState = AppState.idle;
AppState _appState = AppState.busy;
set appState(AppState newState) {
if (newState == AppState.isolatedBusy) {

View File

@ -1,5 +1,6 @@
import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart';
import 'package:collection/collection.dart';
class ServerDataProvider {
static final List<MapEntry> directTypes = [];
@ -10,7 +11,10 @@ class ServerDataProvider {
static int labelToTypeId(String label) => label.contains('پشتیبانی')
? 7
: directTypes.firstWhere((element) => element.value.contains(label)).key;
: directTypes
.firstWhereOrNull((element) => element.value.contains(label))
?.key ??
7;
static Future<void> _getDirectTypes() async {
final service = RequestService(RequestHelper.directTypes);

View File

@ -29,7 +29,8 @@ class UserProvider extends CoreProvier {
isAuthenticated = true;
final RequestService service = RequestService(RequestHelper.userInfo);
await service.httpGet();
if (service.statusCode == 401 || service.result['user'] == null) {
if (service.statusCode == 401 ||
(service.isSuccess && service.result['user'] == null)) {
return false;
}
if (service.isSuccess) {

View File

@ -180,18 +180,24 @@ class RouteGenerator {
final shortestSide = MediaQuery.of(context).size.shortestSide;
final bool useMobileLayout = shortestSide < 600;
if (kIsWeb && !useMobileLayout) {
return Container(
return MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
child: Container(
color: Theme.of(context).colorScheme.background,
alignment: Alignment.center,
child: AspectRatio(aspectRatio: 9 / 16, child: page),
),
);
}
return Container(
return MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
child: Container(
color: Theme.of(context).colorScheme.surface,
child: SafeArea(
child: page,
top: false,
),
),
);
},
);

View File

@ -25,7 +25,6 @@ class CommentsState extends CoreProvier {
int itemId = 0;
Future<void> getComments() async {
appState = AppState.busy;
final service = RequestService(
RequestHelper.comments(itemId, type),
);

View File

@ -43,7 +43,6 @@ class _NewsDetailsState extends State<NewsDetails> {
state: state,
builder: (context, state) => Stack(
children: [
if (state.news.isNotEmpty)
IgnorePointer(
ignoring: state.isFetchingNewItem,
child: DidvanPageView(
@ -55,7 +54,6 @@ class _NewsDetailsState extends State<NewsDetails> {
currentIndex: state.currentIndex,
),
),
if (state.news.isNotEmpty)
Positioned(
bottom: 0,
left: 0,

View File

@ -43,7 +43,6 @@ class _RadarDetailsState extends State<RadarDetails> {
state: state,
builder: (context, state) => Stack(
children: [
if (state.radars.isNotEmpty)
IgnorePointer(
ignoring: state.isFetchingNewItem,
child: DidvanPageView(
@ -55,7 +54,6 @@ class _RadarDetailsState extends State<RadarDetails> {
currentIndex: state.currentIndex,
),
),
if (state.radars.isNotEmpty)
Positioned(
bottom: 0,
left: 0,

View File

@ -6,8 +6,6 @@ import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart';
class FilteredBookmarksState extends CoreProvier {
String search = '';
String lastSearch = '';
final List<OverviewData> bookmarks = [];
final String type;
int page = 1;
@ -15,17 +13,8 @@ class FilteredBookmarksState extends CoreProvier {
FilteredBookmarksState(this.type);
bool get searching => search != '';
Future<void> getBookmarks({required int page}) async {
if (search != '') {
lastSearch = search;
}
if (page == 1) {
bookmarks.clear();
}
this.page = page;
appState = AppState.busy;
String typeString = '';
if (type == 'video' || type == 'podcast') {
typeString = 'studios';

View File

@ -15,7 +15,6 @@ class DirectListState extends CoreProvier {
}
Future<void> getDirectsList() async {
appState = AppState.busy;
final RequestService service = RequestService(RequestHelper.directs);
await service.httpGet();
if (service.isSuccess) {

View File

@ -4,7 +4,9 @@ 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/details_tab_bar.dart';
import 'package:didvan/views/home/studio/studio_details/widgets/studio_details_widget.dart';
import 'package:didvan/views/home/widgets/bookmark_button.dart';
import 'package:didvan/views/widgets/didvan/scaffold.dart';
import 'package:didvan/views/widgets/state_handlers/state_handler.dart';
import 'package:flutter/foundation.dart';
@ -39,7 +41,7 @@ class _StudioDetailsState extends State<StudioDetails> {
() => state.getStudioDetails(widget.pageData['id']),
);
state.args = widget.pageData['args'];
if (!kIsWeb && Platform.isAndroid) WebView.platform = AndroidWebView();
if (Platform.isAndroid) WebView.platform = AndroidWebView();
super.initState();
}
@ -107,9 +109,27 @@ class _StudioDetailsState extends State<StudioDetails> {
appBarData: _isFullScreen
? null
: AppBarData(
trailing: BookmarkButton(
value: state.studio.marked,
onMarkChanged: (value) => widget
.pageData['onMarkChanged'](state.studio.id, value),
gestureSize: 48,
),
isSmall: true,
title: state.studio.title,
),
showSliversFirst: true,
slivers: [
SliverAppBar(
automaticallyImplyLeading: false,
pinned: true,
backgroundColor: Theme.of(context).colorScheme.surface,
toolbarHeight:
(_isFullScreen ? ds.height : ds.width * 9 / 16) +
72 -
MediaQuery.of(context).padding.top,
elevation: 0,
flexibleSpace: Column(
children: [
SizedBox(
width: ds.width,
@ -117,7 +137,8 @@ class _StudioDetailsState extends State<StudioDetails> {
child: Stack(
children: [
WebView(
backgroundColor: Theme.of(context).colorScheme.black,
backgroundColor:
Theme.of(context).colorScheme.black,
allowsInlineMediaPlayback: true,
initialUrl: Uri.dataFromString(
'''
@ -172,9 +193,8 @@ class _StudioDetailsState extends State<StudioDetails> {
],
),
),
const SizedBox(height: 20),
StudioDetailsWidget(
scrollController: _scrollController,
DetailsTabBar(
isVideo: true,
onCommentsTabSelected: () => Future.delayed(
const Duration(milliseconds: 100),
() => _scrollController.animateTo(
@ -183,6 +203,14 @@ class _StudioDetailsState extends State<StudioDetails> {
curve: Curves.easeIn,
),
),
),
],
),
),
],
children: [
StudioDetailsWidget(
scrollController: _scrollController,
studio: state.studio,
),
],

View File

@ -2,18 +2,17 @@ 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/details_tab_bar.dart';
import 'package:didvan/views/home/studio/studio_details/widgets/studio_details_widget.dart';
import 'package:didvan/views/home/widgets/bookmark_button.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;
@ -28,10 +27,6 @@ class _StudioDetailsState extends State<StudioDetails> {
final _scrollController = ScrollController();
bool _isFullScreen = false;
bool _isInit = true;
double _dwInPortrait = 0;
double _scaleInPortrait = 1;
@override
void initState() {
@ -41,7 +36,6 @@ class _StudioDetailsState extends State<StudioDetails> {
() => state.getStudioDetails(widget.pageData['id']),
);
state.args = widget.pageData['args'];
if (!kIsWeb && Platform.isAndroid) WebView.platform = AndroidWebView();
super.initState();
}
@ -77,12 +71,6 @@ class _StudioDetailsState extends State<StudioDetails> {
@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,
@ -121,79 +109,31 @@ class _StudioDetailsState extends State<StudioDetails> {
: AppBarData(
isSmall: true,
title: state.studio.title,
trailing: BookmarkButton(
value: state.studio.marked,
onMarkChanged: (value) => widget
.pageData['onMarkChanged'](state.studio.id, value),
gestureSize: 48,
),
),
showSliversFirst: true,
slivers: [
SliverAppBar(
automaticallyImplyLeading: false,
pinned: true,
elevation: 0,
toolbarHeight:
(_isFullScreen ? ds.height : ds.width * 9 / 16) +
72 -
MediaQuery.of(context).padding.top,
flexibleSpace: Column(
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.studio.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(
scrollController: _scrollController,
DetailsTabBar(
isVideo: true,
onCommentsTabSelected: () => Future.delayed(
const Duration(milliseconds: 100),
() => _scrollController.animateTo(
@ -202,6 +142,14 @@ class _StudioDetailsState extends State<StudioDetails> {
curve: Curves.easeIn,
),
),
),
],
),
),
],
children: [
StudioDetailsWidget(
scrollController: _scrollController,
studio: state.studio,
),
],

View File

@ -110,7 +110,7 @@ class StudioDetailsState extends CoreProvier {
final service = RequestService(RequestHelper.tag(
ids: studio.tags.map((tag) => tag.id).toList(),
itemId: studio.id,
type: studio.media.contains('iframe') ? 'video' : 'podcast',
type: args.type,
));
await service.httpGet();
if (service.isSuccess) {

View File

@ -0,0 +1,134 @@
import 'package:didvan/config/design_config.dart';
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:flutter/material.dart';
import 'package:provider/provider.dart';
class DetailsTabBar extends StatelessWidget {
final bool isVideo;
final VoidCallback onCommentsTabSelected;
const DetailsTabBar({
Key? key,
required this.isVideo,
required this.onCommentsTabSelected,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final state = context.read<StudioDetailsState>();
return WillPopScope(
onWillPop: () async {
state.selectedDetailsIndex = 0;
return true;
},
child: Container(
height: 72,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
boxShadow: [
BoxShadow(
color: const Color(0XFF1B3C59).withOpacity(0.15),
offset: const Offset(0, 8),
blurRadius: 8,
spreadRadius: 0,
)
],
),
child: FittedBox(
fit: BoxFit.scaleDown,
child: SizedBox(
width: MediaQuery.of(context).size.width,
child: Row(
children: [
_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,
),
],
),
),
),
),
);
}
}
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 Expanded(
child: GestureDetector(
onTap: onTap,
child: Container(
color: Colors.transparent,
child: Column(
children: [
Icon(
icon,
color: _color(context),
),
AnimatedContainer(
duration: DesignConfig.lowAnimationDuration,
width: isSelected ? 64 : 0,
height: 1,
color: _color(context),
),
DidvanText(
title,
color: _color(context),
style: Theme.of(context).textTheme.caption,
)
],
),
),
),
);
}
}

View File

@ -5,6 +5,7 @@ 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/studio/studio_details/widgets/details_tab_bar.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';
@ -16,15 +17,15 @@ import 'package:provider/provider.dart';
class StudioDetailsWidget extends StatelessWidget {
final StudioDetailsData studio;
final ScrollController? scrollController;
final VoidCallback onCommentsTabSelected;
final VoidCallback? onCommentsTabSelected;
const StudioDetailsWidget({
Key? key,
required this.studio,
required this.onCommentsTabSelected,
this.onCommentsTabSelected,
this.scrollController,
}) : super(key: key);
bool get _isVideo => studio.media.contains('ifram');
bool get _isVideo => studio.media.contains('iframe');
@override
Widget build(BuildContext context) {
@ -35,38 +36,12 @@ class StudioDetailsWidget extends StatelessWidget {
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,
if (!_isVideo)
DetailsTabBar(
isVideo: _isVideo,
onCommentsTabSelected: onCommentsTabSelected ?? () {},
),
_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),
if (state.selectedDetailsIndex != 1) const SizedBox(height: 16),
StateHandler<StudioDetailsState>(
onRetry: () {},
state: state,
@ -116,7 +91,10 @@ class StudioDetailsWidget extends StatelessWidget {
return ChangeNotifierProvider<CommentsState>(
create: (context) => CommentsState(),
child: SizedBox(
height: ds.height - 180,
height: ds.height -
ds.width * 9 / 16 -
128 -
MediaQuery.of(context).padding.top,
child: Comments(
pageData: {
'id': studio.id,
@ -164,63 +142,6 @@ class StudioDetailsWidget extends StatelessWidget {
}
}
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;
@ -279,7 +200,7 @@ class _StudioPreview extends StatelessWidget {
const SizedBox(height: 8),
DidvanText(
studio.title,
maxLines: 3,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.overline,

View File

@ -114,7 +114,10 @@ class _StudioSliderState extends State<StudioSlider> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var i = 0; i < state.sliders.length; i++)
_SliderIndicator(isCurrentIndex: selectedIndex == i),
_SliderIndicator(
isCurrentIndex: selectedIndex == i,
isVideo: state.videosSelected,
),
],
),
const SizedBox(height: 16),
@ -125,8 +128,19 @@ class _StudioSliderState extends State<StudioSlider> {
class _SliderIndicator extends StatelessWidget {
final bool isCurrentIndex;
const _SliderIndicator({Key? key, required this.isCurrentIndex})
: super(key: key);
final bool isVideo;
const _SliderIndicator({
Key? key,
required this.isCurrentIndex,
required this.isVideo,
}) : super(key: key);
Color _color(BuildContext context) {
if (isVideo) {
return Theme.of(context).colorScheme.secondary;
}
return Theme.of(context).colorScheme.focusedBorder;
}
@override
Widget build(BuildContext context) {
@ -137,11 +151,10 @@ class _SliderIndicator extends StatelessWidget {
margin: const EdgeInsets.only(left: 4),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.focusedBorder,
color: _color(context),
),
shape: BoxShape.circle,
color:
isCurrentIndex ? Theme.of(context).colorScheme.focusedBorder : null,
color: isCurrentIndex ? _color(context) : null,
),
);
}

View File

@ -20,9 +20,7 @@ class StudioTabBar extends StatelessWidget {
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
border: Border.all(
color: state.videosSelected
? Theme.of(context).colorScheme.secondary
: Theme.of(context).colorScheme.primary,
color: Theme.of(context).colorScheme.border,
),
borderRadius: DesignConfig.lowBorderRadius,
),
@ -88,14 +86,12 @@ class _StudioTypeButton extends StatelessWidget {
size: 32,
color: _color(context),
),
if (!isSelected) const SizedBox(height: 18),
if (isSelected)
Container(
width: 88,
AnimatedContainer(
duration: DesignConfig.lowAnimationDuration,
width: isSelected ? 88 : 0,
height: 1,
color: _color(context),
),
if (isSelected)
DidvanText(
title,
style: Theme.of(context).textTheme.overline,

View File

@ -1,3 +1,4 @@
import 'package:didvan/config/design_config.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/view/action_sheet_data.dart';
import 'package:didvan/utils/action_sheet.dart';
@ -43,7 +44,10 @@ class _BookmarkButtonState extends State<BookmarkButton> {
Widget build(BuildContext context) {
return DidvanIconButton(
gestureSize: widget.gestureSize,
color: widget.color,
color: widget.color ??
(DesignConfig.isDark || !_value
? null
: Theme.of(context).colorScheme.primary),
icon: _value ? DidvanIcons.bookmark_solid : DidvanIcons.bookmark_regular,
onPressed: () async {
bool confirm = false;

View File

@ -104,6 +104,9 @@ class _FloatingNavigationBarState extends State<FloatingNavigationBar> {
const Spacer(),
if (widget.isRadar)
BookmarkButton(
color: DesignConfig.isDark
? Theme.of(context).colorScheme.focusedBorder
: Theme.of(context).colorScheme.focused,
askForConfirmation: widget.hasUnmarkConfirmation,
value: widget.item.marked,
onMarkChanged: (value) {
@ -143,6 +146,9 @@ class _FloatingNavigationBarState extends State<FloatingNavigationBar> {
if (!widget.isRadar) const SizedBox(width: 12),
if (!widget.isRadar)
BookmarkButton(
color: DesignConfig.isDark
? Theme.of(context).colorScheme.focusedBorder
: Theme.of(context).colorScheme.focused,
askForConfirmation: widget.hasUnmarkConfirmation,
value: widget.item.marked,
onMarkChanged: (value) {

View File

@ -5,6 +5,7 @@ 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/utils/date_time.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';
@ -127,7 +128,22 @@ class MultitypeOverview extends StatelessWidget {
DateTime.parse(item.createdAt).toPersianDateStr(),
style: Theme.of(context).textTheme.overline,
),
// DidvanText('text'),
const Spacer(),
if ((item.timeToRead ?? item.duration) != null) ...[
const Icon(
DidvanIcons.timer_light,
size: 18,
),
const SizedBox(width: 4),
DidvanText(
item.timeToRead != null
? 'خواندن در ${item.timeToRead} دقیقه'
: DateTimeUtils.normalizeTimeDuration(
Duration(seconds: item.duration!),
),
style: Theme.of(context).textTheme.overline,
),
]
],
),
],

View File

@ -20,7 +20,8 @@ class DidvanAppBar extends StatelessWidget {
return Container(
height: appBarData.isSmall ? 56 : 72,
width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.only(right: 4, left: 20),
padding:
EdgeInsets.only(right: 4, left: appBarData.trailing == null ? 20 : 0),
decoration: BoxDecoration(
border: hasBorder
? Border(

View File

@ -198,7 +198,13 @@ class DidvanBNB extends StatelessWidget {
color: Theme.of(context).colorScheme.surface,
),
),
persistentHeader: Column(
persistentHeader: GestureDetector(
onVerticalDragUpdate: (details) {
if (details.delta.dy > 10) {
Navigator.of(context).pop();
}
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
AudioPlayerWidget(
@ -228,6 +234,7 @@ class DidvanBNB extends StatelessWidget {
),
],
),
),
expandableContent: state.appState == AppState.busy
? const SizedBox()
: StudioDetailsWidget(

View File

@ -10,6 +10,7 @@ class DidvanScaffold extends StatefulWidget {
final Color? backgroundColor;
final bool reverse;
final ScrollController? scrollController;
final bool showSliversFirst;
const DidvanScaffold({
Key? key,
@ -20,6 +21,7 @@ class DidvanScaffold extends StatefulWidget {
this.backgroundColor,
this.reverse = false,
this.scrollController,
this.showSliversFirst = false,
}) : super(key: key);
@override
@ -60,7 +62,7 @@ class _DidvanScaffoldState extends State<DidvanScaffold> {
pinned: true,
flexibleSpace: DidvanAppBar(appBarData: widget.appBarData!),
),
if (widget.children != null)
if (widget.children != null && !widget.showSliversFirst)
SliverPadding(
padding: widget.padding,
sliver: SliverList(
@ -76,6 +78,16 @@ class _DidvanScaffoldState extends State<DidvanScaffold> {
padding: widget.padding,
sliver: widget.slivers![i],
),
if (widget.children != null && widget.showSliversFirst)
SliverPadding(
padding: widget.padding,
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => widget.children![index],
childCount: widget.children!.length,
),
),
),
if (widget.reverse)
SliverToBoxAdapter(
child: SizedBox(

View File

@ -40,7 +40,9 @@ class SliverStateHandler<T extends CoreProvier> extends SliverList {
if (enableEmptyState && state.appState == AppState.idle) {
return Padding(
padding: EdgeInsets.only(
top: centerEmptyState ? 120 : 20,
top: centerEmptyState
? MediaQuery.of(context).size.height / 4
: 20,
bottom: 20,
),
child: emptyState,