Merge pull request 'removed country flags package and read flags from server and implement svg cahce structure' (#117) from flags_cache_svg into main

Reviewed-on: https://repo.ravanertebat.com/TM/tm_app/pulls/117
This commit is contained in:
a.ghabeli
2025-09-11 11:05:01 +03:30
12 changed files with 230 additions and 47 deletions
+4
View File
@@ -33,6 +33,10 @@ class AppConfig {
// } // }
} }
static String get flagUrl {
return 'https://app.opplens.com/api/v1/flags/';
}
// کلیدهای حساس را به این شکل مدیریت کنید // کلیدهای حساس را به این شکل مدیریت کنید
static String get apiKey { static String get apiKey {
if (isDevelopment) { if (isDevelopment) {
+7
View File
@@ -0,0 +1,7 @@
import 'svg_cache.dart';
class CacheInit {
static Future<void> init() async {
await SvgCache().init();
}
}
+78
View File
@@ -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<void> 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<String?> 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<String> 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<String?> _getWebCachedPath(String url) async {
final prefs = await SharedPreferences.getInstance();
final fileName = _getFileName(url);
return prefs.containsKey('svg_cache_$fileName')
? 'web_cache_$fileName'
: null;
}
Future<String> _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._-]'), '_');
}
}
+4
View File
@@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
import 'package:provider/single_child_widget.dart'; import 'package:provider/single_child_widget.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:tm_app/core/config/dependencies.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 'package:tm_app/core/theme/theme_provider.dart';
import 'core/routes/app_routes.dart'; import 'core/routes/app_routes.dart';
@@ -16,6 +17,9 @@ import 'core/utils/logger.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
// Initialize SVG cache
await CacheInit.init();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
// Initialize the logger // Initialize the logger
@@ -1,4 +1,3 @@
import 'package:country_flags/country_flags.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:tm_app/core/constants/assets.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 'package:tm_app/core/utils/size_config.dart';
import '../../../data/services/model/tender_data/tender_data.dart'; import '../../../data/services/model/tender_data/tender_data.dart';
import '../../shared/flag.dart';
import '../strings/tender_details_strings.dart'; import '../strings/tender_details_strings.dart';
class TenderLocationSection extends StatelessWidget { class TenderLocationSection extends StatelessWidget {
@@ -48,14 +48,8 @@ class TenderLocationSection extends StatelessWidget {
), ),
), ),
SizedBox(width: 4.0.w()), SizedBox(width: 4.0.w()),
SizedBox( if (detail.countryCode != null)
width: 32.0.w(), Flag(countryCode: detail.countryCode!),
height: 21.0.h(),
child: CountryFlag.fromCountryCode(
detail.countryCode!,
shape: RoundedRectangle(4),
),
),
], ],
), ),
SizedBox(height: 8.0.h()), SizedBox(height: 8.0.h()),
+3 -9
View File
@@ -1,4 +1,3 @@
import 'package:country_flags/country_flags.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:tm_app/data/services/model/tender_data/tender_data.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/constants/assets.dart';
import '../../core/theme/colors.dart'; import '../../core/theme/colors.dart';
import '../../core/utils/size_config.dart'; import '../../core/utils/size_config.dart';
import '../shared/flag.dart';
class TendersListItem extends StatelessWidget { class TendersListItem extends StatelessWidget {
const TendersListItem({required this.tender, super.key}); const TendersListItem({required this.tender, super.key});
@@ -113,14 +113,8 @@ class TendersListItem extends StatelessWidget {
), ),
), ),
SizedBox(width: 8.0.w()), SizedBox(width: 8.0.w()),
SizedBox( if (tender.countryCode != null)
width: 32.0.w(), Flag(countryCode: tender.countryCode!),
height: 21.0.h(),
child: CountryFlag.fromCountryCode(
tender.countryCode!,
shape: RoundedRectangle(4),
),
),
Spacer(), Spacer(),
], ],
), ),
@@ -1,4 +1,3 @@
import 'package:country_flags/country_flags.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.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 '../../../core/constants/tender_submision_mode.dart';
import '../../../data/services/model/tender_approvals_data/tender_approvals_data.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_bottom_sheet.dart';
import '../../shared/select_submission_dialog.dart'; import '../../shared/select_submission_dialog.dart';
@@ -166,14 +166,8 @@ class LikedListItem extends StatelessWidget {
), ),
), ),
SizedBox(width: 8.0.w()), SizedBox(width: 8.0.w()),
SizedBox( if (tender.countryCode != null)
width: 32.0.w(), Flag(countryCode: tender.countryCode!),
height: 21.0.h(),
child: CountryFlag.fromCountryCode(
tender.countryCode!,
shape: RoundedRectangle(4),
),
),
Spacer(), Spacer(),
InkWell( InkWell(
onTap: () { onTap: () {
+96
View File
@@ -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<CachedSvg> createState() => _CachedSvgState();
}
class _CachedSvgState extends State<CachedSvg> {
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<void> _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,
);
}
}
+26
View File
@@ -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();
}
}
+2 -9
View File
@@ -1,4 +1,3 @@
import 'package:country_flags/country_flags.dart';
import 'package:flutter/material.dart' hide DateUtils; import 'package:flutter/material.dart' hide DateUtils;
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -11,6 +10,7 @@ import '../../../core/utils/date_utils.dart';
import '../../../core/utils/size_config.dart'; import '../../../core/utils/size_config.dart';
import '../../../view_models/tenders_view_model.dart'; import '../../../view_models/tenders_view_model.dart';
import '../../detail/strings/tender_details_strings.dart'; import '../../detail/strings/tender_details_strings.dart';
import '../../shared/flag.dart';
import '../strings/tenders_strings.dart'; import '../strings/tenders_strings.dart';
import 'tender_action_buttons_row.dart'; import 'tender_action_buttons_row.dart';
@@ -253,14 +253,7 @@ class TenderCard extends StatelessWidget {
SizedBox(width: 8.0.w()), SizedBox(width: 8.0.w()),
// Country flag placeholder // Country flag placeholder
if (tender.countryCode != null) if (tender.countryCode != null)
SizedBox( Flag(countryCode: tender.countryCode!),
width: 32.0.w(),
height: 21.0.h(),
child: CountryFlag.fromCountryCode(
tender.countryCode!,
shape: RoundedRectangle(4),
),
),
], ],
), ),
); );
@@ -1,4 +1,3 @@
import 'package:country_flags/country_flags.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:tm_app/core/utils/date_utils.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/constants/tender_approval_status.dart';
import '../../../core/theme/colors.dart'; import '../../../core/theme/colors.dart';
import '../../../core/utils/size_config.dart'; import '../../../core/utils/size_config.dart';
import '../../shared/flag.dart';
class TenderCard extends StatelessWidget { class TenderCard extends StatelessWidget {
final bool isDesktop; final bool isDesktop;
@@ -163,15 +163,8 @@ class TenderCard extends StatelessWidget {
), ),
), ),
SizedBox(width: 8.0.w()), SizedBox(width: 8.0.w()),
// Country flag if (tender.countryCode != null)
SizedBox( Flag(countryCode: tender.countryCode!),
width: 32.0.w(),
height: 21.0.h(),
child: CountryFlag.fromCountryCode(
tender.countryCode!,
shape: RoundedRectangle(4),
),
),
], ],
), ),
), ),
+1 -1
View File
@@ -23,10 +23,10 @@ dependencies:
json_annotation: ^4.8.1 json_annotation: ^4.8.1
http: ^1.5.0 http: ^1.5.0
intl: ^0.20.2 intl: ^0.20.2
country_flags: ^3.3.0
flutter_launcher_icons: ^0.14.4 flutter_launcher_icons: ^0.14.4
toastification: ^3.0.3 toastification: ^3.0.3
pinput: ^5.0.2 pinput: ^5.0.2
path_provider: ^2.1.5
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: