Compare commits
No commits in common. "main" and "fixing-bugs-for-page" have entirely different histories.
main
...
fixing-bug
|
|
@ -9,7 +9,7 @@ plugins {
|
||||||
android {
|
android {
|
||||||
namespace = "com.example.lba"
|
namespace = "com.example.lba"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion = "27.0.12077973"
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
// استفاده از جاوا ۸ برای سازگاری بهتر
|
// استفاده از جاوا ۸ برای سازگاری بهتر
|
||||||
|
|
|
||||||
|
|
@ -28,5 +28,5 @@ plugins {
|
||||||
// The Kotlin plugin version is also managed.
|
// The Kotlin plugin version is also managed.
|
||||||
id("org.jetbrains.kotlin.android") apply false
|
id("org.jetbrains.kotlin.android") apply false
|
||||||
// اضافه کردن این خط برای تعریف پلاگین گوگل سرویسز
|
// اضافه کردن این خط برای تعریف پلاگین گوگل سرویسز
|
||||||
id("com.google.gms.google-services") version "4.3.15" apply false
|
id("com.google.gms.google-services") version "4.4.1" apply false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,6 @@ pluginManagement {
|
||||||
plugins {
|
plugins {
|
||||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
id("com.android.application") version "8.7.0" apply false
|
id("com.android.application") version "8.7.0" apply false
|
||||||
// START: FlutterFire Configuration
|
|
||||||
id("com.google.gms.google-services") version("4.3.15") apply false
|
|
||||||
// END: FlutterFire Configuration
|
|
||||||
// Updated Kotlin version to the latest stable release
|
// Updated Kotlin version to the latest stable release
|
||||||
id("org.jetbrains.kotlin.android") version "2.0.0" apply false
|
id("org.jetbrains.kotlin.android") version "2.0.0" apply false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M9.50978 4.22989L18.0698 8.50989C21.9098 10.4299 21.9098 13.5699 18.0698 15.4899L9.50978 19.7699C3.74978 22.6499 1.39978 20.2899 4.27978 14.5399L5.14978 12.8099C5.36978 12.3699 5.36978 11.6399 5.14978 11.1999L4.27978 9.45989C1.39978 3.70989 3.75978 1.34989 9.50978 4.22989Z" stroke="#292D32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
<path d="M5.43994 12H10.8399" stroke="#292D32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 587 B |
|
|
@ -1 +0,0 @@
|
||||||
{"flutter":{"platforms":{"android":{"default":{"projectId":"lba-app-c4a7e","appId":"1:81202355575:android:6d4c7db49b6120f8239572","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"lba-app-c4a7e","configurations":{"android":"1:81202355575:android:6d4c7db49b6120f8239572","ios":"1:81202355575:ios:d40682afd403fc5b239572","macos":"1:81202355575:ios:d40682afd403fc5b239572","web":"1:81202355575:web:0cf25bf19cb4a8a1239572","windows":"1:81202355575:web:03aeac4a6dbda4d3239572"}}}}}}
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
// File generated by FlutterFire CLI.
|
|
||||||
// ignore_for_file: type=lint
|
|
||||||
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
|
|
||||||
import 'package:flutter/foundation.dart'
|
|
||||||
show defaultTargetPlatform, kIsWeb, TargetPlatform;
|
|
||||||
|
|
||||||
/// Default [FirebaseOptions] for use with your Firebase apps.
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```dart
|
|
||||||
/// import 'firebase_options.dart';
|
|
||||||
/// // ...
|
|
||||||
/// await Firebase.initializeApp(
|
|
||||||
/// options: DefaultFirebaseOptions.currentPlatform,
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
class DefaultFirebaseOptions {
|
|
||||||
static FirebaseOptions get currentPlatform {
|
|
||||||
if (kIsWeb) {
|
|
||||||
return web;
|
|
||||||
}
|
|
||||||
switch (defaultTargetPlatform) {
|
|
||||||
case TargetPlatform.android:
|
|
||||||
return android;
|
|
||||||
case TargetPlatform.iOS:
|
|
||||||
return ios;
|
|
||||||
case TargetPlatform.macOS:
|
|
||||||
return macos;
|
|
||||||
case TargetPlatform.windows:
|
|
||||||
return windows;
|
|
||||||
case TargetPlatform.linux:
|
|
||||||
throw UnsupportedError(
|
|
||||||
'DefaultFirebaseOptions have not been configured for linux - '
|
|
||||||
'you can reconfigure this by running the FlutterFire CLI again.',
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
throw UnsupportedError(
|
|
||||||
'DefaultFirebaseOptions are not supported for this platform.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static const FirebaseOptions web = FirebaseOptions(
|
|
||||||
apiKey: 'AIzaSyCeRwWv-Pep_624hoUVIw-SSlicDVRHGiY',
|
|
||||||
appId: '1:81202355575:web:0cf25bf19cb4a8a1239572',
|
|
||||||
messagingSenderId: '81202355575',
|
|
||||||
projectId: 'lba-app-c4a7e',
|
|
||||||
authDomain: 'lba-app-c4a7e.firebaseapp.com',
|
|
||||||
storageBucket: 'lba-app-c4a7e.firebasestorage.app',
|
|
||||||
measurementId: 'G-8D14ETTE9T',
|
|
||||||
);
|
|
||||||
|
|
||||||
static const FirebaseOptions android = FirebaseOptions(
|
|
||||||
apiKey: 'AIzaSyB22NqmwB_PpI1s37xbc2ABJ5_COEHeC8g',
|
|
||||||
appId: '1:81202355575:android:6d4c7db49b6120f8239572',
|
|
||||||
messagingSenderId: '81202355575',
|
|
||||||
projectId: 'lba-app-c4a7e',
|
|
||||||
storageBucket: 'lba-app-c4a7e.firebasestorage.app',
|
|
||||||
);
|
|
||||||
|
|
||||||
static const FirebaseOptions ios = FirebaseOptions(
|
|
||||||
apiKey: 'AIzaSyCJsr8Baaw0CbucB0zrB_kWv_7fcBNRwIk',
|
|
||||||
appId: '1:81202355575:ios:d40682afd403fc5b239572',
|
|
||||||
messagingSenderId: '81202355575',
|
|
||||||
projectId: 'lba-app-c4a7e',
|
|
||||||
storageBucket: 'lba-app-c4a7e.firebasestorage.app',
|
|
||||||
androidClientId: '81202355575-fchlrrp4fu3irskh6co1ep8i04oi3adr.apps.googleusercontent.com',
|
|
||||||
iosClientId: '81202355575-hcgc97f7glhbpa8h7iqq4d57efcsrtqo.apps.googleusercontent.com',
|
|
||||||
iosBundleId: 'com.example.lba',
|
|
||||||
);
|
|
||||||
|
|
||||||
static const FirebaseOptions macos = FirebaseOptions(
|
|
||||||
apiKey: 'AIzaSyCJsr8Baaw0CbucB0zrB_kWv_7fcBNRwIk',
|
|
||||||
appId: '1:81202355575:ios:d40682afd403fc5b239572',
|
|
||||||
messagingSenderId: '81202355575',
|
|
||||||
projectId: 'lba-app-c4a7e',
|
|
||||||
storageBucket: 'lba-app-c4a7e.firebasestorage.app',
|
|
||||||
androidClientId: '81202355575-fchlrrp4fu3irskh6co1ep8i04oi3adr.apps.googleusercontent.com',
|
|
||||||
iosClientId: '81202355575-hcgc97f7glhbpa8h7iqq4d57efcsrtqo.apps.googleusercontent.com',
|
|
||||||
iosBundleId: 'com.example.lba',
|
|
||||||
);
|
|
||||||
|
|
||||||
static const FirebaseOptions windows = FirebaseOptions(
|
|
||||||
apiKey: 'AIzaSyCeRwWv-Pep_624hoUVIw-SSlicDVRHGiY',
|
|
||||||
appId: '1:81202355575:web:03aeac4a6dbda4d3239572',
|
|
||||||
messagingSenderId: '81202355575',
|
|
||||||
projectId: 'lba-app-c4a7e',
|
|
||||||
authDomain: 'lba-app-c4a7e.firebaseapp.com',
|
|
||||||
storageBucket: 'lba-app-c4a7e.firebasestorage.app',
|
|
||||||
measurementId: 'G-2EZ542PHK0',
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -420,9 +420,6 @@ class $AssetsIconsGen {
|
||||||
SvgGenImage get selectedList =>
|
SvgGenImage get selectedList =>
|
||||||
const SvgGenImage('assets/icons/selected list.svg');
|
const SvgGenImage('assets/icons/selected list.svg');
|
||||||
|
|
||||||
/// File path: assets/icons/send.svg
|
|
||||||
SvgGenImage get send => const SvgGenImage('assets/icons/send.svg');
|
|
||||||
|
|
||||||
/// File path: assets/icons/shield.svg
|
/// File path: assets/icons/shield.svg
|
||||||
SvgGenImage get shield => const SvgGenImage('assets/icons/shield.svg');
|
SvgGenImage get shield => const SvgGenImage('assets/icons/shield.svg');
|
||||||
|
|
||||||
|
|
@ -604,7 +601,6 @@ class $AssetsIconsGen {
|
||||||
routing,
|
routing,
|
||||||
searchNormal,
|
searchNormal,
|
||||||
selectedList,
|
selectedList,
|
||||||
send,
|
|
||||||
shield,
|
shield,
|
||||||
shoppingCart,
|
shoppingCart,
|
||||||
slide2,
|
slide2,
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,8 @@ class MyApp extends StatelessWidget {
|
||||||
title: 'LBA',
|
title: 'LBA',
|
||||||
theme: themeManager.lightTheme,
|
theme: themeManager.lightTheme,
|
||||||
darkTheme: themeManager.darkTheme,
|
darkTheme: themeManager.darkTheme,
|
||||||
themeMode: themeManager.getThemeMode(),
|
themeMode:
|
||||||
|
themeManager.isDarkMode ? ThemeMode.dark : ThemeMode.light,
|
||||||
themeAnimationDuration: const Duration(milliseconds: 300),
|
themeAnimationDuration: const Duration(milliseconds: 300),
|
||||||
themeAnimationCurve: Curves.easeInOutCubic,
|
themeAnimationCurve: Curves.easeInOutCubic,
|
||||||
home: SplashScreen(
|
home: SplashScreen(
|
||||||
|
|
@ -45,5 +46,4 @@ class MyApp extends StatelessWidget {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
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:shared_preferences/shared_preferences.dart';
|
|
||||||
import '../../extension/screenSize.dart';
|
import '../../extension/screenSize.dart';
|
||||||
import '../../gen/assets.gen.dart';
|
import '../../gen/assets.gen.dart';
|
||||||
import '../../res/colors.dart';
|
import '../../res/colors.dart';
|
||||||
|
import 'login_page.dart';
|
||||||
|
|
||||||
class OnboardingPage extends StatefulWidget {
|
class OnboardingPage extends StatefulWidget {
|
||||||
final VoidCallback? onCompleted;
|
const OnboardingPage({super.key});
|
||||||
|
|
||||||
const OnboardingPage({super.key, this.onCompleted});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<OnboardingPage> createState() => _OnboardingPageState();
|
State<OnboardingPage> createState() => _OnboardingPageState();
|
||||||
|
|
@ -30,23 +28,17 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||||
Assets.icons.slide3.path,
|
Assets.icons.slide3.path,
|
||||||
];
|
];
|
||||||
|
|
||||||
void _next() async {
|
void _next() {
|
||||||
if (currentIndex < imageAssets.length - 1) {
|
if (currentIndex < imageAssets.length - 1) {
|
||||||
_pageController.nextPage(
|
_pageController.nextPage(
|
||||||
duration: Duration(milliseconds: 300),
|
duration: Duration(milliseconds: 300),
|
||||||
curve: Curves.easeInOut,
|
curve: Curves.easeInOut,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
Navigator.push(
|
||||||
await prefs.setBool('first_time_user', false);
|
context,
|
||||||
|
MaterialPageRoute(builder: (context) => LoginPage()),
|
||||||
if (mounted) {
|
);
|
||||||
if (widget.onCompleted != null) {
|
|
||||||
widget.onCompleted!();
|
|
||||||
} else {
|
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -122,8 +122,7 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Toggle the flip state of the card
|
huntProvider.selectCard(card);
|
||||||
huntProvider.toggleCardFlip(card.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -247,7 +246,7 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi
|
||||||
card: card,
|
card: card,
|
||||||
onTap: () => _onCardSelected(card),
|
onTap: () => _onCardSelected(card),
|
||||||
isSelected: huntProvider.selectedCard?.id == card.id,
|
isSelected: huntProvider.selectedCard?.id == card.id,
|
||||||
isFlipped: huntProvider.isCardFlipped(card.id),
|
isFlipped: huntProvider.selectedCard?.id == card.id,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ class HuntState extends ChangeNotifier {
|
||||||
bool _isLocationEnabled = false;
|
bool _isLocationEnabled = false;
|
||||||
bool _isCameraPermissionGranted = false;
|
bool _isCameraPermissionGranted = false;
|
||||||
DateTime? _huntStartTime;
|
DateTime? _huntStartTime;
|
||||||
String? _currentlyFlippedCardId;
|
|
||||||
|
|
||||||
List<HuntCard> get cards => _cards;
|
List<HuntCard> get cards => _cards;
|
||||||
HuntCard? get selectedCard => _selectedCard;
|
HuntCard? get selectedCard => _selectedCard;
|
||||||
|
|
@ -28,11 +27,6 @@ class HuntState extends ChangeNotifier {
|
||||||
bool get isLocationEnabled => _isLocationEnabled;
|
bool get isLocationEnabled => _isLocationEnabled;
|
||||||
bool get isCameraPermissionGranted => _isCameraPermissionGranted;
|
bool get isCameraPermissionGranted => _isCameraPermissionGranted;
|
||||||
DateTime? get huntStartTime => _huntStartTime;
|
DateTime? get huntStartTime => _huntStartTime;
|
||||||
String? get currentlyFlippedCardId => _currentlyFlippedCardId;
|
|
||||||
|
|
||||||
bool isCardFlipped(String cardId) {
|
|
||||||
return _currentlyFlippedCardId == cardId;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get hasTimeLeft {
|
bool get hasTimeLeft {
|
||||||
if (_huntStartTime == null) return false;
|
if (_huntStartTime == null) return false;
|
||||||
|
|
@ -68,16 +62,6 @@ class HuntState extends ChangeNotifier {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void toggleCardFlip(String cardId) {
|
|
||||||
if (_currentlyFlippedCardId == cardId) {
|
|
||||||
_currentlyFlippedCardId = null;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
_currentlyFlippedCardId = cardId;
|
|
||||||
}
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void startHunt() {
|
void startHunt() {
|
||||||
if (_selectedCard != null) {
|
if (_selectedCard != null) {
|
||||||
_huntStartTime = DateTime.now();
|
_huntStartTime = DateTime.now();
|
||||||
|
|
@ -116,7 +100,6 @@ class HuntState extends ChangeNotifier {
|
||||||
_selectedCard = null;
|
_selectedCard = null;
|
||||||
_gameState = HuntGameState.cardSelection;
|
_gameState = HuntGameState.cardSelection;
|
||||||
_huntStartTime = null;
|
_huntStartTime = null;
|
||||||
_currentlyFlippedCardId = null;
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -187,7 +170,7 @@ class HuntState extends ChangeNotifier {
|
||||||
question: 'In the digital realm where innovation never sleeps, discover the place where technology meets tomorrow.',
|
question: 'In the digital realm where innovation never sleeps, discover the place where technology meets tomorrow.',
|
||||||
answer: 'TechnoCity',
|
answer: 'TechnoCity',
|
||||||
description: 'Electronics and gadgets store',
|
description: 'Electronics and gadgets store',
|
||||||
targetLatitude: 32.825042058762496,
|
targetLatitude: 32.625042058762496,
|
||||||
targetLongitude: 51.7264112781341,
|
targetLongitude: 51.7264112781341,
|
||||||
hintLatitude: 32.625042058762496,
|
hintLatitude: 32.625042058762496,
|
||||||
hintLongitude: 51.7264112781341,
|
hintLongitude: 51.7264112781341,
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:lba/gen/assets.gen.dart';
|
import 'package:lba/gen/assets.gen.dart';
|
||||||
import 'package:lba/res/colors.dart';
|
import 'package:lba/res/colors.dart';
|
||||||
import 'package:lba/widgets/buildWarpedInfo.dart';
|
import 'package:lba/widgets/buildWarpedInfo.dart';
|
||||||
import 'package:lba/widgets/compact_review_input_widget.dart';
|
|
||||||
import 'package:lba/widgets/leave_review_widget.dart';
|
|
||||||
import 'package:lba/widgets/orderType.dart';
|
import 'package:lba/widgets/orderType.dart';
|
||||||
import 'package:lba/widgets/price_reserve_widget.dart';
|
import 'package:lba/widgets/price_reserve_widget.dart';
|
||||||
import 'package:lba/widgets/rate.dart';
|
import 'package:lba/widgets/rate.dart';
|
||||||
|
|
@ -50,7 +48,6 @@ class _ItemState extends State<Item> with TickerProviderStateMixin {
|
||||||
"yesCount": 2,
|
"yesCount": 2,
|
||||||
"noCount": 0,
|
"noCount": 0,
|
||||||
"date": "Jun 09, 2025",
|
"date": "Jun 09, 2025",
|
||||||
"userLiked": null,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Khalid A",
|
"name": "Khalid A",
|
||||||
|
|
@ -60,7 +57,6 @@ class _ItemState extends State<Item> with TickerProviderStateMixin {
|
||||||
"yesCount": 5,
|
"yesCount": 5,
|
||||||
"noCount": 10,
|
"noCount": 10,
|
||||||
"date": "Dec 26, 2024",
|
"date": "Dec 26, 2024",
|
||||||
"userLiked": null,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Khalid A",
|
"name": "Khalid A",
|
||||||
|
|
@ -70,7 +66,6 @@ class _ItemState extends State<Item> with TickerProviderStateMixin {
|
||||||
"yesCount": 5,
|
"yesCount": 5,
|
||||||
"noCount": 10,
|
"noCount": 10,
|
||||||
"date": "Dec 26, 2024",
|
"date": "Dec 26, 2024",
|
||||||
"userLiked": null,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Sara M",
|
"name": "Sara M",
|
||||||
|
|
@ -80,7 +75,6 @@ class _ItemState extends State<Item> with TickerProviderStateMixin {
|
||||||
"yesCount": 2,
|
"yesCount": 2,
|
||||||
"noCount": 0,
|
"noCount": 0,
|
||||||
"date": "Jun 09, 2025",
|
"date": "Jun 09, 2025",
|
||||||
"userLiked": null,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -158,10 +152,8 @@ class _ItemState extends State<Item> with TickerProviderStateMixin {
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Text(
|
child: Text(
|
||||||
widget.title,
|
widget.title,
|
||||||
style: const TextStyle(
|
style:
|
||||||
fontWeight: FontWeight.bold,
|
const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
0,
|
0,
|
||||||
|
|
@ -217,21 +209,13 @@ class _ItemState extends State<Item> with TickerProviderStateMixin {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 15),
|
const SizedBox(height: 15),
|
||||||
_buildAnimatedWidget(
|
_buildAnimatedWidget(
|
||||||
buildWrappedInfo("Dimensions:", widget.dimensions),
|
buildWrappedInfo("Dimensions:", widget.dimensions), 4),
|
||||||
4,
|
|
||||||
),
|
|
||||||
_buildAnimatedWidget(buildWrappedInfo("Colour:", widget.colour), 5),
|
_buildAnimatedWidget(buildWrappedInfo("Colour:", widget.colour), 5),
|
||||||
_buildAnimatedWidget(
|
_buildAnimatedWidget(
|
||||||
buildWrappedInfo("Material:", widget.material),
|
buildWrappedInfo("Material:", widget.material), 6),
|
||||||
6,
|
|
||||||
),
|
|
||||||
_buildAnimatedWidget(
|
_buildAnimatedWidget(
|
||||||
buildWrappedInfo("Description:", widget.description),
|
buildWrappedInfo("Description:", widget.description), 7),
|
||||||
7,
|
const SizedBox(height: 30),
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
const ReviewComposerWidget(),
|
|
||||||
const SizedBox(height: 15),
|
|
||||||
const Center(
|
const Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
"Top reviews from the United Arab Emirates",
|
"Top reviews from the United Arab Emirates",
|
||||||
|
|
@ -250,9 +234,8 @@ class _ItemState extends State<Item> with TickerProviderStateMixin {
|
||||||
position: Tween<Offset>(
|
position: Tween<Offset>(
|
||||||
begin: const Offset(-1, 0),
|
begin: const Offset(-1, 0),
|
||||||
end: Offset.zero,
|
end: Offset.zero,
|
||||||
).animate(
|
).animate(CurvedAnimation(
|
||||||
CurvedAnimation(parent: animation, curve: Curves.easeInOut),
|
parent: animation, curve: Curves.easeInOut)),
|
||||||
),
|
|
||||||
child: SizeTransition(
|
child: SizeTransition(
|
||||||
sizeFactor: animation,
|
sizeFactor: animation,
|
||||||
axisAlignment: -1,
|
axisAlignment: -1,
|
||||||
|
|
@ -263,35 +246,6 @@ class _ItemState extends State<Item> with TickerProviderStateMixin {
|
||||||
yesCount: review['yesCount'],
|
yesCount: review['yesCount'],
|
||||||
noCount: review['noCount'],
|
noCount: review['noCount'],
|
||||||
date: review['date'],
|
date: review['date'],
|
||||||
initialLikeState: review['userLiked'],
|
|
||||||
onLikeDislike: (isLike) {
|
|
||||||
setState(() {
|
|
||||||
if (isLike) {
|
|
||||||
if (review['userLiked'] == true) {
|
|
||||||
review['userLiked'] = null;
|
|
||||||
} else {
|
|
||||||
if (review['userLiked'] == false) {
|
|
||||||
review['noCount'] =
|
|
||||||
(review['noCount'] as int) - 1;
|
|
||||||
}
|
|
||||||
review['userLiked'] = true;
|
|
||||||
review['yesCount'] =
|
|
||||||
(review['yesCount'] as int) + 1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (review['userLiked'] == false) {
|
|
||||||
review['userLiked'] = null;
|
|
||||||
} else {
|
|
||||||
if (review['userLiked'] == true) {
|
|
||||||
review['yesCount'] =
|
|
||||||
(review['yesCount'] as int) - 1;
|
|
||||||
}
|
|
||||||
review['userLiked'] = false;
|
|
||||||
review['noCount'] = (review['noCount'] as int) + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -325,10 +279,10 @@ class _ItemState extends State<Item> with TickerProviderStateMixin {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 7),
|
const SizedBox(height: 20),
|
||||||
const PriceReserveWidget(),
|
const PriceReserveWidget(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,72 +3,51 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:lba/screens/auth/login_page.dart';
|
import 'package:lba/screens/auth/login_page.dart';
|
||||||
import 'package:lba/screens/auth/onboarding_page.dart';
|
import 'package:lba/screens/auth/onboarding_page.dart';
|
||||||
import 'package:lba/screens/mains/navigation/navigation.dart';
|
import 'package:lba/screens/mains/navigation/navigation.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
|
|
||||||
class SimpleAuthGate extends StatefulWidget {
|
class SimpleAuthGate extends StatelessWidget {
|
||||||
const SimpleAuthGate({super.key});
|
const SimpleAuthGate({super.key});
|
||||||
|
|
||||||
@override
|
|
||||||
State<SimpleAuthGate> createState() => _SimpleAuthGateState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SimpleAuthGateState extends State<SimpleAuthGate> {
|
|
||||||
bool _isLoading = true;
|
|
||||||
Widget? _targetScreen;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_determineInitialScreen();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _determineInitialScreen() async {
|
|
||||||
final user = FirebaseAuth.instance.currentUser;
|
|
||||||
final isLoggedIn = user != null;
|
|
||||||
|
|
||||||
if (isLoggedIn) {
|
|
||||||
_setTargetScreen(const MainScreen());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
final isFirstTime = prefs.getBool('first_time_user') ?? true;
|
|
||||||
|
|
||||||
if (isFirstTime) {
|
|
||||||
_setTargetScreen(OnboardingPage(
|
|
||||||
onCompleted: () {
|
|
||||||
_setTargetScreen(const LoginPage());
|
|
||||||
},
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
_setTargetScreen(const LoginPage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _setTargetScreen(Widget screen) {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_targetScreen = screen;
|
|
||||||
_isLoading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_isLoading || _targetScreen == null) {
|
return StreamBuilder<User?>(
|
||||||
return const Scaffold(
|
stream: FirebaseAuth.instance.authStateChanges(),
|
||||||
body: Center(
|
builder: (context, snapshot) {
|
||||||
child: CircularProgressIndicator(),
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
),
|
return const Scaffold(
|
||||||
);
|
body: Center(
|
||||||
}
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return _targetScreen!;
|
if (snapshot.hasError) {
|
||||||
|
return const Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.error, size: 64, color: Colors.red),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text('An error occurred. Please try again later.'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final user = snapshot.data;
|
||||||
|
final isLoggedIn = user != null;
|
||||||
|
|
||||||
|
if (isLoggedIn) {
|
||||||
|
return const MainScreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
return _shouldShowOnboarding() ? const OnboardingPage() : const LoginPage();
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> resetOnboarding() async {
|
bool _shouldShowOnboarding() {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
return false;
|
||||||
await prefs.setBool('first_time_user', true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,4 @@ class SharedPreferencesKey {
|
||||||
static const String token = 'token';
|
static const String token = 'token';
|
||||||
static const String isDarkMode = 'isDarkMode';
|
static const String isDarkMode = 'isDarkMode';
|
||||||
static const String hasManualThemeOverride = 'hasManualThemeOverride';
|
static const String hasManualThemeOverride = 'hasManualThemeOverride';
|
||||||
static const String hasSeenOnboarding = 'hasSeenOnboarding';
|
|
||||||
static const String selectedLanguage = 'selectedLanguage';
|
|
||||||
static const String selectedFlag = 'selectedFlag';
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:lba/res/colors.dart';
|
import 'package:lba/res/colors.dart';
|
||||||
import 'package:lba/utils/sharedPreferencesKey.dart';
|
import 'package:lba/utils/sharedPreferencesKey.dart';
|
||||||
import 'package:lba/utils/sharedPreferencesManger.dart';
|
import 'package:lba/utils/sharedPreferencesManger.dart';
|
||||||
|
|
||||||
class ThemeManager extends ChangeNotifier with WidgetsBindingObserver {
|
class ThemeManager extends ChangeNotifier {
|
||||||
late SharedPreferencesManager _prefs;
|
late SharedPreferencesManager _prefs;
|
||||||
bool _isDarkMode = false;
|
bool _isDarkMode = false;
|
||||||
bool _isThemeTransitioning = false;
|
bool _isThemeTransitioning = false;
|
||||||
|
|
@ -16,13 +13,6 @@ class ThemeManager extends ChangeNotifier with WidgetsBindingObserver {
|
||||||
ThemeManager() {
|
ThemeManager() {
|
||||||
_prefs = SharedPreferencesManager();
|
_prefs = SharedPreferencesManager();
|
||||||
_loadTheme();
|
_loadTheme();
|
||||||
WidgetsBinding.instance.addObserver(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
|
||||||
super.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isDarkMode => _isDarkMode;
|
bool get isDarkMode => _isDarkMode;
|
||||||
|
|
@ -170,31 +160,6 @@ class ThemeManager extends ChangeNotifier with WidgetsBindingObserver {
|
||||||
|
|
||||||
ThemeData get currentTheme => _isDarkMode ? darkTheme : lightTheme;
|
ThemeData get currentTheme => _isDarkMode ? darkTheme : lightTheme;
|
||||||
|
|
||||||
/// Returns the appropriate ThemeMode based on user preferences
|
|
||||||
ThemeMode getThemeMode() {
|
|
||||||
if (_hasManualOverride) {
|
|
||||||
return _isDarkMode ? ThemeMode.dark : ThemeMode.light;
|
|
||||||
} else {
|
|
||||||
return ThemeMode.system;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangePlatformBrightness() {
|
|
||||||
super.didChangePlatformBrightness();
|
|
||||||
|
|
||||||
// Only respond to system theme changes if user hasn't manually overridden
|
|
||||||
if (!_hasManualOverride) {
|
|
||||||
bool systemIsDark = _getSystemTheme();
|
|
||||||
if (_isDarkMode != systemIsDark) {
|
|
||||||
_isDarkMode = systemIsDark;
|
|
||||||
AppColors.setDarkMode(_isDarkMode);
|
|
||||||
_updateSystemUIOverlay();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _loadTheme() async {
|
Future<void> _loadTheme() async {
|
||||||
await _prefs.init();
|
await _prefs.init();
|
||||||
_hasManualOverride = _prefs.getBool(SharedPreferencesKey.hasManualThemeOverride) ?? false;
|
_hasManualOverride = _prefs.getBool(SharedPreferencesKey.hasManualThemeOverride) ?? false;
|
||||||
|
|
@ -211,7 +176,8 @@ class ThemeManager extends ChangeNotifier with WidgetsBindingObserver {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _getSystemTheme() {
|
bool _getSystemTheme() {
|
||||||
return WidgetsBinding.instance.platformDispatcher.platformBrightness == Brightness.dark;
|
final window = WidgetsBinding.instance.window;
|
||||||
|
return window.platformBrightness == Brightness.dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateSystemUIOverlay() {
|
void _updateSystemUIOverlay() {
|
||||||
|
|
@ -262,4 +228,4 @@ class ThemeManager extends ChangeNotifier with WidgetsBindingObserver {
|
||||||
await _prefs.saveBool(SharedPreferencesKey.isDarkMode, _isDarkMode);
|
await _prefs.saveBool(SharedPreferencesKey.isDarkMode, _isDarkMode);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,604 +0,0 @@
|
||||||
import 'dart:io';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
|
||||||
import 'package:image_picker/image_picker.dart';
|
|
||||||
import 'package:lba/gen/assets.gen.dart';
|
|
||||||
import 'package:lba/res/colors.dart';
|
|
||||||
|
|
||||||
class ReviewComposerWidget extends StatefulWidget {
|
|
||||||
const ReviewComposerWidget({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ReviewComposerWidget> createState() => _ReviewComposerWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ReviewComposerWidgetState extends State<ReviewComposerWidget>
|
|
||||||
with TickerProviderStateMixin {
|
|
||||||
double _rating = 0;
|
|
||||||
final TextEditingController _reviewController = TextEditingController();
|
|
||||||
XFile? _image;
|
|
||||||
final FocusNode _focusNode = FocusNode();
|
|
||||||
bool _isFocused = false;
|
|
||||||
|
|
||||||
late AnimationController _focusController;
|
|
||||||
late AnimationController _buttonController;
|
|
||||||
late AnimationController _starController;
|
|
||||||
late AnimationController _gradientController;
|
|
||||||
late Animation<double> _gradientAnimation;
|
|
||||||
|
|
||||||
late List<AnimationController> _starControllers;
|
|
||||||
late List<Animation<double>> _starAnimations;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_focusController = AnimationController(
|
|
||||||
vsync: this,
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
);
|
|
||||||
_buttonController = AnimationController(
|
|
||||||
vsync: this,
|
|
||||||
duration: const Duration(milliseconds: 400),
|
|
||||||
);
|
|
||||||
_starController = AnimationController(
|
|
||||||
vsync: this,
|
|
||||||
duration: const Duration(milliseconds: 600),
|
|
||||||
);
|
|
||||||
_gradientController = AnimationController(
|
|
||||||
vsync: this,
|
|
||||||
duration: const Duration(milliseconds: 800),
|
|
||||||
);
|
|
||||||
|
|
||||||
_gradientAnimation = Tween<double>(begin: 0, end: 1).animate(
|
|
||||||
CurvedAnimation(parent: _gradientController, curve: Curves.easeInOut),
|
|
||||||
);
|
|
||||||
|
|
||||||
_starControllers = List.generate(
|
|
||||||
5,
|
|
||||||
(index) => AnimationController(
|
|
||||||
vsync: this,
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
_starAnimations =
|
|
||||||
_starControllers
|
|
||||||
.map(
|
|
||||||
(controller) => Tween<double>(begin: 0.0, end: 1.0).animate(
|
|
||||||
CurvedAnimation(parent: controller, curve: Curves.elasticOut),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
_focusNode.addListener(() {
|
|
||||||
setState(() {
|
|
||||||
_isFocused = _focusNode.hasFocus;
|
|
||||||
if (_isFocused) {
|
|
||||||
_focusController.forward();
|
|
||||||
} else {
|
|
||||||
_focusController.reverse();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
_reviewController.addListener(() {
|
|
||||||
if (_reviewController.text.isNotEmpty) {
|
|
||||||
_triggerGradientAnimation();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_reviewController.text.isNotEmpty || _image != null) {
|
|
||||||
_buttonController.forward();
|
|
||||||
} else {
|
|
||||||
_buttonController.reverse();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _triggerGradientAnimation() {
|
|
||||||
if (!_gradientController.isAnimating) {
|
|
||||||
_gradientController.forward().then((_) {
|
|
||||||
_gradientController.reverse();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _animateStarsSequentially(int targetRating) async {
|
|
||||||
for (var controller in _starControllers) {
|
|
||||||
controller.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < targetRating; i++) {
|
|
||||||
if (i > 0) {
|
|
||||||
await Future.delayed(Duration(milliseconds: 120));
|
|
||||||
}
|
|
||||||
|
|
||||||
_starControllers[i].forward().then((_) {
|
|
||||||
Future.delayed(Duration(milliseconds: 100), () {
|
|
||||||
if (mounted) {
|
|
||||||
_starControllers[i].reverse().then((_) {
|
|
||||||
_starControllers[i].forward();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_reviewController.dispose();
|
|
||||||
_focusNode.dispose();
|
|
||||||
_focusController.dispose();
|
|
||||||
_buttonController.dispose();
|
|
||||||
_starController.dispose();
|
|
||||||
_gradientController.dispose();
|
|
||||||
|
|
||||||
for (var controller in _starControllers) {
|
|
||||||
controller.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AnimatedBuilder(
|
|
||||||
animation: _gradientAnimation,
|
|
||||||
builder: (context, child) {
|
|
||||||
return Transform.scale(
|
|
||||||
scale: 1.0 + (_gradientAnimation.value * 0.005),
|
|
||||||
child: AnimatedContainer(
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
margin: const EdgeInsets.symmetric(vertical: 12.0),
|
|
||||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 10),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [
|
|
||||||
AppColors.primary.withOpacity(
|
|
||||||
0.05 + (_gradientAnimation.value * 0.03),
|
|
||||||
),
|
|
||||||
AppColors.borderPrimary.withOpacity(
|
|
||||||
0.03 + (_gradientAnimation.value * 0.02),
|
|
||||||
),
|
|
||||||
AppColors.buttonPrimary.withOpacity(
|
|
||||||
0.04 + (_gradientAnimation.value * 0.02),
|
|
||||||
),
|
|
||||||
AppColors.surface,
|
|
||||||
],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
stops: [
|
|
||||||
0.0,
|
|
||||||
0.3 + (_gradientAnimation.value * 0.2),
|
|
||||||
0.7 + (_gradientAnimation.value * 0.1),
|
|
||||||
1.0,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
border: Border.all(
|
|
||||||
color:
|
|
||||||
_isFocused
|
|
||||||
? AppColors.primary.withOpacity(
|
|
||||||
0.6 + (_gradientAnimation.value * 0.2),
|
|
||||||
)
|
|
||||||
: AppColors.divider.withOpacity(
|
|
||||||
0.3 + (_gradientAnimation.value * 0.1),
|
|
||||||
),
|
|
||||||
width: _isFocused ? 1.5 : 1,
|
|
||||||
),
|
|
||||||
boxShadow: [
|
|
||||||
if (_isFocused)
|
|
||||||
BoxShadow(
|
|
||||||
color: AppColors.primary.withOpacity(
|
|
||||||
0.15 + (_gradientAnimation.value * 0.05),
|
|
||||||
),
|
|
||||||
blurRadius: 12 + (_gradientAnimation.value * 4),
|
|
||||||
spreadRadius: 1 + (_gradientAnimation.value * 0.5),
|
|
||||||
offset: const Offset(0, 4),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withOpacity(
|
|
||||||
0.03 + (_gradientAnimation.value * 0.02),
|
|
||||||
),
|
|
||||||
blurRadius: 4 + (_gradientAnimation.value * 2),
|
|
||||||
spreadRadius: 0,
|
|
||||||
offset: const Offset(0, 1),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_buildHeader(),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
if (_image != null) _buildImagePreview(),
|
|
||||||
_buildInputSection(),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildActionBar(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildHeader() {
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
flex: 3,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
_rating > 0 ? 'Thanks for rating!' : 'Rate Your Experience',
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 15,
|
|
||||||
color:
|
|
||||||
_rating > 0 ? AppColors.primary : AppColors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_rating > 0)
|
|
||||||
Text(
|
|
||||||
_getRatingText(_rating),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: AppColors.textSecondary,
|
|
||||||
fontWeight: FontWeight.w400,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(flex: 2, child: _buildAnimatedStarRating()),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildInputSection() {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color:
|
|
||||||
_isFocused
|
|
||||||
? AppColors.surface.withOpacity(0.7)
|
|
||||||
: AppColors.surface.withOpacity(0.4),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border:
|
|
||||||
_isFocused
|
|
||||||
? Border.all(color: AppColors.primary.withOpacity(0.25))
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
child: TextField(
|
|
||||||
controller: _reviewController,
|
|
||||||
focusNode: _focusNode,
|
|
||||||
maxLines: null,
|
|
||||||
minLines: 2,
|
|
||||||
keyboardType: TextInputType.multiline,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
height: 1.4,
|
|
||||||
color: AppColors.textPrimary,
|
|
||||||
),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: _getHintText(),
|
|
||||||
hintStyle: TextStyle(
|
|
||||||
color: AppColors.textSecondary.withOpacity(0.6),
|
|
||||||
fontSize: 14,
|
|
||||||
height: 1.4,
|
|
||||||
),
|
|
||||||
border: InputBorder.none,
|
|
||||||
focusedBorder: InputBorder.none,
|
|
||||||
enabledBorder: InputBorder.none,
|
|
||||||
isDense: true,
|
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildActionBar() {
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [_buildImageButton(), _buildSubmitButton()],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildImageButton() {
|
|
||||||
return AnimatedContainer(
|
|
||||||
duration: const Duration(milliseconds: 200),
|
|
||||||
child: InkWell(
|
|
||||||
onTap: _pickImage,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppColors.primary.withOpacity(0.08),
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
border: Border.all(color: AppColors.primary.withOpacity(0.15)),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
SvgPicture.asset(
|
|
||||||
Assets.icons.galleryAdd.path,
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
colorFilter: ColorFilter.mode(
|
|
||||||
AppColors.primary,
|
|
||||||
BlendMode.srcIn,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Text(
|
|
||||||
'Photo',
|
|
||||||
style: TextStyle(
|
|
||||||
color: AppColors.primary,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _pickImage() async {
|
|
||||||
final ImagePicker picker = ImagePicker();
|
|
||||||
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
|
|
||||||
if (image != null) {
|
|
||||||
setState(() => _image = image);
|
|
||||||
_buttonController.forward();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _submit() {
|
|
||||||
if (_reviewController.text.isEmpty && _image == null) return;
|
|
||||||
print(
|
|
||||||
'Rating: $_rating, Review: ${_reviewController.text}, Image: ${_image?.path}',
|
|
||||||
);
|
|
||||||
setState(() {
|
|
||||||
_rating = 0;
|
|
||||||
_reviewController.clear();
|
|
||||||
_image = null;
|
|
||||||
});
|
|
||||||
FocusScope.of(context).unfocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSubmitButton() {
|
|
||||||
return ScaleTransition(
|
|
||||||
scale: CurvedAnimation(
|
|
||||||
parent: _buttonController,
|
|
||||||
curve: Curves.elasticOut,
|
|
||||||
),
|
|
||||||
child: FadeTransition(
|
|
||||||
opacity: _buttonController,
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [AppColors.primary, AppColors.primary.withOpacity(0.8)],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: AppColors.primary.withOpacity(0.25),
|
|
||||||
blurRadius: 6,
|
|
||||||
offset: const Offset(0, 3),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: InkWell(
|
|
||||||
onTap: _submit,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
vertical: 8,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
SvgPicture.asset(
|
|
||||||
Assets.icons.send.path,
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
colorFilter: ColorFilter.mode(
|
|
||||||
AppColors.surface,
|
|
||||||
BlendMode.srcIn,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Text(
|
|
||||||
'Post',
|
|
||||||
style: TextStyle(
|
|
||||||
color: AppColors.surface,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 13,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _getRatingText(double rating) {
|
|
||||||
if (rating >= 4.5) return 'Excellent! 🎉';
|
|
||||||
if (rating >= 3.5) return 'Great! 👍';
|
|
||||||
if (rating >= 2.5) return 'Good 👌';
|
|
||||||
if (rating >= 1.5) return 'Fair 😐';
|
|
||||||
return 'Could be better 😔';
|
|
||||||
}
|
|
||||||
|
|
||||||
String _getHintText() {
|
|
||||||
if (_rating >= 4.5) {
|
|
||||||
return 'Tell others what made this experience amazing...';
|
|
||||||
} else if (_rating >= 3.5) {
|
|
||||||
return 'Share what you liked about this experience...';
|
|
||||||
} else if (_rating >= 2.5) {
|
|
||||||
return 'Share your thoughts about this experience...';
|
|
||||||
} else if (_rating >= 1.5) {
|
|
||||||
return 'Help us understand what could be improved...';
|
|
||||||
} else {
|
|
||||||
return 'Share your honest feedback to help others...';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAnimatedStarRating() {
|
|
||||||
return SizedBox(
|
|
||||||
width: 120,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: List.generate(5, (index) {
|
|
||||||
return AnimatedBuilder(
|
|
||||||
animation: _starAnimations[index],
|
|
||||||
builder: (context, child) {
|
|
||||||
final isActive = index < _rating;
|
|
||||||
final animationValue = _starAnimations[index].value;
|
|
||||||
final scaleValue =
|
|
||||||
isActive ? (1.0 + (animationValue * 0.1)) : 1.0;
|
|
||||||
|
|
||||||
return Transform.scale(
|
|
||||||
scale: scaleValue,
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () {
|
|
||||||
setState(() => _rating = index + 1.0);
|
|
||||||
_animateStarsSequentially(index + 1);
|
|
||||||
_starController.forward().then(
|
|
||||||
(_) => _starController.reverse(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
child: Container(
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
child: Stack(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
children: [
|
|
||||||
if (isActive && animationValue > 0.5)
|
|
||||||
Container(
|
|
||||||
width: 14,
|
|
||||||
height: 14,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.green.withOpacity(0.3),
|
|
||||||
blurRadius: 3,
|
|
||||||
spreadRadius: 1,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
SvgPicture.asset(
|
|
||||||
isActive
|
|
||||||
? Assets.icons.starFill.path
|
|
||||||
: Assets.icons.star.path,
|
|
||||||
width: 13,
|
|
||||||
height: 13,
|
|
||||||
// colorFilter: ColorFilter.mode(
|
|
||||||
// isActive
|
|
||||||
// ? AppColors.primary
|
|
||||||
// : AppColors.textSecondary.withOpacity(0.4),
|
|
||||||
// BlendMode.srcIn,
|
|
||||||
// ),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildImagePreview() {
|
|
||||||
return TweenAnimationBuilder<double>(
|
|
||||||
duration: const Duration(milliseconds: 400),
|
|
||||||
tween: Tween(begin: 0.0, end: 1.0),
|
|
||||||
curve: Curves.elasticOut,
|
|
||||||
builder: (context, value, child) {
|
|
||||||
return Transform.scale(
|
|
||||||
scale: value,
|
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.only(bottom: 10.0),
|
|
||||||
padding: const EdgeInsets.all(3),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppColors.surface,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
border: Border.all(
|
|
||||||
color: AppColors.primary.withOpacity(0.15),
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: AppColors.primary.withOpacity(0.08),
|
|
||||||
blurRadius: 4,
|
|
||||||
offset: const Offset(0, 1),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
child: Image.file(
|
|
||||||
File(_image!.path),
|
|
||||||
width: 60,
|
|
||||||
height: 60,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
top: -1,
|
|
||||||
right: -1,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () => setState(() => _image = null),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(3),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.red.withOpacity(0.85),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withOpacity(0.2),
|
|
||||||
blurRadius: 2,
|
|
||||||
offset: const Offset(0, 1),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.close,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,7 +4,6 @@ import 'package:lba/gen/assets.gen.dart';
|
||||||
import 'package:lba/res/colors.dart';
|
import 'package:lba/res/colors.dart';
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import 'package:google_sign_in/google_sign_in.dart';
|
import 'package:google_sign_in/google_sign_in.dart';
|
||||||
import 'package:lba/screens/auth/login_page.dart';
|
|
||||||
|
|
||||||
Future<void> showLogoutDialog(BuildContext context) async {
|
Future<void> showLogoutDialog(BuildContext context) async {
|
||||||
showDialog(
|
showDialog(
|
||||||
|
|
@ -122,22 +121,11 @@ class _AnimatedLogoutDialogState extends State<_AnimatedLogoutDialog>
|
||||||
await GoogleSignIn().signOut();
|
await GoogleSignIn().signOut();
|
||||||
await FirebaseAuth.instance.signOut();
|
await FirebaseAuth.instance.signOut();
|
||||||
print('✅ Logout successful');
|
print('✅ Logout successful');
|
||||||
|
|
||||||
if (context.mounted) {
|
|
||||||
Navigator.of(context).pushAndRemoveUntil(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => const LoginPage(),
|
|
||||||
),
|
|
||||||
(Route<dynamic> route) => false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('❌ Logout error: $e');
|
print('❌ Logout error: $e');
|
||||||
if (context.mounted) {
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
SnackBar(content: Text('خطا در خروج: $e')),
|
||||||
SnackBar(content: Text('خطا در خروج: $e')),
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,7 @@ class PriceReserveWidget extends StatelessWidget {
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 3),
|
||||||
horizontal: 12,
|
|
||||||
vertical: 3,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.offerTimer,
|
color: AppColors.offerTimer,
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
|
@ -83,7 +80,7 @@ class PriceReserveWidget extends StatelessWidget {
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
|
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 150,
|
width: 200,
|
||||||
height: 50,
|
height: 50,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
|
@ -99,10 +96,7 @@ class PriceReserveWidget extends StatelessWidget {
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||||
horizontal: 20,
|
|
||||||
vertical: 12,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Reserve',
|
'Reserve',
|
||||||
|
|
@ -118,4 +112,4 @@ class PriceReserveWidget extends StatelessWidget {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5,17 +5,14 @@ import 'package:lba/res/colors.dart';
|
||||||
import 'package:lba/widgets/buildWarpedInfo.dart';
|
import 'package:lba/widgets/buildWarpedInfo.dart';
|
||||||
import 'package:lba/widgets/rate.dart';
|
import 'package:lba/widgets/rate.dart';
|
||||||
|
|
||||||
enum LikeState { none, liked, disliked }
|
class Reviews extends StatelessWidget {
|
||||||
|
|
||||||
class Reviews extends StatefulWidget {
|
|
||||||
final String name;
|
final String name;
|
||||||
final String comment;
|
final String comment;
|
||||||
final double rate;
|
final double rate;
|
||||||
final int yesCount;
|
final int yesCount;
|
||||||
final int noCount;
|
final int noCount;
|
||||||
final String date;
|
final String date;
|
||||||
final Function(bool isLike)? onLikeDislike;
|
|
||||||
final bool? initialLikeState;
|
|
||||||
|
|
||||||
const Reviews({
|
const Reviews({
|
||||||
super.key,
|
super.key,
|
||||||
|
|
@ -25,483 +22,7 @@ class Reviews extends StatefulWidget {
|
||||||
required this.yesCount,
|
required this.yesCount,
|
||||||
required this.noCount,
|
required this.noCount,
|
||||||
required this.date,
|
required this.date,
|
||||||
this.onLikeDislike,
|
});
|
||||||
this.initialLikeState,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<Reviews> createState() => _ReviewsState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ReviewsState extends State<Reviews> with TickerProviderStateMixin {
|
|
||||||
LikeState _likeState = LikeState.none;
|
|
||||||
int _currentYesCount = 0;
|
|
||||||
int _currentNoCount = 0;
|
|
||||||
|
|
||||||
late AnimationController _likeScaleController;
|
|
||||||
late AnimationController _dislikeScaleController;
|
|
||||||
late AnimationController _particleController;
|
|
||||||
late AnimationController _pulseController;
|
|
||||||
late AnimationController _countController;
|
|
||||||
late AnimationController _likeIconController;
|
|
||||||
late AnimationController _dislikeIconController;
|
|
||||||
|
|
||||||
late Animation<double> _likeScaleAnimation;
|
|
||||||
late Animation<double> _dislikeScaleAnimation;
|
|
||||||
late Animation<double> _particleAnimation;
|
|
||||||
late Animation<double> _pulseAnimation;
|
|
||||||
late Animation<double> _countAnimation;
|
|
||||||
late Animation<Color?> _likeColorAnimation;
|
|
||||||
late Animation<Color?> _dislikeColorAnimation;
|
|
||||||
late Animation<double> _likeIconRotationAnimation;
|
|
||||||
late Animation<double> _dislikeIconRotationAnimation;
|
|
||||||
late Animation<double> _likeIconScaleAnimation;
|
|
||||||
late Animation<double> _dislikeIconScaleAnimation;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_currentYesCount = widget.yesCount;
|
|
||||||
_currentNoCount = widget.noCount;
|
|
||||||
|
|
||||||
if (widget.initialLikeState == true) {
|
|
||||||
_likeState = LikeState.liked;
|
|
||||||
} else if (widget.initialLikeState == false) {
|
|
||||||
_likeState = LikeState.disliked;
|
|
||||||
}
|
|
||||||
|
|
||||||
_likeScaleController = AnimationController(
|
|
||||||
duration: const Duration(milliseconds: 600),
|
|
||||||
vsync: this,
|
|
||||||
);
|
|
||||||
_dislikeScaleController = AnimationController(
|
|
||||||
duration: const Duration(milliseconds: 600),
|
|
||||||
vsync: this,
|
|
||||||
);
|
|
||||||
_particleController = AnimationController(
|
|
||||||
duration: const Duration(milliseconds: 1200),
|
|
||||||
vsync: this,
|
|
||||||
);
|
|
||||||
_pulseController = AnimationController(
|
|
||||||
duration: const Duration(milliseconds: 800),
|
|
||||||
vsync: this,
|
|
||||||
);
|
|
||||||
_countController = AnimationController(
|
|
||||||
duration: const Duration(milliseconds: 400),
|
|
||||||
vsync: this,
|
|
||||||
);
|
|
||||||
_likeIconController = AnimationController(
|
|
||||||
duration: const Duration(milliseconds: 500),
|
|
||||||
vsync: this,
|
|
||||||
);
|
|
||||||
_dislikeIconController = AnimationController(
|
|
||||||
duration: const Duration(milliseconds: 500),
|
|
||||||
vsync: this,
|
|
||||||
);
|
|
||||||
|
|
||||||
_likeScaleAnimation = Tween<double>(begin: 1.0, end: 1.08).animate(
|
|
||||||
CurvedAnimation(parent: _likeScaleController, curve: Curves.easeOutBack),
|
|
||||||
);
|
|
||||||
|
|
||||||
_dislikeScaleAnimation = Tween<double>(begin: 1.0, end: 1.08).animate(
|
|
||||||
CurvedAnimation(
|
|
||||||
parent: _dislikeScaleController,
|
|
||||||
curve: Curves.easeOutBack,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
_particleAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
|
||||||
CurvedAnimation(parent: _particleController, curve: Curves.easeOutCubic),
|
|
||||||
);
|
|
||||||
|
|
||||||
_pulseAnimation = Tween<double>(begin: 1.0, end: 1.15).animate(
|
|
||||||
CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut),
|
|
||||||
);
|
|
||||||
|
|
||||||
_countAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
|
||||||
CurvedAnimation(parent: _countController, curve: Curves.bounceOut),
|
|
||||||
);
|
|
||||||
|
|
||||||
_likeColorAnimation = ColorTween(
|
|
||||||
begin: AppColors.hint,
|
|
||||||
end: const Color(0xFF4CAF50),
|
|
||||||
).animate(_likeScaleController);
|
|
||||||
|
|
||||||
_dislikeColorAnimation = ColorTween(
|
|
||||||
begin: AppColors.hint,
|
|
||||||
end: const Color(0xFFF44336),
|
|
||||||
).animate(_dislikeScaleController);
|
|
||||||
|
|
||||||
_likeIconRotationAnimation = Tween<double>(begin: 0.0, end: 0.1).animate(
|
|
||||||
CurvedAnimation(parent: _likeIconController, curve: Curves.elasticOut),
|
|
||||||
);
|
|
||||||
|
|
||||||
_dislikeIconRotationAnimation = Tween<double>(
|
|
||||||
begin: 0.0,
|
|
||||||
end: -0.1,
|
|
||||||
).animate(
|
|
||||||
CurvedAnimation(parent: _dislikeIconController, curve: Curves.elasticOut),
|
|
||||||
);
|
|
||||||
|
|
||||||
_likeIconScaleAnimation = Tween<double>(begin: 1.0, end: 1.3).animate(
|
|
||||||
CurvedAnimation(parent: _likeIconController, curve: Curves.elasticOut),
|
|
||||||
);
|
|
||||||
|
|
||||||
_dislikeIconScaleAnimation = Tween<double>(begin: 1.0, end: 1.3).animate(
|
|
||||||
CurvedAnimation(parent: _dislikeIconController, curve: Curves.elasticOut),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_likeScaleController.dispose();
|
|
||||||
_dislikeScaleController.dispose();
|
|
||||||
_particleController.dispose();
|
|
||||||
_pulseController.dispose();
|
|
||||||
_countController.dispose();
|
|
||||||
_likeIconController.dispose();
|
|
||||||
_dislikeIconController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onLikeTap() async {
|
|
||||||
if (_likeState == LikeState.liked) {
|
|
||||||
setState(() {
|
|
||||||
_likeState = LikeState.none;
|
|
||||||
_currentYesCount--;
|
|
||||||
});
|
|
||||||
_likeScaleController.reverse();
|
|
||||||
} else {
|
|
||||||
if (_likeState == LikeState.disliked) {
|
|
||||||
_currentNoCount--;
|
|
||||||
_dislikeScaleController.reverse();
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_likeState = LikeState.liked;
|
|
||||||
_currentYesCount++;
|
|
||||||
});
|
|
||||||
|
|
||||||
_likeScaleController.forward();
|
|
||||||
_likeIconController.forward().then((_) => _likeIconController.reverse());
|
|
||||||
_particleController.forward().then((_) => _particleController.reset());
|
|
||||||
_pulseController.forward().then((_) => _pulseController.reverse());
|
|
||||||
_countController.forward().then((_) => _countController.reset());
|
|
||||||
}
|
|
||||||
|
|
||||||
widget.onLikeDislike?.call(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onDislikeTap() async {
|
|
||||||
if (_likeState == LikeState.disliked) {
|
|
||||||
setState(() {
|
|
||||||
_likeState = LikeState.none;
|
|
||||||
_currentNoCount--;
|
|
||||||
});
|
|
||||||
_dislikeScaleController.reverse();
|
|
||||||
} else {
|
|
||||||
if (_likeState == LikeState.liked) {
|
|
||||||
_currentYesCount--;
|
|
||||||
_likeScaleController.reverse();
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_likeState = LikeState.disliked;
|
|
||||||
_currentNoCount++;
|
|
||||||
});
|
|
||||||
|
|
||||||
_dislikeScaleController.forward();
|
|
||||||
_dislikeIconController.forward().then(
|
|
||||||
(_) => _dislikeIconController.reverse(),
|
|
||||||
);
|
|
||||||
_particleController.forward().then((_) => _particleController.reset());
|
|
||||||
_pulseController.forward().then((_) => _pulseController.reverse());
|
|
||||||
_countController.forward().then((_) => _countController.reset());
|
|
||||||
}
|
|
||||||
|
|
||||||
widget.onLikeDislike?.call(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildParticleEffect({required bool isLike}) {
|
|
||||||
return AnimatedBuilder(
|
|
||||||
animation: _particleAnimation,
|
|
||||||
builder: (context, child) {
|
|
||||||
return CustomPaint(
|
|
||||||
painter: ParticleEffectPainter(
|
|
||||||
progress: _particleAnimation.value,
|
|
||||||
color: isLike ? const Color(0xFF4CAF50) : const Color(0xFFF44336),
|
|
||||||
),
|
|
||||||
size: const Size(10, 10),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAnimatedLikeButton() {
|
|
||||||
return AnimatedBuilder(
|
|
||||||
animation: Listenable.merge([
|
|
||||||
_likeScaleController,
|
|
||||||
_pulseController,
|
|
||||||
_countController,
|
|
||||||
]),
|
|
||||||
builder: (context, child) {
|
|
||||||
final isLiked = _likeState == LikeState.liked;
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: _onLikeTap,
|
|
||||||
child: Stack(
|
|
||||||
clipBehavior: Clip.none,
|
|
||||||
children: [
|
|
||||||
if (isLiked)
|
|
||||||
Positioned(
|
|
||||||
child: Transform.scale(
|
|
||||||
scale: _pulseAnimation.value,
|
|
||||||
child: Container(
|
|
||||||
width: 30,
|
|
||||||
height: 25,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(15),
|
|
||||||
color: _likeColorAnimation.value?.withOpacity(0.2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
Transform.scale(
|
|
||||||
scale: _likeScaleAnimation.value,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 8,
|
|
||||||
vertical: 4,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(15),
|
|
||||||
color:
|
|
||||||
isLiked
|
|
||||||
? _likeColorAnimation.value?.withOpacity(0.1)
|
|
||||||
: null,
|
|
||||||
border:
|
|
||||||
isLiked
|
|
||||||
? Border.all(
|
|
||||||
color:
|
|
||||||
_likeColorAnimation.value ??
|
|
||||||
Colors.transparent,
|
|
||||||
width: 1.5,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
AnimatedBuilder(
|
|
||||||
animation: Listenable.merge([
|
|
||||||
_likeColorAnimation,
|
|
||||||
_likeIconController,
|
|
||||||
]),
|
|
||||||
builder: (context, child) {
|
|
||||||
return Transform.rotate(
|
|
||||||
angle: _likeIconRotationAnimation.value,
|
|
||||||
child: Transform.scale(
|
|
||||||
scale:
|
|
||||||
isLiked ? _likeIconScaleAnimation.value : 1.0,
|
|
||||||
child: AnimatedSwitcher(
|
|
||||||
duration: const Duration(milliseconds: 200),
|
|
||||||
child: SvgPicture.asset(
|
|
||||||
isLiked
|
|
||||||
? Assets.icons.like.path
|
|
||||||
: Assets.icons.like.path,
|
|
||||||
key: ValueKey(isLiked ? 'liked' : 'normal'),
|
|
||||||
color:
|
|
||||||
isLiked
|
|
||||||
? _likeColorAnimation.value
|
|
||||||
: AppColors.hint,
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
AnimatedBuilder(
|
|
||||||
animation: _countAnimation,
|
|
||||||
builder: (context, child) {
|
|
||||||
return Transform.scale(
|
|
||||||
scale: 1.0 + (_countAnimation.value * 0.1),
|
|
||||||
child: AnimatedBuilder(
|
|
||||||
animation: _likeColorAnimation,
|
|
||||||
builder: (context, child) {
|
|
||||||
return Text(
|
|
||||||
"Yes ($_currentYesCount)",
|
|
||||||
style: TextStyle(
|
|
||||||
color:
|
|
||||||
isLiked
|
|
||||||
? _likeColorAnimation.value
|
|
||||||
: AppColors.hint,
|
|
||||||
fontWeight:
|
|
||||||
isLiked
|
|
||||||
? FontWeight.w600
|
|
||||||
: FontWeight.normal,
|
|
||||||
fontSize: 12 + (_countAnimation.value * 1),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
if (isLiked)
|
|
||||||
Positioned(
|
|
||||||
top: -5,
|
|
||||||
right: -5,
|
|
||||||
child: _buildParticleEffect(isLike: true),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAnimatedDislikeButton() {
|
|
||||||
return AnimatedBuilder(
|
|
||||||
animation: Listenable.merge([
|
|
||||||
_dislikeScaleController,
|
|
||||||
_pulseController,
|
|
||||||
_countController,
|
|
||||||
]),
|
|
||||||
builder: (context, child) {
|
|
||||||
final isDisliked = _likeState == LikeState.disliked;
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: _onDislikeTap,
|
|
||||||
child: Stack(
|
|
||||||
clipBehavior: Clip.none,
|
|
||||||
children: [
|
|
||||||
if (isDisliked)
|
|
||||||
Positioned(
|
|
||||||
child: Transform.scale(
|
|
||||||
scale: _pulseAnimation.value,
|
|
||||||
child: Container(
|
|
||||||
width: 30,
|
|
||||||
height: 25,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(15),
|
|
||||||
color: _dislikeColorAnimation.value?.withOpacity(0.2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
Transform.scale(
|
|
||||||
scale: _dislikeScaleAnimation.value,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 8,
|
|
||||||
vertical: 4,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(15),
|
|
||||||
color:
|
|
||||||
isDisliked
|
|
||||||
? _dislikeColorAnimation.value?.withOpacity(0.1)
|
|
||||||
: null,
|
|
||||||
border:
|
|
||||||
isDisliked
|
|
||||||
? Border.all(
|
|
||||||
color:
|
|
||||||
_dislikeColorAnimation.value ??
|
|
||||||
Colors.transparent,
|
|
||||||
width: 1.5,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
AnimatedBuilder(
|
|
||||||
animation: Listenable.merge([
|
|
||||||
_dislikeColorAnimation,
|
|
||||||
_dislikeIconController,
|
|
||||||
]),
|
|
||||||
builder: (context, child) {
|
|
||||||
return Transform.rotate(
|
|
||||||
angle: _dislikeIconRotationAnimation.value,
|
|
||||||
child: Transform.scale(
|
|
||||||
scale:
|
|
||||||
isDisliked
|
|
||||||
? _dislikeIconScaleAnimation.value
|
|
||||||
: 1.0,
|
|
||||||
child: AnimatedSwitcher(
|
|
||||||
duration: const Duration(milliseconds: 200),
|
|
||||||
child: SvgPicture.asset(
|
|
||||||
isDisliked
|
|
||||||
? Assets.icons.dislike.path
|
|
||||||
: Assets.icons.dislike.path,
|
|
||||||
key: ValueKey(
|
|
||||||
isDisliked ? 'disliked' : 'normal',
|
|
||||||
),
|
|
||||||
color:
|
|
||||||
isDisliked
|
|
||||||
? _dislikeColorAnimation.value
|
|
||||||
: AppColors.hint,
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
AnimatedBuilder(
|
|
||||||
animation: _countAnimation,
|
|
||||||
builder: (context, child) {
|
|
||||||
return Transform.scale(
|
|
||||||
scale: 1.0 + (_countAnimation.value * 0.1),
|
|
||||||
child: AnimatedBuilder(
|
|
||||||
animation: _dislikeColorAnimation,
|
|
||||||
builder: (context, child) {
|
|
||||||
return Text(
|
|
||||||
"No ($_currentNoCount)",
|
|
||||||
style: TextStyle(
|
|
||||||
color:
|
|
||||||
isDisliked
|
|
||||||
? _dislikeColorAnimation.value
|
|
||||||
: AppColors.hint,
|
|
||||||
fontWeight:
|
|
||||||
isDisliked
|
|
||||||
? FontWeight.w600
|
|
||||||
: FontWeight.normal,
|
|
||||||
fontSize: 12 + (_countAnimation.value * 1),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
if (isDisliked)
|
|
||||||
Positioned(
|
|
||||||
top: -5,
|
|
||||||
right: -5,
|
|
||||||
child: _buildParticleEffect(isLike: false),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -517,8 +38,8 @@ class _ReviewsState extends State<Reviews> with TickerProviderStateMixin {
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(widget.name),
|
Text(name),
|
||||||
const SizedBox(width: 2),
|
SizedBox(width: 2),
|
||||||
Image.asset(Assets.images.usa.path),
|
Image.asset(Assets.images.usa.path),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -528,68 +49,54 @@ class _ReviewsState extends State<Reviews> with TickerProviderStateMixin {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
CustomStarRating(rating: widget.rate),
|
CustomStarRating(rating: rate),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 7),
|
SizedBox(height: 7),
|
||||||
buildWrappedInfo("", widget.comment),
|
buildWrappedInfo(
|
||||||
const SizedBox(height: 10),
|
"",
|
||||||
|
comment,
|
||||||
|
),
|
||||||
|
SizedBox(height: 10,),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
_buildAnimatedLikeButton(),
|
InkWell(
|
||||||
const SizedBox(width: 7),
|
child: Row(
|
||||||
_buildAnimatedDislikeButton(),
|
children: [
|
||||||
|
SvgPicture.asset(Assets.icons.like.path),
|
||||||
|
SizedBox(width: 1,),
|
||||||
|
Text("Yes (${yesCount.toString()})",style: TextStyle(
|
||||||
|
color: AppColors.hint,
|
||||||
|
),),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 10,),
|
||||||
|
InkWell(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
SvgPicture.asset(Assets.icons.dislike.path),
|
||||||
|
SizedBox(width: 1,),
|
||||||
|
Text("No (${noCount.toString()})",style: TextStyle(
|
||||||
|
color: AppColors.hint,
|
||||||
|
),),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Text(widget.date, style: TextStyle(color: AppColors.hint)),
|
Text(date,style: TextStyle(
|
||||||
|
color: AppColors.hint
|
||||||
|
),)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 5),
|
SizedBox(height: 5,),
|
||||||
const Divider(thickness: 1.2, height: 2),
|
Divider(thickness: 1.2, height: 2),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ParticleEffectPainter extends CustomPainter {
|
|
||||||
final double progress;
|
|
||||||
final Color color;
|
|
||||||
|
|
||||||
ParticleEffectPainter({required this.progress, required this.color});
|
|
||||||
|
|
||||||
@override
|
|
||||||
void paint(Canvas canvas, Size size) {
|
|
||||||
if (progress == 0.0) return;
|
|
||||||
|
|
||||||
final paint =
|
|
||||||
Paint()
|
|
||||||
..color = color.withOpacity(1.0 - progress)
|
|
||||||
..style = PaintingStyle.fill;
|
|
||||||
|
|
||||||
final particles = [
|
|
||||||
[0.2, -0.3, 3.0],
|
|
||||||
[0.5, -0.5, 2.5],
|
|
||||||
[0.8, -0.2, 2.0],
|
|
||||||
[0.1, -0.6, 1.5],
|
|
||||||
[0.9, -0.4, 2.0],
|
|
||||||
];
|
|
||||||
|
|
||||||
for (final particle in particles) {
|
|
||||||
final x =
|
|
||||||
size.width * particle[0] + (progress * 20 * (particle[0] - 0.5));
|
|
||||||
final y = size.height * particle[1] * progress;
|
|
||||||
final particleSize = particle[2] * (1.0 - progress * 0.5);
|
|
||||||
|
|
||||||
canvas.drawCircle(Offset(x, y), particleSize, paint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool shouldRepaint(ParticleEffectPainter oldDelegate) {
|
|
||||||
return oldDelegate.progress != progress;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue