2 Commits

Author SHA1 Message Date
mohsen 2d54088e75 fix some bug 1 year ago
mohsen ff49c9f45e fix some bug 1 year ago
  1. 17
      data/data_core/local_db/local_db_core/lib/lib/boxes/box_list/setting_box/app_setting_box.dart
  2. 1
      data/data_core/local_db/local_db_core/lib/lib/boxes/box_list/setting_box/box_keys.dart
  3. 34
      data/data_core/network/network_core/lib/initializer/gamification_initializer.dart
  4. 37
      data/data_core/network/network_core/lib/initializer/hamrah_cloud_initializer.dart
  5. 39
      data/data_core/network/network_core/lib/initializer/logger_initializer.dart
  6. 14
      data/data_core/network/network_core/lib/network/helper/config.dart
  7. 132
      data/data_core/network/network_core/lib/network/networking/api_endpoint.dart
  8. 5
      data/data_core/network/network_core/lib/network/networking/api_service.dart
  9. 22
      data/data_core/network/network_core/lib/network/networking/interceptors/api_interceptor.dart
  10. 86
      data/data_types/data/lib/app_api_data/app_api_repository_impl.dart
  11. 10
      data/data_types/data/lib/app_setting_data/repository/app_setting_box_repository_impl.dart
  12. 2
      data/data_types/data/pubspec.yaml
  13. 19
      domain/repositories/lib/app_api_domain/models/comment_model.dart
  14. 19
      domain/repositories/lib/app_api_domain/models/contact_us_model.dart
  15. 22
      domain/repositories/lib/app_api_domain/models/login_model.dart
  16. 13
      domain/repositories/lib/app_api_domain/models/login_response_model.dart
  17. 43
      domain/repositories/lib/app_api_domain/models/post_model.dart
  18. 19
      domain/repositories/lib/app_api_domain/models/send_comment_model.dart
  19. 45
      domain/repositories/lib/app_api_domain/models/single_post_model.dart
  20. 23
      domain/repositories/lib/app_api_domain/repository/app_api_repository.dart
  21. 4
      domain/repositories/lib/app_setting_box_domain/repository/app_setting_box_repository.dart
  22. 45
      lib/core/select_language/cubit/select_language_cubit.dart
  23. 16
      lib/core/utils/app_utils.dart
  24. 2
      lib/features/about_us/about_us_screen.dart
  25. 170
      lib/features/main/main_screen.dart
  26. 77
      lib/features/posts/cubit/posts_cubit.dart
  27. 80
      lib/features/posts/screen/posts_screen.dart
  28. 41
      lib/features/posts/widgets/post_item_widget.dart
  29. 19
      lib/features/single_post/cubit/single_post_cubit.dart
  30. 53
      lib/features/single_post/screen/single_post_screen.dart
  31. 2
      lib/features/splash/cubit/splash_cubit.dart
  32. 117
      pubspec.lock
  33. 3
      pubspec.yaml

17
data/data_core/local_db/local_db_core/lib/lib/boxes/box_list/setting_box/app_setting_box.dart

@ -50,7 +50,7 @@ class AppSettingBox implements ConfigBoxInterface<BoxKeys> {
} }
Future<UserDataModel?> getUserData() async { Future<UserDataModel?> getUserData() async {
if(!Hive.isBoxOpen(getBoxName())) {
if (!Hive.isBoxOpen(getBoxName())) {
await Hive.openBox(getBoxName()); await Hive.openBox(getBoxName());
} }
Map<dynamic, dynamic>? data = await _box!.get(getKeyName(BoxKeys.userData)); Map<dynamic, dynamic>? data = await _box!.get(getKeyName(BoxKeys.userData));
@ -63,4 +63,19 @@ class AppSettingBox implements ConfigBoxInterface<BoxKeys> {
Future<void> setUserData(Map<String, dynamic> data) async { Future<void> setUserData(Map<String, dynamic> data) async {
await _box!.put(getKeyName(BoxKeys.userData), data); await _box!.put(getKeyName(BoxKeys.userData), data);
} }
Future<void> setUserToken(Map<String, dynamic> data) async {
await _box!.put(getKeyName(BoxKeys.token), data);
}
Future<String> getUserToken() async {
if (!Hive.isBoxOpen(getBoxName())) {
await Hive.openBox(getBoxName());
}
Map<dynamic, dynamic>? data = await _box!.get(getKeyName(BoxKeys.userData));
if (data == null) {
return 'token';
}
return data['token'];
}
} }

1
data/data_core/local_db/local_db_core/lib/lib/boxes/box_list/setting_box/box_keys.dart

@ -1,6 +1,7 @@
enum BoxKeys { enum BoxKeys {
currentLanguage("current-language"), currentLanguage("current-language"),
userData('user-data'), userData('user-data'),
token('token'),
appDirection("app-direction"); appDirection("app-direction");
final String value; final String value;

34
data/data_core/network/network_core/lib/initializer/gamification_initializer.dart

@ -1,34 +0,0 @@
import 'package:dio/dio.dart';
import 'package:network_core/network/interface/network_initializer_interface.dart';
import 'package:network_core/network/networking/api_endpoint.dart';
import 'package:network_core/network/networking/api_service.dart';
import 'package:network_core/network/networking/dio_service.dart';
class GamificationInitializer implements NetworkInitializerInterface {
factory GamificationInitializer() {
return instance;
}
GamificationInitializer.privateConstructor();
static final GamificationInitializer instance = GamificationInitializer.privateConstructor();
@override
DioService getDioService() {
return DioService(
dioClient: getDioClient(),
interceptors: [],
);
}
@override
Dio getDioClient() {
final baseOptions = BaseOptions(baseUrl: ApiEndpoint.gamificationBaseUrl);
return Dio(baseOptions);
}
@override
ApiService getApiService() {
return ApiService(getDioService());
}
}

37
data/data_core/network/network_core/lib/initializer/hamrah_cloud_initializer.dart

@ -1,37 +0,0 @@
import 'package:dio/dio.dart';
import 'package:network_core/network/interface/network_initializer_interface.dart';
import 'package:network_core/network/networking/api_endpoint.dart';
import 'package:network_core/network/networking/api_service.dart';
import 'package:network_core/network/networking/dio_service.dart';
import 'package:network_core/network/networking/interceptors/api_interceptor.dart';
class HamrahCloudInitializer implements NetworkInitializerInterface {
factory HamrahCloudInitializer() {
return instance;
}
HamrahCloudInitializer.privateConstructor();
static final HamrahCloudInitializer instance = HamrahCloudInitializer.privateConstructor();
@override
DioService getDioService() {
return DioService(
dioClient: getDioClient(),
interceptors: [
ApiInterceptor(useToken: false, addAllowMethod: false),
],
);
}
@override
Dio getDioClient() {
final baseOptions = BaseOptions(baseUrl: ApiEndpoint.hamrahCloudBaseUrl);
return Dio(baseOptions);
}
@override
ApiService getApiService() {
return ApiService(getDioService());
}
}

39
data/data_core/network/network_core/lib/initializer/logger_initializer.dart

@ -1,39 +0,0 @@
import 'package:dio/dio.dart';
import 'package:network_core/network/interface/network_initializer_interface.dart';
import 'package:network_core/network/networking/api_endpoint.dart';
import 'package:network_core/network/networking/api_service.dart';
import 'package:network_core/network/networking/dio_service.dart';
import 'package:network_core/network/networking/interceptors/api_interceptor.dart';
class LoggerInitializer implements NetworkInitializerInterface {
factory LoggerInitializer() {
return instance;
}
LoggerInitializer.privateConstructor();
static final LoggerInitializer instance = LoggerInitializer.privateConstructor();
@override
DioService getDioService() {
return DioService(
dioClient: getDioClient(),
interceptors: [
ApiInterceptor(),
],
);
}
@override
Dio getDioClient() {
final baseOptions = BaseOptions(
baseUrl: ApiEndpoint.loggerBaseUrl,
);
return Dio(baseOptions);
}
@override
ApiService getApiService() {
return ApiService(getDioService());
}
}

14
data/data_core/network/network_core/lib/network/helper/config.dart

@ -4,17 +4,5 @@ import 'package:flutter/foundation.dart';
class Config { class Config {
const Config._(); const Config._();
static const baseUrl = String.fromEnvironment('BASE_URL', defaultValue: 'https://api-game.zarebin.ir/api');
static const loggerBaseUrl = String.fromEnvironment(
'BASE_URL',
defaultValue: 'https://supervisor.pr.mci.dev/api/v1/',
);
static const hamrahCloudBaseUrl = String.fromEnvironment(
'BASE_URL',
defaultValue: 'https://hamrahi.cloud/live/api/v1',
);
static const gamificationBaseUrl = String.fromEnvironment('BASE_URL', defaultValue: 'https://eagle-eyes.st.mci.dev');
static const baseUrl = String.fromEnvironment('BASE_URL', defaultValue: 'https://sonnat.net/api/mobile/');
} }

132
data/data_core/network/network_core/lib/network/networking/api_endpoint.dart

@ -6,135 +6,21 @@ class ApiEndpoint {
const ApiEndpoint._(); const ApiEndpoint._();
static const baseUrl = Config.baseUrl; static const baseUrl = Config.baseUrl;
static const loggerBaseUrl = Config.loggerBaseUrl;
static const hamrahCloudBaseUrl = Config.hamrahCloudBaseUrl;
static const gamificationBaseUrl = Config.gamificationBaseUrl;
static String account(AccountEndpoint endpoint) {
return '/account/${endpoint.value}';
static String api(AppEndpoint endpoint) {
return '${endpoint.value}';
} }
static String game(GameEndpoint endpoint) {
return '/game/${endpoint.value}';
}
static String general(GeneralEndPoint endPoint) {
return '/general/${endPoint.value}';
}
static String leaderboard(LeaderboardEndpoint endpoint) {
return '/leaderboard/${endpoint.value}';
}
static String gamePlayer(GamePlayerEndpoint endpoint) {
return '/game-player/${endpoint.value}';
}
static String getFriendReq(FriendRequestEndPoint endPoint) {
return '/player/friendRequest/${endPoint.value}';
}
static String logger(LogEndPoint endPoint) {
return endPoint.value;
}
static String hamrahCloud(HamrahCloudEndpoint endPoint) {
return endPoint.value;
}
static String campaigns(CampaignsEndPoint endPoint) {
return '/api/v1/campaigns/${endPoint.value}/';
}
}
enum AccountEndpoint {
profile('profile');
final String value;
const AccountEndpoint(this.value);
} }
enum GamePlayerEndpoint {
playerInfo('gamePlayerInfo'),
myRank('best');
final String value;
const GamePlayerEndpoint(this.value);
}
enum GameEndpoint {
banners('banners'),
categories('categories'),
favoriteGames('favorite-games'),
gameProfile('game-profile'),
games('games'),
recentPlayedGames('recent-played-games'),
enum AppEndpoint {
login('login'),
sendComment('send_comment'),
search('search'), search('search'),
searchHistory('search-history'),
suggestedGames('suggested-games'),
topGames('top-games');
final String value;
const GameEndpoint(this.value);
}
enum GeneralEndPoint {
streams('streams'),
whiteList('channels/whitelist'),
deepLink('deeplink'),
lv('lv'),
rootItems('root_items');
final String value;
const GeneralEndPoint(this.value);
}
enum LeaderboardEndpoint {
campaignLeaderboard('campaign-leaderboard'),
games('games'),
leaderboards('leaderboards');
final String value;
const LeaderboardEndpoint(this.value);
}
enum FriendRequestEndPoint {
createFriendReq(''),
answerFriendReq('answer'),
getWaitingAddFriendReq('waiting');
final String value;
const FriendRequestEndPoint(this.value);
}
enum LogEndPoint {
multiEvent('events'),
singleEvent('event');
final String value;
const LogEndPoint(this.value);
}
enum HamrahCloudEndpoint {
list('/vod/viewers/list-vod/'),
search('/live/search/pinobject/387/?');
final String value;
const HamrahCloudEndpoint(this.value);
}
enum CampaignsEndPoint {
events('events');
contactUs('contact_us'),
singlePost('getarticle'),
postList('getarticles');
final String value; final String value;
const CampaignsEndPoint(this.value);
const AppEndpoint(this.value);
} }

5
data/data_core/network/network_core/lib/network/networking/api_service.dart

@ -24,11 +24,6 @@ class ApiService implements ApiInterface {
try { try {
data = await _dioService.getData( data = await _dioService.getData(
endpoint: endpoint, endpoint: endpoint,
options: Options(
extra: <String, Object?>{
'requiresAuthToken': requiresAuthToken,
},
),
queryParams: queryParams, queryParams: queryParams,
cancelToken: cancelToken, cancelToken: cancelToken,
); );

22
data/data_core/network/network_core/lib/network/networking/interceptors/api_interceptor.dart

@ -1,15 +1,13 @@
import 'package:data/app_setting_data/repository/app_setting_box_repository_impl.dart'; import 'package:data/app_setting_data/repository/app_setting_box_repository_impl.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:local_db_core/lib/boxes/box_list/setting_box/app_setting_box.dart'; import 'package:local_db_core/lib/boxes/box_list/setting_box/app_setting_box.dart';
import 'package:repositories/app_setting_box_domain/model/user_data_model.dart';
import 'package:repositories/app_setting_box_domain/repository/app_setting_box_repository.dart'; import 'package:repositories/app_setting_box_domain/repository/app_setting_box_repository.dart';
class ApiInterceptor extends Interceptor { class ApiInterceptor extends Interceptor {
final bool useToken; final bool useToken;
final bool addAllowMethod;
AppSettingBoxRepository _repository = AppSettingBoxRepositoryImpl(appSettingBox: AppSettingBox()); AppSettingBoxRepository _repository = AppSettingBoxRepositoryImpl(appSettingBox: AppSettingBox());
ApiInterceptor({this.useToken = true, this.addAllowMethod = true});
ApiInterceptor({this.useToken = true});
@override @override
Future<void> onRequest(RequestOptions options, RequestInterceptorHandler handler) async { Future<void> onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
@ -18,19 +16,13 @@ class ApiInterceptor extends Interceptor {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': '*/*', 'Accept': '*/*',
}); });
if (addAllowMethod) {
options.headers.addAll(<String, Object?>{
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE, HEAD',
'Access-Control-Allow-Origin': '*',
});
}
UserDataModel? userData = await _repository.getUserData();
options.headers.addAll(<String, Object?>{
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE, HEAD',
'Access-Control-Allow-Origin': '*',
});
String token = await _repository.getUserToken();
if (useToken) { if (useToken) {
if (userData != null) {
options.headers.addAll(<String, Object?>{
'Authorization': 'Bearer ${userData.token}',
});
}
options.headers.addAll(<String, Object?>{'Authorization': '$token'});
} }
} }
options.extra.remove('requiresAuthToken'); options.extra.remove('requiresAuthToken');

86
data/data_types/data/lib/app_api_data/app_api_repository_impl.dart

@ -0,0 +1,86 @@
import 'package:network_core/network/networking/api_endpoint.dart';
import 'package:network_core/network/networking/api_service.dart';
import 'package:repositories/app_api_domain/models/login_response_model.dart';
import 'package:repositories/app_api_domain/models/post_model.dart';
import 'package:repositories/app_api_domain/models/single_post_model.dart';
import 'package:repositories/app_api_domain/repository/app_api_repository.dart';
class AppApiRepositoryImpl implements AppApiRepository {
final ApiService _apiService;
AppApiRepositoryImpl({required ApiService apiService}) : _apiService = apiService;
@override
Future<void> contactUs({required Map<String, dynamic> data}) async {
return await _apiService.setData(
endpoint: ApiEndpoint.api(AppEndpoint.contactUs),
converter: (response) {},
data: data,
);
}
@override
Future<List<PostModel>> getPostList({
required String catId,
required String sort,
required int page,
required String languageCode,
}) async {
return await _apiService.getCollectionData(
endpoint:
'${ApiEndpoint.api(AppEndpoint.postList)}?cat=$catId&sort=$sort&page=$page&language_code=$languageCode',
converter: (response) {
return List<PostModel>.from(
response.data['data'].map((game) {
return PostModel.fromJson(game);
}),
);
},
);
}
@override
Future<SinglePostModel> getSinglePost({required int postId}) async {
return await _apiService.getDocumentData<SinglePostModel>(
endpoint: '${ApiEndpoint.api(AppEndpoint.singlePost)}?post=$postId',
converter: (response) {
return SinglePostModel.fromJson(response['data']);
},
);
}
@override
Future<LoginResponseModel> login({required Map<String, dynamic> data}) async {
return await _apiService.setData<LoginResponseModel>(
endpoint: ApiEndpoint.api(AppEndpoint.login),
converter: (response) {
return LoginResponseModel.fromJson(response.data);
},
queryParams: data,
requiresAuthToken: false,
);
}
@override
Future<List<PostModel>> searchPost({required String query, required String languageCode}) async {
return await _apiService.getCollectionData<List<PostModel>>(
endpoint: '${ApiEndpoint.api(AppEndpoint.search)}/?s=$query&language_code=$languageCode',
converter: (response) {
return List<PostModel>.from(
response.data.map((game) {
return PostModel.fromJson(game);
}),
);
},
);
}
@override
Future<void> sendComment({required Map<String, dynamic> data}) async {
return await _apiService.setData(
endpoint: ApiEndpoint.api(AppEndpoint.sendComment),
converter: (response) {},
data: data,
);
}
}

10
data/data_types/data/lib/app_setting_data/repository/app_setting_box_repository_impl.dart

@ -99,4 +99,14 @@ class AppSettingBoxRepositoryImpl extends AppSettingBoxRepository {
} }
return utf8.decode(base64Url.decode(output)); return utf8.decode(base64Url.decode(output));
} }
@override
Future<String> getUserToken() async {
return await _appSettingBox.getUserToken();
}
@override
Future<void> setUserToken(Map<String, dynamic> data) async {
return await _appSettingBox.setUserToken(data);
}
} }

2
data/data_types/data/pubspec.yaml

@ -14,6 +14,8 @@ dependencies:
path: ../../../domain/repositories path: ../../../domain/repositories
local_db_core: local_db_core:
path: ../../data_core/local_db/local_db_core path: ../../data_core/local_db/local_db_core
network_core:
path: ../../data_core/network/network_core
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

19
domain/repositories/lib/app_api_domain/models/comment_model.dart

@ -0,0 +1,19 @@
class CommentModel {
final String fullName;
final String createdDate;
final String text;
const CommentModel({
required this.text,
required this.createdDate,
required this.fullName,
});
factory CommentModel.fromJson(Map<String, dynamic> json) {
return CommentModel(
text: json['text'],
createdDate: json['created_date'],
fullName: json['full_name'],
);
}
}

19
domain/repositories/lib/app_api_domain/models/contact_us_model.dart

@ -0,0 +1,19 @@
class ContactUsModel {
final String email;
final String text;
final String fullName;
const ContactUsModel({
required this.text,
required this.fullName,
required this.email,
});
Map<String, dynamic> toJson() {
return {
'email': email,
'full_name': fullName,
'text': text,
};
}
}

22
domain/repositories/lib/app_api_domain/models/login_model.dart

@ -0,0 +1,22 @@
class LoginModel {
final String language;
final String mobileDeviceId;
final String timeZone;
final String apiVersion;
const LoginModel({
required this.language,
required this.apiVersion,
required this.mobileDeviceId,
required this.timeZone,
});
Map<String, dynamic> toJson() {
return {
'language': language,
'mobile_device_id': mobileDeviceId,
'timezone': timeZone,
'api_version': apiVersion,
};
}
}

13
domain/repositories/lib/app_api_domain/models/login_response_model.dart

@ -0,0 +1,13 @@
class LoginResponseModel {
final String token;
const LoginResponseModel({required this.token});
factory LoginResponseModel.fromJson(Map<String, dynamic> json) {
return LoginResponseModel(token: json['token']);
}
Map<String, dynamic> toJson() {
return {'token': token};
}
}

43
domain/repositories/lib/app_api_domain/models/post_model.dart

@ -0,0 +1,43 @@
class PostModel {
final int id;
final String title;
final String content;
final String summary;
final String publishDate;
final String? thumbnail;
final String languageCode;
final int viewCount;
final int commentCount;
final String websiteUrl;
final bool isPinned;
const PostModel({
required this.title,
required this.viewCount,
required this.commentCount,
required this.websiteUrl,
required this.content,
required this.id,
required this.summary,
required this.isPinned,
required this.languageCode,
required this.publishDate,
required this.thumbnail,
});
factory PostModel.fromJson(Map<String, dynamic> json) {
return PostModel(
id: json['id'],
title: json['title'],
content: json['content'],
commentCount: json['comm_count'],
viewCount: json['view_count'],
summary: json['summery'],
publishDate: json['publish_date'][0],
websiteUrl: json['website_url'],
thumbnail: json['thumbnail'] ?? '',
languageCode: json['language_code']['slug'],
isPinned: json['is_pinned'],
);
}
}

19
domain/repositories/lib/app_api_domain/models/send_comment_model.dart

@ -0,0 +1,19 @@
class SendCommentModel {
final int postId;
final String text;
final String fullName;
const SendCommentModel({
required this.text,
required this.fullName,
required this.postId,
});
Map<String, dynamic> toJson() {
return {
'post_id': postId,
'full_name': fullName,
'text': text,
};
}
}

45
domain/repositories/lib/app_api_domain/models/single_post_model.dart

@ -0,0 +1,45 @@
import 'package:repositories/app_api_domain/models/comment_model.dart';
class SinglePostModel {
final int id;
final String title;
final String summary;
final String publishDate;
final String? thumbnail;
final String languageCode;
final String websiteUrl;
final int viewCount;
final int commentCount;
final List<CommentModel> comments;
final String content;
const SinglePostModel({
required this.title,
required this.websiteUrl,
required this.content,
required this.id,
required this.comments,
required this.summary,
required this.languageCode,
required this.publishDate,
required this.thumbnail,
required this.commentCount,
required this.viewCount,
});
factory SinglePostModel.fromJson(Map<String, dynamic> json) {
return SinglePostModel(
id: json['id'],
websiteUrl: json['website_url'],
title: json['title'],
content: json['content'],
summary: json['summery'],
publishDate: json['publish_date'][0],
thumbnail: json['thumbnail'],
languageCode: json['language_code']['slug'],
comments: List<CommentModel>.from(json['comments'].map((post) => CommentModel.fromJson(post))),
commentCount: json['comm_count'],
viewCount: json['view_count'],
);
}
}

23
domain/repositories/lib/app_api_domain/repository/app_api_repository.dart

@ -0,0 +1,23 @@
import 'package:repositories/app_api_domain/models/login_response_model.dart';
import 'package:repositories/app_api_domain/models/post_model.dart';
import 'package:repositories/app_api_domain/models/single_post_model.dart';
import 'package:repositories/typedefs.dart';
abstract class AppApiRepository {
Future<LoginResponseModel> login({required JSON data});
Future<List<PostModel>> getPostList({
required String catId,
required String sort,
required int page,
required String languageCode,
});
Future<SinglePostModel> getSinglePost({required int postId});
Future<void> sendComment({required JSON data});
Future<List<PostModel>> searchPost({required String query, required String languageCode});
Future<void> contactUs({required JSON data});
}

4
domain/repositories/lib/app_setting_box_domain/repository/app_setting_box_repository.dart

@ -18,4 +18,8 @@ abstract class AppSettingBoxRepository {
Future<bool> isUserLogin(); Future<bool> isUserLogin();
Future<String?> getUserId(); Future<String?> getUserId();
Future<void> setUserToken(Map<String, dynamic> data);
Future<String> getUserToken();
} }

45
lib/core/select_language/cubit/select_language_cubit.dart

@ -1,34 +1,63 @@
import 'package:data/app_api_data/app_api_repository_impl.dart';
import 'package:data/app_setting_data/repository/app_setting_box_repository_impl.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:sonnat/core/language/language_cubit.dart';
import 'package:local_db_core/lib/boxes/box_list/setting_box/app_setting_box.dart';
import 'package:network_core/initializer/base_initializer.dart';
import 'package:network_core/network/networking/custom_exception.dart';
import 'package:repositories/app_api_domain/models/login_model.dart';
import 'package:repositories/app_api_domain/models/login_response_model.dart';
import 'package:repositories/app_api_domain/repository/app_api_repository.dart';
import 'package:repositories/app_setting_box_domain/repository/app_setting_box_repository.dart';
import 'package:sonnat/core/language/languages.dart'; import 'package:sonnat/core/language/languages.dart';
import 'package:sonnat/core/language/translator.dart'; import 'package:sonnat/core/language/translator.dart';
import 'package:sonnat/core/utils/app_utils.dart';
import 'package:sonnat/core/utils/base_cubit_type.dart'; import 'package:sonnat/core/utils/base_cubit_type.dart';
class SelectLanguageCubit extends Cubit<BaseCubitType<SelectLanguageState>> { class SelectLanguageCubit extends Cubit<BaseCubitType<SelectLanguageState>> {
final AppSettingBoxRepository _repository = AppSettingBoxRepositoryImpl(appSettingBox: AppSettingBox());
final AppApiRepository _remoteRepository = AppApiRepositoryImpl(apiService: BaseInitializer.instance.getApiService());
SelectLanguageCubit() : super(BaseCubitType(eventName: SelectLanguageState.empty)); SelectLanguageCubit() : super(BaseCubitType(eventName: SelectLanguageState.empty));
void empty() => emit(BaseCubitType(eventName: SelectLanguageState.empty)); void empty() => emit(BaseCubitType(eventName: SelectLanguageState.empty));
Future<void> selectLanguage(String language) async {
void selectLanguage(String language) {
switch (language) { switch (language) {
case 'fa': case 'fa':
await Translator.setNewLanguage(Languages.fa);
emit(BaseCubitType(eventName: SelectLanguageState.loaded, data: language));
login(Languages.fa);
return; return;
case 'en': case 'en':
await Translator.setNewLanguage(Languages.en);
emit(BaseCubitType(eventName: SelectLanguageState.loaded, data: language));
login(Languages.en);
return; return;
case 'ar': case 'ar':
await Translator.setNewLanguage(Languages.ar);
emit(BaseCubitType(eventName: SelectLanguageState.loaded, data: language));
login(Languages.ar);
return; return;
} }
} }
Future<void> login(Languages newLanguage) async {
await Translator.setNewLanguage(newLanguage);
await _repository.setCurrentLanguage(newLanguage.value);
String? deviceId = await Utils.instance.getId();
deviceId ??= 'user-device';
LoginModel loginModel = LoginModel(
language: newLanguage.value,
apiVersion: '1',
mobileDeviceId: deviceId,
timeZone: '${DateTime.now().timeZoneOffset.inHours}:${DateTime.now().timeZoneOffset.inMinutes}',
);
try {
LoginResponseModel responseModel = await _remoteRepository.login(data: loginModel.toJson());
await _repository.setUserToken(responseModel.toJson());
emit(BaseCubitType(eventName: SelectLanguageState.loaded, data: newLanguage.value));
} on CustomException {
emit(BaseCubitType(eventName: SelectLanguageState.error));
}
}
} }
enum SelectLanguageState { enum SelectLanguageState {
empty, empty,
loaded, loaded,
error,
} }

16
lib/core/utils/app_utils.dart

@ -1,3 +1,6 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shamsi_date/shamsi_date.dart'; import 'package:shamsi_date/shamsi_date.dart';
@ -115,4 +118,17 @@ class Utils {
Jalali jalali = Jalali.fromDateTime(DateTime.fromMillisecondsSinceEpoch(dateInMilliseconds)); Jalali jalali = Jalali.fromDateTime(DateTime.fromMillisecondsSinceEpoch(dateInMilliseconds));
return '${jalali.year}/${jalali.month}/${jalali.day}'; return '${jalali.year}/${jalali.month}/${jalali.day}';
} }
Future<String?> getId() async {
var deviceInfo = DeviceInfoPlugin();
if (Platform.isIOS) {
var iosDeviceInfo = await deviceInfo.iosInfo;
return iosDeviceInfo.identifierForVendor; // unique ID on iOS
}
if (Platform.isAndroid) {
var androidDeviceInfo = await deviceInfo.androidInfo;
return androidDeviceInfo.androidId; // unique ID on Android
}
return null;
}
} }

2
lib/features/about_us/about_us_screen.dart

@ -41,6 +41,7 @@ class _AboutUsScreenState extends State<AboutUsScreen> {
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
), ),
), ),
@ -173,6 +174,7 @@ class _AboutUsScreenState extends State<AboutUsScreen> {
style: const TextStyle(color: Color(0xffffffff), fontSize: 12), style: const TextStyle(color: Color(0xffffffff), fontSize: 12),
), ),
), ),
SizedBox(height: context.height * 9 / AppConstants.instance.appHeight),
], ],
), ),
), ),

170
lib/features/main/main_screen.dart

