diff --git a/lib/core/config/app_config.dart b/lib/core/config/app_config.dart index 542bc74..c8f7095 100644 --- a/lib/core/config/app_config.dart +++ b/lib/core/config/app_config.dart @@ -33,6 +33,10 @@ class AppConfig { // } } + static String get flagUrl { + return 'https://app.opplens.com/api/v1/flags/'; + } + // کلیدهای حساس را به این شکل مدیریت کنید static String get apiKey { if (isDevelopment) { diff --git a/lib/core/services/cache_init.dart b/lib/core/services/cache_init.dart new file mode 100644 index 0000000..c533e69 --- /dev/null +++ b/lib/core/services/cache_init.dart @@ -0,0 +1,7 @@ +import 'svg_cache.dart'; + +class CacheInit { + static Future init() async { + await SvgCache().init(); + } +} diff --git a/lib/core/services/svg_cache.dart b/lib/core/services/svg_cache.dart new file mode 100644 index 0000000..1eb7f31 --- /dev/null +++ b/lib/core/services/svg_cache.dart @@ -0,0 +1,78 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class SvgCache { + static final SvgCache _instance = SvgCache._internal(); + factory SvgCache() => _instance; + SvgCache._internal(); + + final Dio _dio = Dio(); + Directory? _cacheDir; + + Future init() async { + if (kIsWeb) { + // Web doesn't need directory initialization + return; + } + + final appDir = await getApplicationDocumentsDirectory(); + _cacheDir = Directory('${appDir.path}/svg_cache'); + if (!await _cacheDir!.exists()) { + await _cacheDir!.create(recursive: true); + } + } + + Future getCachedPath(String url) async { + if (kIsWeb) { + return await _getWebCachedPath(url); + } + + final fileName = _getFileName(url); + final file = File('${_cacheDir!.path}/$fileName'); + return await file.exists() ? file.path : null; + } + + Future cacheSvg(String url) async { + if (kIsWeb) { + return await _cacheWebSvg(url); + } + + final fileName = _getFileName(url); + final file = File('${_cacheDir!.path}/$fileName'); + final response = await _dio.get( + url, + options: Options(responseType: ResponseType.plain), + ); + await file.writeAsString(response.data); + return file.path; + } + + Future _getWebCachedPath(String url) async { + final prefs = await SharedPreferences.getInstance(); + final fileName = _getFileName(url); + return prefs.containsKey('svg_cache_$fileName') + ? 'web_cache_$fileName' + : null; + } + + Future _cacheWebSvg(String url) async { + final response = await _dio.get( + url, + options: Options(responseType: ResponseType.plain), + ); + final fileName = _getFileName(url); + final prefs = await SharedPreferences.getInstance(); + + // Store SVG data in SharedPreferences for web + await prefs.setString('svg_cache_$fileName', response.data); + return 'web_cache_$fileName'; + } + + String _getFileName(String url) { + return url.split('/').last.replaceAll(RegExp(r'[^a-zA-Z0-9._-]'), '_'); + } +} diff --git a/lib/main.dart b/lib/main.dart index 9fb1f48..94becb5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,6 +7,7 @@ import 'package:provider/provider.dart'; import 'package:provider/single_child_widget.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tm_app/core/config/dependencies.dart'; +import 'package:tm_app/core/services/cache_init.dart'; import 'package:tm_app/core/theme/theme_provider.dart'; import 'core/routes/app_routes.dart'; @@ -16,6 +17,9 @@ import 'core/utils/logger.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + // Initialize SVG cache + await CacheInit.init(); + await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); // Initialize the logger diff --git a/lib/views/detail/widgets/tender_location_section.dart b/lib/views/detail/widgets/tender_location_section.dart index 16dd056..954e59e 100644 --- a/lib/views/detail/widgets/tender_location_section.dart +++ b/lib/views/detail/widgets/tender_location_section.dart @@ -1,4 +1,3 @@ -import 'package:country_flags/country_flags.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:tm_app/core/constants/assets.dart'; @@ -6,6 +5,7 @@ import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/utils/size_config.dart'; import '../../../data/services/model/tender_data/tender_data.dart'; +import '../../shared/flag.dart'; import '../strings/tender_details_strings.dart'; class TenderLocationSection extends StatelessWidget { @@ -48,14 +48,8 @@ class TenderLocationSection extends StatelessWidget { ), ), SizedBox(width: 4.0.w()), - SizedBox( - width: 32.0.w(), - height: 21.0.h(), - child: CountryFlag.fromCountryCode( - detail.countryCode!, - shape: RoundedRectangle(4), - ), - ), + if (detail.countryCode != null) + Flag(countryCode: detail.countryCode!), ], ), SizedBox(height: 8.0.h()), diff --git a/lib/views/home/tenders_list_item.dart b/lib/views/home/tenders_list_item.dart index ccf647b..1cd774c 100644 --- a/lib/views/home/tenders_list_item.dart +++ b/lib/views/home/tenders_list_item.dart @@ -1,4 +1,3 @@ -import 'package:country_flags/country_flags.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:tm_app/data/services/model/tender_data/tender_data.dart'; @@ -8,6 +7,7 @@ import 'package:tm_app/views/home/widgets/tender_card_progress_bar.dart'; import '../../core/constants/assets.dart'; import '../../core/theme/colors.dart'; import '../../core/utils/size_config.dart'; +import '../shared/flag.dart'; class TendersListItem extends StatelessWidget { const TendersListItem({required this.tender, super.key}); @@ -113,14 +113,8 @@ class TendersListItem extends StatelessWidget { ), ), SizedBox(width: 8.0.w()), - SizedBox( - width: 32.0.w(), - height: 21.0.h(), - child: CountryFlag.fromCountryCode( - tender.countryCode!, - shape: RoundedRectangle(4), - ), - ), + if (tender.countryCode != null) + Flag(countryCode: tender.countryCode!), Spacer(), ], ), diff --git a/lib/views/liked_tenders/widgets/liked_tenders_list_item.dart b/lib/views/liked_tenders/widgets/liked_tenders_list_item.dart index 545a741..31ad653 100644 --- a/lib/views/liked_tenders/widgets/liked_tenders_list_item.dart +++ b/lib/views/liked_tenders/widgets/liked_tenders_list_item.dart @@ -1,4 +1,3 @@ -import 'package:country_flags/country_flags.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; @@ -12,6 +11,7 @@ import 'package:tm_app/views/home/strings/home_strings.dart'; import '../../../core/constants/tender_submision_mode.dart'; import '../../../data/services/model/tender_approvals_data/tender_approvals_data.dart'; +import '../../shared/flag.dart'; import '../../shared/select_submission_bottom_sheet.dart'; import '../../shared/select_submission_dialog.dart'; @@ -166,14 +166,8 @@ class LikedListItem extends StatelessWidget { ), ), SizedBox(width: 8.0.w()), - SizedBox( - width: 32.0.w(), - height: 21.0.h(), - child: CountryFlag.fromCountryCode( - tender.countryCode!, - shape: RoundedRectangle(4), - ), - ), + if (tender.countryCode != null) + Flag(countryCode: tender.countryCode!), Spacer(), InkWell( onTap: () { diff --git a/lib/views/shared/cached_svg.dart b/lib/views/shared/cached_svg.dart new file mode 100644 index 0000000..f06b4b0 --- /dev/null +++ b/lib/views/shared/cached_svg.dart @@ -0,0 +1,96 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../../core/services/svg_cache.dart'; + +class CachedSvg extends StatefulWidget { + final String url; + final double? width; + final double? height; + final BoxFit fit; + + const CachedSvg({ + required this.url, + this.width, + this.height, + this.fit = BoxFit.contain, + super.key, + }); + + @override + State createState() => _CachedSvgState(); +} + +class _CachedSvgState extends State { + String? _svgData; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadSvg(); + } + + @override + void didUpdateWidget(covariant CachedSvg oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.url != widget.url) { + _isLoading = true; + _svgData = null; + _loadSvg(); + } + } + + Future _loadSvg() async { + try { + final cache = SvgCache(); + String? cachedPath = await cache.getCachedPath(widget.url); + cachedPath ??= await cache.cacheSvg(widget.url); + if (kIsWeb && cachedPath.startsWith('web_cache_')) { + final prefs = await SharedPreferences.getInstance(); + final fileName = cachedPath.replaceFirst('web_cache_', ''); + _svgData = prefs.getString('svg_cache_$fileName'); + } else { + _svgData = await File(cachedPath).readAsString(); + } + + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + if (_isLoading) { + return SizedBox( + width: widget.width ?? 20, + height: widget.height ?? 20, + child: const Center(child: CircularProgressIndicator()), + ); + } + + if (_svgData == null) { + return const Icon(Icons.error); + } + + return SvgPicture.string( + _svgData!, + width: widget.width, + height: widget.height, + fit: widget.fit, + ); + } +} diff --git a/lib/views/shared/flag.dart b/lib/views/shared/flag.dart new file mode 100644 index 0000000..5e59288 --- /dev/null +++ b/lib/views/shared/flag.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:tm_app/core/utils/size_config.dart'; +import 'package:tm_app/views/shared/cached_svg.dart'; + +import '../../core/config/app_config.dart'; + +class Flag extends StatelessWidget { + const Flag({required this.countryCode, super.key}); + + final String countryCode; + + @override + Widget build(BuildContext context) { + return countryCode.isNotEmpty + ? ClipRRect( + borderRadius: BorderRadius.circular(6), + child: CachedSvg( + url: '${AppConfig.flagUrl}$countryCode', + width: 32.0.w(), + height: 21.0.h(), + fit: BoxFit.cover, + ), + ) + : const SizedBox(); + } +} diff --git a/lib/views/tenders/widgets/tender_card.dart b/lib/views/tenders/widgets/tender_card.dart index 4b366f8..9dfc6a1 100644 --- a/lib/views/tenders/widgets/tender_card.dart +++ b/lib/views/tenders/widgets/tender_card.dart @@ -1,4 +1,3 @@ -import 'package:country_flags/country_flags.dart'; import 'package:flutter/material.dart' hide DateUtils; import 'package:flutter_svg/svg.dart'; import 'package:provider/provider.dart'; @@ -11,6 +10,7 @@ import '../../../core/utils/date_utils.dart'; import '../../../core/utils/size_config.dart'; import '../../../view_models/tenders_view_model.dart'; import '../../detail/strings/tender_details_strings.dart'; +import '../../shared/flag.dart'; import '../strings/tenders_strings.dart'; import 'tender_action_buttons_row.dart'; @@ -253,14 +253,7 @@ class TenderCard extends StatelessWidget { SizedBox(width: 8.0.w()), // Country flag placeholder if (tender.countryCode != null) - SizedBox( - width: 32.0.w(), - height: 21.0.h(), - child: CountryFlag.fromCountryCode( - tender.countryCode!, - shape: RoundedRectangle(4), - ), - ), + Flag(countryCode: tender.countryCode!), ], ), ); diff --git a/lib/views/your_tenders/widgets/tender_card.dart b/lib/views/your_tenders/widgets/tender_card.dart index d6405e3..7501949 100644 --- a/lib/views/your_tenders/widgets/tender_card.dart +++ b/lib/views/your_tenders/widgets/tender_card.dart @@ -1,4 +1,3 @@ -import 'package:country_flags/country_flags.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:tm_app/core/utils/date_utils.dart'; @@ -8,6 +7,7 @@ import '../../../core/constants/assets.dart'; import '../../../core/constants/tender_approval_status.dart'; import '../../../core/theme/colors.dart'; import '../../../core/utils/size_config.dart'; +import '../../shared/flag.dart'; class TenderCard extends StatelessWidget { final bool isDesktop; @@ -163,15 +163,8 @@ class TenderCard extends StatelessWidget { ), ), SizedBox(width: 8.0.w()), - // Country flag - SizedBox( - width: 32.0.w(), - height: 21.0.h(), - child: CountryFlag.fromCountryCode( - tender.countryCode!, - shape: RoundedRectangle(4), - ), - ), + if (tender.countryCode != null) + Flag(countryCode: tender.countryCode!), ], ), ), diff --git a/pubspec.yaml b/pubspec.yaml index 4a07e05..7795c29 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,10 +23,10 @@ dependencies: json_annotation: ^4.8.1 http: ^1.5.0 intl: ^0.20.2 - country_flags: ^3.3.0 flutter_launcher_icons: ^0.14.4 toastification: ^3.0.3 pinput: ^5.0.2 + path_provider: ^2.1.5 dev_dependencies: flutter_test: