Merge branch 'dev' into 'master'

Dev

See merge request Didvan/didvan-app!14
This commit is contained in:
MohammadTaha Basiri 2022-04-08 11:27:31 +00:00
commit f9de89dd0e
98 changed files with 3126 additions and 1383 deletions

View File

@ -3,10 +3,14 @@
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:label="Didvan"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true">
android:usesCleartextTraffic="true"
android:requestLegacyExternalStorage="true">
<activity
android:name=".MainActivity"

View File

@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
platform :ios, '10.0'
platform :ios, '11.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

View File

@ -1,33 +1,42 @@
PODS:
- audio_session (0.0.1):
- assets_audio_player (0.0.1):
- Flutter
- Firebase/CoreOnly (8.11.0):
- FirebaseCore (= 8.11.0)
- Firebase/Messaging (8.11.0):
- assets_audio_player_web (0.0.1):
- Flutter
- better_player (0.0.1):
- Cache (~> 6.0.0)
- Flutter
- GCDWebServer
- HLSCachingReverseProxyServer
- PINCache
- Cache (6.0.0)
- Firebase/CoreOnly (8.14.0):
- FirebaseCore (= 8.14.0)
- Firebase/Messaging (8.14.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 8.11.0)
- firebase_core (1.13.1):
- Firebase/CoreOnly (= 8.11.0)
- FirebaseMessaging (~> 8.14.0)
- firebase_core (1.14.0):
- Firebase/CoreOnly (= 8.14.0)
- Flutter
- firebase_messaging (11.2.8):
- Firebase/Messaging (= 8.11.0)
- firebase_messaging (11.2.12):
- Firebase/Messaging (= 8.14.0)
- firebase_core
- Flutter
- FirebaseCore (8.11.0):
- FirebaseCore (8.14.0):
- FirebaseCoreDiagnostics (~> 8.0)
- GoogleUtilities/Environment (~> 7.7)
- GoogleUtilities/Logger (~> 7.7)
- FirebaseCoreDiagnostics (8.12.0):
- FirebaseCoreDiagnostics (8.14.0):
- GoogleDataTransport (~> 9.1)
- GoogleUtilities/Environment (~> 7.7)
- GoogleUtilities/Logger (~> 7.7)
- nanopb (~> 2.30908.0)
- FirebaseInstallations (8.12.0):
- FirebaseInstallations (8.14.0):
- FirebaseCore (~> 8.0)
- GoogleUtilities/Environment (~> 7.7)
- GoogleUtilities/UserDefaults (~> 7.7)
- PromisesObjC (< 3.0, >= 1.2)
- FirebaseMessaging (8.11.0):
- FirebaseMessaging (8.14.0):
- FirebaseCore (~> 8.0)
- FirebaseInstallations (~> 8.0)
- GoogleDataTransport (~> 9.1)
@ -44,6 +53,9 @@ PODS:
- FMDB (2.7.5):
- FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5)
- GCDWebServer (3.5.4):
- GCDWebServer/Core (= 3.5.4)
- GCDWebServer/Core (3.5.4)
- GoogleDataTransport (9.1.2):
- GoogleUtilities/Environment (~> 7.2)
- nanopb (~> 2.30908.0)
@ -65,13 +77,14 @@ PODS:
- GoogleUtilities/Logger
- GoogleUtilities/UserDefaults (7.7.0):
- GoogleUtilities/Logger
- HLSCachingReverseProxyServer (0.1.0):
- GCDWebServer (~> 3.5)
- PINCache (>= 3.0.1-beta.3)
- image_cropper (0.0.4):
- Flutter
- TOCropViewController (~> 2.6.1)
- image_picker (0.0.1):
- Flutter
- just_audio (0.0.1):
- Flutter
- nanopb (2.30908.0):
- nanopb/decode (= 2.30908.0)
- nanopb/encode (= 2.30908.0)
@ -79,6 +92,16 @@ PODS:
- nanopb/encode (2.30908.0)
- path_provider_ios (0.0.1):
- Flutter
- permission_handler_apple (9.0.4):
- Flutter
- PINCache (3.0.3):
- PINCache/Arc-exception-safe (= 3.0.3)
- PINCache/Core (= 3.0.3)
- PINCache/Arc-exception-safe (3.0.3):
- PINCache/Core
- PINCache/Core (3.0.3):
- PINOperation (~> 1.2.1)
- PINOperation (1.2.1)
- PromisesObjC (2.0.0)
- record (0.0.1):
- Flutter
@ -88,11 +111,15 @@ PODS:
- TOCropViewController (2.6.1)
- url_launcher_ios (0.0.1):
- Flutter
- wakelock (0.0.1):
- Flutter
- webview_flutter_wkwebview (0.0.1):
- Flutter
DEPENDENCIES:
- audio_session (from `.symlinks/plugins/audio_session/ios`)
- assets_audio_player (from `.symlinks/plugins/assets_audio_player/ios`)
- assets_audio_player_web (from `.symlinks/plugins/assets_audio_player_web/ios`)
- better_player (from `.symlinks/plugins/better_player/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- Flutter (from `Flutter`)
@ -100,30 +127,40 @@ DEPENDENCIES:
- flutter_vibrate (from `.symlinks/plugins/flutter_vibrate/ios`)
- image_cropper (from `.symlinks/plugins/image_cropper/ios`)
- image_picker (from `.symlinks/plugins/image_picker/ios`)
- just_audio (from `.symlinks/plugins/just_audio/ios`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- record (from `.symlinks/plugins/record/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- wakelock (from `.symlinks/plugins/wakelock/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
SPEC REPOS:
trunk:
- Cache
- Firebase
- FirebaseCore
- FirebaseCoreDiagnostics
- FirebaseInstallations
- FirebaseMessaging
- FMDB
- GCDWebServer
- GoogleDataTransport
- GoogleUtilities
- HLSCachingReverseProxyServer
- nanopb
- PINCache
- PINOperation
- PromisesObjC
- TOCropViewController
EXTERNAL SOURCES:
audio_session:
:path: ".symlinks/plugins/audio_session/ios"
assets_audio_player:
:path: ".symlinks/plugins/assets_audio_player/ios"
assets_audio_player_web:
:path: ".symlinks/plugins/assets_audio_player_web/ios"
better_player:
:path: ".symlinks/plugins/better_player/ios"
firebase_core:
:path: ".symlinks/plugins/firebase_core/ios"
firebase_messaging:
@ -138,46 +175,56 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/image_cropper/ios"
image_picker:
:path: ".symlinks/plugins/image_picker/ios"
just_audio:
:path: ".symlinks/plugins/just_audio/ios"
path_provider_ios:
:path: ".symlinks/plugins/path_provider_ios/ios"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
record:
:path: ".symlinks/plugins/record/ios"
sqflite:
:path: ".symlinks/plugins/sqflite/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
wakelock:
:path: ".symlinks/plugins/wakelock/ios"
webview_flutter_wkwebview:
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
SPEC CHECKSUMS:
audio_session: 4f3e461722055d21515cf3261b64c973c062f345
Firebase: 44dd9724c84df18b486639e874f31436eaa9a20c
firebase_core: 08f6a85f62060111de5e98d6a214810d11365de9
firebase_messaging: 36238f3d0b933af8c919aef608408aae06ba22e8
FirebaseCore: 2f4f85b453cc8fea4bb2b37e370007d2bcafe3f0
FirebaseCoreDiagnostics: 3b40dfadef5b90433a60ae01f01e90fe87aa76aa
FirebaseInstallations: 25764cf322e77f99449395870a65b2bef88e1545
FirebaseMessaging: 02e248e8997f71fa8cc9d78e9d49ec1a701ba14a
assets_audio_player: edee322b9cb625571b830b35872ead1a295fd917
assets_audio_player_web: 19826380c44375761aa0b9053665c1e3fbc3b86b
better_player: 2406bfe8175203c7a46fa15f9d778d73b12e1646
Cache: 4ca7e00363fca5455f26534e5607634c820ffc2d
Firebase: 7e8fe528c161b9271d365217a74c16aaf834578e
firebase_core: b0b382f1497ab407aceb25e41e3036c8798c1609
firebase_messaging: 34dd10d1aa6d8f40d03660eeacd0452d62eec7aa
FirebaseCore: b84a44ee7ba999e0f9f76d198a9c7f60a797b848
FirebaseCoreDiagnostics: fd0c8490f34287229c1d6c103d3a55f81ec85712
FirebaseInstallations: 7d1d967a307c12f1aadd76844fc321cef699b1ce
FirebaseMessaging: 5ebc42d281567658a2cb72b9ef3506e4a1a1a6e4
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
flutter_vibrate: 9f4c2ab57008965f78969472367c329dd77eb801
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
GCDWebServer: 2c156a56c8226e2d5c0c3f208a3621ccffbe3ce4
GoogleDataTransport: 629c20a4d363167143f30ea78320d5a7eb8bd940
GoogleUtilities: e0913149f6b0625b553d70dae12b49fc62914fd1
HLSCachingReverseProxyServer: 59935e1e0244ad7f3375d75b5ef46e8eb26ab181
image_cropper: 60c2789d1f1a78c873235d4319ca0c34a69f2d98
image_picker: 9aa50e1d8cdacdbed739e925b7eea16d014367e6
just_audio: baa7252489dbcf47a4c7cc9ca663e9661c99aafa
image_picker: 541dcbb3b9cf32d87eacbd957845d8651d6c62c3
nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96
path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce
PINCache: 7a8fc1a691173d21dbddbf86cd515de6efa55086
PINOperation: 00c935935f1e8cf0d1e2d6b542e75b88fc3e5e20
PromisesObjC: 68159ce6952d93e17b2dfe273b8c40907db5ba58
record: 7ee2393532f8553bbb09fa19e95478323b7c0a99
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863
url_launcher_ios: 02f1989d4e14e998335b02b67a7590fa34f971af
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
webview_flutter_wkwebview: 005fbd90c888a42c5690919a1527ecc6649e1162
PODFILE CHECKSUM: fe0e1ee7f3d1f7d00b11b474b62dd62134535aea
PODFILE CHECKSUM: 7368163408c647b7eb699d0d788ba6718e18fb8d
COCOAPODS: 1.11.2

View File

@ -28,6 +28,10 @@
<string>Main</string>
<key>FirebaseAppDelegateProxyEnabled</key>
<false/>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<key>NSMicrophoneUsageDescription</key>
<string>We need to access to the microphone to record audio file</string>
<key>NSPhotoLibraryUsageDescription</key>

View File

@ -1,6 +1,6 @@
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/main.dart';
import 'package:didvan/providers/theme_provider.dart';
import 'package:didvan/providers/theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';

View File

@ -1,8 +1,10 @@
import 'package:bot_toast/bot_toast.dart';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/providers/theme_provider.dart';
import 'package:didvan/providers/user_provider.dart';
import 'package:didvan/providers/media.dart';
import 'package:didvan/providers/theme.dart';
import 'package:didvan/providers/user.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';
@ -20,12 +22,18 @@ class Didvan extends StatelessWidget {
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<MediaProvider>(
create: (context) => MediaProvider(),
),
ChangeNotifierProvider<UserProvider>(
create: (context) => UserProvider(),
),
ChangeNotifierProvider<ThemeProvider>(
create: (context) => ThemeProvider(),
),
ChangeNotifierProvider<StudioDetailsState>(
create: (context) => StudioDetailsState(),
),
],
child: Consumer<ThemeProvider>(
builder: (context, themeProvider, child) => MaterialApp(

View File

@ -6,9 +6,9 @@ class CommentData {
int id;
final String text;
final String createdAt;
final bool liked;
final bool disliked;
FeedbackData feedback;
bool liked;
bool disliked;
final FeedbackData feedback;
final UserOverview user;
final List<Reply> replies;

View File

@ -1,8 +1,8 @@
class FeedbackData {
final int like;
final int dislike;
int like;
int dislike;
const FeedbackData({required this.like, required this.dislike});
FeedbackData({required this.like, required this.dislike});
factory FeedbackData.fromJson(Map<String, dynamic> json) => FeedbackData(
like: json['like'],

View File

@ -1,4 +1,5 @@
import 'package:didvan/models/category.dart';
import 'package:html/parser.dart';
class OverviewData {
final int id;
@ -8,7 +9,8 @@ class OverviewData {
final int? timeToRead;
final int? duration;
final String? reference;
final String? media;
final String? link;
final String? iframe;
final bool forManagers;
final String createdAt;
final String type;
@ -26,18 +28,23 @@ class OverviewData {
required this.marked,
required this.comments,
required this.forManagers,
this.media,
this.link,
this.iframe,
this.duration,
this.timeToRead,
this.reference,
this.categories,
});
factory OverviewData.fromJson(Map<String, dynamic> json) => OverviewData(
factory OverviewData.fromJson(Map<String, dynamic> json) {
final document = parse(json['description']);
final String parsedString =
parse(document.body!.text).documentElement!.text;
return OverviewData(
id: json['id'],
title: json['title'],
image: json['image'],
description: json['description'],
description: parsedString,
timeToRead: json['timeToRead'],
reference: json['reference'],
forManagers: json['forManagers'] ?? false,
@ -45,8 +52,9 @@ class OverviewData {
createdAt: json['createdAt'],
duration: json['duration'],
type: json['type'] ?? '',
marked: json['marked'] ?? false,
media: json['media'],
marked: json['marked'] ?? true,
link: json['link'],
iframe: json['iframe'],
categories: json['categories'] != null
? List<CategoryData>.from(
json['categories'].map(
@ -55,6 +63,7 @@ class OverviewData {
)
: null,
);
}
Map<String, dynamic> toJson() => {
'id': id,

View File

@ -3,9 +3,11 @@ class StudioRequestArgs {
final String? search;
final String? order;
final String? type;
final bool? asc;
const StudioRequestArgs({
required this.page,
this.asc,
this.search,
this.order,
this.type,

View File

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

View File

@ -7,7 +7,8 @@ class StudioDetailsData {
final String title;
final String description;
final String image;
final String media;
final String link;
final String? iframe;
final String createdAt;
final int order;
bool marked;
@ -21,7 +22,8 @@ class StudioDetailsData {
required this.title,
required this.description,
required this.image,
required this.media,
required this.link,
required this.iframe,
required this.createdAt,
required this.order,
required this.marked,
@ -36,7 +38,8 @@ class StudioDetailsData {
title: json['title'],
description: json['description'],
image: json['image'],
media: json['media'],
link: json['link'],
iframe: json['iframe'],
createdAt: json['createdAt'],
order: json['order'],
marked: json['marked'],
@ -51,7 +54,6 @@ class StudioDetailsData {
'title': title,
'description': description,
'image': image,
'media': media,
'createdAt': createdAt,
'order': order,
'marked': marked,

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) {

60
lib/providers/media.dart Normal file
View File

@ -0,0 +1,60 @@
import 'dart:io';
import 'package:didvan/models/enums.dart';
import 'package:didvan/providers/core.dart';
import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/storage/storage.dart';
class MediaProvider extends CoreProvier {
static final List<int> downloadedItemIds = [];
final List<String> downloadQueue = [];
Future<void> getDownloadsList() async {
downloadedItemIds.clear();
final videosDir = Directory(
StorageService.appDocsDir + ('/videos'),
);
final podcastsDir = Directory(
StorageService.appDocsDir + ('/podcasts'),
);
if (!await videosDir.exists()) {
await videosDir.create();
}
if (!await podcastsDir.exists()) {
await podcastsDir.create();
}
videosDir.list(recursive: false).listen(
(event) {
downloadedItemIds.add(
int.parse(
event.path.split('/').last.split('-').last.split('.').first,
),
);
},
);
podcastsDir.list(recursive: false).listen(
(event) {
downloadedItemIds.add(
int.parse(
event.path.split('/').last.split('-').last.split('.').first,
),
);
},
);
await Future.delayed(const Duration(milliseconds: 300), notifyListeners);
}
Future<void> download({
required String url,
required String fileName,
required bool isVideo,
}) async {
appState = AppState.busy;
downloadQueue.add(url);
notifyListeners();
final service = RequestService(url);
await service.download(fileName, isVideo ? 'videos' : 'podcasts');
downloadQueue.remove(url);
getDownloadsList();
}
}

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

@ -1,4 +1,4 @@
import 'package:didvan/providers/core_provider.dart';
import 'package:didvan/providers/core.dart';
import 'package:flutter/material.dart';
class SettingsProvider extends CoreProvier {

View File

@ -1,4 +1,4 @@
import 'package:didvan/providers/core_provider.dart';
import 'package:didvan/providers/core.dart';
import 'package:flutter/material.dart';
class ThemeProvider extends CoreProvier {

View File

@ -2,7 +2,8 @@ import 'package:collection/collection.dart';
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/user.dart';
import 'package:didvan/models/view/alert_data.dart';
import 'package:didvan/providers/core_provider.dart';
import 'package:didvan/providers/core.dart';
import 'package:didvan/services/app_initalizer.dart';
import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart';
import 'package:didvan/services/storage/storage.dart';
@ -29,16 +30,26 @@ class UserProvider extends CoreProvier {
isAuthenticated = true;
final RequestService service = RequestService(RequestHelper.userInfo);
await service.httpGet();
if (service.isSuccess) {
user = User.fromJson(service.result['user']);
return true;
}
if (service.statusCode == 401) {
if (service.statusCode == 401 ||
(service.isSuccess && service.result['user'] == null)) {
return false;
}
if (service.isSuccess) {
user = User.fromJson(service.result['user']);
AppInitializer.initializeFirebase().then((_) => _registerFirebaseToken());
_registerFirebaseToken();
return true;
}
throw 'Getting user from API failed!';
}
Future<void> _registerFirebaseToken() async {
final service = RequestService(RequestHelper.firebaseToken, body: {
'token': AppInitializer.fcmToken,
});
await service.put();
}
Future<bool> setProfilePhoto(dynamic file) async {
appState = AppState.isolatedBusy;
final RequestService service =
@ -132,7 +143,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 +159,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 +175,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

@ -1,4 +1,3 @@
import 'package:didvan/models/tag.dart';
import 'package:didvan/views/authentication/authentication.dart';
import 'package:didvan/views/authentication/authentication_state.dart';
import 'package:didvan/views/home/comments/comments.dart';
@ -25,8 +24,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 +64,6 @@ class RouteGenerator {
ChangeNotifierProvider<StudioState>(
create: (context) => StudioState(),
),
ChangeNotifierProvider<StudioDetailsState>(
create: (context) => StudioDetailsState(),
),
],
child: const Home(),
),
@ -106,12 +103,9 @@ class RouteGenerator {
);
case Routes.studioDetails:
return _createRoute(
ChangeNotifierProvider<StudioDetailsState>.value(
value: (settings.arguments as Map<String, dynamic>)['state'],
child: StudioDetails(
StudioDetails(
pageData: settings.arguments as Map<String, dynamic>,
),
),
);
case Routes.directList:
return _createRoute(
@ -147,17 +141,19 @@ class RouteGenerator {
return _createRoute(
ChangeNotifierProvider<HashtagState>(
create: (context) => HashtagState(),
child: Hashtag(tag: settings.arguments as Tag),
child:
Hashtag(pageData: settings.arguments as Map<String, dynamic>),
),
);
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:
@ -184,18 +180,31 @@ class RouteGenerator {
final shortestSide = MediaQuery.of(context).size.shortestSide;
final bool useMobileLayout = shortestSide < 600;
if (kIsWeb && !useMobileLayout) {
return Container(
final deviceSize = MediaQuery.of(context).size;
return MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaleFactor: 1.0,
size: Size(
deviceSize.width / 16 * 9,
deviceSize.height,
),
),
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

@ -1,5 +1,4 @@
import 'package:didvan/models/settings_data.dart';
import 'package:didvan/services/media/media.dart';
import 'package:didvan/services/storage/storage.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
@ -8,13 +7,13 @@ import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
class AppInitializer {
static String? fcmToken;
static Future<void> setupServices() async {
if (!kIsWeb) {
StorageService.appDocsDir =
(await getApplicationDocumentsDirectory()).path;
StorageService.appTempsDir = (await getTemporaryDirectory()).path;
await _initializeFirebase();
MediaService.init();
}
}
@ -34,11 +33,11 @@ class AppInitializer {
} else {
await StorageService.setValue(
key: 'notificationTimeRangeStart',
value: '00:00',
value: '0',
);
await StorageService.setValue(
key: 'notificationTimeRangeEnd',
value: '23:59',
value: '24',
);
await StorageService.setValue(
key: 'fontFamily',
@ -60,7 +59,7 @@ class AppInitializer {
}
}
static Future<void> _initializeFirebase() async {
static Future<void> initializeFirebase() async {
try {
await Firebase.initializeApp(
options: const FirebaseOptions(
@ -74,6 +73,8 @@ class AppInitializer {
Firebase.app();
}
final FirebaseMessaging fcm = FirebaseMessaging.instance;
fcmToken = await fcm.getToken();
await fcm.subscribeToTopic('general');
await fcm.requestPermission(
alert: true,
announcement: false,

View File

@ -1,64 +1,79 @@
import 'package:didvan/models/requests/studio.dart';
import 'package:didvan/models/studio_details_data.dart';
import 'package:didvan/providers/media.dart';
import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart';
import 'package:didvan/services/storage/storage.dart';
import 'package:flutter/foundation.dart';
import 'package:image_picker/image_picker.dart';
import 'package:just_audio/just_audio.dart';
import 'package:assets_audio_player/assets_audio_player.dart';
class MediaService {
static final AudioPlayer audioPlayer = AudioPlayer();
static final audioPlayer = AssetsAudioPlayer();
static String? audioPlayerTag;
static StudioDetailsData? currentPodcast;
static StudioRequestArgs? podcastPlaylistArgs;
static void init() {
audioPlayer.positionStream.listen((event) {
if (audioPlayer.duration != null && audioPlayer.duration! < event) {
audioPlayer.stop();
audioPlayer.seek(const Duration(seconds: 0));
}
});
}
static Duration? get duration => audioPlayer.current.value?.audio.duration;
static Future<void> handleAudioPlayback({
required dynamic audioSource,
required int id,
bool isNetworkAudio = true,
bool isVoiceMessage = true,
void Function(bool isNext)? onTrackChanged,
}) async {
bool isNetworkAudio = audioSource.runtimeType == String;
String tag;
if (isNetworkAudio) {
tag = audioSource;
} else {
tag = audioSource.path;
tag = '${isVoiceMessage ? 'message' : 'podcast'}-$id';
if (!isVoiceMessage && MediaProvider.downloadedItemIds.contains(id)) {
audioSource = StorageService.appDocsDir + '/podcasts/podcast-$id.mp3';
isNetworkAudio = false;
}
if (audioPlayerTag == tag) {
if (audioPlayer.playing) {
await audioPlayer.pause();
} else {
await audioPlayer.play();
await audioPlayer.playOrPause();
return;
}
} else {
await audioPlayer.stop();
audioPlayerTag = tag;
Audio audio;
String source;
if (isNetworkAudio) {
await audioPlayer.setUrl(
isVoiceMessage
? (RequestHelper.baseUrl +
if (isVoiceMessage) {
source = RequestHelper.baseUrl +
audioSource +
'?accessToken=${RequestService.token}')
: audioSource,
'?accessToken=${RequestService.token}';
} else {
source = audioSource;
}
audio = Audio.network(
kIsWeb ? source.replaceAll('%3A', ':') : source,
metas: isVoiceMessage
? null
: Metas(
artist: 'استودیو دیدوان',
title: currentPodcast!.title,
),
);
} else {
if (kIsWeb) {
await audioPlayer
.setUrl(audioSource!.uri.path.replaceAll('%3A', ':'));
} else {
await audioPlayer.setFilePath(audioSource.path);
}
}
audioPlayer.play();
audio = Audio.file(
audioSource,
metas: isVoiceMessage
? null
: Metas(
artist: 'استودیو دیدوان',
title: currentPodcast!.title,
),
);
}
await audioPlayer.open(
audio,
showNotification: !isVoiceMessage,
notificationSettings: NotificationSettings(
customStopAction: (_) => resetAudioPlayer(),
customNextAction: (_) => onTrackChanged?.call(true),
customPrevAction: (_) => onTrackChanged?.call(false),
),
);
}
static Future<void> resetAudioPlayer() async {

View File

@ -1,7 +1,9 @@
import 'dart:convert';
import 'dart:developer';
import 'package:didvan/services/storage/storage.dart';
import 'package:http/http.dart' as http;
import 'package:http_parser/http_parser.dart' as parser;
import 'package:permission_handler/permission_handler.dart';
class RequestService {
static late String token;
@ -162,6 +164,16 @@ class RequestService {
}
}
Future<void> download(String fileName, String subDirectory) async {
await Permission.storage.request();
final response = await http.get(Uri.parse(url));
StorageService.createFile(
bytes: response.bodyBytes,
subDirectory: subDirectory,
name: fileName,
);
}
void _handleResponse(http.Response? response) {
statusCode = response?.statusCode;
if (_handleError(response)) {

View File

@ -3,7 +3,7 @@ import 'package:didvan/models/requests/radar.dart';
import 'package:didvan/models/requests/studio.dart';
class RequestHelper {
static const String baseUrl = 'https://api.didvan.app';
static const String baseUrl = 'https://test.api.didvan.app';
static const String _baseUserUrl = baseUrl + '/user';
static const String _baseRadarUrl = baseUrl + '/radar';
static const String _baseNewsUrl = baseUrl + '/news';
@ -15,6 +15,8 @@ class RequestHelper {
static const String login = _baseUserUrl + '/login';
static const String directs = _baseUserUrl + '/direct';
static const String userInfo = _baseUserUrl + '/info';
static const String firebaseToken = _baseUserUrl + '/firebaseToken';
static const String silenceInterval = _baseUserUrl + '/silenceInterval';
static const String updateProfilePhoto = _baseUserUrl + '/profile/photo';
static const String checkUsername = _baseUserUrl + '/CheckUsername';
static const String updateProfile = _baseUserUrl + '/profile/edit';
@ -54,11 +56,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 +76,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 +94,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' +
@ -116,6 +106,7 @@ class RequestHelper {
MapEntry('type', args.type),
MapEntry('order', args.order),
MapEntry('search', args.search),
MapEntry('asc', args.asc),
]);
static String studioOverviews({required StudioRequestArgs args}) =>
_baseStudioUrl +
@ -124,8 +115,19 @@ class RequestHelper {
MapEntry('type', args.type),
MapEntry('order', args.order),
MapEntry('search', args.search),
MapEntry('asc', args.asc),
]);
static String mark(int id, String type) => baseUrl + '/$type/$id/mark';
static String tracking(int id, String type) =>
baseUrl + '/$type/$id/tracking';
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,3 +1,6 @@
import 'dart:typed_data';
import 'dart:io' as io;
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:universal_html/html.dart';
@ -6,6 +9,21 @@ class StorageService {
static late String appTempsDir;
static const FlutterSecureStorage _storage = FlutterSecureStorage();
static Future<void> createFile({
required Uint8List bytes,
required String subDirectory,
required String name,
}) async {
final dir = io.Directory(appDocsDir + '/$subDirectory');
if (!await dir.exists()) {
await dir.create(recursive: true);
}
final file = await io.File(
appDocsDir + '/$subDirectory/$name',
).create(recursive: true);
await file.writeAsBytes(bytes);
}
static Future<void> setValue({
required String key,
required dynamic value,

View File

@ -17,6 +17,7 @@ class ActionSheetUtils {
static Future<void> showLogoLoadingIndicator() async {
await showDialog(
barrierDismissible: false,
context: context,
builder: (context) => Padding(
padding: EdgeInsets.symmetric(
@ -79,7 +80,9 @@ class ActionSheetUtils {
isScrollControlled: true,
context: context,
builder: (context) => Container(
padding: data.hasPadding ? const EdgeInsets.all(20) : EdgeInsets.zero,
padding: data.hasPadding
? const EdgeInsets.all(20).copyWith(top: 0)
: EdgeInsets.zero,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(

View File

@ -1,7 +1,7 @@
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/view/alert_data.dart';
import 'package:didvan/providers/core_provider.dart';
import 'package:didvan/providers/user_provider.dart';
import 'package:didvan/providers/core.dart';
import 'package:didvan/providers/user.dart';
import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart';
import 'package:didvan/utils/action_sheet.dart';

View File

@ -1,8 +1,8 @@
import 'dart:developer';
import 'package:didvan/models/view/action_sheet_data.dart';
import 'package:didvan/providers/server_data_provider.dart';
import 'package:didvan/providers/user_provider.dart';
import 'package:didvan/providers/server_data.dart';
import 'package:didvan/providers/user.dart';
import 'package:didvan/routes/routes.dart';
import 'package:didvan/utils/action_sheet.dart';
import 'package:didvan/views/authentication/authentication_state.dart';
@ -76,7 +76,9 @@ class _PasswordInputState extends State<PasswordInput> {
final token = await state.login(userProvider);
if (token != null) {
log(token);
ActionSheetUtils.showLogoLoadingIndicator();
await ServerDataProvider.getData();
ActionSheetUtils.pop();
Navigator.of(context).pushReplacementNamed(Routes.home);
_showResetPasswordDialog();
}

View File

@ -1,4 +1,4 @@
import 'package:didvan/providers/user_provider.dart';
import 'package:didvan/providers/user.dart';
import 'package:didvan/routes/routes.dart';
import 'package:didvan/views/authentication/authentication_state.dart';
import 'package:didvan/views/authentication/widgets/authentication_layout.dart';

View File

@ -2,7 +2,7 @@ import 'dart:async';
import 'package:didvan/config/design_config.dart';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/providers/user_provider.dart';
import 'package:didvan/providers/user.dart';
import 'package:didvan/views/authentication/authentication_state.dart';
import 'package:didvan/views/authentication/widgets/authentication_layout.dart';
import 'package:didvan/views/widgets/didvan/button.dart';

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;
@ -55,12 +59,15 @@ class _CommentsState extends State<Comments> {
child: Stack(
children: [
DidvanScaffold(
physics: const BouncingScrollPhysics(),
backgroundColor: Theme.of(context).colorScheme.surface,
appBarData: AppBarData(
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,7 +78,17 @@ class _CommentsState extends State<Comments> {
itemPadding: const EdgeInsets.symmetric(vertical: 16),
childCount: state.comments.length,
placeholder: const _CommentPlaceholder(),
centerEmptyState: _isPage,
enableEmptyState: state.comments.isEmpty,
emptyState: EmptyState(
asset: Assets.emptyChat,
title: 'اولین نظر را بنویسید...',
),
builder: (context, state, index) => Comment(
key: ValueKey(
state.comments[index].id.toString() +
state.comments[index].text,
),
focusNode: _focusNode,
comment: state.comments[index],
),

View File

@ -4,8 +4,8 @@ import 'package:didvan/models/comment/feedback.dart';
import 'package:didvan/models/comment/reply.dart';
import 'package:didvan/models/comment/user.dart';
import 'package:didvan/models/enums.dart';
import 'package:didvan/providers/core_provider.dart';
import 'package:didvan/providers/user_provider.dart';
import 'package:didvan/providers/core.dart';
import 'package:didvan/providers/user.dart';
import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart';
import 'package:provider/provider.dart';
@ -17,19 +17,16 @@ 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) {
@ -47,18 +44,40 @@ class CommentsState extends CoreProvier {
appState = AppState.failed;
}
Future<void> feedback(int id, bool like, bool dislike) async {
Future<void> feedback({
required int id,
required bool like,
required bool dislike,
required int likeCount,
required int dislikeCount,
int? replyId,
}) async {
_feedbackQueue.addAll({id: MapEntry(like, dislike)});
dynamic comment;
if (replyId == null) {
comment = comments.firstWhere((comment) => comment.id == id);
} else {
comment = comments
.firstWhere((comment) => comment.id == id)
.replies
.firstWhere((element) => element.id == replyId);
}
if (comment != null) {
comment.feedback.like = likeCount;
comment.feedback.dislike = dislikeCount;
comment.disliked = dislike;
comment.liked = like;
}
Future.delayed(const Duration(milliseconds: 500), () async {
if (!_feedbackQueue.containsKey(id)) return;
final service = RequestService(
isRadar
? RequestHelper.feedbackRadarComment(itemId, id)
: RequestHelper.feedbackNewsComment(itemId, id),
RequestHelper.feedback(itemId, id, type),
body: {
'like': _feedbackQueue[id]!.key,
'dislike': _feedbackQueue[id]!.value,
});
},
);
await service.put();
_feedbackQueue.remove(id);
});
@ -74,7 +93,7 @@ class CommentsState extends CoreProvier {
createdAt: DateTime.now().toString(),
liked: false,
disliked: false,
feedback: const FeedbackData(like: 0, dislike: 0),
feedback: FeedbackData(like: 0, dislike: 0),
toUser: replyingTo!,
user: UserOverview(
id: user.id,
@ -92,7 +111,7 @@ class CommentsState extends CoreProvier {
createdAt: DateTime.now().toString(),
liked: false,
disliked: false,
feedback: const FeedbackData(like: 0, dislike: 0),
feedback: FeedbackData(like: 0, dislike: 0),
user: UserOverview(
id: user.id,
fullName: user.fullName,
@ -119,10 +138,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

@ -49,7 +49,7 @@ class CommentState extends State<Comment> {
duration: DesignConfig.lowAnimationDuration,
isVisible: _showSubComments,
child: _commentBuilder(
isSubComment: true,
isReply: true,
comment: _comment.replies[i],
),
),
@ -57,11 +57,10 @@ class CommentState extends State<Comment> {
);
}
Widget _commentBuilder({required comment, bool isSubComment = false}) =>
Container(
Widget _commentBuilder({required comment, bool isReply = false}) => Container(
decoration: BoxDecoration(
border: Border(
right: isSubComment
right: isReply
? BorderSide(color: Theme.of(context).colorScheme.caption)
: BorderSide.none,
),
@ -69,7 +68,7 @@ class CommentState extends State<Comment> {
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (isSubComment) const SizedBox(width: 12),
if (isReply) const SizedBox(width: 12),
if (comment.user.photo == null)
const Icon(DidvanIcons.avatar_light),
if (comment.user.photo != null)
@ -99,7 +98,7 @@ class CommentState extends State<Comment> {
],
),
const SizedBox(height: 8),
if (isSubComment)
if (isReply)
DidvanText(
'پاسخ به ${comment.toUser.fullName}',
style: Theme.of(context).textTheme.caption,
@ -124,8 +123,8 @@ class CommentState extends State<Comment> {
color: Theme.of(context).colorScheme.primary,
),
),
if (!isSubComment) const SizedBox(width: 20),
if (!isSubComment && comment.replies.isNotEmpty)
if (!isReply) const SizedBox(width: 20),
if (!isReply && comment.replies.isNotEmpty)
InkWrapper(
onPressed: () => setState(
() => _showSubComments = !_showSubComments,
@ -154,8 +153,15 @@ class CommentState extends State<Comment> {
dislikeCount: comment.feedback.dislike,
likeValue: comment.liked,
dislikeValue: comment.disliked,
onFeedback: (like, dislike) =>
state.feedback(comment.id, like, dislike),
onFeedback: (like, dislike, likeCount, dislikeCount) =>
state.feedback(
id: _comment.id,
like: like,
dislike: dislike,
likeCount: likeCount,
dislikeCount: dislikeCount,
replyId: isReply ? comment.id : null,
),
),
],
),
@ -172,7 +178,8 @@ class _FeedbackButtons extends StatefulWidget {
final int dislikeCount;
final bool likeValue;
final bool dislikeValue;
final void Function(bool like, bool dislike) onFeedback;
final void Function(bool like, bool dislike, int likeCount, int dislikeCount)
onFeedback;
const _FeedbackButtons({
Key? key,
required this.onFeedback,
@ -228,7 +235,8 @@ class _FeedbackButtonsState extends State<_FeedbackButtons> {
}
_likeValue = !_likeValue;
});
widget.onFeedback(_likeValue, _dislikeValue);
widget.onFeedback(
_likeValue, _dislikeValue, _likeCount, _dislikeCount);
},
),
const SizedBox(width: 16),
@ -257,7 +265,8 @@ class _FeedbackButtonsState extends State<_FeedbackButtons> {
}
_dislikeValue = !_dislikeValue;
});
widget.onFeedback(_likeValue, _dislikeValue);
widget.onFeedback(
_likeValue, _dislikeValue, _likeCount, _dislikeCount);
},
),
],

View File

@ -1,7 +1,7 @@
import 'package:didvan/constants/assets.dart';
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/view/app_bar_data.dart';
import 'package:didvan/providers/server_data_provider.dart';
import 'package:didvan/providers/server_data.dart';
import 'package:didvan/services/media/media.dart';
import 'package:didvan/views/home/direct/direct_state.dart';
import 'package:didvan/views/home/direct/widgets/message.dart';

View File

@ -3,7 +3,8 @@ import 'dart:io';
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/message_data/message_data.dart';
import 'package:didvan/models/message_data/radar_attachment.dart';
import 'package:didvan/providers/core_provider.dart';
import 'package:didvan/providers/core.dart';
import 'package:didvan/services/media/media.dart';
import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart';
import 'package:flutter/foundation.dart';
@ -46,6 +47,7 @@ class DirectState extends CoreProvier {
}
Future<void> startRecording() async {
text = null;
await _recorder.hasPermission();
if (!kIsWeb) {
Vibrate.feedback(FeedbackType.medium);
@ -88,6 +90,7 @@ class DirectState extends CoreProvier {
Future<void> sendMessage() async {
if ((text == null || text!.isEmpty) && recordedFile == null) return;
MediaService.audioPlayer.stop();
messages.insert(
0,
MessageData(

View File

@ -1,31 +1,39 @@
import 'dart:io';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/services/media/media.dart';
import 'package:didvan/views/home/widgets/audio/audio_slider.dart';
import 'package:didvan/views/home/widgets/player_controller_button.dart';
import 'package:didvan/views/widgets/didvan/icon_button.dart';
import 'package:flutter/material.dart';
class AudioWidget extends StatelessWidget {
final String? audioUrl;
final File? audioFile;
const AudioWidget({Key? key, this.audioUrl, this.audioFile})
: super(key: key);
final int id;
const AudioWidget({
Key? key,
this.audioUrl,
this.audioFile,
required this.id,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return StreamBuilder<bool>(
stream: MediaService.audioPlayer.playingStream,
stream: MediaService.audioPlayer.isPlaying,
builder: (context, snapshot) {
return Row(
children: [
Expanded(
child: AudioSlider(
tag: audioUrl ?? audioFile!.path,
tag: 'message-$id',
),
),
AudioControllerButton(
_AudioControllerButton(
audioFile: audioFile,
audioUrl: audioUrl,
id: id,
),
],
);
@ -33,3 +41,33 @@ class AudioWidget extends StatelessWidget {
);
}
}
class _AudioControllerButton extends StatelessWidget {
final String? audioUrl;
final File? audioFile;
final int id;
const _AudioControllerButton(
{Key? key, this.audioUrl, this.audioFile, required this.id})
: super(key: key);
bool get _nowPlaying => MediaService.audioPlayerTag == 'message-$id';
@override
Widget build(BuildContext context) {
return DidvanIconButton(
icon: MediaService.audioPlayer.isPlaying.value && _nowPlaying
? DidvanIcons.pause_circle_solid
: DidvanIcons.play_circle_solid,
color: Theme.of(context).colorScheme.focusedBorder,
onPressed: () {
MediaService.handleAudioPlayback(
audioSource: audioFile?.path ?? audioUrl,
id: id,
isNetworkAudio: audioFile == null,
isVoiceMessage: true,
);
},
);
}
}

View File

@ -65,6 +65,7 @@ class Message extends StatelessWidget {
AudioWidget(
audioFile: message.audioFile,
audioUrl: message.audio,
id: message.id,
),
if (message.radar != null) const DidvanDivider(),
if (message.radar != null) const SizedBox(height: 4),

View File

@ -211,7 +211,10 @@ class _RecordChecking extends StatelessWidget {
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: AudioWidget(audioFile: state.recordedFile!),
child: AudioWidget(
audioFile: state.recordedFile!,
id: 0,
),
),
),
DidvanIconButton(

View File

@ -1,8 +1,11 @@
import 'package:didvan/models/requests/studio.dart';
import 'package:didvan/models/tag.dart';
import 'package:didvan/models/view/app_bar_data.dart';
import 'package:didvan/views/home/hashtag/hashtag_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/sliver_state_handler.dart';
import 'package:flutter/material.dart';
@ -10,18 +13,20 @@ import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:provider/provider.dart';
class Hashtag extends StatefulWidget {
final Tag tag;
const Hashtag({Key? key, required this.tag}) : super(key: key);
final Map<String, dynamic> pageData;
const Hashtag({Key? key, required this.pageData}) : super(key: key);
@override
_HashtagState createState() => _HashtagState();
}
class _HashtagState extends State<Hashtag> {
Tag get _tag => widget.pageData['tag'];
@override
void initState() {
final state = context.read<HashtagState>();
state.id = widget.tag.id;
state.id = _tag.id;
Future.delayed(Duration.zero, () => state.getTagItems(page: 1));
super.initState();
}
@ -29,7 +34,7 @@ class _HashtagState extends State<Hashtag> {
@override
Widget build(BuildContext context) {
return DidvanScaffold(
appBarData: AppBarData(title: '#' + widget.tag.label, hasBack: true),
appBarData: AppBarData(title: '#' + _tag.label, hasBack: true),
slivers: [
Consumer<HashtagState>(
builder: (context, state, child) => SliverStateHandler<HashtagState>(
@ -50,19 +55,38 @@ class _HashtagState extends State<Hashtag> {
}
final item = state.items[index];
final type = item.type;
if (type == 'radar') {
switch (type) {
case 'radar':
return RadarOverview(
radar: item,
onCommentsChanged: (id, count) => item.comments = count,
onMarkChanged: (id, value) => item.marked = value,
onMarkChanged: (_, value, __) =>
_changeMark(item.id, value, type),
onCommentsChanged: (_, count) => item.comments = count,
);
} else if (type == 'news') {
case 'news':
return NewsOverview(
news: item,
onMarkChanged: (id, value) => item.marked = value,
onMarkChanged: (_, value, __) =>
_changeMark(item.id, value, type),
);
case 'podcast':
return PodcastOverview(
podcast: item,
onMarkChanged: (_, value, __) =>
_changeMark(item.id, value, type),
studioRequestArgs:
const StudioRequestArgs(page: 0, type: 'podcast'),
);
case 'video':
return VideoOverview(
video: item,
onMarkChanged: (_, value, __) =>
_changeMark(item.id, value, type),
studioRequestArgs:
const StudioRequestArgs(page: 0, type: 'video'),
);
}
return Container();
return const SizedBox();
},
childCount:
state.items.length + (state.page != state.lastPage ? 1 : 0),
@ -72,4 +96,15 @@ class _HashtagState extends State<Hashtag> {
],
);
}
void _changeMark(int id, bool value, String type) {
final state = context.read<HashtagState>();
state.items
.firstWhere((element) => element.id == id && element.type == type)
.marked = value;
state.update();
if (type == widget.pageData['type']) {
widget.pageData['onMarkChanged'](id, value);
}
}
}

View File

@ -1,6 +1,6 @@
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/overview_data.dart';
import 'package:didvan/providers/core_provider.dart';
import 'package:didvan/providers/core.dart';
import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart';
@ -18,13 +18,12 @@ class HashtagState extends CoreProvier {
}
final service = RequestService(RequestHelper.tag(
ids: [id],
itemId: 1,
type: 'radar',
limit: 15,
page: page,
));
await service.httpGet();
if (service.isSuccess) {
lastPage = service.result['lastPage'];
final contents = service.result['contents'];
for (var i = 0; i < contents.length; i++) {
items.add(OverviewData.fromJson(contents[i]));

View File

@ -5,7 +5,7 @@ import 'package:didvan/views/home/radar/radar.dart';
import 'package:didvan/views/home/settings/settings.dart';
import 'package:didvan/views/home/statistics/statistics.dart';
import 'package:didvan/views/home/studio/studio.dart';
import 'package:didvan/views/home/widgets/bnb.dart';
import 'package:didvan/views/widgets/didvan/bnb.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

View File

@ -1,4 +1,4 @@
import 'package:didvan/providers/core_provider.dart';
import 'package:didvan/providers/core.dart';
class HomeState extends CoreProvier {
int _currentPageIndex = 2;

View File

@ -66,7 +66,7 @@ class _NewsState extends State<News> {
final news = state.news[index];
return NewsOverview(
news: news,
onMarkChanged: (id, value) => state.onMarkChanged(id, value),
onMarkChanged: state.onMarkChanged,
newsRequestArgs: NewsRequestArgs(
page: state.page,
endDate: state.endDate,

View File

@ -38,12 +38,16 @@ class _NewsDetailsState extends State<NewsDetails> {
Widget build(BuildContext context) {
return Scaffold(
body: Consumer<NewsDetailsState>(
builder: (context, state, child) => StateHandler<NewsDetailsState>(
builder: (context, state, child) => WillPopScope(
onWillPop: () async {
state.handleTracking(sendRequest: true);
return true;
},
child: StateHandler<NewsDetailsState>(
onRetry: () => state.getNewsDetails(state.currentNews.id),
state: state,
builder: (context, state) => Stack(
children: [
if (state.news.isNotEmpty)
IgnorePointer(
ignoring: state.isFetchingNewItem,
child: DidvanPageView(
@ -53,9 +57,10 @@ class _NewsDetailsState extends State<NewsDetails> {
scrollController: _scrollController,
items: state.news,
currentIndex: state.currentIndex,
onMarkChanged: (id, value) =>
widget.pageData['onMarkChanged'](id, value),
),
),
if (state.news.isNotEmpty)
Positioned(
bottom: 0,
left: 0,
@ -77,6 +82,7 @@ class _NewsDetailsState extends State<NewsDetails> {
),
),
),
),
);
}

View File

@ -5,7 +5,7 @@ import 'package:didvan/models/enums.dart';
import 'package:didvan/models/news_details_data.dart';
import 'package:didvan/models/overview_data.dart';
import 'package:didvan/models/requests/news.dart';
import 'package:didvan/providers/core_provider.dart';
import 'package:didvan/providers/core.dart';
import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart';
@ -32,7 +32,7 @@ class NewsDetailsState extends CoreProvier {
}
final service = RequestService(RequestHelper.newsDetails(id, args));
await service.httpGet();
_handleTracking(sendRequest: isForward != null);
handleTracking(sendRequest: isForward != null);
if (service.isSuccess) {
final result = service.result;
final newsItem = NewsDetailsData.fromJson(result['news']);
@ -88,15 +88,21 @@ class NewsDetailsState extends CoreProvier {
notifyListeners();
}
Future<void> _handleTracking({bool sendRequest = true}) async {
Future<void> handleTracking({bool sendRequest = true}) async {
if (!sendRequest) {
_trackingTimerCounter = 0;
_trackingTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
_trackingTimerCounter++;
});
return;
}
//send request
_trackingTimerCounter = 0;
final service = RequestService(
RequestHelper.tracking(currentNews.id, 'news'),
body: {
'sec': _trackingTimerCounter,
},
);
service.put();
}
Future<void> getRelatedContents() async {

View File

@ -1,8 +1,7 @@
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/overview_data.dart';
import 'package:didvan/models/requests/news.dart';
import 'package:didvan/providers/core_provider.dart';
import 'package:didvan/providers/user_provider.dart';
import 'package:didvan/providers/core.dart';
import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart';
@ -69,10 +68,11 @@ class NewsState extends CoreProvier {
appState = AppState.failed;
}
Future<void> onMarkChanged(int id, bool value) async {
Future<void> onMarkChanged(int id, bool value, bool shouldUpdate) async {
news.firstWhere((element) => element.id == id).marked = value;
if (shouldUpdate) {
notifyListeners();
UserProvider.changeNewsMark(id, value);
}
}
bool get isFiltering => startDate != null || endDate != null;

View File

@ -139,7 +139,7 @@ class _RadarState extends State<Radar> {
final radar = state.radars[index];
return RadarOverview(
radar: radar,
onMarkChanged: (id, value) => state.changeMark(id, value),
onMarkChanged: state.changeMark,
onCommentsChanged: (id, count) =>
state.onCommentsChanged(id, count),
radarRequestArgs: RadarRequestArgs(

View File

@ -38,12 +38,16 @@ class _RadarDetailsState extends State<RadarDetails> {
Widget build(BuildContext context) {
return Scaffold(
body: Consumer<RadarDetailsState>(
builder: (context, state, child) => StateHandler<RadarDetailsState>(
builder: (context, state, child) => WillPopScope(
onWillPop: () async {
state.handleTracking(sendRequest: true);
return true;
},
child: StateHandler<RadarDetailsState>(
onRetry: () => state.getRadarDetails(widget.pageData['id']),
state: state,
builder: (context, state) => Stack(
children: [
if (state.radars.isNotEmpty)
IgnorePointer(
ignoring: state.isFetchingNewItem,
child: DidvanPageView(
@ -53,9 +57,10 @@ class _RadarDetailsState extends State<RadarDetails> {
scrollController: _scrollController,
items: state.radars,
currentIndex: state.currentIndex,
onMarkChanged: (id, value) =>
widget.pageData['onMarkChanged']?.call(id, value),
),
),
if (state.radars.isNotEmpty)
Positioned(
bottom: 0,
left: 0,
@ -84,6 +89,7 @@ class _RadarDetailsState extends State<RadarDetails> {
),
),
),
),
);
}

View File

@ -5,7 +5,7 @@ import 'package:didvan/models/enums.dart';
import 'package:didvan/models/overview_data.dart';
import 'package:didvan/models/radar_details_data.dart';
import 'package:didvan/models/requests/radar.dart';
import 'package:didvan/providers/core_provider.dart';
import 'package:didvan/providers/core.dart';
import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart';
@ -38,7 +38,7 @@ class RadarDetailsState extends CoreProvier {
}
final service = RequestService(RequestHelper.radarDetails(id, args));
await service.httpGet();
_handleTracking(sendRequest: isForward != null);
handleTracking(sendRequest: isForward != null);
if (service.isSuccess) {
final result = service.result;
final radar = RadarDetailsData.fromJson(result['radar']);
@ -121,15 +121,21 @@ class RadarDetailsState extends CoreProvier {
notifyListeners();
}
Future<void> _handleTracking({bool sendRequest = true}) async {
Future<void> handleTracking({bool sendRequest = true}) async {
if (!sendRequest) {
_trackingTimerCounter = 0;
_trackingTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
_trackingTimerCounter++;
});
return;
}
//send request
_trackingTimerCounter = 0;
final service = RequestService(
RequestHelper.tracking(currentRadar.id, 'radar'),
body: {
'sec': _trackingTimerCounter,
},
);
service.put();
}
@override

View File

@ -3,8 +3,7 @@ import 'package:didvan/models/enums.dart';
import 'package:didvan/models/overview_data.dart';
import 'package:didvan/models/requests/radar.dart';
import 'package:didvan/models/view/radar_category.dart';
import 'package:didvan/providers/core_provider.dart';
import 'package:didvan/providers/user_provider.dart';
import 'package:didvan/providers/core.dart';
import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart';
@ -82,10 +81,11 @@ class RadarState extends CoreProvier {
appState = AppState.failed;
}
Future<void> changeMark(int id, bool value) async {
Future<void> changeMark(int id, bool value, bool shouldUpdate) async {
radars.firstWhere((element) => element.id == id).marked = value;
if (shouldUpdate) {
notifyListeners();
UserProvider.changeRadarMark(id, value);
}
}
void onCommentsChanged(int id, int count) {

View File

@ -1,7 +1,6 @@
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/overview_data.dart';
import 'package:didvan/providers/core_provider.dart';
import 'package:didvan/providers/user_provider.dart';
import 'package:didvan/providers/core.dart';
import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart';
@ -41,16 +40,6 @@ class BookmarksState extends CoreProvier {
void onMarkChanged(int id, bool value) {
if (value) return;
final type = bookmarks.firstWhere((element) => element.id == id).type;
switch (type) {
case 'radar':
UserProvider.changeRadarMark(id, value);
break;
case 'news':
UserProvider.changeNewsMark(id, value);
break;
default:
}
bookmarks.removeWhere((element) => element.id == id);
notifyListeners();
}

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,11 +71,29 @@ class _FilteredBookmarksState extends State<FilteredBookmarks> {
hasUnmarkConfirmation: true,
);
}
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,
);
},
childCount: state.bookmarks.length,
onRetry: () => state.getBookmarks(page: state.page),
@ -81,9 +103,10 @@ class _FilteredBookmarksState extends State<FilteredBookmarks> {
);
}
Future<void> _onBookmarkChanged(int id, bool value) async {
Future<void> _onBookmarkChanged(int id, bool value, bool shouldUpdate) async {
if (value) return;
final state = context.read<FilteredBookmarksState>();
state.onMarkChanged(id, false);
widget.onDeleted?.call(id);
}
}

View File

@ -1,13 +1,10 @@
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/overview_data.dart';
import 'package:didvan/providers/core_provider.dart';
import 'package:didvan/providers/user_provider.dart';
import 'package:didvan/providers/core.dart';
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 +12,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';
@ -56,15 +44,6 @@ 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:
}
bookmarks.removeWhere((element) => element.id == id);
notifyListeners();
}

View File

@ -36,10 +36,13 @@ class _DirectListState extends State<DirectList> {
title: 'پیام‌ها',
trailing: state.unreadCount == 0
? null
: DidvanBadge(
: Padding(
padding: const EdgeInsets.only(left: 20),
child: DidvanBadge(
text: state.unreadCount.toString(),
),
),
),
slivers: [
SliverStateHandler<DirectListState>(
onRetry: state.getDirectsList,

View File

@ -1,6 +1,6 @@
import 'package:didvan/models/chat_room/chat_room.dart';
import 'package:didvan/models/enums.dart';
import 'package:didvan/providers/core_provider.dart';
import 'package:didvan/providers/core.dart';
import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart';
@ -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

@ -3,10 +3,12 @@ import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/chat_room/chat_room.dart';
import 'package:didvan/routes/routes.dart';
import 'package:didvan/utils/date_time.dart';
import 'package:didvan/views/home/settings/direct_list/direct_list_state.dart';
import 'package:didvan/views/widgets/didvan/badge.dart';
import 'package:didvan/views/widgets/didvan/divider.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ChatRoomItem extends StatelessWidget {
final ChatRoom chatRoom;
@ -20,7 +22,10 @@ class ChatRoomItem extends StatelessWidget {
Routes.direct,
arguments: {'type': chatRoom.type},
);
final state = context.read<DirectListState>();
int unreadCount = chatRoom.unread;
chatRoom.unread = 0;
state.unreadCount -= unreadCount;
},
child: Container(
color: Colors.transparent,

View File

@ -6,8 +6,9 @@ import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/constants/assets.dart';
import 'package:didvan/models/view/action_sheet_data.dart';
import 'package:didvan/models/view/app_bar_data.dart';
import 'package:didvan/providers/theme_provider.dart';
import 'package:didvan/providers/theme.dart';
import 'package:didvan/utils/action_sheet.dart';
import 'package:didvan/utils/date_time.dart';
import 'package:didvan/views/home/settings/general_settings/settings_state.dart';
import 'package:didvan/views/home/widgets/menu_item.dart';
import 'package:didvan/views/widgets/didvan/card.dart';
@ -36,6 +37,9 @@ class _GeneralSettingsState extends State<GeneralSettings> {
return 'کوچک';
}
int _intervalStart = 0;
int _intervalEnd = 24;
@override
Widget build(BuildContext context) {
return Consumer<GeneralSettingsState>(
@ -50,7 +54,13 @@ class _GeneralSettingsState extends State<GeneralSettings> {
title: 'زمان دریافت اعلان',
onTap: () => _pickTimeRange(context),
icon: DidvanIcons.notification_regular,
suffix: state.notificationTimeRange[0],
suffix: DateTimeUtils.normalizeTimeDuration(
Duration(minutes: state.notificationTimeRange[1]),
) +
' - ' +
DateTimeUtils.normalizeTimeDuration(
Duration(minutes: state.notificationTimeRange[0]),
),
),
),
const ItemTitle(
@ -183,6 +193,9 @@ class _GeneralSettingsState extends State<GeneralSettings> {
}
Future<void> _pickTimeRange(BuildContext context) async {
final state = context.read<GeneralSettingsState>();
_intervalStart = state.notificationTimeRange[0];
_intervalEnd = state.notificationTimeRange[1];
ActionSheetUtils.showBottomSheet(
data: ActionSheetData(
content: Row(
@ -198,6 +211,9 @@ class _GeneralSettingsState extends State<GeneralSettings> {
),
title: 'زمان دریافت اعلان',
titleIcon: DidvanIcons.notification_regular,
onConfirmed: () {
state.notificationTimeRange = [_intervalStart, _intervalEnd];
},
),
);
}
@ -222,7 +238,9 @@ class _GeneralSettingsState extends State<GeneralSettings> {
color: Theme.of(context).colorScheme.border,
),
),
child: DidvanText(state.notificationTimeRange[index]),
child: DidvanText(DateTimeUtils.normalizeTimeDuration(
Duration(minutes: index == 0 ? _intervalStart : _intervalEnd),
)),
),
),
),
@ -230,7 +248,6 @@ class _GeneralSettingsState extends State<GeneralSettings> {
}
Future<void> _openTimePicker(BuildContext context, int index) async {
final GeneralSettingsState state = context.read<GeneralSettingsState>();
await Navigator.of(context).push(
showPicker(
okText: 'تایید',
@ -240,8 +257,9 @@ class _GeneralSettingsState extends State<GeneralSettings> {
cancelStyle: Theme.of(context).textTheme.bodyText2!,
unselectedColor: Theme.of(context).colorScheme.text,
blurredBackground: true,
hourLabel: 'ساعت',
minuteLabel: 'دقیقه',
disableMinute: true,
hourLabel: ':',
minuteLabel: '',
is24HrFormat: true,
iosStylePicker: true,
minuteInterval: MinuteInterval.FIFTEEN,
@ -249,12 +267,11 @@ class _GeneralSettingsState extends State<GeneralSettings> {
value: const TimeOfDay(hour: 0, minute: 0),
themeData: Theme.of(context),
onChange: (time) {
state.notificationTimeRange = state.notificationTimeRange
..replaceRange(
index,
index + 1,
['${time.hour}:${time.minute}'],
);
if (index == 0) {
_intervalStart = time.hour;
return;
}
_intervalEnd = time.hour;
},
),
);

View File

@ -1,5 +1,7 @@
import 'package:didvan/models/enums.dart';
import 'package:didvan/providers/core_provider.dart';
import 'package:didvan/providers/core.dart';
import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart';
import 'package:didvan/services/storage/storage.dart';
class GeneralSettingsState extends CoreProvier {
@ -7,24 +9,26 @@ class GeneralSettingsState extends CoreProvier {
getSettingsFromStorage();
}
List _notificationTimeRange = ['00:00', '23:59'];
List<int> _notificationTimeRange = [0, 24];
String _fontFamily = 'Dana-FA';
double _fontSizeScale = 1;
String _brightness = 'light';
set notificationTimeRange(List value) {
set notificationTimeRange(List<int> value) {
_notificationTimeRange = value;
StorageService.setValue(
key: 'notificationTimeRangeStart',
value: value[0],
);
StorageService.setValue(
key: 'notificationTimeRangeStart',
key: 'notificationTimeRangeEnd',
value: value[1],
);
notifyListeners();
_setSilenceInterval();
}
List get notificationTimeRange => _notificationTimeRange;
List<int> get notificationTimeRange => _notificationTimeRange;
set fontFamily(String value) {
_fontFamily = value;
@ -59,12 +63,27 @@ class GeneralSettingsState extends CoreProvier {
String get brightness => _brightness;
Future<void> _setSilenceInterval() async {
final service = RequestService(RequestHelper.silenceInterval, body: {
'start': notificationTimeRange[0],
'end': notificationTimeRange[1]
});
await service.put();
}
Future<void> getSettingsFromStorage() async {
appState = AppState.busy;
_notificationTimeRange[0] =
await StorageService.getValue(key: 'notificationTimeRangeStart');
_notificationTimeRange[1] =
await StorageService.getValue(key: 'notificationTimeRangeEnd');
try {
_notificationTimeRange[0] = int.parse(
await StorageService.getValue(key: 'notificationTimeRangeStart'),
);
_notificationTimeRange[1] = int.parse(
await StorageService.getValue(key: 'notificationTimeRangeEnd'),
);
} catch (e) {
notificationTimeRange = [0, 0];
}
_fontFamily = await StorageService.getValue(key: 'fontFamily');
_brightness = await StorageService.getValue(key: 'brightness');
final scale = await StorageService.getValue(key: 'fontSizeScale');

View File

@ -2,7 +2,7 @@ import 'dart:async';
import 'package:didvan/config/design_config.dart';
import 'package:didvan/models/view/app_bar_data.dart';
import 'package:didvan/providers/user_provider.dart';
import 'package:didvan/providers/user.dart';
import 'package:didvan/routes/routes.dart';
import 'package:didvan/views/home/settings/profile/widgets/profile_photo.dart';
import 'package:didvan/views/home/widgets/menu_item.dart';

View File

@ -5,7 +5,7 @@ import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/view/action_sheet_data.dart';
import 'package:didvan/models/view/alert_data.dart';
import 'package:didvan/providers/user_provider.dart';
import 'package:didvan/providers/user.dart';
import 'package:didvan/services/media/media.dart';
import 'package:didvan/utils/action_sheet.dart';
import 'package:didvan/views/home/widgets/menu_item.dart';
@ -149,7 +149,7 @@ class _ProfilePhotoState extends State<ProfilePhoto> {
cancelButtonTitle: 'بازگشت',
),
androidUiSettings: const AndroidUiSettings(toolbarTitle: 'برش تصویر'),
compressQuality: 70,
compressQuality: 30,
);
if (file == null) return;
}

View File

@ -1,6 +1,6 @@
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/providers/theme_provider.dart';
import 'package:didvan/providers/user_provider.dart';
import 'package:didvan/providers/theme.dart';
import 'package:didvan/providers/user.dart';
import 'package:didvan/routes/routes.dart';
import 'package:didvan/services/storage/storage.dart';
import 'package:didvan/views/home/widgets/logo_app_bar.dart';
@ -108,7 +108,7 @@ class Settings extends StatelessWidget {
),
const SizedBox(height: 16),
DidvanText(
'نسخه نرم‌افزار: 1.1.4',
'نسخه نرم‌افزار: 1.5.0',
style: Theme.of(context).textTheme.caption,
),
],

View File

@ -1,26 +1,227 @@
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/assets.dart';
import 'package:didvan/views/home/widgets/logo_app_bar.dart';
import 'package:didvan/views/widgets/state_handlers/empty_state.dart';
import 'package:flutter/material.dart';
import 'dart:async';
class Studio extends StatelessWidget {
import 'package:didvan/config/design_config.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/enums.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';
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/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';
import 'package:didvan/views/widgets/item_title.dart';
import 'package:didvan/views/widgets/state_handlers/empty_result.dart';
import 'package:didvan/views/widgets/state_handlers/sliver_state_handler.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Studio extends StatefulWidget {
const Studio({Key? key}) : super(key: key);
@override
State<Studio> createState() => _StudioState();
}
class _StudioState extends State<Studio> {
final _focusNode = FocusNode();
Timer? _timer;
@override
void initState() {
context.read<StudioState>().init();
super.initState();
}
@override
Widget build(BuildContext context) {
return Column(
return Consumer<StudioState>(
builder: (context, state, child) => CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Row(
children: [
const LogoAppBar(),
Expanded(
child: EmptyState(
asset: Assets.emptyStudio,
title: 'استودیو آینده',
subtitle: 'به زودی...',
titleColor: Theme.of(context).colorScheme.title,
const Expanded(child: LogoAppBar(type: 'studio')),
Padding(
padding:
EdgeInsets.only(top: MediaQuery.of(context).padding.top),
child: DidvanIconButton(
icon: DidvanIcons.bookmark_regular,
onPressed: () => Navigator.of(context).pushNamed(
Routes.filteredBookmarks,
arguments: {'type': state.type, 'onDeleted': (_) {}},
),
),
),
],
),
),
const SliverToBoxAdapter(
child: StudioTabBar(),
),
if (state.appState != AppState.failed)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: SearchField(
title: 'استودیو',
onChanged: _onChanged,
focusNode: _focusNode,
),
),
),
if (state.appState != AppState.failed)
SliverToBoxAdapter(
child: AnimatedVisibility(
isVisible: !state.searching,
duration: DesignConfig.lowAnimationDuration,
child: const StudioSlider(),
),
),
if (state.appState != AppState.failed && state.studios.isNotEmpty)
const SliverPadding(
padding: EdgeInsets.symmetric(horizontal: 16),
sliver: SliverToBoxAdapter(
child: DidvanDivider(
verticalPadding: 0,
),
),
),
if (state.appState != AppState.failed && state.studios.isNotEmpty)
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverToBoxAdapter(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
AnimatedVisibility(
isVisible: !state.searching,
duration: DesignConfig.lowAnimationDuration,
child: ItemTitle(title: state.orderString),
),
DidvanIconButton(
gestureSize: 36,
icon: DidvanIcons.sort_regular,
onPressed: _showSortDialog,
),
],
),
),
),
SliverStateHandler<StudioState>(
state: state,
itemPadding: const EdgeInsets.only(
bottom: 8,
left: 16,
right: 16,
),
emptyState: EmptyResult(
onNewSearch: () => _focusNode.requestFocus(),
),
centerEmptyState: true,
enableEmptyState: state.studios.isEmpty,
placeholder: state.videosSelected
? VideoOverview.placeHolder
: PodcastOverview.placeholder,
builder: (context, state, index) => state.videosSelected
? VideoOverview(
onMarkChanged: state.changeMark,
hasUnmarkConfirmation: false,
video: state.studios[index],
studioRequestArgs: StudioRequestArgs(
page: state.page,
order: state.order,
search: state.search,
type: state.type,
asc: state.selectedSortTypeIndex == 1,
),
)
: PodcastOverview(
podcast: state.studios[index],
onMarkChanged: state.changeMark,
studioRequestArgs: StudioRequestArgs(
page: state.page,
order: state.order,
search: state.search,
type: state.type,
asc: state.selectedSortTypeIndex == 1,
),
),
childCount: state.studios.length,
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(
data: ActionSheetData(
content: StatefulBuilder(
builder: (context, setState) => Column(
children: [
DidvanRadialButton(
title: 'تازه‌ترین‌ها',
onSelected: () => setState(
() => state.selectedSortTypeIndex = 0,
),
value: state.selectedSortTypeIndex == 0,
),
const SizedBox(height: 24),
DidvanRadialButton(
title: 'قدیمی‌ترین‌ها',
onSelected: () => setState(
() => state.selectedSortTypeIndex = 1,
),
value: state.selectedSortTypeIndex == 1,
),
const SizedBox(height: 24),
DidvanRadialButton(
title: 'پربازدیدترین‌ها',
onSelected: () => setState(
() => state.selectedSortTypeIndex = 2,
),
value: state.selectedSortTypeIndex == 2,
),
const SizedBox(height: 24),
DidvanRadialButton(
title: 'پربحث‌ترین‌ها',
onSelected: () => setState(
() => state.selectedSortTypeIndex = 3,
),
value: state.selectedSortTypeIndex == 3,
),
],
),
),
title: 'مرتب‌‌سازی',
titleIcon: DidvanIcons.sort_regular,
hasDismissButton: false,
confrimTitle: 'مرتب سازی',
onConfirmed: () => state.getStudios(page: 1),
),
);
}
}

View File

@ -1,170 +0,0 @@
import 'dart:io';
import 'package:didvan/config/design_config.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/widgets/didvan/scaffold.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;
@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(
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(
'''
<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,
),
),
),
],
),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,140 @@
import 'package:better_player/better_player.dart';
import 'package:didvan/models/view/app_bar_data.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_widget.dart';
import 'package:didvan/views/home/widgets/bookmark_button.dart';
import 'package:didvan/views/widgets/didvan/app_bar.dart';
import 'package:didvan/views/widgets/state_handlers/state_handler.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.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> {
int _currentlyPlayingId = 0;
late BetterPlayerController _betterPlayerController;
@override
void initState() {
_betterPlayerController = BetterPlayerController(
const BetterPlayerConfiguration(
aspectRatio: 16 / 9,
showPlaceholderUntilPlay: true,
autoDispose: false,
fullScreenAspectRatio: 16 / 9,
),
);
final state = context.read<StudioDetailsState>();
state.args = widget.pageData['args'];
Future.delayed(
Duration.zero,
() => state.getStudioDetails(widget.pageData['id']),
);
super.initState();
}
@override
Widget build(BuildContext context) {
final d = MediaQuery.of(context);
return Consumer<StudioDetailsState>(
builder: (context, state, child) => StateHandler<StudioDetailsState>(
state: state,
onRetry: () {
try {
state.getStudioDetails(state.studio.id);
} catch (e) {
state.getStudioDetails(widget.pageData['id']);
}
},
builder: (context, state) {
if (_currentlyPlayingId != state.studio.id) {
_handleVideoPlayback(state);
}
return WillPopScope(
onWillPop: () async {
if (MediaService.currentPodcast != null) {
state.studio = MediaService.currentPodcast!;
}
return true;
},
child: SafeArea(
child: Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(56),
child: DidvanAppBar(
appBarData: AppBarData(
trailing: BookmarkButton(
itemId: state.studio.id,
type: 'video',
value: state.studio.marked,
onMarkChanged: (value) {
widget.pageData['onMarkChanged'](
state.studio.id, value);
},
gestureSize: 48,
),
isSmall: true,
title: state.studio.title,
),
),
),
body: SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: SizedBox(
height: d.size.height - d.padding.top - 56,
child: Column(
children: [
BetterPlayer(controller: _betterPlayerController),
Expanded(
child: StudioDetailsWidget(
onMarkChanged: (id, value) => widget
.pageData['onMarkChanged'](id, value, true),
),
),
],
),
),
),
),
),
);
},
),
);
}
Future<void> _handleVideoPlayback(state) async {
final betterPlayerDataSource = BetterPlayerDataSource(
BetterPlayerDataSourceType.network,
state.studio.link,
);
await _betterPlayerController.clearCache();
await _betterPlayerController.setupDataSource(betterPlayerDataSource);
_betterPlayerController.setBetterPlayerControlsConfiguration(
BetterPlayerControlsConfiguration(
enablePlaybackSpeed: false,
enableSubtitles: false,
enableAudioTracks: false,
progressBarPlayedColor: Theme.of(context).colorScheme.secondary,
progressBarHandleColor: Theme.of(context).colorScheme.secondary,
),
);
_currentlyPlayingId = state.studio.id;
}
@override
void dispose() {
_betterPlayerController.pause();
_betterPlayerController.dispose();
super.dispose();
}
}

View File

@ -0,0 +1,111 @@
import 'dart:ui' as ui;
import 'package:didvan/models/view/app_bar_data.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_widget.dart';
import 'package:didvan/views/home/widgets/bookmark_button.dart';
import 'package:didvan/views/widgets/didvan/app_bar.dart';
import 'package:didvan/views/widgets/state_handlers/state_handler.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:universal_html/html.dart' as html;
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> {
@override
void initState() {
final state = context.read<StudioDetailsState>();
state.args = widget.pageData['args'];
Future.delayed(
Duration.zero,
() => state.getStudioDetails(widget.pageData['id']),
);
super.initState();
}
@override
Widget build(BuildContext context) {
final d = MediaQuery.of(context);
return Consumer<StudioDetailsState>(
builder: (context, state, child) => StateHandler<StudioDetailsState>(
state: state,
onRetry: () => state.getStudioDetails(state.studio.id),
builder: (context, state) {
// 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.studio.iframe!,
mimeType: 'text/html',
).toString()
..style.border = 'none',
);
return WillPopScope(
onWillPop: () async {
if (MediaService.currentPodcast != null) {
state.studio = MediaService.currentPodcast!;
}
return true;
},
child: SafeArea(
child: Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(56),
child: DidvanAppBar(
appBarData: AppBarData(
trailing: BookmarkButton(
itemId: state.studio.id,
type: 'video',
value: state.studio.marked,
onMarkChanged: (value) {
widget.pageData['onMarkChanged'](
state.studio.id, value);
},
gestureSize: 48,
),
isSmall: true,
title: state.studio.title,
),
),
),
body: SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: SizedBox(
height: d.size.height - d.padding.top - 56,
child: Column(
children: [
const AspectRatio(
aspectRatio: 16 / 9,
child: HtmlElementView(viewType: 'video'),
),
Expanded(
child: StudioDetailsWidget(
onMarkChanged: (id, value) => widget
.pageData['onMarkChanged'](id, value, true),
),
),
],
),
),
),
),
),
);
},
),
);
}
}

View File

@ -1,141 +1,170 @@
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/providers/core.dart';
import 'package:didvan/services/media/media.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 StudioDetailsData studio;
StudioDetailsData? nextStudio;
StudioDetailsData? prevStudio;
late int initialIndex;
late StudioRequestArgs args;
int _selectedDetailsIndex = 0;
bool isFetchingNewItem = false;
StudioRequestArgs? podcastArgs;
final List<int> relatedQueue = [];
bool _positionListenerActivated = false;
AppState alongSideState = AppState.idle;
int _currentIndex = 0;
int get currentIndex => _currentIndex;
int _selectedDetailsIndex = 0;
Timer? timer;
int timerValue = 10;
bool stopOnPodcastEnds = false;
int get selectedDetailsIndex => _selectedDetailsIndex;
set selectedDetailsIndex(int value) {
_selectedDetailsIndex = value;
if (value == 2) {
getRelatedContents();
}
notifyListeners();
}
StudioDetailsData get currentStudio {
try {
return studios[_currentIndex]!;
} catch (e) {
return studios[_currentIndex + 1]!;
}
}
Future<void> getStudioDetails(int id,
{bool? isForward, StudioRequestArgs? args}) async {
Future<void> getStudioDetails(
int id, {
StudioRequestArgs? args,
bool? isForward,
bool fetchOnly = false,
}) async {
if (args != null) {
this.args = args;
}
if (this.args.type == 'podcast') {
podcastArgs = this.args;
}
if (MediaService.currentPodcast?.id == id &&
this.args.type == 'podcast' &&
!fetchOnly) {
return;
}
_selectedDetailsIndex = 0;
if (isForward != null) {
if (isForward) {
prevStudio = studio;
studio = nextStudio!;
nextStudio = null;
} else {
nextStudio = studio;
studio = prevStudio!;
prevStudio = null;
}
notifyListeners();
_handlePodcastPlayback(studio);
}
if (isForward == null) {
if (this.args.type == 'podcast') {
MediaService.audioPlayerTag =
'podcast-${MediaService.currentPodcast?.id ?? ''}';
}
appState = AppState.busy;
} else {
isFetchingNewItem = true;
alongSideState = AppState.busy;
notifyListeners();
}
final service = RequestService(RequestHelper.studioDetails(id, this.args));
await service.httpGet();
nextStudio = null;
prevStudio = null;
if (stopOnPodcastEnds) {
timerValue = 10;
}
stopOnPodcastEnds = false;
if (service.isSuccess) {
final result = service.result;
final studio = StudioDetailsData.fromJson(result['studio']);
if (this.args.page == 0) {
studios.add(studio);
initialIndex = 0;
studio = StudioDetailsData.fromJson(result['studio']);
if (result['nextStudio'].isNotEmpty && this.args.page != 0) {
nextStudio = StudioDetailsData.fromJson(result['nextStudio']);
}
if (result['prevStudio'].isNotEmpty && this.args.page != 0) {
prevStudio = StudioDetailsData.fromJson(result['prevStudio']);
}
if (isForward == null && !fetchOnly) {
await _handlePodcastPlayback(studio);
}
alongSideState = AppState.idle;
appState = AppState.idle;
return;
}
if (this.args.type == 'podcast') {
if (isForward == null) {
appState = AppState.failed;
} else {
alongSideState = AppState.failed;
notifyListeners();
}
}
Future<void> _handlePodcastPlayback(StudioDetailsData studio) async {
if (args.type == 'podcast') {
MediaService.currentPodcast = studio;
MediaService.podcastPlaylistArgs = args;
await MediaService.handleAudioPlayback(
audioSource: studio.media,
audioSource: studio.link,
id: studio.id,
isVoiceMessage: false,
onTrackChanged: (isNext) {
if (isNext && nextStudio != null) {
getStudioDetails(nextStudio!.id);
} else if (!isNext && prevStudio != null) {
getStudioDetails(prevStudio!.id);
}
},
);
}
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;
if (nextStudio != null && !_positionListenerActivated) {
_positionListenerActivated = true;
MediaService.audioPlayer.currentPosition.listen((event) {
if (MediaService.audioPlayerTag?.contains('message') == true) {
return;
}
//why? total page state shouldn't die!
if (isForward == null) {
appState = AppState.failed;
final duration =
MediaService.duration ?? Duration(seconds: studio.duration);
if (event.compareTo(duration) > 0 && nextStudio != null) {
if (stopOnPodcastEnds) {
MediaService.resetAudioPlayer();
return;
}
getStudioDetails(nextStudio!.id, isForward: true);
}
});
}
} else {
MediaService.audioPlayer.pause();
}
}
Future<void> getRelatedContents() async {
if (currentStudio.relatedContents.isNotEmpty) return;
relatedQueue.add(currentStudio.id);
if (studio.relatedContents.isNotEmpty) return;
relatedQueue.add(studio.id);
final service = RequestService(RequestHelper.tag(
ids: currentStudio.tags.map((tag) => tag.id).toList(),
itemId: currentStudio.id,
type: 'studio',
ids: studio.tags.map((tag) => tag.id).toList(),
itemId: studio.id,
type: args.type,
));
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]));
studio.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;
studio.comments = count;
notifyListeners();
}
}

View File

@ -0,0 +1,126 @@
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;
const DetailsTabBar({
Key? key,
required this.isVideo,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final state = context.watch<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: 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;
},
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.hint;
}
@override
Widget build(BuildContext context) {
return Expanded(
child: GestureDetector(
onTap: onTap,
child: Container(
color: Colors.transparent,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
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

@ -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,280 @@
import 'dart:math';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/enums.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/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';
import 'package:didvan/views/widgets/shimmer_placeholder.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_html/flutter_html.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
class StudioDetailsWidget extends StatelessWidget {
final void Function(int id, bool value) onMarkChanged;
const StudioDetailsWidget({
Key? key,
required this.onMarkChanged,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final ds = MediaQuery.of(context).size;
return SafeArea(
bottom: true,
child: Consumer<StudioDetailsState>(
builder: (context, state, child) {
bool isVideo = state.studio.iframe != null;
return Container(
height: max(
ds.height -
ds.width * 9 / 16 -
72 -
MediaQuery.of(context).padding.top,
0),
color: Theme.of(context).colorScheme.surface,
child: Stack(
children: [
Positioned(
top: 72,
left: 0,
right: 0,
bottom: 0,
child: StateHandler<StudioDetailsState>(
onRetry: () {},
state: state,
builder: (context, state) {
if (state.selectedDetailsIndex == 0) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Html(
key: ValueKey(state.studio.id),
data: state.studio.description,
onAnchorTap: (href, context, map, element) =>
launch(href!),
style: {
'*': Style(
direction: TextDirection.rtl,
textAlign: TextAlign.right,
lineHeight: LineHeight.percent(135),
margin: EdgeInsets.zero,
padding: EdgeInsets.zero,
),
},
),
if (state.studio.tags.isNotEmpty)
const SizedBox(height: 20),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (var i = 0;
i < state.studio.tags.length;
i++)
TagItem(
tag: state.studio.tags[i],
onMarkChanged: (id, value) =>
_onMarkChanged(id, value, state),
type: isVideo ? 'video' : 'podcast',
),
],
),
const SizedBox(height: 20),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
const SizedBox(),
if (state.nextStudio != null &&
state.alongSideState == AppState.idle)
_StudioPreview(
isNext: true,
studio: state.nextStudio!,
),
if (state.alongSideState == AppState.busy)
_StudioPreview.placeHolder,
if (state.prevStudio != null &&
state.alongSideState == AppState.idle)
_StudioPreview(
isNext: false,
studio: state.prevStudio!,
),
if (state.alongSideState == AppState.busy)
_StudioPreview.placeHolder,
const SizedBox(),
],
),
],
),
);
}
if (state.selectedDetailsIndex == 1) {
return ChangeNotifierProvider<CommentsState>(
create: (context) => CommentsState(),
child: SizedBox(
height: ds.height -
ds.width * 9 / 16 -
172 -
MediaQuery.of(context).padding.top,
child: Comments(
pageData: {
'id': state.studio.id,
'type': 'studio',
'title': state.studio.title,
'onCommentsChanged': state.onCommentsChanged,
'isPage': false,
},
),
),
);
}
return Column(
children: [
if (state.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 < state.studio.relatedContents.length;
i++)
Padding(
padding: const EdgeInsets.only(
bottom: 8,
left: 16,
right: 16,
),
child: MultitypeOverview(
item: state.studio.relatedContents[i],
onMarkChanged: (id, value) {},
),
),
],
);
},
),
),
DetailsTabBar(
isVideo: isVideo,
),
],
),
);
},
),
);
}
void _onMarkChanged(id, value, state) {
onMarkChanged(id, value);
if (state.studio.id == id) {
state.studio.marked = value;
} else if (state.nextStudio?.id == id) {
state.nextStudio!.marked = value;
} else if (state.prevStudio?.id == id) {
state.prevStudio!.marked = value;
}
}
}
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.iframe != null) {
return 'ویدئو ${isNext ? 'بعدی' : 'قبلی'} ';
}
return 'پادکست ${isNext ? 'بعدی' : 'قبلی'} ';
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
final state = context.read<StudioDetailsState>();
state.getStudioDetails(
isNext ? state.nextStudio!.id : state.prevStudio!.id,
args: state.args,
isForward: isNext,
);
},
child: Container(
width: 88,
height: 216,
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,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.overline,
color: Theme.of(context).colorScheme.caption,
),
],
),
),
);
}
static Widget get placeHolder => SizedBox(
width: 88,
height: 216,
child: Column(
children: const [
ShimmerPlaceholder(width: 88, height: 88),
SizedBox(height: 8),
ShimmerPlaceholder(height: 20, width: 20),
SizedBox(height: 16),
ShimmerPlaceholder(height: 14, width: 60),
SizedBox(height: 16),
ShimmerPlaceholder(height: 12, width: double.infinity),
SizedBox(height: 8),
ShimmerPlaceholder(height: 12, width: 40),
],
),
);
}

View File

@ -1,16 +1,19 @@
import 'dart:async';
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/overview_data.dart';
import 'package:didvan/models/requests/studio.dart';
import 'package:didvan/providers/core_provider.dart';
import 'package:didvan/providers/user_provider.dart';
import 'package:didvan/models/slider_data.dart';
import 'package:didvan/providers/core.dart';
import 'package:didvan/services/network/request.dart';
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,11 +23,32 @@ class StudioState extends CoreProvier {
bool get videosSelected => _videosSelected;
bool get searching => search.isNotEmpty;
set videosSelected(bool value) {
if (_videosSelected == value) return;
if (_videosSelected == value || appState == AppState.busy) return;
_videosSelected = value;
studios.clear();
getStudioOverviews(page: page);
selectedSortTypeIndex = 0;
_getSliders();
getStudios(page: page);
}
String get order {
if (selectedSortTypeIndex == 0 || selectedSortTypeIndex == 1) return 'date';
if (selectedSortTypeIndex == 2) return 'view';
return 'comment';
}
String get orderString {
if (selectedSortTypeIndex == 0) return 'تازه‌ترین‌ها';
if (selectedSortTypeIndex == 1) return 'قدیمی‌ترین‌ها';
if (selectedSortTypeIndex == 2) return 'پربازدیدترین‌ها';
return 'پربحث‌نرین‌ها';
}
String get type {
if (videosSelected) return 'video';
return 'podcast';
}
void init() {
@ -33,23 +57,29 @@ class StudioState extends CoreProvier {
_videosSelected = true;
selectedSortTypeIndex = 0;
Future.delayed(Duration.zero, () {
getStudioOverviews(page: 1);
_getSliders();
getStudios(page: 1);
});
}
String get order {
if (selectedSortTypeIndex == 0) return 'date';
if (selectedSortTypeIndex == 1) return 'view';
return 'comment';
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();
}
String get type {
if (videosSelected) return 'video';
return 'podcast';
}
Future<void> getStudioOverviews({required int page}) async {
Future<void> getStudios({required int page}) async {
this.page = page;
lastSearch = search;
if (page == 1) {
appState = AppState.busy;
}
@ -60,10 +90,10 @@ class StudioState extends CoreProvier {
type: type,
search: search,
order: order,
asc: selectedSortTypeIndex == 1,
),
),
);
await service.httpGet();
if (service.isSuccess) {
if (page == 1) {
@ -80,10 +110,11 @@ class StudioState extends CoreProvier {
appState = AppState.failed;
}
Future<void> changeMark(int id, bool value) async {
Future<void> changeMark(int id, bool value, bool shouldUpdate) async {
studios.firstWhere((element) => element.id == id).marked = value;
if (shouldUpdate) {
notifyListeners();
UserProvider.changeStudioMark(id, value);
}
}
void onCommentsChanged(int id, int count) {

View File

@ -1,28 +1,146 @@
import 'package:carousel_slider/carousel_slider.dart';
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/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/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'),
if (state.appState == AppState.busy)
const Padding(
padding: EdgeInsets.symmetric(horizontal: 4),
child: ShimmerPlaceholder(),
),
if (state.appState == AppState.idle)
for (var i = 0; i < state.sliders.length; i++)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: GestureDetector(
onTap: () {
if (state.videosSelected) {
Navigator.of(context)
.pushNamed(Routes.studioDetails, arguments: {
'onMarkChanged': state.changeMark,
'id': state.sliders[i].id,
'args':
const StudioRequestArgs(page: 0, type: 'video'),
'hasUnmarkConfirmation': false,
'isVideo': true,
});
return;
}
context.read<StudioDetailsState>().getStudioDetails(
state.sliders[i].id,
args: const StudioRequestArgs(
page: 0,
type: 'podcast',
),
);
},
child: Stack(
alignment: Alignment.center,
children: [
SkeletonImage(
borderRadius: DesignConfig.mediumBorderRadius,
imageUrl: state.sliders[i].image,
width: double.infinity,
height: 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,
),
),
),
if (state.videosSelected)
Container(
height: 52,
width: 52,
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,
size: 48,
),
),
],
),
),
),
],
options: CarouselOptions(
autoPlayAnimationDuration: DesignConfig.mediumAnimationDuration,
onPageChanged: (index, reason) => setState(
() => selectedIndex = index,
),
viewportFraction: 0.94,
aspectRatio: 16 / 9,
autoPlay: true,
autoPlay: state.appState == AppState.idle,
),
),
Row(),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var i = 0; i < state.sliders.length; i++)
_SliderIndicator(
isCurrentIndex: selectedIndex == i,
isVideo: state.videosSelected,
),
],
),
const SizedBox(height: 16),
],
);
}
@ -30,21 +148,33 @@ class StudioSlider extends StatelessWidget {
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) {
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,
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,
),
@ -78,7 +76,10 @@ class _StudioTypeButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
onTap: () {
onTap();
FocusScope.of(context).unfocus();
},
child: Container(
color: Colors.transparent,
child: Column(
@ -88,14 +89,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,15 +1,26 @@
import 'dart:async';
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/enums.dart';
import 'package:didvan/models/studio_details_data.dart';
import 'package:didvan/models/view/action_sheet_data.dart';
import 'package:didvan/services/media/media.dart';
import 'package:didvan/utils/action_sheet.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/widgets/audio/audio_slider.dart';
import 'package:didvan/views/home/widgets/bookmark_button.dart';
import 'package:didvan/views/widgets/didvan/button.dart';
import 'package:didvan/views/widgets/didvan/icon_button.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/ink_wrapper.dart';
import 'package:didvan/views/widgets/item_title.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;
@ -17,6 +28,7 @@ class AudioPlayerWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final state = context.read<StudioDetailsState>();
return Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.vertical(top: Radius.circular(8)),
@ -47,60 +59,126 @@ class AudioPlayerWidget extends StatelessWidget {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: AudioSlider(
tag: podcast.media,
tag: 'podcast-${podcast.id}',
showTimer: true,
duration: podcast.duration,
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Center(
child: StatefulBuilder(
builder: (context, setState) => Column(
children: [
DidvanIconButton(
icon: DidvanIcons.sleep_timer_regular,
onPressed: () {},
icon: state.timer == null && !state.stopOnPodcastEnds
? DidvanIcons.sleep_timer_regular
: DidvanIcons.sleep_enabled_regular,
color: Theme.of(context).colorScheme.title,
onPressed: () => _showSleepTimer(
state,
() => setState(() {}),
),
Column(
),
if (state.timer != null)
DidvanText(
state.stopOnPodcastEnds
? 'پایان پادکست'
: '\'' + state.timerValue.toString(),
isEnglishFont: true,
style: Theme.of(context).textTheme.overline,
color: Theme.of(context).colorScheme.title,
),
],
),
),
),
),
Expanded(
child: Center(
child: Column(
children: [
DidvanIconButton(
color: Theme.of(context).colorScheme.title,
size: 32,
icon: DidvanIcons.media_forward_solid,
onPressed: () {
MediaService.audioPlayer.seek(
Duration(
seconds:
MediaService.audioPlayer.position.inSeconds + 30,
seconds: MediaService.audioPlayer.currentPosition
.value.inSeconds +
30,
),
);
},
),
const DidvanText('30', isEnglishFont: true),
DidvanText(
'30',
isEnglishFont: true,
color: Theme.of(context).colorScheme.title,
),
],
),
_PlayPouseAnimatedIcon(
audioSource: podcast.media,
),
Column(
),
Expanded(
child: Center(
child: StreamBuilder<bool>(
stream: MediaService.audioPlayer.isPlaying,
builder: (context, snapshot) {
return _PlayPouseAnimatedIcon(
audioSource: podcast.link,
id: podcast.id,
);
},
),
),
),
Expanded(
child: Center(
child: Column(
children: [
DidvanIconButton(
size: 32,
icon: DidvanIcons.media_backward_solid,
color: Theme.of(context).colorScheme.title,
onPressed: () {
MediaService.audioPlayer.seek(
Duration(
seconds:
MediaService.audioPlayer.position.inSeconds - 10,
seconds: max(
0,
MediaService.audioPlayer.currentPosition.value
.inSeconds -
10,
),
),
);
},
),
const DidvanText('10', isEnglishFont: true),
DidvanText(
'10',
isEnglishFont: true,
color: Theme.of(context).colorScheme.title,
),
],
),
BookmarkButton(
),
),
Expanded(
child: Center(
child: BookmarkButton(
itemId: state.studio.id,
type: 'podcast',
gestureSize: 48,
color: Theme.of(context).colorScheme.title,
value: podcast.marked,
onMarkChanged: (value) {},
onMarkChanged: (value) => context
.read<StudioState>()
.changeMark(podcast.id, value, true),
),
),
),
],
),
@ -108,11 +186,136 @@ class AudioPlayerWidget extends StatelessWidget {
),
);
}
Future<void> _showSleepTimer(StudioDetailsState state, update) async {
int timerValue = 10;
final controller = FixedExtentScrollController();
bool isInit = true;
Future.delayed(
const Duration(milliseconds: 100),
() async {
await controller.animateTo(
state.timerValue * 10,
duration: DesignConfig.lowAnimationDuration,
curve: Curves.easeIn,
);
isInit = false;
},
);
await ActionSheetUtils.showBottomSheet(
data: ActionSheetData(
content: StatefulBuilder(
builder: (context, setState) => Column(
children: [
const ItemTitle(
title: 'زمان خواب',
icon: DidvanIcons.sleep_timer_regular,
),
const SizedBox(height: 24),
DidvanText(
timerValue.toString() + ' دقیقه',
style: Theme.of(context).textTheme.headline3,
),
const SizedBox(height: 12),
const Icon(DidvanIcons.caret_down_solid),
const SizedBox(height: 8),
SizedBox(
height: 50,
child: RotatedBox(
quarterTurns: 3,
child: ListWheelScrollView(
physics: const FixedExtentScrollPhysics(),
controller: controller,
itemExtent: 10,
onSelectedItemChanged: (index) {
if (!isInit) {
state.stopOnPodcastEnds = false;
}
final minutes = index == 0 ? 1 : index;
timerValue = minutes;
setState(() {});
},
children: [
for (var i = 0; i < 61; i++) ...[
if (i % 5 == 0)
Center(
child: Container(
color: Theme.of(context).colorScheme.text,
width: 50,
height: 3,
),
),
if (i % 5 != 0) const SizedBox(height: 3),
],
],
),
),
),
const SizedBox(height: 32),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 64),
child: DidvanButton(
style: state.timerValue == MediaService.duration?.inMinutes &&
state.stopOnPodcastEnds
? ButtonStyleMode.primary
: ButtonStyleMode.flat,
title: 'پایان پادکست',
onPressed: () async {
state.timerValue = MediaService.duration!.inMinutes -
MediaService
.audioPlayer.currentPosition.value.inMinutes;
await controller.animateTo(
state.timerValue * 10,
duration: DesignConfig.lowAnimationDuration,
curve: Curves.easeIn,
);
state.stopOnPodcastEnds = true;
setState(() {});
},
),
),
],
),
),
onConfirmed: () {
if (!state.stopOnPodcastEnds) {
state.timer = Timer.periodic(
const Duration(minutes: 1),
(timer) {
timerValue--;
if (timerValue == 0) {
MediaService.audioPlayer.stop();
state.stopOnPodcastEnds = false;
state.timer?.cancel();
state.timer = null;
state.timerValue = 10;
state.update();
}
},
);
}
state.timerValue = timerValue;
update();
},
confrimTitle: 'شروع زمان خواب',
dismissTitle: 'لغو',
onDismissed: () {
state.timer?.cancel();
state.timer = null;
state.timerValue = 10;
update();
},
),
);
controller.dispose();
}
}
class _PlayPouseAnimatedIcon extends StatefulWidget {
final String audioSource;
const _PlayPouseAnimatedIcon({Key? key, required this.audioSource})
final int id;
const _PlayPouseAnimatedIcon(
{Key? key, required this.audioSource, required this.id})
: super(key: key);
@override
@ -123,6 +326,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 +339,13 @@ class __PlayPouseAnimatedIconState extends State<_PlayPouseAnimatedIcon>
vsync: this,
duration: DesignConfig.lowAnimationDuration,
);
if (MediaService.audioPlayer.playing) {
}
void _handleAnimation() {
if (MediaService.audioPlayer.isPlaying.value) {
_animationController.forward();
} else {
_animationController.reverse();
}
}
@ -143,12 +357,9 @@ class __PlayPouseAnimatedIconState extends State<_PlayPouseAnimatedIcon>
MediaService.handleAudioPlayback(
audioSource: widget.audioSource,
isVoiceMessage: false,
id: widget.id,
);
if (MediaService.audioPlayer.playing) {
_animationController.forward();
} else {
_animationController.reverse();
}
_handleAnimation();
},
child: Container(
padding: const EdgeInsets.all(8),

View File

@ -22,11 +22,14 @@ class AudioSlider extends StatelessWidget {
@override
Widget build(BuildContext context) {
return IgnorePointer(
ignoring: MediaService.audioPlayerTag != tag,
ignoring: !_isPlaying,
child: Directionality(
textDirection: TextDirection.ltr,
child: StreamBuilder<Duration>(
stream: _isPlaying ? MediaService.audioPlayer.positionStream : null,
stream:
_isPlaying && MediaService.audioPlayer.currentPosition.hasValue
? MediaService.audioPlayer.currentPosition
: null,
builder: (context, snapshot) => ProgressBar(
thumbColor: Theme.of(context).colorScheme.title,
progressBarColor: DesignConfig.isDark
@ -34,17 +37,14 @@ class AudioSlider extends StatelessWidget {
: Theme.of(context).colorScheme.primary,
baseBarColor: Theme.of(context).colorScheme.border,
bufferedBarColor: Theme.of(context).colorScheme.splash,
total: MediaService.audioPlayer.duration ??
Duration(seconds: duration ?? 0),
total: MediaService.duration ?? Duration(seconds: duration ?? 0),
progress: snapshot.data ?? Duration.zero,
buffered: _isPlaying
? MediaService.audioPlayer.bufferedPosition
: Duration.zero,
thumbRadius: disableThumb ? 0 : 6,
barHeight: 3,
timeLabelTextStyle: TextStyle(
fontSize: showTimer ? null : 0,
height: showTimer ? 3 : 0,
color: Theme.of(context).colorScheme.text,
fontFamily: DesignConfig.fontFamily.replaceAll(
'-FA',
'',

View File

@ -1,292 +0,0 @@
import 'package:didvan/config/design_config.dart';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.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/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';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:expandable_bottom_sheet/expandable_bottom_sheet.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class DidvanBNB extends StatelessWidget {
final int currentTabIndex;
final void Function(int index) onTabChanged;
const DidvanBNB(
{Key? key, required this.currentTabIndex, required this.onTabChanged})
: super(key: key);
bool get _enablePlayerController =>
MediaService.currentPodcast != null || MediaService.audioPlayer.playing;
@override
Widget build(BuildContext context) {
return StreamBuilder<bool>(
stream: MediaService.audioPlayer.playingStream,
builder: (context, snapshot) {
return Stack(
children: [
GestureDetector(
onTap: () => _showPlayerBottomSheet(context),
child: AnimatedContainer(
padding: const EdgeInsets.only(top: 12),
duration: DesignConfig.lowAnimationDuration,
height: _enablePlayerController ? 120 : 72,
decoration: BoxDecoration(
color: DesignConfig.isDark
? Theme.of(context).colorScheme.focused
: Theme.of(context).colorScheme.navigation,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(16),
),
),
child: !_enablePlayerController
? const SizedBox()
: SizedBox(
height: 48,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(
right: 12,
left: 16,
),
child: DidvanIconButton(
icon: DidvanIcons.close_regular,
color: DesignConfig.isDark
? null
: Theme.of(context).colorScheme.secondCTA,
gestureSize: 28,
onPressed: MediaService.resetAudioPlayer,
),
),
SkeletonImage(
imageUrl: MediaService.currentPodcast!.image,
width: 32,
height: 32,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DidvanText(
MediaService.currentPodcast!.title,
color: DesignConfig.isDark
? null
: Theme.of(context)
.colorScheme
.secondCTA,
),
AudioSlider(
disableThumb: true,
tag: MediaService.audioPlayerTag!,
),
],
),
),
Padding(
padding: const EdgeInsets.only(
left: 12,
right: 16,
),
child: DidvanIconButton(
gestureSize: 28,
color: DesignConfig.isDark
? null
: Theme.of(context).colorScheme.secondCTA,
icon: snapshot.data!
? DidvanIcons.pause_solid
: DidvanIcons.play_solid,
onPressed: () {
MediaService.handleAudioPlayback(
audioSource: MediaService.audioPlayerTag,
);
},
),
),
],
),
),
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
height: 72,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius:
const BorderRadius.vertical(top: Radius.circular(16)),
boxShadow: DesignConfig.defaultShadow,
),
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
_NavBarItem(
isSelected: currentTabIndex == 0,
title: 'اخبار',
selectedIcon: DidvanIcons.news_solid,
unselectedIcon: DidvanIcons.news_light,
onTap: () => onTabChanged(0),
),
_NavBarItem(
isSelected: currentTabIndex == 1,
title: 'آمار',
selectedIcon: DidvanIcons.chart_solid,
unselectedIcon: DidvanIcons.chart_light,
onTap: () => onTabChanged(1),
),
_NavBarItem(
isSelected: currentTabIndex == 2,
title: 'رادار',
selectedIcon: DidvanIcons.radar_solid,
unselectedIcon: DidvanIcons.radar_light,
onTap: () => onTabChanged(2),
),
_NavBarItem(
isSelected: currentTabIndex == 3,
title: 'استودیو',
selectedIcon: DidvanIcons.play_circle_solid,
unselectedIcon: DidvanIcons.play_circle_light,
onTap: () => onTabChanged(3),
),
_NavBarItem(
isSelected: currentTabIndex == 4,
title: 'تنظیمات',
selectedIcon: DidvanIcons.setting_solid,
unselectedIcon: DidvanIcons.setting_light,
onTap: () => onTabChanged(4),
),
],
),
),
),
],
);
});
}
void _showPlayerBottomSheet(BuildContext context) {
final sheetKey = GlobalKey<ExpandableBottomSheetState>();
bool isExpanded = false;
final detailsState = context.read<StudioDetailsState>();
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,
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: const StudioDetailsWidget(),
),
),
);
}
}
class _NavBarItem extends StatelessWidget {
final VoidCallback onTap;
final bool isSelected;
final String title;
final IconData selectedIcon;
final IconData unselectedIcon;
const _NavBarItem({
Key? key,
required this.isSelected,
required this.title,
required this.selectedIcon,
required this.unselectedIcon,
required this.onTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Expanded(
child: Tooltip(
message: title,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.title,
borderRadius: DesignConfig.highBorderRadius,
boxShadow: DesignConfig.defaultShadow,
),
child: GestureDetector(
onTap: onTap,
child: Container(
color: Colors.transparent,
child: Column(
children: [
const SizedBox(
height: 4,
),
AnimatedContainer(
padding: const EdgeInsets.all(4),
duration: DesignConfig.lowAnimationDuration,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isSelected
? Theme.of(context).colorScheme.focused
: Theme.of(context).colorScheme.surface,
),
child: Icon(
isSelected ? selectedIcon : unselectedIcon,
size: 32,
color: DesignConfig.isDark
? Theme.of(context).colorScheme.text
: Theme.of(context).colorScheme.title,
),
),
DidvanText(
title,
style: Theme.of(context).textTheme.caption,
color: Theme.of(context).colorScheme.title,
),
const Spacer(),
],
),
),
),
),
);
}
}

View File

@ -1,5 +1,7 @@
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/providers/user.dart';
import 'package:didvan/utils/action_sheet.dart';
import 'package:didvan/views/widgets/didvan/icon_button.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
@ -7,15 +9,21 @@ import 'package:flutter/material.dart';
class BookmarkButton extends StatefulWidget {
final bool value;
final Color? color;
final void Function(bool value) onMarkChanged;
final bool askForConfirmation;
final double gestureSize;
final String type;
final int itemId;
const BookmarkButton({
Key? key,
required this.value,
required this.onMarkChanged,
this.askForConfirmation = false,
required this.gestureSize,
required this.type,
required this.itemId,
this.askForConfirmation = false,
this.color,
}) : super(key: key);
@override
@ -41,6 +49,10 @@ class _BookmarkButtonState extends State<BookmarkButton> {
Widget build(BuildContext context) {
return DidvanIconButton(
gestureSize: widget.gestureSize,
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;
@ -62,6 +74,21 @@ class _BookmarkButtonState extends State<BookmarkButton> {
_value = !_value;
});
widget.onMarkChanged(_value);
switch (widget.type) {
case 'radar':
UserProvider.changeRadarMark(widget.itemId, _value);
break;
case 'news':
UserProvider.changeNewsMark(widget.itemId, _value);
break;
case 'podcast':
UserProvider.changeStudioMark(widget.itemId, _value);
break;
case 'video':
UserProvider.changeStudioMark(widget.itemId, _value);
break;
default:
}
}
},
);

View File

@ -19,11 +19,10 @@ class DurationWidget extends StatelessWidget {
borderRadius: BorderRadius.circular(5),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
DidvanIcons.timer_regular,
size: 16,
size: 18,
color: Theme.of(context).colorScheme.focusedBorder,
),
const SizedBox(width: 4),
@ -32,12 +31,13 @@ class DurationWidget extends StatelessWidget {
Duration(seconds: duration),
),
isEnglishFont: true,
style: Theme.of(context).textTheme.caption,
color: Theme.of(context).colorScheme.focusedBorder,
),
const SizedBox(width: 4),
Icon(
DidvanIcons.play_circle_regular,
size: 16,
size: 18,
color: Theme.of(context).colorScheme.focusedBorder,
),
],

View File

@ -104,6 +104,11 @@ class _FloatingNavigationBarState extends State<FloatingNavigationBar> {
const Spacer(),
if (widget.isRadar)
BookmarkButton(
itemId: widget.item.id,
type: 'radar',
color: DesignConfig.isDark
? Theme.of(context).colorScheme.focusedBorder
: Theme.of(context).colorScheme.focused,
askForConfirmation: widget.hasUnmarkConfirmation,
value: widget.item.marked,
onMarkChanged: (value) {
@ -130,7 +135,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,
},
@ -143,6 +148,11 @@ class _FloatingNavigationBarState extends State<FloatingNavigationBar> {
if (!widget.isRadar) const SizedBox(width: 12),
if (!widget.isRadar)
BookmarkButton(
itemId: widget.item.id,
type: 'news',
color: DesignConfig.isDark
? Theme.of(context).colorScheme.focusedBorder
: Theme.of(context).colorScheme.focused,
askForConfirmation: widget.hasUnmarkConfirmation,
value: widget.item.marked,
onMarkChanged: (value) {

View File

@ -3,13 +3,17 @@ 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/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';
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 +27,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,
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': item.type == 'radar'
? const RadarRequestArgs(page: 0)
: const NewsRequestArgs(page: 0),
'args': _targetPageArgs,
'hasUnmarkConfirmation': hasUnmarkConfirmation,
},
),
);
},
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -52,9 +96,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,
),
@ -86,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

@ -13,7 +13,7 @@ import 'package:flutter/material.dart';
class NewsOverview extends StatelessWidget {
final OverviewData news;
final NewsRequestArgs? newsRequestArgs;
final void Function(int id, bool value) onMarkChanged;
final void Function(int id, bool value, bool shouldUpdate) onMarkChanged;
final bool hasUnmarkConfirmation;
const NewsOverview({
Key? key,
@ -29,7 +29,7 @@ class NewsOverview extends StatelessWidget {
onTap: () => Navigator.of(context).pushNamed(
Routes.newsDetails,
arguments: {
'onMarkChanged': onMarkChanged,
'onMarkChanged': (id, value) => onMarkChanged(id, value, true),
'id': news.id,
'args': newsRequestArgs,
'hasUnmarkConfirmation': hasUnmarkConfirmation,
@ -79,9 +79,11 @@ class NewsOverview extends StatelessWidget {
],
),
BookmarkButton(
gestureSize: 24,
itemId: news.id,
type: 'news',
gestureSize: 32,
value: news.marked,
onMarkChanged: (value) => onMarkChanged(news.id, value),
onMarkChanged: (value) => onMarkChanged(news.id, value, false),
askForConfirmation: hasUnmarkConfirmation,
),
],

View File

@ -1,7 +1,9 @@
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/overview_data.dart';
import 'package:didvan/models/requests/studio.dart';
import 'package:didvan/providers/media.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';
@ -12,18 +14,21 @@ 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/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class PodcastOverview extends StatelessWidget {
final OverviewData podcast;
final void Function(int id, bool value) onMarkChanged;
final StudioRequestArgs? studioRequestArgs;
final void Function(int id, bool value, bool shouldUpdate) onMarkChanged;
final StudioRequestArgs studioRequestArgs;
final bool hasUnmarkConfirmation;
const PodcastOverview({
Key? key,
required this.podcast,
required this.onMarkChanged,
this.studioRequestArgs,
required this.studioRequestArgs,
this.hasUnmarkConfirmation = false,
}) : super(key: key);
@override
@ -71,28 +76,62 @@ class PodcastOverview extends StatelessWidget {
overflow: TextOverflow.ellipsis,
),
const DidvanDivider(verticalPadding: 8),
Row(
Consumer<MediaProvider>(
builder: (context, state, child) => Row(
children: [
DurationWidget(duration: podcast.duration!),
const Spacer(),
if (!kIsWeb) ...[
if (state.appState == AppState.idle ||
!state.downloadQueue.contains(podcast.link))
DidvanIconButton(
gestureSize: 28,
icon: DidvanIcons.download_regular,
onPressed: () {},
color: _isDownloaded
? Theme.of(context).colorScheme.primary
: null,
icon: _isDownloaded
? DidvanIcons.download_solid
: DidvanIcons.download_regular,
onPressed: _isDownloaded
? () {}
: () => state.download(
fileName: 'podcast-${podcast.id}.mp3',
isVideo: false,
url: podcast.link!,
),
),
if (state.appState == AppState.busy &&
state.downloadQueue.contains(podcast.link))
const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
const SizedBox(width: 16),
],
BookmarkButton(
gestureSize: 24,
itemId: podcast.id,
type: 'podcast',
askForConfirmation: hasUnmarkConfirmation,
gestureSize: 32,
value: podcast.marked,
onMarkChanged: (value) => onMarkChanged(podcast.id, value),
onMarkChanged: (value) =>
onMarkChanged(podcast.id, value, false),
),
],
),
),
],
),
);
}
bool get _isDownloaded {
return MediaProvider.downloadedItemIds.contains(podcast.id);
}
static Widget get placeholder => DidvanCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,

View File

@ -16,7 +16,7 @@ import 'package:flutter/material.dart';
class RadarOverview extends StatelessWidget {
final OverviewData radar;
final void Function(int id, int count) onCommentsChanged;
final void Function(int id, bool value) onMarkChanged;
final void Function(int id, bool value, bool shouldUpdate) onMarkChanged;
final bool hasUnmarkConfirmation;
final RadarRequestArgs? radarRequestArgs;
const RadarOverview({
@ -34,7 +34,7 @@ class RadarOverview extends StatelessWidget {
onTap: () => Navigator.of(context).pushNamed(
Routes.radarDetails,
arguments: {
'onMarkChanged': onMarkChanged,
'onMarkChanged': (id, value) => onMarkChanged(id, value, true),
'onCommentsChanged': onCommentsChanged,
'id': radar.id,
'args': radarRequestArgs,
@ -102,13 +102,6 @@ class RadarOverview extends StatelessWidget {
const DidvanDivider(),
Row(
children: [
BookmarkButton(
gestureSize: 24,
value: radar.marked,
onMarkChanged: (value) => onMarkChanged(radar.id, value),
askForConfirmation: hasUnmarkConfirmation,
),
const Spacer(),
if (radar.comments != 0) DidvanText(radar.comments.toString()),
const SizedBox(width: 4),
DidvanIconButton(
@ -117,7 +110,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) =>
@ -125,10 +118,19 @@ class RadarOverview extends StatelessWidget {
},
),
),
const SizedBox(width: 16),
// const SizedBox(width: 16),
// const DidvanText('10'),
// const SizedBox(width: 4),
// const Icon(DidvanIcons.evaluation_regular),
const Spacer(),
BookmarkButton(
itemId: radar.id,
type: 'radar',
gestureSize: 32,
value: radar.marked,
onMarkChanged: (value) => onMarkChanged(radar.id, value, false),
askForConfirmation: hasUnmarkConfirmation,
),
],
),
],

View File

@ -4,31 +4,26 @@ 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';
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';
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 void Function(int id, bool value, bool shouldUpdate) onMarkChanged;
final bool hasUnmarkConfirmation;
final StudioRequestArgs? studioRequestArgs;
final StudioRequestArgs studioRequestArgs;
const VideoOverview({
Key? key,
required this.video,
required this.onCommentsChanged,
required this.onMarkChanged,
required this.hasUnmarkConfirmation,
this.studioRequestArgs,
required this.studioRequestArgs,
this.hasUnmarkConfirmation = false,
}) : super(key: key);
@override
@ -37,43 +32,36 @@ class VideoOverview extends StatelessWidget {
onTap: () => Navigator.of(context).pushNamed(
Routes.studioDetails,
arguments: {
'onMarkChanged': onMarkChanged,
'onCommentsChanged': onCommentsChanged,
'onMarkChanged': (id, value) => onMarkChanged(id, value, true),
'id': video.id,
'args': studioRequestArgs,
'hasUnmarkConfirmation': hasUnmarkConfirmation,
'isVideo': true,
'state': context.read<StudioDetailsState>(),
},
),
child: Row(
children: [
Stack(
alignment: Alignment.center,
children: [
SkeletonImage(
imageUrl: video.image,
height: 108,
width: 108,
),
Positioned.fill(
child: Center(
child: Container(
Container(
height: 28,
width: 28,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context)
.colorScheme
.secondary
.withOpacity(0.7),
color:
Theme.of(context).colorScheme.secondary.withOpacity(0.7),
),
child: Icon(
DidvanIcons.play_solid,
color: Theme.of(context).colorScheme.white,
),
),
),
),
],
),
const SizedBox(width: 8),
@ -105,16 +93,21 @@ class VideoOverview extends StatelessWidget {
children: [
DurationWidget(duration: video.duration!),
const Spacer(),
DidvanIconButton(
gestureSize: 28,
icon: DidvanIcons.download_regular,
onPressed: () {},
),
const SizedBox(width: 16),
// DidvanIconButton(
// gestureSize: 28,
// icon: DidvanIcons.download_regular,
// onPressed: () =>
// context.read<StudioState>().download(video.media!),
// ),
// const SizedBox(width: 16),
BookmarkButton(
gestureSize: 24,
itemId: video.id,
type: 'video',
gestureSize: 32,
value: video.marked,
onMarkChanged: (value) => onMarkChanged(video.id, value),
onMarkChanged: (value) =>
onMarkChanged(video.id, value, false),
askForConfirmation: hasUnmarkConfirmation,
),
],
),

View File

@ -1,34 +0,0 @@
import 'dart:io';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/services/media/media.dart';
import 'package:didvan/views/widgets/didvan/icon_button.dart';
import 'package:flutter/material.dart';
class AudioControllerButton extends StatelessWidget {
final String? audioUrl;
final File? audioFile;
const AudioControllerButton({Key? key, this.audioUrl, this.audioFile})
: super(key: key);
bool get _nowPlaying =>
MediaService.audioPlayerTag == audioUrl ||
audioFile != null && MediaService.audioPlayerTag == audioFile!.path;
@override
Widget build(BuildContext context) {
return DidvanIconButton(
icon: MediaService.audioPlayer.playing == true && _nowPlaying
? DidvanIcons.pause_circle_solid
: DidvanIcons.play_circle_solid,
color: Theme.of(context).colorScheme.focusedBorder,
onPressed: () {
MediaService.handleAudioPlayback(
audioSource: audioFile ?? audioUrl,
);
},
);
}
}

View File

@ -125,6 +125,7 @@ class _SearchFieldState extends State<SearchField> {
@override
void dispose() {
widget.focusNode.removeListener(() {});
super.dispose();
}
}

View File

@ -9,18 +9,26 @@ import 'package:flutter/material.dart';
class TagItem extends StatelessWidget {
final Tag tag;
final void Function(int id, bool value) onMarkChanged;
final String type;
const TagItem({
Key? key,
required this.tag,
required this.onMarkChanged,
required this.type,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return InkWrapper(
borderRadius: DesignConfig.lowBorderRadius,
onPressed: () =>
Navigator.of(context).pushNamed(Routes.hashtag, arguments: tag),
onPressed: () => Navigator.of(context).pushNamed(Routes.hashtag,
arguments: {
'tag': tag,
'onMarkChanged': onMarkChanged,
'type': type
}),
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 4,

View File

@ -2,9 +2,10 @@ import 'dart:developer';
import 'package:didvan/config/design_config.dart';
import 'package:didvan/main.dart';
import 'package:didvan/providers/server_data_provider.dart';
import 'package:didvan/providers/theme_provider.dart';
import 'package:didvan/providers/user_provider.dart';
import 'package:didvan/providers/media.dart';
import 'package:didvan/providers/server_data.dart';
import 'package:didvan/providers/theme.dart';
import 'package:didvan/providers/user.dart';
import 'package:didvan/routes/routes.dart';
import 'package:didvan/services/app_initalizer.dart';
import 'package:didvan/services/network/request.dart';
@ -49,8 +50,8 @@ class _SplashState extends State<Splash> {
value: DesignConfig.systemUiOverlayStyle.copyWith(
systemNavigationBarColor: Theme.of(context).colorScheme.background,
),
child: Scaffold(
body: Container(
child: Material(
child: Container(
alignment: Alignment.center,
padding: const EdgeInsets.all(60),
color: Theme.of(context).colorScheme.background,
@ -93,7 +94,6 @@ class _SplashState extends State<Splash> {
.removeWhere((key, value) => key == 'image-cache');
});
}
await AppInitializer.setupServices();
final settingsData = await AppInitializer.initilizeSettings();
final themeProvider = context.read<ThemeProvider>();
themeProvider.themeMode = settingsData.themeMode;
@ -105,10 +105,12 @@ class _SplashState extends State<Splash> {
_isGettingThemeData = false;
}),
);
await AppInitializer.setupServices();
final userProvider = context.read<UserProvider>();
final String? token = await userProvider.setAndGetToken();
if (token != null) {
log(token);
context.read<MediaProvider>().getDownloadsList();
RequestService.token = token;
final result = await userProvider.getUserInfo();
if (!result) {

View File

@ -20,7 +20,7 @@ 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: const EdgeInsets.only(right: 4),
decoration: BoxDecoration(
border: hasBorder
? Border(

View File

@ -0,0 +1,413 @@
import 'package:assets_audio_player/assets_audio_player.dart';
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_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';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:expandable_bottom_sheet/expandable_bottom_sheet.dart';
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:provider/provider.dart';
class DidvanBNB extends StatelessWidget {
final int currentTabIndex;
final void Function(int index) onTabChanged;
const DidvanBNB(
{Key? key, required this.currentTabIndex, required this.onTabChanged})
: super(key: key);
@override
Widget build(BuildContext context) {
return Stack(
children: [
const _PlayerNavBar(),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
height: 72,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius:
const BorderRadius.vertical(top: Radius.circular(16)),
boxShadow: [
BoxShadow(
color: const Color(0XFF1B3C59).withOpacity(0.15),
blurRadius: 8,
spreadRadius: 0,
offset: const Offset(0, -8),
)
],
),
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
_NavBarItem(
isSelected: currentTabIndex == 0,
title: 'اخبار',
selectedIcon: DidvanIcons.news_solid,
unselectedIcon: DidvanIcons.news_light,
onTap: () => onTabChanged(0),
),
_NavBarItem(
isSelected: currentTabIndex == 1,
title: 'آمار',
selectedIcon: DidvanIcons.chart_solid,
unselectedIcon: DidvanIcons.chart_light,
onTap: () => onTabChanged(1),
),
_NavBarItem(
isSelected: currentTabIndex == 2,
title: 'رادار',
selectedIcon: DidvanIcons.radar_solid,
unselectedIcon: DidvanIcons.radar_light,
onTap: () => onTabChanged(2),
),
_NavBarItem(
isSelected: currentTabIndex == 3,
title: 'استودیو',
selectedIcon: DidvanIcons.play_circle_solid,
unselectedIcon: DidvanIcons.play_circle_light,
onTap: () => onTabChanged(3),
),
_NavBarItem(
isSelected: currentTabIndex == 4,
title: 'تنظیمات',
selectedIcon: DidvanIcons.setting_solid,
unselectedIcon: DidvanIcons.setting_light,
onTap: () => onTabChanged(4),
),
],
),
),
),
],
);
}
}
class _PlayerNavBar extends StatelessWidget {
const _PlayerNavBar({Key? key}) : super(key: key);
bool _enablePlayerController(StudioDetailsState state) =>
MediaService.currentPodcast != null ||
(MediaService.audioPlayerTag?.contains('podcast') ?? false);
@override
Widget build(BuildContext context) {
return StreamBuilder<bool>(
stream: MediaService.audioPlayer.isPlaying,
builder: (context, snapshot) => GestureDetector(
onTap: () => MediaService.currentPodcast == null
? null
: _showPlayerBottomSheet(context),
child: Consumer<StudioDetailsState>(
builder: (context, state, child) => AnimatedContainer(
padding: const EdgeInsets.only(top: 12),
duration: DesignConfig.lowAnimationDuration,
height: _enablePlayerController(state) ? 128 : 72,
decoration: BoxDecoration(
color: DesignConfig.isDark
? Theme.of(context).colorScheme.focused
: Theme.of(context).colorScheme.navigation,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(16),
),
),
alignment: Alignment.topCenter,
child: Builder(builder: (context) {
if (!_enablePlayerController(state)) return const SizedBox();
if (state.appState == AppState.failed) {
Future.delayed(const Duration(seconds: 2), () {
MediaService.resetAudioPlayer();
});
return DidvanText(
'اتصال اینترنت برقرار نمی‌باشد',
color: DesignConfig.isDark
? Theme.of(context).colorScheme.title
: Theme.of(context).colorScheme.secondCTA,
);
}
if (MediaService.currentPodcast == null) {
return SizedBox(
height: 32,
child: Center(
child: SpinKitThreeBounce(
size: 18,
color: DesignConfig.isDark
? Theme.of(context).colorScheme.title
: Theme.of(context).colorScheme.secondCTA,
),
),
);
}
return SizedBox(
height: 56,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(
right: 12,
left: 16,
),
child: DidvanIconButton(
icon: DidvanIcons.close_regular,
color: DesignConfig.isDark
? null
: Theme.of(context).colorScheme.secondCTA,
gestureSize: 28,
onPressed: MediaService.resetAudioPlayer,
),
),
SkeletonImage(
imageUrl: MediaService.currentPodcast!.image,
width: 32,
height: 32,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DidvanText(
MediaService.currentPodcast!.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
color: DesignConfig.isDark
? null
: Theme.of(context).colorScheme.secondCTA,
),
AudioSlider(
disableThumb: true,
tag: MediaService.audioPlayerTag!,
),
],
),
),
StreamBuilder<PlayingAudio?>(
stream: MediaService.audioPlayer.onReadyToPlay,
builder: (context, snapshot) {
if (snapshot.data == null ||
state.appState == AppState.busy) {
return Padding(
padding: const EdgeInsets.only(
top: 4,
left: 16,
right: 16,
),
child: SizedBox(
height: 18,
width: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: DesignConfig.isDark
? Theme.of(context).colorScheme.title
: Theme.of(context).colorScheme.secondCTA,
),
),
);
}
return const SizedBox();
},
),
if (state.appState != AppState.busy &&
snapshot.data != null)
Padding(
padding: const EdgeInsets.only(
left: 12,
right: 16,
),
child: DidvanIconButton(
gestureSize: 28,
color: DesignConfig.isDark
? null
: Theme.of(context).colorScheme.secondCTA,
icon: snapshot.data!
? DidvanIcons.pause_solid
: DidvanIcons.play_solid,
onPressed: () {
if (state.args.type == 'video') {
state.getStudioDetails(
MediaService.currentPodcast!.id,
args: state.podcastArgs,
fetchOnly: true,
);
}
MediaService.handleAudioPlayback(
audioSource: MediaService.currentPodcast!.link,
id: MediaService.currentPodcast!.id,
isVoiceMessage: false,
);
},
),
),
],
),
);
}),
),
),
),
);
}
void _showPlayerBottomSheet(BuildContext context) {
final sheetKey = GlobalKey<ExpandableBottomSheetState>();
bool isExpanded = false;
final detailsState = context.read<StudioDetailsState>();
if (detailsState.args.type == 'video') {
detailsState.getStudioDetails(
MediaService.currentPodcast!.id,
args: detailsState.podcastArgs,
fetchOnly: true,
);
}
final state = context.read<StudioState>();
showModalBottomSheet(
backgroundColor: Colors.transparent,
context: context,
isScrollControlled: true,
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,
),
),
persistentHeader: GestureDetector(
onVerticalDragUpdate: (details) {
if (details.delta.dy > 10) {
Navigator.of(context).pop();
}
},
child: 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_up_regular,
onPressed: () {
if (!isExpanded) {
sheetKey.currentState?.expand();
isExpanded = true;
} else {
isExpanded = false;
sheetKey.currentState?.contract();
}
},
),
const SizedBox(height: 16),
],
),
),
],
),
),
expandableContent: state.appState == AppState.busy
? Container(
height: MediaQuery.of(context).size.height / 2,
alignment: Alignment.center,
child: SpinKitSpinningLines(
color: Theme.of(context).colorScheme.primary,
),
)
: StudioDetailsWidget(
onMarkChanged: (id, value) =>
context.read<StudioState>().changeMark(id, value, true),
),
),
),
),
);
}
}
class _NavBarItem extends StatelessWidget {
final VoidCallback onTap;
final bool isSelected;
final String title;
final IconData selectedIcon;
final IconData unselectedIcon;
const _NavBarItem({
Key? key,
required this.isSelected,
required this.title,
required this.selectedIcon,
required this.unselectedIcon,
required this.onTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Expanded(
child: Tooltip(
message: title,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.title,
borderRadius: DesignConfig.highBorderRadius,
boxShadow: DesignConfig.defaultShadow,
),
child: GestureDetector(
onTap: onTap,
child: Container(
color: Colors.transparent,
child: Column(
children: [
const SizedBox(
height: 4,
),
AnimatedContainer(
padding: const EdgeInsets.all(4),
duration: DesignConfig.lowAnimationDuration,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isSelected
? Theme.of(context).colorScheme.focused
: Theme.of(context).colorScheme.surface,
),
child: Icon(
isSelected ? selectedIcon : unselectedIcon,
size: 32,
color: DesignConfig.isDark
? Theme.of(context).colorScheme.text
: Theme.of(context).colorScheme.title,
),
),
DidvanText(
title,
style: Theme.of(context).textTheme.caption,
color: Theme.of(context).colorScheme.title,
),
const Spacer(),
],
),
),
),
),
);
}
}

View File

@ -21,6 +21,7 @@ class DidvanPageView extends StatefulWidget {
final int initialIndex;
final int currentIndex;
final bool isRadar;
final void Function(int id, bool value) onMarkChanged;
final ScrollController scrollController;
final void Function(int index) onPageChanged;
@ -32,6 +33,7 @@ class DidvanPageView extends StatefulWidget {
required this.onPageChanged,
required this.isRadar,
required this.currentIndex,
required this.onMarkChanged,
}) : super(key: key);
@override
@ -119,7 +121,11 @@ class _DidvanPageViewState extends State<DidvanPageView> {
runSpacing: 8,
children: [
for (var i = 0; i < item.tags.length; i++)
TagItem(tag: item.tags[i]),
TagItem(
tag: item.tags[i],
onMarkChanged: widget.onMarkChanged,
type: widget.isRadar ? 'radar' : 'news',
),
],
),
),

View File

@ -9,15 +9,21 @@ class DidvanScaffold extends StatefulWidget {
final EdgeInsets padding;
final Color? backgroundColor;
final bool reverse;
final ScrollPhysics? physics;
final ScrollController? scrollController;
final bool showSliversFirst;
const DidvanScaffold({
Key? key,
this.slivers,
required this.appBarData,
this.children,
this.physics,
this.padding = const EdgeInsets.symmetric(horizontal: 16),
this.backgroundColor,
this.reverse = false,
this.scrollController,
this.showSliversFirst = false,
}) : super(key: key);
@override
@ -25,7 +31,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,10 +45,13 @@ 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(
physics: widget.physics,
controller: _scrollController,
reverse: widget.reverse,
slivers: [
@ -50,7 +65,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(
@ -66,6 +81,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

@ -24,29 +24,23 @@ class SkeletonImage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return _aspectRatioGenerator(
child: ClipRRect(
borderRadius: borderRadius,
child: CachedNetworkImage(
fit: BoxFit.cover,
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
httpHeaders: {'Authorization': 'Bearer ${RequestService.token}'},
width: width,
height: height,
imageUrl: RequestHelper.baseUrl + imageUrl,
imageBuilder: (context, imageProvider) => ClipRRect(
borderRadius: borderRadius ?? DesignConfig.lowBorderRadius,
child: Image(
image: imageProvider,
fit: BoxFit.cover,
),
),
progressIndicatorBuilder: (context, url, progress) =>
ShimmerPlaceholder(
borderRadius: borderRadius,
placeholder: (context, _) => const ShimmerPlaceholder(),
),
),
);
}
Widget _aspectRatioGenerator({required Widget child}) => aspectRatio == null
? SizedBox(key: ValueKey(imageUrl), child: child)
? child
: AspectRatio(
key: ValueKey(imageUrl),
aspectRatio: aspectRatio!,

View File

@ -1,5 +1,5 @@
import 'package:didvan/models/enums.dart';
import 'package:didvan/providers/core_provider.dart';
import 'package:didvan/providers/core.dart';
import 'package:didvan/views/widgets/state_handlers/empty_connection.dart';
import 'package:flutter/material.dart';
@ -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,

View File

@ -1,5 +1,5 @@
import 'package:didvan/models/enums.dart';
import 'package:didvan/providers/core_provider.dart';
import 'package:didvan/providers/core.dart';
import 'package:didvan/views/widgets/state_handlers/empty_connection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';

View File

@ -1,6 +1,20 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
assets_audio_player:
dependency: "direct main"
description:
name: assets_audio_player
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.4+1"
assets_audio_player_web:
dependency: transitive
description:
name: assets_audio_player_web
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.4+1"
async:
dependency: transitive
description:
@ -8,13 +22,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.8.2"
audio_session:
dependency: transitive
description:
name: audio_session
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.6+1"
audio_video_progress_bar:
dependency: "direct main"
description:
@ -22,6 +29,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.0"
better_player:
dependency: "direct main"
description:
name: better_player
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.81"
boolean_selector:
dependency: transitive
description:
@ -126,7 +140,7 @@ packages:
name: day_night_time_picker
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4+1"
version: "1.0.5"
expandable_bottom_sheet:
dependency: "direct main"
description:
@ -161,7 +175,7 @@ packages:
name: firebase_core
url: "https://pub.dartlang.org"
source: hosted
version: "1.13.1"
version: "1.14.0"
firebase_core_platform_interface:
dependency: transitive
description:
@ -182,21 +196,21 @@ packages:
name: firebase_messaging
url: "https://pub.dartlang.org"
source: hosted
version: "11.2.8"
version: "11.2.12"
firebase_messaging_platform_interface:
dependency: transitive
description:
name: firebase_messaging_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.1"
version: "3.2.2"
firebase_messaging_web:
dependency: transitive
description:
name: firebase_messaging_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.9"
version: "2.2.10"
flutter:
dependency: "direct main"
description: flutter
@ -208,7 +222,7 @@ packages:
name: flutter_blurhash
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.0"
version: "0.6.4"
flutter_cache_manager:
dependency: transitive
description:
@ -297,7 +311,7 @@ packages:
name: flutter_svg
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
version: "1.0.3"
flutter_test:
dependency: "direct dev"
description: flutter
@ -315,6 +329,20 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_widget_from_html_core:
dependency: transitive
description:
name: flutter_widget_from_html_core
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.5+1"
fwfh_text_style:
dependency: transitive
description:
name: fwfh_text_style
url: "https://pub.dartlang.org"
source: hosted
version: "2.7.3+1"
graphs:
dependency: transitive
description:
@ -349,28 +377,28 @@ packages:
name: image_cropper
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.0"
version: "1.5.1"
image_picker:
dependency: "direct main"
description:
name: image_picker
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.4+4"
version: "0.8.4+11"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.4"
version: "2.1.6"
image_picker_platform_interface:
dependency: transitive
description:
name: image_picker_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.1"
version: "2.4.4"
intl:
dependency: transitive
description:
@ -385,27 +413,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.3"
just_audio:
dependency: "direct main"
description:
name: just_audio
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.18"
just_audio_platform_interface:
dependency: transitive
description:
name: just_audio_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0"
just_audio_web:
dependency: transitive
description:
name: just_audio_web
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.2"
lints:
dependency: transitive
description:
@ -482,49 +489,49 @@ packages:
name: path_provider
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
version: "2.0.9"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.11"
version: "2.0.12"
path_provider_ios:
dependency: transitive
description:
name: path_provider_ios
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.7"
version: "2.0.8"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.4"
version: "2.1.5"
path_provider_macos:
dependency: transitive
description:
name: path_provider_macos
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
version: "2.0.5"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
version: "2.0.3"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
version: "2.0.5"
pedantic:
dependency: transitive
description:
@ -532,6 +539,41 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.11.1"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
url: "https://pub.dartlang.org"
source: hosted
version: "9.2.0"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
url: "https://pub.dartlang.org"
source: hosted
version: "9.0.2+1"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
url: "https://pub.dartlang.org"
source: hosted
version: "9.0.4"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "3.7.0"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.0"
persian_datetime_picker:
dependency: "direct main"
description:
@ -594,7 +636,7 @@ packages:
name: record
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.2"
version: "3.0.4"
record_platform_interface:
dependency: transitive
description:
@ -648,14 +690,14 @@ packages:
name: sqflite
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
version: "2.0.2"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
version: "2.2.1"
stack_trace:
dependency: transitive
description:
@ -683,7 +725,7 @@ packages:
name: synchronized
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
version: "3.0.0+2"
term_glyph:
dependency: transitive
description:
@ -725,63 +767,63 @@ packages:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.18"
version: "6.0.20"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.14"
version: "6.0.15"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.14"
version: "6.0.15"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.3"
version: "3.0.0"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.3"
version: "3.0.0"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
version: "2.0.5"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.6"
version: "2.0.9"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
version: "3.0.0"
uuid:
dependency: transitive
description:
name: uuid
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.5"
version: "3.0.6"
vector_math:
dependency: transitive
description:
@ -789,6 +831,48 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
visibility_detector:
dependency: transitive
description:
name: visibility_detector
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.2"
wakelock:
dependency: transitive
description:
name: wakelock
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.6"
wakelock_macos:
dependency: transitive
description:
name: wakelock_macos
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.0"
wakelock_platform_interface:
dependency: transitive
description:
name: wakelock_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
wakelock_web:
dependency: transitive
description:
name: wakelock_web
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.0"
wakelock_windows:
dependency: transitive
description:
name: wakelock_windows
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
webview_flutter:
dependency: "direct main"
description:
@ -802,7 +886,7 @@ packages:
name: webview_flutter_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.8.3"
version: "2.8.4"
webview_flutter_platform_interface:
dependency: transitive
description:
@ -823,14 +907,14 @@ packages:
name: win32
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.3"
version: "2.5.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
version: "0.2.0+1"
xml:
dependency: transitive
description:
@ -840,4 +924,4 @@ packages:
version: "5.3.1"
sdks:
dart: ">=2.16.0 <3.0.0"
flutter: ">=2.5.0"
flutter: ">=2.10.0"

View File

@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.2.0+1
version: 1.5.0+10
environment:
sdk: ">=2.12.0 <3.0.0"
@ -40,7 +40,7 @@ dependencies:
pin_code_fields: ^7.3.0
rive: ^0.7.33
image_picker: ^0.8.4+4
day_night_time_picker: ^1.0.3+1
day_night_time_picker: ^1.0.5
path_provider: ^2.0.8
flutter_spinkit: ^5.1.0
flutter_svg: ^1.0.0
@ -50,7 +50,6 @@ dependencies:
flutter_vibrate: ^1.3.0
universal_html: ^2.0.8
record: ^3.0.2
just_audio: ^0.9.18
record_web: ^0.2.1
persian_datetime_picker: ^2.4.0
persian_number_utility: ^1.1.1
@ -64,6 +63,9 @@ dependencies:
firebase_core: ^1.13.1
webview_flutter: ^3.0.1
expandable_bottom_sheet: ^1.1.1+1
permission_handler: ^9.2.0
better_player: ^0.0.81
assets_audio_player: ^3.0.4+1
dev_dependencies:
@ -90,42 +92,12 @@ flutter:
# To add assets to your application, add an assets section, like this:
assets:
- lib/assets/images/logos/logo-vertical-dark.svg
- lib/assets/images/logos/logo-vertical-light.svg
- lib/assets/images/logos/logo-horizontal-dark.svg
- lib/assets/images/logos/logo-horizontal-light.svg
- lib/assets/images/logos/studio-dark.svg
- lib/assets/images/logos/studio-light.svg
- lib/assets/images/categories/business-light.svg
- lib/assets/images/categories/economic-light.svg
- lib/assets/images/categories/enviromental-light.svg
- lib/assets/images/categories/political-light.svg
- lib/assets/images/categories/social-light.svg
- lib/assets/images/categories/tech-light.svg
- lib/assets/images/categories/business-dark.svg
- lib/assets/images/categories/economic-dark.svg
- lib/assets/images/categories/enviromental-dark.svg
- lib/assets/images/categories/political-dark.svg
- lib/assets/images/categories/social-dark.svg
- lib/assets/images/categories/tech-dark.svg
- lib/assets/images/themes/theme-light.svg
- lib/assets/images/themes/theme-dark.svg
- lib/assets/images/records/record-dark.svg
- lib/assets/images/records/record-light.svg
- lib/assets/images/empty_states/bookmark-light.svg
- lib/assets/images/empty_states/chart-light.svg
- lib/assets/images/empty_states/chat-light.svg
- lib/assets/images/empty_states/connection-light.svg
- lib/assets/images/empty_states/result-light.svg
- lib/assets/images/empty_states/studio-light.svg
- lib/assets/images/empty_states/bookmark-dark.svg
- lib/assets/images/empty_states/chart-dark.svg
- lib/assets/images/empty_states/chat-dark.svg
- lib/assets/images/empty_states/connection-dark.svg
- lib/assets/images/empty_states/result-dark.svg
- lib/assets/images/empty_states/studio-dark.svg
- lib/assets/animations/indicator-light.riv
- lib/assets/animations/indicator-dark.riv
- lib/assets/images/logos/
- lib/assets/images/categories/
- lib/assets/images/themes/
- lib/assets/images/records/
- lib/assets/images/empty_states/
- lib/assets/animations/
- lib/assets/loading.gif

13
release.sh Normal file
View File

@ -0,0 +1,13 @@
flutter clean
flutter build apk
cp build/app/outputs/flutter-apk/app-release.apk /users/arytan/desktop
flutter build web --web-renderer canvaskit
cd build/web
fandogh login --username didvan --password 12799721
fandogh image publish --version $1
fandogh service apply -f ../../.fandogh/fandogh.yaml
cd ../..
flutter build ipa
xcodebuild -exportArchive -exportOptionsPlist ios/runner/info.plist -archivePath build/ios/archive/runner.xcarchive -exportPath /users/arytan/desktop/App.ipa
cp /users/arytan/desktop/didvan.ipa/didvan.ipa /users/arytan/desktop
echo "Done!"

View File

@ -60,7 +60,7 @@
}
scriptLoaded = true;
var scriptTag = document.createElement('script');
scriptTag.src = 'main.dart.js';
scriptTag.src = `main.dart.js?version=${Math.random()}`;
scriptTag.type = 'application/javascript';
document.body.append(scriptTag);
}