@ -4,11 +4,7 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:sonnat/core/extensions/context_extension.dart'; import 'package:sonnat/core/extensions/context_extension.dart';
import 'package:sonnat/core/extensions/string_extension.dart'; import 'package:sonnat/core/extensions/string_extension.dart';
import 'package:sonnat/core/language/translator.dart'; import 'package:sonnat/core/language/translator.dart';
import 'package:sonnat/core/select_language/cubit/select_language_cubit.dart';
import 'package:sonnat/core/select_language/screen/select_language_screen.dart';
import 'package:sonnat/core/utils/app_constants.dart'; import 'package:sonnat/core/utils/app_constants.dart';
import 'package:sonnat/core/widgets/more_options_widget.dart';
import 'package:sonnat/features/about_us/about_us_screen.dart';
import 'package:sonnat/features/main/widget/main_item_widget.dart'; import 'package:sonnat/features/main/widget/main_item_widget.dart';
import 'package:sonnat/features/posts/cubit/posts_cubit.dart'; import 'package:sonnat/features/posts/cubit/posts_cubit.dart';
import 'package:sonnat/features/posts/screen/posts_screen.dart'; import 'package:sonnat/features/posts/screen/posts_screen.dart';
@ -29,13 +25,13 @@ class _MainScreenState extends State<MainScreen> {
'ic_akhbar', 'ic_akhbar',
'ic_video', 'ic_video',
]; ];
final List<String> _names = [
Translator.translate('forbidden'),
Translator.translate('doubts'),
Translator.translate('criticism_of_ideas'),
Translator.translate('specials'),
Translator.translate('news'),
Translator.translate('video'),
final List<Map<String, String>> _names = [
{'name': Translator.translate('forbidden'), 'catId': '77,78,79'},
{'name': Translator.translate('doubts'), 'catId': '83'},
{'name': Translator.translate('criticism_of_ideas'), 'catId': '101'},
{'name': Translator.translate('specials'), 'catId': '109'},
{'name': Translator.translate('news'), 'catId': '104'},
{'name': Translator.translate('video'), 'catId': ''},
]; ];
@override @override
@ -45,31 +41,16 @@ class _MainScreenState extends State<MainScreen> {
body: SafeArea( body: SafeArea(
child: Column( child: Column(
children: [ children: [
Stack(
children: [
Container(
margin: EdgeInsets.only(
left: context.width * 86 / AppConstants.instance.appWidth,
right: context.width * 86 / AppConstants.instance.appWidth,
),
child: Image.asset(
'ic_main_header'.pngPath,
width: context.width * 200 / AppConstants.instance.appWidth,
height: context.height * 200 / AppConstants.instance.appHeight,
),
),
PositionedDirectional(
end: context.width * 35 / AppConstants.instance.appWidth,
top: 16,
child: Align(
alignment: AlignmentDirectional.centerStart,
child: GestureDetector(
onTap: _showOptions,
child: SvgPicture.asset('ic_more'.svgPath),
),
),
),
],
Container(
margin: EdgeInsets.only(
left: context.width * 86 / AppConstants.instance.appWidth,
right: context.width * 86 / AppConstants.instance.appWidth,
),
child: Image.asset(
'ic_main_header'.pngPath,
width: context.width * 200 / AppConstants.instance.appWidth,
height: context.height * 200 / AppConstants.instance.appHeight,
),
), ),
Text( Text(
Translator.translate('main_header_text'), Translator.translate('main_header_text'),
@ -87,14 +68,17 @@ class _MainScreenState extends State<MainScreen> {
), ),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return BlocProvider(
create: (context) => PostsCubit(),
child: const PostsScreen(title: '', searchMode: true),
);
},
));
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return BlocProvider(
create: (context) => PostsCubit(lastCatId: '77,78,79'),
child: const PostsScreen(title: '', searchMode: true),
);
},
),
);
}, },
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
@ -133,17 +117,17 @@ class _MainScreenState extends State<MainScreen> {
child: Row( child: Row(
children: [ children: [
GestureDetector( GestureDetector(
child: MainItemWidget(icon: _icons[2], name: _names[2]),
child: MainItemWidget(icon: _icons[2], name: _names[2]['name']!),
onTap: () => _openItem(index: 2), onTap: () => _openItem(index: 2),
), ),
SizedBox(width: context.width * 13 / AppConstants.instance.appWidth), SizedBox(width: context.width * 13 / AppConstants.instance.appWidth),
GestureDetector( GestureDetector(
child: MainItemWidget(icon: _icons[1], name: _names[1]),
child: MainItemWidget(icon: _icons[1], name: _names[1]['name']!),
onTap: () => _openItem(index: 1), onTap: () => _openItem(index: 1),
), ),
SizedBox(width: context.width * 13 / AppConstants.instance.appWidth), SizedBox(width: context.width * 13 / AppConstants.instance.appWidth),
GestureDetector( GestureDetector(
child: MainItemWidget(icon: _icons[0], name: _names[0]),
child: MainItemWidget(icon: _icons[0], name: _names[0]['name']!),
onTap: () => _openItem(index: 0), onTap: () => _openItem(index: 0),
), ),
], ],
@ -155,17 +139,17 @@ class _MainScreenState extends State<MainScreen> {
child: Row( child: Row(
children: [ children: [
GestureDetector( GestureDetector(
child: MainItemWidget(icon: _icons[5], name: _names[5]),
child: MainItemWidget(icon: _icons[5], name: _names[5]['name']!),
onTap: () => _openItem(index: 5), onTap: () => _openItem(index: 5),
), ),
SizedBox(width: context.width * 13 / AppConstants.instance.appWidth), SizedBox(width: context.width * 13 / AppConstants.instance.appWidth),
GestureDetector( GestureDetector(
child: MainItemWidget(icon: _icons[4], name: _names[4]),
child: MainItemWidget(icon: _icons[4], name: _names[4]['name']!),
onTap: () => _openItem(index: 4), onTap: () => _openItem(index: 4),
), ),
SizedBox(width: context.width * 13 / AppConstants.instance.appWidth), SizedBox(width: context.width * 13 / AppConstants.instance.appWidth),
GestureDetector( GestureDetector(
child: MainItemWidget(icon: _icons[3], name: _names[3]),
child: MainItemWidget(icon: _icons[3], name: _names[3]['name']!),
onTap: () => _openItem(index: 3), onTap: () => _openItem(index: 3),
), ),
], ],
@ -179,81 +163,19 @@ class _MainScreenState extends State<MainScreen> {
} }
void _openItem({required int index}) { void _openItem({required int index}) {
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return BlocProvider(
create: (context) => PostsCubit(),
child: PostsScreen(title: _names[index]),
);
},
));
}
void _showOptions() {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) {
return Container(
height: 120,
width: context.width,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.white, width: 0.2),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return BlocProvider(
create: (context) => PostsCubit(lastCatId: _names[index]['catId']!),
child: PostsScreen(
title: _names[index]['name']!,
showFilters: index == 0,
), ),
),
padding: const EdgeInsets.only(
top: 16,
left: 4,
right: 4,
),
child: Column(
children: [
MoreOptionsWidget(
title: Translator.translate('about_us'),
onTap: () {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return const AboutUsScreen();
},
),
);
},
),
const SizedBox(height: 8),
Container(
width: context.width,
height: 1,
color: const Color(0xffD3D8E9),
),
const SizedBox(height: 8),
MoreOptionsWidget(
title: Translator.translate('select_language'),
onTap: () {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return BlocProvider(
child: const SelectLanguageScreen(),
create: (context) => SelectLanguageCubit(),
);
},
),
);
},
),
],
),
);
},
);
},
),
); );
} }
} }

77
lib/features/posts/cubit/posts_cubit.dart

@ -1,24 +1,37 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:flutter/services.dart';
import 'package:data/app_api_data/app_api_repository_impl.dart';
import 'package:data/app_setting_data/repository/app_setting_box_repository_impl.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:local_db_core/lib/boxes/box_list/setting_box/app_setting_box.dart';
import 'package:network_core/initializer/base_initializer.dart';
import 'package:repositories/app_api_domain/models/post_model.dart';
import 'package:repositories/app_api_domain/repository/app_api_repository.dart';
import 'package:repositories/app_setting_box_domain/repository/app_setting_box_repository.dart';
import 'package:sonnat/core/utils/base_cubit_type.dart'; import 'package:sonnat/core/utils/base_cubit_type.dart';
import 'package:sonnat/features/single_post/view_models/post.dart';
class PostsCubit extends Cubit<BaseCubitType<PostsState>> { class PostsCubit extends Cubit<BaseCubitType<PostsState>> {
List<Post> allData = [];
List<Post> postList = [];
List<Post> searchedList = [];
List<PostModel> postList = [];
List<PostModel> searchedList = [];
bool hasReachedMax = false; bool hasReachedMax = false;
String _query = ''; String _query = '';
String lastCatId = '';
int _page = 1;
final AppApiRepository _remoteRepository = AppApiRepositoryImpl(apiService: BaseInitializer.instance.getApiService());
final AppSettingBoxRepository _repository = AppSettingBoxRepositoryImpl(appSettingBox: AppSettingBox());
PostsCubit() : super(BaseCubitType(eventName: PostsState.empty));
PostsCubit({required this.lastCatId}) : super(BaseCubitType(eventName: PostsState.empty));
void empty() => emit(BaseCubitType(eventName: PostsState.empty)); void empty() => emit(BaseCubitType(eventName: PostsState.empty));
Future<void> getData() async {
Future<void> getData({required String catId}) async {
if(lastCatId != catId) {
lastCatId = catId;
hasReachedMax = false;
_page = 1;
postList.clear();
}
if (hasReachedMax) { if (hasReachedMax) {
return; return;
} }
@ -27,25 +40,21 @@ class PostsCubit extends Cubit<BaseCubitType<PostsState>> {
} else { } else {
emit(BaseCubitType(eventName: PostsState.loading)); emit(BaseCubitType(eventName: PostsState.loading));
} }
String data = await rootBundle.loadString('assets/meta/data.json');
allData = List<Post>.from(
jsonDecode(data)['posts'].map(
(post) => Post.fromJson(post).copyWith(DateTime.now().millisecondsSinceEpoch),
),
List<PostModel> data = await _remoteRepository.getPostList(
catId: catId,
sort: 'newest',
page: _page,
languageCode: _repository.getCurrentLanguage() ?? 'en',
); );
if (allData.isNotEmpty) {
List<Post> posts = allData.getRange(postList.length, min(postList.length + 20, allData.length)).toList();
hasReachedMax = posts.length < 20;
postList += posts;
emit(BaseCubitType(eventName: PostsState.data));
} else {
emit(BaseCubitType(eventName: PostsState.data));
}
hasReachedMax = data.length < 15;
_page += 1;
postList.addAll(data);
emit(BaseCubitType(eventName: PostsState.data));
} }
Future<void> search({required String query}) async { Future<void> search({required String query}) async {
if (query.isEmpty) { if (query.isEmpty) {
unawaited(getData());
unawaited(getData(catId: lastCatId));
_query = ''; _query = '';
searchedList.clear(); searchedList.clear();
emit(BaseCubitType(eventName: PostsState.data)); emit(BaseCubitType(eventName: PostsState.data));
@ -65,37 +74,21 @@ class PostsCubit extends Cubit<BaseCubitType<PostsState>> {
emit(BaseCubitType(eventName: PostsState.loading)); emit(BaseCubitType(eventName: PostsState.loading));
} }
await Future.delayed(Duration(milliseconds: Random().nextInt(1000))); await Future.delayed(Duration(milliseconds: Random().nextInt(1000)));
String data = await rootBundle.loadString('assets/meta/data.json');
allData = List<Post>.from(
jsonDecode(data)['posts'].map(
(post) => Post.fromJson(post).copyWith(
DateTime.now().millisecondsSinceEpoch,
),
),
);
if (allData.isNotEmpty) {
List<Post> posts = allData.where((element) => element.name.contains(query.trim())).toList();
hasReachedMax = posts.length < 20;
searchedList += posts;
emit(BaseCubitType(eventName: PostsState.data));
} else {
emit(BaseCubitType(eventName: PostsState.data));
}
} }
String get query => _query; String get query => _query;
void clearSearch() { void clearSearch() {
unawaited(getData());
unawaited(getData(catId: lastCatId));
_query = ''; _query = '';
searchedList.clear(); searchedList.clear();
emit(BaseCubitType(eventName: PostsState.data)); emit(BaseCubitType(eventName: PostsState.data));
} }
Future<void> changeFilter(int index) async {
Future<void> changeFilter(int index, String catId) async {
emit(BaseCubitType(eventName: PostsState.changeFilterIndex, data: index)); emit(BaseCubitType(eventName: PostsState.changeFilterIndex, data: index));
await Future.delayed(Duration(milliseconds: Random().nextInt(1000)));
await getData();
await Future.delayed(Duration(milliseconds: 500));
await getData(catId: catId);
} }
} }

