responsive for web

This commit is contained in:
mohamadmahdi jebeli 2025-07-22 10:29:54 +03:30
parent d2b16244bf
commit fc8ac99ea3
7 changed files with 230 additions and 185 deletions

View File

@ -41,6 +41,7 @@ import 'package:didvan/views/news/news_details/news_details_state.dart';
import 'package:didvan/views/news/news_state.dart'; import 'package:didvan/views/news/news_state.dart';
import 'package:didvan/views/notification_time/notification_time_state.dart'; import 'package:didvan/views/notification_time/notification_time_state.dart';
import 'package:didvan/views/podcasts/podcasts.dart'; import 'package:didvan/views/podcasts/podcasts.dart';
import 'package:didvan/views/podcasts/studio_details/studio_details_state.dart';
import 'package:didvan/views/profile/profile.dart'; import 'package:didvan/views/profile/profile.dart';
import 'package:didvan/views/radar/radar.dart'; import 'package:didvan/views/radar/radar.dart';
import 'package:didvan/views/radar/radar_details/radar_details.dart'; import 'package:didvan/views/radar/radar_details/radar_details.dart';
@ -298,8 +299,11 @@ class RouteGenerator {
case Routes.studioDetails: case Routes.studioDetails:
if (settings.arguments is Map<String, dynamic>) { if (settings.arguments is Map<String, dynamic>) {
return _createRoute( return _createRoute(
StudioDetails( ChangeNotifierProvider<StudioDetailsState>(
pageData: settings.arguments as Map<String, dynamic>, create: (context) => StudioDetailsState(),
child: StudioDetails(
pageData: settings.arguments as Map<String, dynamic>,
),
), ),
); );
} }

View File

@ -9,7 +9,6 @@ import 'package:provider/provider.dart';
class StoryService { class StoryService {
static Future<List<UserStories>> getStories() async { static Future<List<UserStories>> getStories() async {
// دریافت UserProvider از طریق navigatorKey
final userProvider = final userProvider =
Provider.of<UserProvider>(navigatorKey.currentContext!, listen: false); Provider.of<UserProvider>(navigatorKey.currentContext!, listen: false);
final userId = userProvider.user.id; final userId = userProvider.user.id;

View File

@ -14,6 +14,7 @@ import 'package:didvan/views/home/main/widgets/story_section.dart';
import 'package:didvan/views/widgets/didvan/slider.dart'; import 'package:didvan/views/widgets/didvan/slider.dart';
import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/state_handlers/state_handler.dart'; import 'package:didvan/views/widgets/state_handlers/state_handler.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -21,6 +22,27 @@ import 'package:didvan/services/network/request.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'package:didvan/views/home/main/widgets/swot_item_card.dart'; import 'package:didvan/views/home/main/widgets/swot_item_card.dart';
import 'package:flutter/foundation.dart' show kIsWeb, defaultTargetPlatform;
import 'package:flutter/material.dart'; // برای دسترسی به TargetPlatform
// این پکیج فقط برای نسخه وب استفاده میشود
import 'package:universal_html/html.dart' as html;
/// تشخیص میدهد که آیا دستگاه یک موبایل است (چه نیتیو و چه وب)
bool isAnyMobile() {
// اگر کد روی وب در حال اجراست
if (kIsWeb) {
final userAgent = html.window.navigator.userAgent.toLowerCase();
return userAgent.contains('mobile') ||
userAgent.contains('android') ||
userAgent.contains('ios');
}
// اگر کد روی پلتفرم نیتیو در حال اجراست
// defaultTargetPlatform پلتفرم اصلی (مثلا اندروید یا iOS) را برمیگرداند
return defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS;
}
class MainPage extends StatefulWidget { class MainPage extends StatefulWidget {
const MainPage({ const MainPage({
super.key, super.key,
@ -46,97 +68,96 @@ class _MainPageState extends State<MainPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
print("DEBUG: _MainPageState build called"); print("DEBUG: _MainPageState build called");
return StateHandler<MainPageState>( return StateHandler<MainPageState>(
onRetry: () => { onRetry: () =>
print("DEBUG: _MainPageState onRetry called"), {print("DEBUG: _MainPageState onRetry called"), context.read<MainPageState>().init},
context.read<MainPageState>().init
},
state: context.watch<MainPageState>(), state: context.watch<MainPageState>(),
builder: (context, state) { builder: (context, state) {
print("DEBUG: FutureBuilder waiting"); print("DEBUG: FutureBuilder waiting");
print("DEBUG: FutureBuilder state.stories.isNotEmpty: ${state.stories.isNotEmpty}"); print("DEBUG: FutureBuilder state.stories.isNotEmpty: ${state.stories.isNotEmpty}");
print("DEBUG: FutureBuilder state.content: ${state.content!.lists}"); print("DEBUG: FutureBuilder state.content: ${state.content!.lists}");
print("DEBUG: FutureBuilder state.content != null: ${state.content != null}"); print("DEBUG: FutureBuilder state.content != null: ${state.content != null}");
print("DEBUG: FutureBuilder state.content!.lists.isNotEmpty: ${state.content!.lists.isNotEmpty}"); print(
"DEBUG: FutureBuilder state.content!.lists.isNotEmpty: ${state.content!.lists.isNotEmpty}");
return ListView( return ListView(
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
children: [ children: [
if (state.stories.isNotEmpty) StorySection(stories: state.stories), if (state.stories.isNotEmpty) StorySection(stories: state.stories),
const SizedBox(height: 12), const SizedBox(height: 12),
const MainPageMainContent(), const MainPageMainContent(),
Builder(builder: (context) { Builder(builder: (context) {
final List<Widget> pageContent = []; final List<Widget> pageContent = [];
if (state.content != null && state.content!.lists.isNotEmpty) { if (state.content != null && state.content!.lists.isNotEmpty) {
final lists = state.content!.lists; final lists = state.content!.lists;
for (int i = 0; i < lists.length; i++) { for (int i = 0; i < lists.length; i++) {
final currentList = lists[i]; final currentList = lists[i];
if (i == 4) { if (i == 4) {
pageContent.add( pageContent.add(
Padding( Padding(
padding: const EdgeInsets.only(top: 32), padding: const EdgeInsets.only(top: 32),
child: Column( child: Column(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 16, left: 16,
right: 16, right: 16,
bottom: 16, bottom: 16,
top: 28, top: 28,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const InfoTitle(),
GestureDetector(
onTap: () => {
Navigator.of(context)
.pushNamed(Routes.infography)
},
child: Row(
children: [
DidvanText(
"همه",
color: Theme.of(context)
.colorScheme
.primary,
),
Icon(
DidvanIcons.angle_left_light,
color: Theme.of(context)
.colorScheme
.primary,
)
],
),
)
],
),
), ),
child: Row( const MainPageBanner(
mainAxisAlignment: MainAxisAlignment.spaceBetween, isFirst: false,
children: [
const InfoTitle(),
GestureDetector(
onTap: () => {
Navigator.of(context)
.pushNamed(Routes.infography)
},
child: Row(
children: [
DidvanText(
"همه",
color: Theme.of(context)
.colorScheme
.primary,
),
Icon(
DidvanIcons.angle_left_light,
color: Theme.of(context)
.colorScheme
.primary,
)
],
),
)
],
), ),
), ],
const MainPageBanner( ),
isFirst: false,
),
],
), ),
), );
); }
}
pageContent.add(_MainPageSection( pageContent.add(_MainPageSection(
list: currentList, list: currentList,
isLast: i == lists.length - 1, isLast: i == lists.length - 1,
)); ));
if (currentList.type == 'startup') { if (currentList.type == 'startup') {
pageContent.add(_SwotSection(swotItems: state.swotItems)); pageContent.add(_SwotSection(swotItems: state.swotItems));
}
} }
} }
} print("DEBUG: FutureBuilder error");
print("DEBUG: FutureBuilder error"); return Column(children: pageContent);
return Column(children: pageContent); }),
}), ],
], );
);
}, },
); );
} }
@ -210,7 +231,7 @@ class _SwotSection extends StatelessWidget {
DidvanSlider( DidvanSlider(
height: 330, height: 330,
itemCount: 7, itemCount: 7,
viewportFraction: 0.65, viewportFraction: isAnyMobile() ? 0.65 : 0.55,
itemBuilder: (context, index, realIndex) => Padding( itemBuilder: (context, index, realIndex) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 0.0), padding: const EdgeInsets.symmetric(horizontal: 0.0),
child: Padding( child: Padding(
@ -390,7 +411,10 @@ class _MainPageSection extends StatelessWidget {
return DidvanSlider( return DidvanSlider(
height: 260 + (_maxSublistCount() - (list.type == 'radar' ? 1 : 0)) * 20, height: 260 + (_maxSublistCount() - (list.type == 'radar' ? 1 : 0)) * 20,
itemCount: list.contents.length, itemCount: list.contents.length,
viewportFraction: 0.65, // -- START: کد اصلاح شده در اینجا قرار دارد --
// از isMobile() برای تنظیم اندازه آیتمها بر اساس موبایل یا دسکتاپ بودن استفاده میکنیم
viewportFraction: isAnyMobile() ? 0.65 : 0.55,
// -- END: کد اصلاح شده --
itemBuilder: (context, index, realIndex) => Padding( itemBuilder: (context, index, realIndex) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 4), padding: const EdgeInsets.symmetric(horizontal: 4),
child: MainPageGeneralItem( child: MainPageGeneralItem(

View File

@ -66,32 +66,42 @@ class _StudioDetailsState extends State<StudioDetails> {
} }
Future<void> _initializePlayer(StudioDetailsData studio) async { Future<void> _initializePlayer(StudioDetailsData studio) async {
// Disposing old controllers before creating new ones. if (widget.pageData['args']['type'] == 'video') {
_videoPlayerController?.dispose(); // Disposing old controllers before creating new ones.
_chewieController?.dispose(); _videoPlayerController?.dispose();
_chewieController?.dispose();
_videoPlayerController = VideoPlayerController.network(studio.link); _videoPlayerController = VideoPlayerController.network(studio.link);
try { try {
await _videoPlayerController!.initialize(); await _videoPlayerController!.initialize();
if (mounted) { if (mounted) {
setState(() { setState(() {
_chewieController = ChewieController( _chewieController = ChewieController(
videoPlayerController: _videoPlayerController!, videoPlayerController: _videoPlayerController!,
customControls: const PrimaryControls(), customControls: const PrimaryControls(),
autoPlay: true, autoPlay: true,
looping: true, looping: true,
aspectRatio: 16 / 9, aspectRatio: 16 / 9,
materialProgressColors: ChewieProgressColors( materialProgressColors: ChewieProgressColors(
playedColor: Theme.of(context).colorScheme.title, playedColor: Theme.of(context).colorScheme.title,
handleColor: Theme.of(context).colorScheme.title, handleColor: Theme.of(context).colorScheme.title,
), ),
); );
_currentlyPlayingId = studio.id; _currentlyPlayingId = studio.id;
}); });
}
} catch (e) {
debugPrint("Error initializing video player: $e");
} }
} catch (e) { } else {
debugPrint("Error initializing video player: $e"); // Handle audio playback using MediaService
await MediaService.handleAudioPlayback(
audioSource: studio.link,
id: studio.id,
isVoiceMessage: false,
);
_currentlyPlayingId = studio.id;
} }
} }
@ -139,7 +149,7 @@ class _StudioDetailsState extends State<StudioDetails> {
appBarData: AppBarData( appBarData: AppBarData(
trailing: BookmarkButton( trailing: BookmarkButton(
itemId: state.studio.id, itemId: state.studio.id,
type: 'video', type: state.args?.type == 'video' ? 'video' : 'podcast',
value: state.studio.marked, value: state.studio.marked,
onMarkChanged: (value) { onMarkChanged: (value) {
widget.pageData['onMarkChanged']( widget.pageData['onMarkChanged'](
@ -157,20 +167,21 @@ class _StudioDetailsState extends State<StudioDetails> {
height: d.size.height - d.padding.top - 56, height: d.size.height - d.padding.top - 56,
child: Column( child: Column(
children: [ children: [
AspectRatio( if (state.args?.type == 'video')
aspectRatio: 16 / 9, AspectRatio(
child: (_chewieController != null && aspectRatio: 16 / 9,
_chewieController! child: (_chewieController != null &&
.videoPlayerController.value.isInitialized) _chewieController!
? Chewie(controller: _chewieController!) .videoPlayerController.value.isInitialized)
: Center( ? Chewie(controller: _chewieController!)
child: Image.asset( : Center(
Assets.loadingAnimation, child: Image.asset(
width: 100, Assets.loadingAnimation,
height: 100, width: 100,
height: 100,
),
), ),
), ),
),
Expanded( Expanded(
child: StudioDetailsWidget( child: StudioDetailsWidget(
onMarkChanged: (id, value) => widget onMarkChanged: (id, value) => widget

View File

@ -1,6 +1,8 @@
// ignore_for_file: unused_element // ignore_for_file: unused_element
import 'package:didvan/config/theme_data.dart'; import 'package:didvan/config/theme_data.dart';
import 'package:didvan/models/requests/radar.dart';
import 'package:didvan/routes/routes.dart';
import 'package:expandable_bottom_sheet/expandable_bottom_sheet.dart'; import 'package:expandable_bottom_sheet/expandable_bottom_sheet.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart';
@ -40,24 +42,24 @@ class _PlayerNavBarState extends State<PlayerNavBar> {
stream: MediaService.audioPlayer.playingStream, stream: MediaService.audioPlayer.playingStream,
builder: (context, isPlaying) => GestureDetector( builder: (context, isPlaying) => GestureDetector(
onTap: () { onTap: () {
// (MediaService.currentPodcast == null && (MediaService.currentPodcast == null &&
// (MediaService.audioPlayerTag ?? '') (MediaService.audioPlayerTag ?? '')
// .split('-')[1] .split('-')[1]
// .isNotEmpty) || .isNotEmpty) ||
// MediaService.currentPodcast?.description == 'radar' MediaService.currentPodcast?.description == 'radar'
// ? Navigator.of(context).pushNamed( ? Navigator.of(context).pushNamed(
// Routes.radarDetails, Routes.radarDetails,
// arguments: { arguments: {
// 'onMarkChanged': (id, value) {}, 'onMarkChanged': (id, value) {},
// 'onCommentsChanged': (id, value) {}, 'onCommentsChanged': (id, value) {},
// 'id': MediaService.currentPodcast?.id, 'id': MediaService.currentPodcast?.id,
// 'args': const RadarRequestArgs(page: 0), 'args': const RadarRequestArgs(page: 0),
// 'hasUnmarkConfirmation': false, 'hasUnmarkConfirmation': false,
// }, },
// ) )
// : (MediaService.audioPlayerTag ?? '').split('-')[1].isNotEmpty : (MediaService.audioPlayerTag ?? '').split('-')[1].isNotEmpty
// ? _showPlayerBottomSheet(context) ? _showPlayerBottomSheet(context)
// : null; : null;
}, },
child: Consumer<StudioDetailsState>( child: Consumer<StudioDetailsState>(
builder: (context, state, child) => AnimatedContainer( builder: (context, state, child) => AnimatedContainer(

View File

@ -36,9 +36,14 @@ class PodcastOverview extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DidvanCard( return DidvanCard(
onTap: () { onTap: () {
context Navigator.of(context).pushNamed(
.read<StudioDetailsState>() Routes.studioDetails,
.getStudioDetails(podcast.id, args: studioRequestArgs); arguments: {
'id': podcast.id,
'args': studioRequestArgs,
'onMarkChanged': onMarkChanged,
},
);
}, },
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -149,50 +154,50 @@ class PodcastOverview extends StatelessWidget {
} }
static Widget get placeholder => DidvanCard( static Widget get placeholder => DidvanCard(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
const Row( ShimmerPlaceholder(height: 64, width: 64),
crossAxisAlignment: CrossAxisAlignment.center, SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ShimmerPlaceholder(height: 64, width: 64), ShimmerPlaceholder(height: 18, width: 200),
SizedBox(width: 8), SizedBox(height: 8),
Column( ShimmerPlaceholder(height: 18, width: 100),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ShimmerPlaceholder(height: 18, width: 200),
SizedBox(height: 8),
ShimmerPlaceholder(height: 18, width: 100),
],
),
],
),
const SizedBox(height: 16),
const ShimmerPlaceholder(
height: 16,
width: double.infinity,
),
const SizedBox(height: 8),
const ShimmerPlaceholder(
height: 16,
width: 200,
),
const SizedBox(height: 4),
const DidvanDivider(verticalPadding: 8),
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),
], ],
), ),
], ],
), ),
); const SizedBox(height: 16),
const ShimmerPlaceholder(
height: 16,
width: double.infinity,
),
const SizedBox(height: 8),
const ShimmerPlaceholder(
height: 16,
width: 200,
),
const SizedBox(height: 4),
const DidvanDivider(verticalPadding: 8),
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

@ -49,7 +49,7 @@ dependencies:
cached_network_image: ^3.2.0 cached_network_image: ^3.2.0
skeleton_text: ^3.0.0 skeleton_text: ^3.0.0
carousel_slider: ^5.0.0 carousel_slider: ^5.0.0
universal_html: ^2.0.8 universal_html: ^2.2.4
record: ^5.1.2 record: ^5.1.2
persian_number_utility: ^1.1.1 persian_number_utility: ^1.1.1