From 1ed8e69262fc97be884cc3a040542f2601013656 Mon Sep 17 00:00:00 2001 From: "mr.mojtaba" Date: Wed, 21 May 2025 14:43:26 +0330 Subject: [PATCH] feat: actions and map page --- assets/icons/search.svg | 6 + assets/vec/search.svg.vec | Bin 0 -> 203 bytes lib/presentations/common/assets.dart | 43 ++++ packages/core/lib/core.dart | 2 +- .../lib/presentation/common/app_fonts.dart | 9 + .../lib/presentation/common/assets.gen.dart | 8 + .../widget/app_bar/r_app_bar.dart | 23 +- .../bottom_navigation_1.dart | 13 +- .../presentation/widget/chips/r_chips.dart | 85 +++++++ .../lib/presentation/widget/map/logic.dart | 80 ++++++- .../lib/presentation/widget/map/view.dart | 69 +++++- .../core/lib/presentation/widget/widget.dart | 1 + .../lib/presentation/page/action/view.dart | 208 +++++++++++++++++- .../lib/presentation/page/map/logic.dart | 3 + .../lib/presentation/page/map/view.dart | 16 +- .../lib/presentation/page/root/logic.dart | 5 +- .../lib/presentation/page/root/view.dart | 40 +++- .../lib/presentation/routes/app_pages.dart | 4 + tools/vecGeneratoe.sh | 4 +- 19 files changed, 586 insertions(+), 33 deletions(-) create mode 100644 assets/icons/search.svg create mode 100644 assets/vec/search.svg.vec create mode 100644 lib/presentations/common/assets.dart create mode 100644 packages/core/lib/presentation/widget/chips/r_chips.dart diff --git a/assets/icons/search.svg b/assets/icons/search.svg new file mode 100644 index 0000000..4a4e42d --- /dev/null +++ b/assets/icons/search.svg @@ -0,0 +1,6 @@ + + + + diff --git a/assets/vec/search.svg.vec b/assets/vec/search.svg.vec new file mode 100644 index 0000000000000000000000000000000000000000..da0271d4916ce3d2156595941fc6690f9daf8a0a GIT binary patch literal 203 zcmYe&?O const SvgGenImage('assets/icons/scan_barcode.svg'); + /// File path: assets/icons/search.svg + SvgGenImage get search => const SvgGenImage('assets/icons/search.svg'); + /// File path: assets/icons/security_time.svg SvgGenImage get securityTime => const SvgGenImage('assets/icons/security_time.svg'); @@ -147,6 +150,7 @@ class $AssetsIconsGen { receiptDiscount, scan, scanBarcode, + search, securityTime, setting, tagUser, @@ -256,6 +260,9 @@ class $AssetsVecGen { /// File path: assets/vec/scan_barcode.svg.vec SvgGenImage get scanBarcodeSvg => const SvgGenImage.vec('assets/vec/scan_barcode.svg.vec'); + /// File path: assets/vec/search.svg.vec + SvgGenImage get searchSvg => const SvgGenImage.vec('assets/vec/search.svg.vec'); + /// File path: assets/vec/security_time.svg.vec SvgGenImage get securityTimeSvg => const SvgGenImage.vec('assets/vec/security_time.svg.vec'); @@ -304,6 +311,7 @@ class $AssetsVecGen { receiptDiscountSvg, scanSvg, scanBarcodeSvg, + searchSvg, securityTimeSvg, settingSvg, tagUserSvg, diff --git a/packages/core/lib/presentation/widget/app_bar/r_app_bar.dart b/packages/core/lib/presentation/widget/app_bar/r_app_bar.dart index 47f7da9..6db58bb 100644 --- a/packages/core/lib/presentation/widget/app_bar/r_app_bar.dart +++ b/packages/core/lib/presentation/widget/app_bar/r_app_bar.dart @@ -8,6 +8,8 @@ class RAppBar extends StatelessWidget implements PreferredSizeWidget { final String title; final Color backgroundColor; final Color iconColor; + final bool hasBack; + final bool centerTitle; final TextStyle? titleTextStyle; final VoidCallback? onBackPressed; final List? additionalActions; @@ -22,6 +24,8 @@ class RAppBar extends StatelessWidget implements PreferredSizeWidget { this.onBackPressed, this.additionalActions, this.leading, + this.hasBack = false, + this.centerTitle = false, }); @override @@ -32,6 +36,7 @@ class RAppBar extends StatelessWidget implements PreferredSizeWidget { elevation: 0, excludeHeaderSemantics: true, scrolledUnderElevation: 0, + centerTitle: centerTitle, titleTextStyle: titleTextStyle ?? AppFonts.yekan16.copyWith(color:Colors.white), @@ -43,14 +48,18 @@ class RAppBar extends StatelessWidget implements PreferredSizeWidget { ) : null, actions: [ if (additionalActions != null) ...additionalActions!, - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: IconButton( - onPressed: onBackPressed ?? () => Get.back(), - icon: const Icon(CupertinoIcons.chevron_back), - color: iconColor, + if(hasBack)...{ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: IconButton( + onPressed: onBackPressed ?? () => Get.back(), + icon: const Icon(CupertinoIcons.chevron_back), + color: iconColor, + ), ), - ), + } + + ], ); diff --git a/packages/core/lib/presentation/widget/bottom_navigation/bottom_navigation_1.dart b/packages/core/lib/presentation/widget/bottom_navigation/bottom_navigation_1.dart index 07f98f4..b5c569e 100644 --- a/packages/core/lib/presentation/widget/bottom_navigation/bottom_navigation_1.dart +++ b/packages/core/lib/presentation/widget/bottom_navigation/bottom_navigation_1.dart @@ -60,16 +60,17 @@ class BottomNavigation1Item extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - vecWidget( - icon, - width: 32, - height: 32, - color: isSelected ? AppColor.blueNormal : Colors.white, + SvgGenImage.vec(icon).svg( + width: 32, + height: 32, + colorFilter: ColorFilter.mode( + isSelected ? AppColor.blueNormal : Colors.white, + BlendMode.srcIn) ), const SizedBox(height: 5), Text( label, - style: AppFonts.yekan14.copyWith( + style: AppFonts.yekan10.copyWith( color: isSelected ? AppColor.blueNormal : Colors.white, ), ), diff --git a/packages/core/lib/presentation/widget/chips/r_chips.dart b/packages/core/lib/presentation/widget/chips/r_chips.dart new file mode 100644 index 0000000..a7f7d02 --- /dev/null +++ b/packages/core/lib/presentation/widget/chips/r_chips.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:rasadyar_core/core.dart'; + +class CustomChip extends StatelessWidget { + final bool isSelected; + final String title; + final int index; + final Function(int) onTap; + final Color selectedColor; + final Color unSelectedColor; + + const CustomChip({ + super.key, + this.isSelected = false, + required this.title, + required this.index, + required this.onTap, + this.selectedColor = AppColor.blueNormal, + this.unSelectedColor = AppColor.whiteGreyNormal, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => onTap.call(index), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: isSelected ? selectedColor : unSelectedColor, + borderRadius: BorderRadius.circular(8), + border: + isSelected + ? Border.fromBorderSide(BorderSide.none) + : Border.all(width: 0.25, color: const Color(0xFFB0B0B0)), + ), + child: Text( + title, + textAlign: TextAlign.center, + style: + isSelected + ? AppFonts.yekan10.copyWith(color: AppColor.whiteLight) + : AppFonts.yekan10, + ), + ), + ); + } +} + +class RFilterChips extends StatelessWidget { + const RFilterChips({ + super.key, + this.isSelected = false, + required this.title, + required this.index, + required this.onTap, + this.selectedColor = AppColor.blueNormal, + this.unSelectedColor = AppColor.whiteGreyNormal, + }); + + final bool isSelected; + final String title; + final int index; + final Function(int) onTap; + final Color selectedColor; + final Color unSelectedColor; + + @override + Widget build(BuildContext context) { + return FilterChip( + labelStyle: isSelected + ? AppFonts.yekan10.copyWith(color: AppColor.mediumGreyDarkActive) + : AppFonts.yekan10, + label: Text( + title, + textAlign: TextAlign.center), + deleteIconColor: Colors.red, + selected: isSelected, + selectedColor: selectedColor, + onSelected: (bool selected) { + onTap.call(index); + }, + ); + } +} diff --git a/packages/core/lib/presentation/widget/map/logic.dart b/packages/core/lib/presentation/widget/map/logic.dart index 40b1aa4..4435248 100644 --- a/packages/core/lib/presentation/widget/map/logic.dart +++ b/packages/core/lib/presentation/widget/map/logic.dart @@ -1,5 +1,83 @@ +import 'dart:async'; + +import 'package:flutter/animation.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map_animations/flutter_map_animations.dart'; +import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; +import 'package:latlong2/latlong.dart'; -class MapLogic extends GetxController { +class MapWidgetLogic extends GetxController with GetTickerProviderStateMixin { + Rx currentLocation = LatLng(35.824891, 50.948025).obs; + String tileType = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'; + RxList markers = [].obs; + RxList allMarkers = [].obs; + Rx mapController = MapController().obs; + + late final AnimatedMapController animatedMapController; + Timer? _debounceTimer; + RxBool isLoading = false.obs; + + @override + void onInit() { + super.onInit(); + animatedMapController = AnimatedMapController( + vsync: this, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + cancelPreviousAnimations: true, + ); + determineCurrentPosition(); + } + + @override + void onClose() { + super.onClose(); + animatedMapController.dispose(); + mapController.close(); + } + + Future determineCurrentPosition() async { + final position = await Geolocator.getCurrentPosition( + locationSettings: AndroidSettings(accuracy: LocationAccuracy.best), + ); + final latLng = LatLng(position.latitude, position.longitude); + + currentLocation.value = latLng; + markers.add(latLng); + animatedMapController.animateTo( + dest: latLng, + zoom: 18, + curve: Curves.easeInOut, + duration: const Duration(milliseconds: 1500), + ); + } + + void debouncedUpdateVisibleMarkers({required LatLng center}) { + _debounceTimer?.cancel(); + _debounceTimer = Timer(const Duration(milliseconds: 300), () { + final filtered = filterNearbyMarkers({ + 'markers': allMarkers, + 'centerLat': center.latitude, + 'centerLng': center.longitude, + 'radius': 1000.0, + }); + + markers.addAll(filtered); + }); + } + + List filterNearbyMarkers(Map args) { + final List rawMarkers = args['markers']; + final double centerLat = args['centerLat']; + final double centerLng = args['centerLng']; + final double radiusInMeters = args['radius']; + final center = LatLng(centerLat, centerLng); + final distance = Distance(); + + return rawMarkers + .where((marker) => distance(center, marker) <= radiusInMeters) + .toList(); + } } diff --git a/packages/core/lib/presentation/widget/map/view.dart b/packages/core/lib/presentation/widget/map/view.dart index f71f1e0..aa9e2cf 100644 --- a/packages/core/lib/presentation/widget/map/view.dart +++ b/packages/core/lib/presentation/widget/map/view.dart @@ -1,13 +1,76 @@ import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:get/get.dart'; +import 'package:rasadyar_core/presentation/common/app_color.dart'; +import 'package:rasadyar_core/presentation/common/assets.gen.dart'; +import 'package:rasadyar_core/presentation/widget/buttons/fab.dart'; import 'logic.dart'; -class MapPage extends GetView { - const MapPage({super.key}); +class MapWidget extends GetView { + const MapWidget({super.key}); @override Widget build(BuildContext context) { - return Container(); + return Stack( + children: [_buildMap(), _buildGpsButton(), _buildFilterButton()], + ); + } + + Widget _buildMap() { + return ObxValue((currentLocation) { + return FlutterMap( + mapController: controller.animatedMapController.mapController, + options: MapOptions( + initialCenter: currentLocation.value, + initialZoom: 18, + onPositionChanged: (camera, hasGesture) { + controller.debouncedUpdateVisibleMarkers(center: camera.center); + }, + ), + children: [ + TileLayer(urlTemplate: controller.tileType), + /* ObxValue((markers) { + return MarkerLayer( + markers: + markers + .map((e) => markerWidget(marker: e, onTap: () {})) + .toList(), + ); + }, controller.markers),*/ + ], + ); + }, controller.currentLocation); + } + + Widget _buildGpsButton() { + return Positioned( + right: 10, + bottom: 83, + child: ObxValue((data) { + return RFab.small( + backgroundColor: AppColor.greenNormal, + isLoading: data.value, + icon: Assets.vec.gpsSvg.svg(), + onPressed: () async { + controller.isLoading.value = true; + await controller.determineCurrentPosition(); + controller.isLoading.value = false; + }, + ); + }, controller.isLoading), + ); + } + + Widget _buildFilterButton() { + return Positioned( + right: 10, + bottom: 30, + child: RFab.small( + backgroundColor: AppColor.blueNormal, + icon: Assets.vec.filterSvg.svg(width: 24, height: 24), + onPressed: () {}, + ), + ); } } diff --git a/packages/core/lib/presentation/widget/widget.dart b/packages/core/lib/presentation/widget/widget.dart index 0f3dc7b..546efec 100644 --- a/packages/core/lib/presentation/widget/widget.dart +++ b/packages/core/lib/presentation/widget/widget.dart @@ -15,3 +15,4 @@ export 'tabs/new_tab.dart'; export 'tabs/tab.dart'; export 'vec_widget.dart'; export 'card/card_with_icon_with_border.dart'; +export 'chips/r_chips.dart'; diff --git a/packages/livestock/lib/presentation/page/action/view.dart b/packages/livestock/lib/presentation/page/action/view.dart index 3068f47..2f7dfac 100644 --- a/packages/livestock/lib/presentation/page/action/view.dart +++ b/packages/livestock/lib/presentation/page/action/view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:rasadyar_core/core.dart'; + import 'logic.dart'; class ActionPage extends GetView { @@ -7,6 +8,211 @@ class ActionPage extends GetView { @override Widget build(BuildContext context) { - return Container(); + return Scaffold( + appBar: RAppBar( + title: 'لیست درخواست‌ها', + hasBack: false, + centerTitle: true, + ), + body: Column( + children: [ + SizedBox(height: 16), + _searchWidget(), + SizedBox(height: 16), + SizedBox( + height: 50, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.symmetric(horizontal: 12), + child: Row( + spacing: 12, + children: [ + CustomChip( + title: 'انتخاب فیلتر', + index: 0, + isSelected: true, + selectedColor: AppColor.blueNormal, + onTap: (index) {}, + ), + + RFilterChips( + title: 'درخواست‌های من', + index: 1, + isSelected: true, + selectedColor: AppColor.yellowNormal, + onTap: (index) {}, + ), + + RFilterChips( + title: 'در انتظار ثبت ', + index: 2, + selectedColor: AppColor.greenLightActive, + isSelected: true, + onTap: (index) {}, + ), + + RFilterChips( + title: 'ارجاع به تعاونی', + index: 3, + selectedColor: AppColor.blueLightHover, + isSelected: true, + onTap: (index) {}, + ), + ], + ), + ), + ), + SizedBox(height: 10), + Expanded( + child: ListView.separated( + itemCount: 10, + physics: BouncingScrollPhysics(), + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), + separatorBuilder: (context, index) => SizedBox(height: 16), + itemBuilder: (context, index) { + return Container( + width: Get.width, + height: 75, + decoration: BoxDecoration( + color: + index < 3 + ? AppColor.yellowNormal + : index < 7 + ? AppColor.greenLightActive + : AppColor.blueLight, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + SizedBox(width: 5), + Expanded( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topRight: Radius.circular(8), + bottomRight: Radius.circular(8), + ), + ), + child: Row( + children: [ + SizedBox(width: 10), + Text( + 'محمد احمدی', + textAlign: TextAlign.center, + style: AppFonts.yekan12.copyWith( + color: AppColor.blueNormal, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(width: 20), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + + children: [ + Text( + 'پنج شنبه 14 اردیبهشت', + textAlign: TextAlign.center, + style: AppFonts.yekan10.copyWith( + color: AppColor.darkGreyNormal, + ), + ), + Text( + ' همدان - نهاوند - روستای - همدان - نهاوند - روستای ', + textAlign: TextAlign.center, + style: AppFonts.yekan10.copyWith( + color: AppColor.darkGreyNormal, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ], + ), + ), + SizedBox(width: 20), + GestureDetector( + onTap: () { + // controller.onTapMap(); + }, + child: SizedBox( + width: 50, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Assets.vec.mapSvg.svg( + width: 20, + height: 20, + colorFilter: ColorFilter.mode( + AppColor.blueNormal, + BlendMode.srcIn, + ), + ), + SizedBox(height: 8), + Text( + 'مسیریابی', + textAlign: TextAlign.center, + style: AppFonts.yekan10.copyWith( + color: AppColor.blueNormal, + ), + ), + ], + ), + ), + ), + + SizedBox(width: 20), + ], + ), + ), + ), + + Container( + width: 20, + child: Center( + child: RotatedBox( + quarterTurns: 3, + child: Text( + index < 3 + ? ' بازرسی' + : index < 7 + ? 'اطلاعات' + : 'ارجاع به تعاونی', + style: AppFonts.yekan8, + textAlign: TextAlign.center, + ), + ), + ), + ), + ], + ), + ); + }, + ), + ), + ], + ), + ); + } + + Widget _searchWidget() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: RTextField( + suffixIcon: Padding( + padding: const EdgeInsets.all(12.0), + child: Assets.vec.searchSvg.svg( + width: 10, + height: 10, + colorFilter: ColorFilter.mode(AppColor.blueNormal, BlendMode.srcIn), + ), + ), + hintText: 'جستجو', + onChanged: (value) { + //controller.search(value); + }, + controller: TextEditingController(), + ), + ); } } diff --git a/packages/livestock/lib/presentation/page/map/logic.dart b/packages/livestock/lib/presentation/page/map/logic.dart index 73d5b78..c54ed07 100644 --- a/packages/livestock/lib/presentation/page/map/logic.dart +++ b/packages/livestock/lib/presentation/page/map/logic.dart @@ -2,4 +2,7 @@ import 'package:rasadyar_core/core.dart'; class MapLogic extends GetxController { + + + } diff --git a/packages/livestock/lib/presentation/page/map/view.dart b/packages/livestock/lib/presentation/page/map/view.dart index 0bb76db..d05b39f 100644 --- a/packages/livestock/lib/presentation/page/map/view.dart +++ b/packages/livestock/lib/presentation/page/map/view.dart @@ -1,14 +1,22 @@ import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' as mt; import 'package:rasadyar_core/core.dart'; +import 'package:rasadyar_core/presentation/widget/map/view.dart'; + + import 'logic.dart'; class MapPage extends GetView { MapPage({super.key}); - - @override Widget build(BuildContext context) { - return Container(); + return Scaffold( + body: Stack( + children: [ + MapWidget(), + ], + ), + ); } -} +} \ No newline at end of file diff --git a/packages/livestock/lib/presentation/page/root/logic.dart b/packages/livestock/lib/presentation/page/root/logic.dart index c6aceec..843ec7b 100644 --- a/packages/livestock/lib/presentation/page/root/logic.dart +++ b/packages/livestock/lib/presentation/page/root/logic.dart @@ -6,7 +6,7 @@ import 'package:rasadyar_livestock/presentation/page/profile/view.dart'; class RootLogic extends GetxController { List pages = [ActionPage(), MapPage(), ProfilePage()]; - RxInt indexedPage = 1.obs; + RxInt currentIndex = 1.obs; @override void onReady() { @@ -19,4 +19,7 @@ class RootLogic extends GetxController { // TODO: implement onClose super.onClose(); } + + void changePage(int i) => currentIndex.value = i; + } diff --git a/packages/livestock/lib/presentation/page/root/view.dart b/packages/livestock/lib/presentation/page/root/view.dart index 88d0fc3..ca4c3ff 100644 --- a/packages/livestock/lib/presentation/page/root/view.dart +++ b/packages/livestock/lib/presentation/page/root/view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:rasadyar_core/core.dart'; + import 'logic.dart'; class RootPage extends GetView { @@ -7,14 +8,39 @@ class RootPage extends GetView { @override Widget build(BuildContext context) { - return Scaffold( - body: ObxValue((data) { - return IndexedStack( + return ObxValue((currentIndex) { + return Scaffold( + body: IndexedStack( children: controller.pages, - index: data.value, + index: currentIndex.value, sizing: StackFit.expand, - ); - }, controller.indexedPage), - ); + ), + + bottomNavigationBar: BottomNavigation1( + items: [ + BottomNavigation1Item( + icon: Assets.vec.filterSvg.path, + label: 'درخواست‌ها', + isSelected: currentIndex.value == 0, + onTap: () => controller.changePage(0), + ), + + BottomNavigation1Item( + icon: Assets.vec.mapSvg.path, + label: 'نقشه', + isSelected: currentIndex.value == 1, + onTap: () => controller.changePage(1), + ), + + BottomNavigation1Item( + icon: Assets.vec.profileUserSvg.path, + label: 'پروفایل', + isSelected: currentIndex.value == 2, + onTap: () => controller.changePage(2), + ), + ], + ), + ); + }, controller.currentIndex); } } diff --git a/packages/livestock/lib/presentation/routes/app_pages.dart b/packages/livestock/lib/presentation/routes/app_pages.dart index 3795a4b..6570bf8 100644 --- a/packages/livestock/lib/presentation/routes/app_pages.dart +++ b/packages/livestock/lib/presentation/routes/app_pages.dart @@ -1,10 +1,12 @@ import 'package:rasadyar_auth/auth.dart'; import 'package:rasadyar_core/core.dart'; +import 'package:rasadyar_core/presentation/widget/map/logic.dart'; import 'package:rasadyar_livestock/presentation/page/action/logic.dart'; import 'package:rasadyar_livestock/presentation/page/map/logic.dart'; import 'package:rasadyar_livestock/presentation/page/profile/logic.dart'; import 'package:rasadyar_livestock/presentation/page/root/logic.dart'; import 'package:rasadyar_livestock/presentation/page/root/view.dart'; + part 'app_routes.dart'; sealed class LiveStockPages { @@ -21,6 +23,8 @@ sealed class LiveStockPages { Get.lazyPut(() => ActionLogic()); Get.lazyPut(() => MapLogic()); Get.lazyPut(() => ProfileLogic()); + Get.lazyPut(() => ProfileLogic()); + Get.lazyPut(() => MapWidgetLogic()); }), ), ]; diff --git a/tools/vecGeneratoe.sh b/tools/vecGeneratoe.sh index 1b84fb3..4c6cf89 100644 --- a/tools/vecGeneratoe.sh +++ b/tools/vecGeneratoe.sh @@ -11,8 +11,8 @@ PACKAGE_ABS_PATH="$SCRIPT_DIR/$PACKAGE_PATH" echo "🗃️ package path: $PACKAGE_ABS_PATH" # Directory to read files from -sourcePath="$PACKAGE_ABS_PATH/assets/icons" -targetPath="$PACKAGE_ABS_PATH/assets/vec" +sourcePath="../assets/icons" +targetPath="../assets/vec" echo "🗃️ sourcePath path: $sourcePath" echo "🗃️ targetPath path: $targetPath"