80
lib/features/posts/screen/posts_screen.dart

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:repositories/app_api_domain/models/post_model.dart';
import 'package:sonnat/core/extensions/context_extension.dart'; import 'package:sonnat/core/extensions/context_extension.dart';
import 'package:sonnat/core/extensions/string_extension.dart'; import 'package:sonnat/core/extensions/string_extension.dart';
import 'package:sonnat/core/language/translator.dart'; import 'package:sonnat/core/language/translator.dart';
@ -18,13 +19,18 @@ import 'package:sonnat/features/posts/widgets/post_item_widget.dart';
import 'package:sonnat/features/posts/widgets/search_widget.dart'; import 'package:sonnat/features/posts/widgets/search_widget.dart';
import 'package:sonnat/features/single_post/cubit/single_post_cubit.dart'; import 'package:sonnat/features/single_post/cubit/single_post_cubit.dart';
import 'package:sonnat/features/single_post/screen/single_post_screen.dart'; import 'package:sonnat/features/single_post/screen/single_post_screen.dart';
import 'package:sonnat/features/single_post/view_models/post.dart';
class PostsScreen extends StatefulWidget { class PostsScreen extends StatefulWidget {
final String title; final String title;
final bool searchMode; final bool searchMode;
final bool showFilters;
const PostsScreen({super.key, required this.title, this.searchMode = false});
const PostsScreen({
super.key,
required this.title,
this.searchMode = false,
this.showFilters = false,
});
@override @override
State<PostsScreen> createState() => _PostsScreenState(); State<PostsScreen> createState() => _PostsScreenState();
@ -36,26 +42,19 @@ class _PostsScreenState extends State<PostsScreen> {
bool _getListFlag = false; bool _getListFlag = false;
late bool _searchMode; late bool _searchMode;
final ScrollController _controller = ScrollController(); final ScrollController _controller = ScrollController();
int _selectedFilterIndex = 1;
int _selectedFilterIndex = -1;
final List<FilterItem> _filterList = [ final List<FilterItem> _filterList = [
const FilterItem(selected: false, title: 'پر تکرارترین‌ها'),
const FilterItem(selected: true, title: 'مشابه‌ها'),
const FilterItem(selected: false, title: 'محبوبترین‌ها'),
const FilterItem(selected: false, title: 'برای‌ شما'),
const FilterItem(selected: false, title: 'محبوبترین‌ها'),
const FilterItem(selected: false, title: 'پر تکرارترین‌ها'),
const FilterItem(selected: true, title: 'مشابه‌ها'),
const FilterItem(selected: false, title: 'محبوبترین‌ها'),
const FilterItem(selected: false, title: 'برای‌ شما'),
const FilterItem(selected: false, title: 'محبوبترین‌ها'),
const FilterItem(selected: false, title: 'ابوبکر', catId: '77'),
const FilterItem(selected: true, title: 'عمر', catId: '78'),
const FilterItem(selected: false, title: 'عثمان', catId: '79'),
]; ];
@override @override
void initState() { void initState() {
_searchMode = widget.searchMode; _searchMode = widget.searchMode;
_cubit = BlocProvider.of<PostsCubit>(context); _cubit = BlocProvider.of<PostsCubit>(context);
_cubit.getData();
_cubit.getData(catId: _cubit.lastCatId);
_controller.addListener(_onScroll); _controller.addListener(_onScroll);
super.initState(); super.initState();
} }
@ -66,7 +65,7 @@ class _PostsScreenState extends State<PostsScreen> {
if (maxScroll - currentScroll <= 400) { if (maxScroll - currentScroll <= 400) {
if (_getListFlag) { if (_getListFlag) {
_getListFlag = false; _getListFlag = false;
_cubit.getData();
_cubit.getData(catId: _cubit.lastCatId);
} }
} }
} }
@ -91,6 +90,7 @@ class _PostsScreenState extends State<PostsScreen> {
break; break;
case PostsState.changeFilterIndex: case PostsState.changeFilterIndex:
_selectedFilterIndex = state.data; _selectedFilterIndex = state.data;
_controller.jumpTo(0);
_loading = true; _loading = true;
break; break;
} }
@ -143,9 +143,7 @@ class _PostsScreenState extends State<PostsScreen> {
alignment: AlignmentDirectional.centerStart, alignment: AlignmentDirectional.centerStart,
child: GestureDetector( child: GestureDetector(
onTap: _showOptions, onTap: _showOptions,
child: SvgPicture.asset(
'ic_more'.svgPath,
),
child: SvgPicture.asset('ic_more'.svgPath),
), ),
), ),
PositionedDirectional( PositionedDirectional(
@ -242,27 +240,28 @@ class _PostsScreenState extends State<PostsScreen> {
), ),
), ),
SizedBox(height: context.height * 35 / AppConstants.instance.appHeight), SizedBox(height: context.height * 35 / AppConstants.instance.appHeight),
SizedBox(
height: context.height * 31 / AppConstants.instance.appHeight,
child: ListView.builder(
padding: EdgeInsetsDirectional.only(
start: context.width * 26 / AppConstants.instance.appWidth,
if (widget.showFilters)
SizedBox(
height: context.height * 31 / AppConstants.instance.appHeight,
child: ListView.builder(
padding: EdgeInsetsDirectional.only(
start: context.width * 26 / AppConstants.instance.appWidth,
),
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
_cubit.changeFilter(index, _filterList[index].catId);
},
child: FilterItemWidget(
title: _filterList[index].title,
selected: index == _selectedFilterIndex,
),
);
},
itemCount: _filterList.length,
scrollDirection: Axis.horizontal,
), ),
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
_cubit.changeFilter(index);
},
child: FilterItemWidget(
title: _filterList[index].title,
selected: index == _selectedFilterIndex,
),
);
},
itemCount: _filterList.length,
scrollDirection: Axis.horizontal,
), ),
),
SizedBox(height: context.height * 26 / AppConstants.instance.appHeight), SizedBox(height: context.height * 26 / AppConstants.instance.appHeight),
], ],
), ),
@ -303,13 +302,13 @@ class _PostsScreenState extends State<PostsScreen> {
); );
} }
void _clickOnPost(Post post) {
void _clickOnPost(PostModel post) {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) { builder: (context) {
return BlocProvider( return BlocProvider(
child: SinglePostScreen(post: post),
child: SinglePostScreen(postId: post.id),
create: (context) => SinglePostCubit(), create: (context) => SinglePostCubit(),
); );
}, },
@ -389,6 +388,7 @@ class _PostsScreenState extends State<PostsScreen> {
class FilterItem { class FilterItem {
final bool selected; final bool selected;
final String title; final String title;
final String catId;
const FilterItem({required this.selected, required this.title});
const FilterItem({required this.selected, required this.title, required this.catId});
} }

41
lib/features/posts/widgets/post_item_widget.dart

@ -1,16 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:repositories/app_api_domain/models/post_model.dart';
import 'package:sonnat/core/extensions/context_extension.dart'; import 'package:sonnat/core/extensions/context_extension.dart';
import 'package:sonnat/core/utils/app_constants.dart'; import 'package:sonnat/core/utils/app_constants.dart';
import 'package:sonnat/core/utils/app_utils.dart';
import 'package:sonnat/features/single_post/view_models/post.dart';
class PostItemWidget extends StatelessWidget { class PostItemWidget extends StatelessWidget {
final Post post;
final PostModel post;
const PostItemWidget({super.key, required this.post}); const PostItemWidget({super.key, required this.post});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final String? thumbnail = post.thumbnail;
return Container( return Container(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
horizontal: context.width * 11 / AppConstants.instance.appWidth, horizontal: context.width * 11 / AppConstants.instance.appWidth,
@ -30,20 +30,37 @@ class PostItemWidget extends StatelessWidget {
children: [ children: [
Container( Container(
height: context.height * 174 / AppConstants.instance.appHeight, height: context.height * 174 / AppConstants.instance.appHeight,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
decoration: const BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16), topLeft: Radius.circular(16),
topRight: Radius.circular(16), topRight: Radius.circular(16),
), ),
image: DecorationImage(
image: AssetImage(post.image),
fit: BoxFit.cover,
),
), ),
child: thumbnail == null
? Center(
child: Icon(
Icons.image_search,
color: Colors.grey.withOpacity(0.5),
size: 150,
),
)
: Image.network(
thumbnail,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Center(
child: Icon(
Icons.image_search,
color: Colors.grey.withOpacity(0.5),
size: 150,
),
);
},
),
), ),
SizedBox(height: context.height * 14 / AppConstants.instance.appHeight), SizedBox(height: context.height * 14 / AppConstants.instance.appHeight),
Text( Text(
post.name,
post.title,
style: const TextStyle( style: const TextStyle(
color: Color(0xff222D4E), color: Color(0xff222D4E),
fontSize: 16, fontSize: 16,
@ -52,7 +69,7 @@ class PostItemWidget extends StatelessWidget {
), ),
SizedBox(height: context.height * 8 / AppConstants.instance.appHeight), SizedBox(height: context.height * 8 / AppConstants.instance.appHeight),
Text( Text(
post.description,
post.summary,
style: const TextStyle( style: const TextStyle(
color: Color(0xff8990A1), color: Color(0xff8990A1),
fontSize: 13, fontSize: 13,
@ -63,7 +80,7 @@ class PostItemWidget extends StatelessWidget {
Align( Align(
alignment: AlignmentDirectional.centerEnd, alignment: AlignmentDirectional.centerEnd,
child: Text( child: Text(
Utils.instance.dateToString(post.date),
post.publishDate,
style: const TextStyle( style: const TextStyle(
color: Color(0xff8D95AB), color: Color(0xff8D95AB),
fontSize: 11, fontSize: 11,

19
lib/features/single_post/cubit/single_post_cubit.dart

@ -1,11 +1,18 @@
import 'dart:math'; import 'dart:math';
import 'package:data/app_api_data/app_api_repository_impl.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:network_core/initializer/base_initializer.dart';
import 'package:network_core/network/networking/custom_exception.dart';
import 'package:repositories/app_api_domain/models/single_post_model.dart';
import 'package:repositories/app_api_domain/repository/app_api_repository.dart';
import 'package:sonnat/core/utils/base_cubit_type.dart'; import 'package:sonnat/core/utils/base_cubit_type.dart';
import 'package:sonnat/features/single_post/view_models/comment.dart'; import 'package:sonnat/features/single_post/view_models/comment.dart';
class SinglePostCubit extends Cubit<BaseCubitType<SinglePostState>> { class SinglePostCubit extends Cubit<BaseCubitType<SinglePostState>> {
List<Comment> commentList = []; List<Comment> commentList = [];
SinglePostModel? model;
final AppApiRepository _remoteRepository = AppApiRepositoryImpl(apiService: BaseInitializer.instance.getApiService());
SinglePostCubit() : super(BaseCubitType(eventName: SinglePostState.empty)); SinglePostCubit() : super(BaseCubitType(eventName: SinglePostState.empty));
@ -26,9 +33,21 @@ class SinglePostCubit extends Cubit<BaseCubitType<SinglePostState>> {
commentList.removeWhere((element) => element.id == id); commentList.removeWhere((element) => element.id == id);
emit(BaseCubitType(eventName: SinglePostState.data)); emit(BaseCubitType(eventName: SinglePostState.data));
} }
Future<void> getSinglePostData({required int postId}) async {
try {
emit(BaseCubitType(eventName: SinglePostState.loading));
model = await _remoteRepository.getSinglePost(postId: postId);
emit(BaseCubitType(eventName: SinglePostState.data));
} on CustomException {
emit(BaseCubitType(eventName: SinglePostState.error));
}
}
} }
enum SinglePostState { enum SinglePostState {
empty, empty,
loading,
error,
data, data,
} }

53
lib/features/single_post/screen/single_post_screen.dart

@ -6,17 +6,15 @@ import 'package:sonnat/core/extensions/string_extension.dart';
import 'package:sonnat/core/html/html_viewer.dart'; import 'package:sonnat/core/html/html_viewer.dart';
import 'package:sonnat/core/language/translator.dart'; import 'package:sonnat/core/language/translator.dart';
import 'package:sonnat/core/utils/app_constants.dart'; import 'package:sonnat/core/utils/app_constants.dart';
import 'package:sonnat/core/utils/app_utils.dart';
import 'package:sonnat/core/utils/base_cubit_type.dart'; import 'package:sonnat/core/utils/base_cubit_type.dart';
import 'package:sonnat/features/single_post/cubit/single_post_cubit.dart'; import 'package:sonnat/features/single_post/cubit/single_post_cubit.dart';
import 'package:sonnat/features/single_post/view_models/post.dart';
import 'package:sonnat/features/single_post/widget/add_comment_widget.dart'; import 'package:sonnat/features/single_post/widget/add_comment_widget.dart';
import 'package:sonnat/features/single_post/widget/post_comment_widget.dart'; import 'package:sonnat/features/single_post/widget/post_comment_widget.dart';
class SinglePostScreen extends StatefulWidget { class SinglePostScreen extends StatefulWidget {
final Post post;
final int postId;
const SinglePostScreen({super.key, required this.post});
const SinglePostScreen({super.key, required this.postId});
@override @override
State<SinglePostScreen> createState() => _SinglePostScreenState(); State<SinglePostScreen> createState() => _SinglePostScreenState();
@ -24,10 +22,12 @@ class SinglePostScreen extends StatefulWidget {
class _SinglePostScreenState extends State<SinglePostScreen> { class _SinglePostScreenState extends State<SinglePostScreen> {
late final SinglePostCubit _cubit; late final SinglePostCubit _cubit;
bool _loading = true;
@override @override
void initState() { void initState() {
_cubit = BlocProvider.of<SinglePostCubit>(context); _cubit = BlocProvider.of<SinglePostCubit>(context);
_cubit.getSinglePostData(postId: widget.postId);
super.initState(); super.initState();
} }
@ -41,8 +41,22 @@ class _SinglePostScreenState extends State<SinglePostScreen> {
case SinglePostState.empty: case SinglePostState.empty:
break; break;
case SinglePostState.data: case SinglePostState.data:
_loading = false;
break; break;
case SinglePostState.loading:
_loading = true;
break;
case SinglePostState.error:
_loading = false;
break;
}
if (_loading) {
return const Center(child: CircularProgressIndicator());
}
if(_cubit.model == null) {
return const Center(child: Text('خطا در دریافت اطلاعات'));
} }
String? thumbnail = _cubit.model!.thumbnail;
return SingleChildScrollView( return SingleChildScrollView(
child: Column( child: Column(
children: [ children: [
@ -51,10 +65,27 @@ class _SinglePostScreenState extends State<SinglePostScreen> {
child: Stack( child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
Image.asset(
widget.post.image,
fit: BoxFit.cover,
),
thumbnail == null
? Center(
child: Icon(
Icons.image_search,
color: Colors.grey.withOpacity(0.5),
size: 150,
),
)
: Image.network(
thumbnail,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Center(
child: Icon(
Icons.image_search,
color: Colors.grey.withOpacity(0.5),
size: 150,
),
);
},
),
PositionedDirectional( PositionedDirectional(
end: 16, end: 16,
top: 16, top: 16,
@ -152,18 +183,18 @@ class _SinglePostScreenState extends State<SinglePostScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
widget.post.name,
_cubit.model!.title,
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 16, fontSize: 16,
), ),
), ),
Text( Text(
Utils.instance.dateToString(widget.post.date),
_cubit.model!.publishDate,
style: const TextStyle(fontSize: 11), style: const TextStyle(fontSize: 11),
), ),
HTMLViewer( HTMLViewer(
htmlContent: widget.post.description,
htmlContent: _cubit.model!.content,
fontSizeFactor: 1, fontSizeFactor: 1,
), ),
const SizedBox(height: 30), const SizedBox(height: 30),

2
lib/features/splash/cubit/splash_cubit.dart

@ -13,7 +13,7 @@ class SplashCubit extends Cubit<BaseCubitType<SplashCubitState>> {
Future<void> checkLanguageSet() async { Future<void> checkLanguageSet() async {
String? language = _repository.getCurrentLanguage(); String? language = _repository.getCurrentLanguage();
if(language == null || language == '') {
if (language == null || language == '') {
emit(BaseCubitType(eventName: SplashCubitState.notSet)); emit(BaseCubitType(eventName: SplashCubitState.notSet));
} else { } else {
emit(BaseCubitType(eventName: SplashCubitState.set)); emit(BaseCubitType(eventName: SplashCubitState.set));

117
pubspec.lock

@ -125,10 +125,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: collection name: collection
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.17.1"
version: "1.17.2"
convert: convert:
dependency: transitive dependency: transitive
description: description:
@ -168,6 +168,70 @@ packages:
relative: true relative: true
source: path source: path
version: "0.0.1" version: "0.0.1"
device_info_plus:
dependency: "direct main"
description:
name: device_info_plus
sha256: c2386729379f04cd39ee0d5d4c48d8c8a0e70f7622dac626cbf5e396392602fd
url: "https://pub.dev"
source: hosted
version: "3.2.4"
device_info_plus_linux:
dependency: transitive
description:
name: device_info_plus_linux
sha256: e4eb5db4704f5534e872148a21cfcd39581022b63df556da6720d88f7c9f91a9
url: "https://pub.dev"
source: hosted
version: "2.1.1"
device_info_plus_macos:
dependency: transitive
description:
name: device_info_plus_macos
sha256: "38871fd2ad31871399d8307630c9f4eb5941dd2c643ee221c44d58de95d367a1"
url: "https://pub.dev"
source: hosted
version: "2.2.3"
device_info_plus_platform_interface:
dependency: transitive
description:
name: device_info_plus_platform_interface
sha256: b2743934f0efc3e291880d76fb341ea114b7e8417d77ee0f93bd21f5dfd3e8d2
url: "https://pub.dev"
source: hosted
version: "2.6.1"
device_info_plus_web:
dependency: transitive
description:
name: device_info_plus_web
sha256: "38715ad1ef3bee8915dd7bee08a9ac9ab54472a8df425c887062a3046209f663"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
device_info_plus_windows:
dependency: transitive
description:
name: device_info_plus_windows
sha256: "8fb1403fc94636d6ab48aeebb5f9379f2ca51cde3b337167ec6f39db09234492"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
dio:
dependency: transitive
description:
name: dio
sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8"
url: "https://pub.dev"
source: hosted
version: "4.0.6"
dio_cache_interceptor:
dependency: transitive
description:
name: dio_cache_interceptor
sha256: a89166e6fb9c90a4bf2b7f20b5c055087969bc445ced3282e32505543e296e0f
url: "https://pub.dev"
source: hosted
version: "3.4.2"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -180,10 +244,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: ffi name: ffi
sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99
sha256: "13a6ccf6a459a125b3fcdb6ec73bd5ff90822e071207c663bfd1f70062d51d18"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.2"
version: "1.2.1"
file: file:
dependency: transitive dependency: transitive
description: description:
@ -378,18 +442,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb"
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.15"
version: "0.12.16"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.0"
version: "0.5.0"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@ -406,6 +470,13 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.0.0"
network_core:
dependency: "direct main"
description:
path: "data/data_core/network/network_core"
relative: true
source: path
version: "0.0.1"
octo_image: octo_image:
dependency: transitive dependency: transitive
description: description:
@ -474,10 +545,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path_provider_windows name: path_provider_windows
sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6
sha256: a34ecd7fb548f8e57321fd8e50d865d266941b54e6c3b7758cf8f37c24116905
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.6"
version: "2.0.7"
pedantic: pedantic:
dependency: transitive dependency: transitive
description: description:
@ -566,10 +637,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: source_span name: source_span
sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1"
version: "1.10.0"
sqflite: sqflite:
dependency: transitive dependency: transitive
description: description:
@ -630,10 +701,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.1"
version: "0.6.0"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@ -822,18 +893,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: wakelock_windows name: wakelock_windows
sha256: "857f77b3fe6ae82dd045455baa626bc4b93cb9bb6c86bf3f27c182167c3a5567"
sha256: "108b1b73711f1664ee462e73af34a9286ff496e27d4d8371e2fb4da8fde4cdac"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.1"
version: "0.2.0"
web:
dependency: transitive
description:
name: web
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
url: "https://pub.dev"
source: hosted
version: "0.1.4-beta"
win32: win32:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4
sha256: c0e3a4f7be7dae51d8f152230b86627e3397c1ba8c3fa58e63d44a9f3edc9cef
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.4"
version: "2.6.1"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
@ -859,5 +938,5 @@ packages:
source: hosted source: hosted
version: "3.1.2" version: "3.1.2"
sdks: sdks:
dart: ">=3.0.0-417 <4.0.0"
dart: ">=3.1.0-185.0.dev <4.0.0"
flutter: ">=3.7.0-0" flutter: ">=3.7.0-0"

3
pubspec.yaml

@ -22,6 +22,7 @@ dependencies:
list_counter: ^1.0.2 list_counter: ^1.0.2
shamsi_date: ^1.0.1 shamsi_date: ^1.0.1
url_launcher: ^6.1.11 url_launcher: ^6.1.11
device_info_plus: ^3.2.3
cached_network_image: ^3.1.0 cached_network_image: ^3.1.0
google_fonts: ^4.0.4 google_fonts: ^4.0.4
chewie_audio: ^1.5.0 chewie_audio: ^1.5.0
@ -31,6 +32,8 @@ dependencies:
path: domain/repositories path: domain/repositories
data: data:
path: data/data_types/data path: data/data_types/data
network_core:
path: data/data_core/network/network_core
video_player: ^2.6.1 video_player: ^2.6.1
dev_dependencies: dev_dependencies:

Loading…
Cancel
Save