Compare commits
40 Commits
28db4ce298
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| f182c80210 | |||
| cefabd1c70 | |||
| f1055d40a4 | |||
| cfe0184e3a | |||
| faa52dcaa2 | |||
| 946d2ada41 | |||
| 7cb92a7b83 | |||
| dd59a605ad | |||
| 34c0ea5fa1 | |||
| 893b925a04 | |||
| bdcd4507c2 | |||
| fb5538ab79 | |||
| 0a22b5c051 | |||
| 73fe273c75 | |||
| f5e1407281 | |||
| 27da063c34 | |||
| c0c3ef2ca0 | |||
| 9335e8e694 | |||
| 04ee6d1699 | |||
| 65c8f56e20 | |||
| 0170505376 | |||
| 15105a7f33 | |||
| 1e5b235a6c | |||
| eaa8b138a4 | |||
| db39169907 | |||
| c8965dab4e | |||
| 97664fdb5a | |||
| cfa6ef9a67 | |||
| 7335c55703 | |||
| 1eeff4209e | |||
| 3b593ad733 | |||
| 0438a6feec | |||
| d6f64465b3 | |||
| e57c7dc0ea | |||
| e7073cec67 | |||
| 80da7e9008 | |||
| 7907dcf6c2 | |||
| 46ba11cd57 | |||
| d8e68f9b34 | |||
| e95fb08e31 |
@@ -23,7 +23,7 @@ if (flutterVersionName == null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace "com.example.flutter_application_1"
|
namespace "com.example.gym_app"
|
||||||
compileSdk flutter.compileSdkVersion
|
compileSdk flutter.compileSdkVersion
|
||||||
ndkVersion flutter.ndkVersion
|
ndkVersion flutter.ndkVersion
|
||||||
|
|
||||||
@@ -42,11 +42,11 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
applicationId "com.example.flutter_application_1"
|
applicationId "com.example.gym_app"
|
||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion flutter.targetSdkVersion
|
targetSdkVersion 34
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<application
|
<application
|
||||||
android:label="flutter_application_1"
|
android:label="Example Gym App"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<activity
|
<activity
|
||||||
@@ -10,6 +11,7 @@
|
|||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
the Android process has started. This theme is visible to the user
|
the Android process has started. This theme is visible to the user
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.flutter_application_1
|
package com.example.gym_app
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
@@ -22,6 +22,11 @@ class GymLinkAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
width: 24,
|
width: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
semanticsLabel: 'GymLink Logo',
|
semanticsLabel: 'GymLink Logo',
|
||||||
|
colorFilter: ColorFilter.mode(
|
||||||
|
Theme.of(context).brightness == Brightness.dark
|
||||||
|
? Colors.white
|
||||||
|
: Colors.black,
|
||||||
|
BlendMode.srcIn),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Align(
|
Align(
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gymlink_module_web/pages/detail.dart';
|
||||||
|
import 'package:gymlink_module_web/tools/routes.dart';
|
||||||
|
|
||||||
class BasketItemCard extends StatelessWidget {
|
class BasketItemCard extends StatelessWidget {
|
||||||
final String name;
|
final String name;
|
||||||
final String price;
|
final String price;
|
||||||
final String id;
|
final String id;
|
||||||
final Image image;
|
final Widget image;
|
||||||
final String quantity;
|
final String quantity;
|
||||||
final VoidCallback onTapPlus;
|
final VoidCallback onTapPlus;
|
||||||
final VoidCallback onTapMinus;
|
final VoidCallback onTapMinus;
|
||||||
@@ -32,52 +34,58 @@ class BasketItemCard extends StatelessWidget {
|
|||||||
minWidth: 400,
|
minWidth: 400,
|
||||||
maxWidth: 600,
|
maxWidth: 600,
|
||||||
),
|
),
|
||||||
child: Card(
|
child: GestureDetector(
|
||||||
elevation: 4,
|
onTap: () {
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
Navigator.of(context).push(
|
||||||
shape: RoundedRectangleBorder(
|
CustomPageRoute(builder: (context) => DetailPage(id: id)));
|
||||||
borderRadius: BorderRadius.circular(30),
|
},
|
||||||
),
|
child: Card(
|
||||||
child: Padding(
|
elevation: 4,
|
||||||
padding: const EdgeInsetsDirectional.symmetric(horizontal: 20),
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
child: Row(
|
shape: RoundedRectangleBorder(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
borderRadius: BorderRadius.circular(30),
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
),
|
||||||
children: [
|
child: Padding(
|
||||||
Row(
|
padding: const EdgeInsetsDirectional.symmetric(horizontal: 20),
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
child: Row(
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
image,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
const SizedBox(width: 20),
|
children: [
|
||||||
Column(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
image,
|
||||||
name,
|
const SizedBox(width: 20),
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
Column(
|
||||||
),
|
mainAxisSize: MainAxisSize.min,
|
||||||
Text('\$$price'),
|
children: [
|
||||||
],
|
Text(
|
||||||
)
|
name,
|
||||||
],
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
),
|
),
|
||||||
Row(
|
Text('$price руб.'),
|
||||||
mainAxisSize: MainAxisSize.min,
|
],
|
||||||
children: [
|
)
|
||||||
IconButton(
|
],
|
||||||
icon: const Icon(Icons.remove),
|
),
|
||||||
onPressed: onTapMinus,
|
Row(
|
||||||
),
|
mainAxisSize: MainAxisSize.min,
|
||||||
const SizedBox(width: 10),
|
children: [
|
||||||
Text(quantity),
|
IconButton(
|
||||||
const SizedBox(width: 10),
|
icon: const Icon(Icons.remove),
|
||||||
IconButton(
|
onPressed: onTapMinus,
|
||||||
icon: const Icon(Icons.add),
|
),
|
||||||
onPressed: onTapPlus,
|
const SizedBox(width: 10),
|
||||||
),
|
Text(quantity),
|
||||||
],
|
const SizedBox(width: 10),
|
||||||
)
|
IconButton(
|
||||||
],
|
icon: const Icon(Icons.add),
|
||||||
|
onPressed: onTapPlus,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gymlink_module_web/pages/main.dart';
|
||||||
|
import 'package:gymlink_module_web/tools/routes.dart';
|
||||||
|
|
||||||
class GymLinkHeader extends StatelessWidget {
|
class GymLinkHeader extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
const GymLinkHeader({super.key, required this.title});
|
final bool toMain;
|
||||||
|
const GymLinkHeader({super.key, required this.title, this.toMain = false});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -13,7 +16,13 @@ class GymLinkHeader extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => toMain
|
||||||
|
? Navigator.pushAndRemoveUntil(
|
||||||
|
context,
|
||||||
|
CustomPageRoute(
|
||||||
|
builder: (context) => const MainPage()),
|
||||||
|
(route) => route.isFirst)
|
||||||
|
: Navigator.pop(context),
|
||||||
icon: const Icon(Icons.arrow_back)),
|
icon: const Icon(Icons.arrow_back)),
|
||||||
Text(title, style: Theme.of(context).textTheme.titleLarge),
|
Text(title, style: Theme.of(context).textTheme.titleLarge),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
|
import 'package:gymlink_module_web/pages/order_info.dart';
|
||||||
|
import 'package:gymlink_module_web/tools/routes.dart';
|
||||||
|
|
||||||
enum OrderStatus { created, inProgress, completed, canceled }
|
enum OrderStatus { created, inProgress, completed, canceled }
|
||||||
|
|
||||||
@@ -14,15 +16,13 @@ class HistoryItemCard extends StatelessWidget {
|
|||||||
final String id;
|
final String id;
|
||||||
final String cost;
|
final String cost;
|
||||||
final String date;
|
final String date;
|
||||||
final Image image;
|
final Widget image;
|
||||||
final OrderStatus status;
|
|
||||||
|
|
||||||
const HistoryItemCard({
|
const HistoryItemCard({
|
||||||
super.key,
|
super.key,
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.cost,
|
required this.cost,
|
||||||
required this.date,
|
required this.date,
|
||||||
required this.status,
|
|
||||||
required this.image,
|
required this.image,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -38,38 +38,42 @@ class HistoryItemCard extends StatelessWidget {
|
|||||||
minWidth: 600,
|
minWidth: 600,
|
||||||
maxWidth: 800,
|
maxWidth: 800,
|
||||||
),
|
),
|
||||||
child: Card(
|
child: GestureDetector(
|
||||||
elevation: 4,
|
onTap: () {
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
Navigator.of(context).push(
|
||||||
shape: RoundedRectangleBorder(
|
CustomPageRoute(builder: (context) => OrderInfoPage(id: id)));
|
||||||
borderRadius: BorderRadius.circular(30),
|
},
|
||||||
),
|
child: Card(
|
||||||
child: Padding(
|
elevation: 4,
|
||||||
padding: const EdgeInsetsDirectional.symmetric(horizontal: 20),
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
child: Row(
|
shape: RoundedRectangleBorder(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
borderRadius: BorderRadius.circular(30),
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
),
|
||||||
children: [
|
child: Padding(
|
||||||
Row(
|
padding: const EdgeInsetsDirectional.symmetric(horizontal: 20),
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
child: Row(
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
image,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
const SizedBox(width: 20),
|
children: [
|
||||||
Column(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
image,
|
||||||
MarkdownBody(
|
const SizedBox(width: 20),
|
||||||
data: '### Заказ **№$id** от $date',
|
Column(
|
||||||
),
|
mainAxisSize: MainAxisSize.min,
|
||||||
MarkdownBody(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
data: 'Статус: **${orderStatusMap[status]}**'),
|
children: [
|
||||||
MarkdownBody(data: 'Сумма: **\$$cost**'),
|
MarkdownBody(
|
||||||
],
|
data: '### Заказ **№$id** от $date',
|
||||||
)
|
),
|
||||||
],
|
MarkdownBody(data: 'Сумма: **$cost руб.**'),
|
||||||
),
|
],
|
||||||
],
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class ProductCard extends StatelessWidget {
|
class ProductCard extends StatelessWidget {
|
||||||
final Image imagePath;
|
final Widget imagePath;
|
||||||
final String name;
|
final String name;
|
||||||
final String price;
|
final String price;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
@@ -15,10 +15,10 @@ class ProductCard extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
double getCardHeight({required BuildContext context}) {
|
double getCardHeight({required BuildContext context}) {
|
||||||
if (MediaQuery.of(context).size.width > 600) {
|
if (MediaQuery.of(context).size.width > 400) {
|
||||||
return 200;
|
return 300;
|
||||||
}
|
}
|
||||||
return 100;
|
return 160;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -27,7 +27,10 @@ class ProductCard extends StatelessWidget {
|
|||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
minHeight: 80, maxHeight: getCardHeight(context: context)),
|
minHeight: 160,
|
||||||
|
maxHeight: getCardHeight(context: context),
|
||||||
|
minWidth: 180,
|
||||||
|
maxWidth: 250),
|
||||||
child: Card(
|
child: Card(
|
||||||
elevation: 3,
|
elevation: 3,
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
@@ -42,11 +45,12 @@ class ProductCard extends StatelessWidget {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
name,
|
name,
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
maxLines: 2,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'\$$price',
|
'$price руб.',
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class OrderConfirmItemCard extends StatelessWidget {
|
|||||||
final String name;
|
final String name;
|
||||||
final int count;
|
final int count;
|
||||||
final double price;
|
final double price;
|
||||||
final Image image;
|
final Widget image;
|
||||||
|
|
||||||
const OrderConfirmItemCard({
|
const OrderConfirmItemCard({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -46,7 +46,8 @@ class OrderConfirmItemCard extends StatelessWidget {
|
|||||||
name,
|
name,
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
),
|
),
|
||||||
Text('\$$price x $count = \$${price * count}'),
|
Text(
|
||||||
|
'${price.toStringAsFixed(2)} руб. x $count = ${(price * count).toStringAsFixed(2)} руб.'),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|||||||
58
lib/components/order_detail_item_card.dart
Normal file
58
lib/components/order_detail_item_card.dart
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
|
|
||||||
|
class OrderDetailCardItemCard extends StatelessWidget {
|
||||||
|
final String name;
|
||||||
|
final int count;
|
||||||
|
final double price;
|
||||||
|
final Widget image;
|
||||||
|
const OrderDetailCardItemCard(
|
||||||
|
{super.key,
|
||||||
|
required this.image,
|
||||||
|
required this.name,
|
||||||
|
required this.count,
|
||||||
|
required this.price});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsetsDirectional.symmetric(horizontal: 10, vertical: 10),
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(minHeight: 130),
|
||||||
|
child: Card(
|
||||||
|
elevation: 4,
|
||||||
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(30),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsetsDirectional.symmetric(horizontal: 20),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
image,
|
||||||
|
const SizedBox(width: 20),
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
MarkdownBody(data: '# $name'),
|
||||||
|
Text(
|
||||||
|
'${price.toStringAsFixed(2)} руб. x $count = ${(price * count).toStringAsFixed(2)} руб.'),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
MarkdownBody(data: '# X$count')
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
325
lib/interfaces/items.dart
Normal file
325
lib/interfaces/items.dart
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class ItemsDataResponse {
|
||||||
|
final List<GymItem> rows;
|
||||||
|
|
||||||
|
ItemsDataResponse({
|
||||||
|
required this.rows,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ItemsDataResponse.fromRawJson(String str) =>
|
||||||
|
ItemsDataResponse.fromJson(json.decode(str));
|
||||||
|
|
||||||
|
String toRawJson() => json.encode(toJson());
|
||||||
|
|
||||||
|
factory ItemsDataResponse.fromJson(Map<String, dynamic> json) =>
|
||||||
|
ItemsDataResponse(
|
||||||
|
rows: List<GymItem>.from(json["rows"].map((x) => GymItem.fromJson(x))),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
"rows": List<dynamic>.from(rows.map((x) => x.toJson())),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class GymItem {
|
||||||
|
final String id;
|
||||||
|
final String externalId;
|
||||||
|
final String title;
|
||||||
|
final String description;
|
||||||
|
final int count;
|
||||||
|
final double price;
|
||||||
|
final String categoryId;
|
||||||
|
final List<GymImage> images;
|
||||||
|
final String supplierId;
|
||||||
|
final String supplierName;
|
||||||
|
int localCount = 0;
|
||||||
|
|
||||||
|
GymItem({
|
||||||
|
required this.id,
|
||||||
|
required this.externalId,
|
||||||
|
required this.title,
|
||||||
|
required this.description,
|
||||||
|
required this.count,
|
||||||
|
required this.price,
|
||||||
|
required this.categoryId,
|
||||||
|
required this.images,
|
||||||
|
required this.supplierId,
|
||||||
|
required this.supplierName,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory GymItem.fromRawJson(String str) => GymItem.fromJson(json.decode(str));
|
||||||
|
|
||||||
|
String toRawJson() => json.encode(toJson());
|
||||||
|
|
||||||
|
factory GymItem.fromJson(Map<String, dynamic> json) {
|
||||||
|
return GymItem(
|
||||||
|
id: json["id"],
|
||||||
|
externalId: json["ExternalId"],
|
||||||
|
title: json["title"],
|
||||||
|
description: json["description"],
|
||||||
|
count: json["count"],
|
||||||
|
price: json["price"] / 100,
|
||||||
|
categoryId: json["categoryId"],
|
||||||
|
images:
|
||||||
|
List<GymImage>.from(json["images"].map((x) => GymImage.fromJson(x))),
|
||||||
|
supplierId: json["supplier"] == null ? '' : json["supplier"]["id"] ?? '',
|
||||||
|
supplierName: json["supplier"] == null
|
||||||
|
? ''
|
||||||
|
: json["supplier"]["title"] ?? "Поставщик",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
"id": id,
|
||||||
|
"ExternalId": externalId,
|
||||||
|
"title": title,
|
||||||
|
"description": description,
|
||||||
|
"count": count,
|
||||||
|
"price": price,
|
||||||
|
"categoryId": categoryId,
|
||||||
|
"images": List<dynamic>.from(images.map((x) => x.toJson())),
|
||||||
|
"supplier":
|
||||||
|
supplierId == '' ? null : {"id": supplierId, "title": supplierName},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class GymImage {
|
||||||
|
final String id;
|
||||||
|
final dynamic deletedAt;
|
||||||
|
final String url;
|
||||||
|
|
||||||
|
GymImage({
|
||||||
|
required this.id,
|
||||||
|
required this.deletedAt,
|
||||||
|
required this.url,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory GymImage.fromRawJson(String str) =>
|
||||||
|
GymImage.fromJson(json.decode(str));
|
||||||
|
|
||||||
|
String toRawJson() => json.encode(toJson());
|
||||||
|
|
||||||
|
factory GymImage.fromJson(Map<String, dynamic> json) => GymImage(
|
||||||
|
id: json["id"],
|
||||||
|
deletedAt: json["deletedAt"],
|
||||||
|
url: json["url"],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
"id": id,
|
||||||
|
"deletedAt": deletedAt,
|
||||||
|
"url": url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class CategoryDataResponse {
|
||||||
|
final List<GymCategory> rows;
|
||||||
|
|
||||||
|
CategoryDataResponse({
|
||||||
|
required this.rows,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory CategoryDataResponse.fromRawJson(String str) =>
|
||||||
|
CategoryDataResponse.fromJson(json.decode(str));
|
||||||
|
|
||||||
|
String toRawJson() => json.encode(toJson());
|
||||||
|
|
||||||
|
factory CategoryDataResponse.fromJson(Map<String, dynamic> json) =>
|
||||||
|
CategoryDataResponse(
|
||||||
|
rows: List<GymCategory>.from(
|
||||||
|
json["rows"].map((x) => GymCategory.fromJson(x))),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
"rows": List<dynamic>.from(rows.map((x) => x.toJson())),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class GymCategory {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
GymCategory({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory GymCategory.fromRawJson(String str) =>
|
||||||
|
GymCategory.fromJson(json.decode(str));
|
||||||
|
|
||||||
|
String toRawJson() => json.encode(toJson());
|
||||||
|
|
||||||
|
factory GymCategory.fromJson(Map<String, dynamic> json) => GymCategory(
|
||||||
|
id: json["id"],
|
||||||
|
name: json["name"],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
"id": id,
|
||||||
|
"name": name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class GymHistoryItem {
|
||||||
|
String id;
|
||||||
|
final String date;
|
||||||
|
final String sum;
|
||||||
|
final String photo;
|
||||||
|
final String timestamp;
|
||||||
|
|
||||||
|
GymHistoryItem(
|
||||||
|
{required this.id,
|
||||||
|
required this.date,
|
||||||
|
required this.sum,
|
||||||
|
required this.photo,
|
||||||
|
required this.timestamp});
|
||||||
|
|
||||||
|
factory GymHistoryItem.fromRawJson(String str) =>
|
||||||
|
GymHistoryItem.fromJson(json.decode(str));
|
||||||
|
|
||||||
|
String toRawJson() => json.encode(toJson());
|
||||||
|
|
||||||
|
factory GymHistoryItem.fromJson(Map<String, dynamic> json) => GymHistoryItem(
|
||||||
|
id: json["id"],
|
||||||
|
date: json["date"],
|
||||||
|
sum: json["sum"],
|
||||||
|
photo: json["photo"],
|
||||||
|
timestamp: json["timestamp"],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
"id": id,
|
||||||
|
"date": date,
|
||||||
|
"sum": sum,
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"photo": photo,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class GymHistoryItemDetail {
|
||||||
|
String id;
|
||||||
|
final String date;
|
||||||
|
final String sum;
|
||||||
|
String? payUrl;
|
||||||
|
final String receiver;
|
||||||
|
final String email;
|
||||||
|
final String timestamp;
|
||||||
|
final String address;
|
||||||
|
final List<GymHistoryItemDetailProvider> providers;
|
||||||
|
|
||||||
|
GymHistoryItemDetail(
|
||||||
|
{required this.id,
|
||||||
|
required this.date,
|
||||||
|
required this.sum,
|
||||||
|
this.payUrl,
|
||||||
|
required this.providers,
|
||||||
|
required this.receiver,
|
||||||
|
required this.email,
|
||||||
|
required this.address,
|
||||||
|
required this.timestamp});
|
||||||
|
|
||||||
|
factory GymHistoryItemDetail.fromRawJson(String str) =>
|
||||||
|
GymHistoryItemDetail.fromJson(json.decode(str));
|
||||||
|
|
||||||
|
String toRawJson() => json.encode(toJson());
|
||||||
|
|
||||||
|
factory GymHistoryItemDetail.fromJson(Map<String, dynamic> json) =>
|
||||||
|
GymHistoryItemDetail(
|
||||||
|
id: json["id"],
|
||||||
|
date: json["date"],
|
||||||
|
sum: json["sum"],
|
||||||
|
receiver: json["receiver"],
|
||||||
|
email: json["email"],
|
||||||
|
address: json["address"],
|
||||||
|
payUrl: json["pay_url"] as String?,
|
||||||
|
providers: List<GymHistoryItemDetailProvider>.from(json["providers"]
|
||||||
|
.map((x) => GymHistoryItemDetailProvider.fromJson(x))),
|
||||||
|
timestamp: json["timestamp"]);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
"id": id,
|
||||||
|
"date": date,
|
||||||
|
"sum": sum,
|
||||||
|
"pay_url": payUrl,
|
||||||
|
"providers": List<dynamic>.from(providers.map((x) => x.toJson())),
|
||||||
|
"receiver": receiver,
|
||||||
|
"email": email,
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"address": address,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class GymHistoryItemDetailProvider {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
String status;
|
||||||
|
final List<GymHistoryItemDetailItem> items;
|
||||||
|
|
||||||
|
GymHistoryItemDetailProvider({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.status,
|
||||||
|
required this.items,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory GymHistoryItemDetailProvider.fromRawJson(String str) =>
|
||||||
|
GymHistoryItemDetailProvider.fromJson(json.decode(str));
|
||||||
|
|
||||||
|
String toRawJson() => json.encode(toJson());
|
||||||
|
|
||||||
|
factory GymHistoryItemDetailProvider.fromJson(Map<String, dynamic> json) {
|
||||||
|
return GymHistoryItemDetailProvider(
|
||||||
|
id: json["id"],
|
||||||
|
name: json["name"],
|
||||||
|
status: json["status"],
|
||||||
|
items: List<GymHistoryItemDetailItem>.from(
|
||||||
|
json["items"].map((x) => GymHistoryItemDetailItem.fromJson(x))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
"id": id,
|
||||||
|
"name": name,
|
||||||
|
"status": status,
|
||||||
|
"items": List<dynamic>.from(items.map((x) => x.toJson())),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class GymHistoryItemDetailItem {
|
||||||
|
final String photo;
|
||||||
|
final String id;
|
||||||
|
final int count;
|
||||||
|
final String name;
|
||||||
|
final String price;
|
||||||
|
|
||||||
|
GymHistoryItemDetailItem({
|
||||||
|
required this.photo,
|
||||||
|
required this.id,
|
||||||
|
required this.count,
|
||||||
|
required this.price,
|
||||||
|
required this.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory GymHistoryItemDetailItem.fromRawJson(String str) =>
|
||||||
|
GymHistoryItemDetailItem.fromJson(json.decode(str));
|
||||||
|
|
||||||
|
String toRawJson() => json.encode(toJson());
|
||||||
|
|
||||||
|
factory GymHistoryItemDetailItem.fromJson(Map<String, dynamic> json) =>
|
||||||
|
GymHistoryItemDetailItem(
|
||||||
|
photo: json["photo"],
|
||||||
|
id: json["id"],
|
||||||
|
count: json["count"],
|
||||||
|
price: json["price"],
|
||||||
|
name: json["name"],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
"photo": photo,
|
||||||
|
"id": id,
|
||||||
|
"count": count,
|
||||||
|
"price": price,
|
||||||
|
"name": name,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,19 +1,40 @@
|
|||||||
|
import 'dart:convert';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:gymlink_module_web/main_mobile.dart';
|
import 'package:gymlink_module_web/main_mobile.dart';
|
||||||
import 'package:gymlink_module_web/providers/main.dart';
|
import 'package:gymlink_module_web/providers/main.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const MyExampleApp());
|
runApp(const MyExampleApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String> getToken(String token, String clientId) async {
|
||||||
|
var url = Uri.https('gymlink.freemyip.com', 'api/auth/authorize_client');
|
||||||
|
try {
|
||||||
|
var response = await http.post(url,
|
||||||
|
body: {'GymKey': token, 'id': clientId}); // Just testing token
|
||||||
|
var decodedBody = jsonDecode(response.body) as Map;
|
||||||
|
if (decodedBody['payload'] == null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return decodedBody['payload']['token'];
|
||||||
|
} catch (e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class MyExampleApp extends StatelessWidget {
|
class MyExampleApp extends StatelessWidget {
|
||||||
const MyExampleApp({super.key});
|
const MyExampleApp({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
SystemChrome.setPreferredOrientations(
|
||||||
|
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'GymLink Example App',
|
title: 'GymLink Example App',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
@@ -35,18 +56,49 @@ Widget getDrawer(BuildContext context) => Drawer(
|
|||||||
children: [
|
children: [
|
||||||
const DrawerHeader(child: Text('Drawer Header')),
|
const DrawerHeader(child: Text('Drawer Header')),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.home),
|
leading: const Icon(Icons.home),
|
||||||
title: const Text('Home'),
|
title: const Text('Home'),
|
||||||
onTap: () => Navigator.of(context).push(
|
onTap: () {
|
||||||
MaterialPageRoute(
|
Future.microtask(() async {
|
||||||
builder: (context) => const ExampleMainPage(),
|
final prefs = await SharedPreferences.getInstance();
|
||||||
),
|
prefs.remove('token');
|
||||||
),
|
prefs.remove('history');
|
||||||
),
|
prefs.remove('cart');
|
||||||
|
prefs.remove('detail_history');
|
||||||
|
});
|
||||||
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const ExampleMainPage(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.sell),
|
||||||
|
title: const Text('Club 2'),
|
||||||
|
onTap: () {
|
||||||
|
Future.microtask(() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
prefs.remove('token');
|
||||||
|
prefs.remove('history');
|
||||||
|
prefs.remove('cart');
|
||||||
|
prefs.remove('detail_history');
|
||||||
|
});
|
||||||
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => ChangeNotifierProvider(
|
||||||
|
create: (_) => GymLinkProvider(),
|
||||||
|
child: Consumer<GymLinkProvider>(
|
||||||
|
builder: (_, value, __) => const ExampleClub2Page(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.search),
|
leading: const Icon(Icons.search),
|
||||||
title: const Text('Example page'),
|
title: const Text('Example page'),
|
||||||
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
onTap: () =>
|
||||||
|
Navigator.of(context).pushReplacement(MaterialPageRoute(
|
||||||
builder: (context) => const ExampleSecondPage(),
|
builder: (context) => const ExampleSecondPage(),
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
@@ -71,28 +123,54 @@ class _ExamplePageState extends State<ExamplePage> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
Future.microtask(
|
// Future.microtask(
|
||||||
() => context.read<GymLinkProvider>().onTokenReceived('token123'));
|
// () => context.read<GymLinkProvider>().onTokenReceived('token123'));
|
||||||
Future.microtask(() => context
|
Future.microtask(() => context
|
||||||
.read<GymLinkProvider>()
|
.read<GymLinkProvider>()
|
||||||
.setTheme(ThemeData.light(useMaterial3: true)));
|
.setTheme(ThemeData.light(useMaterial3: true)));
|
||||||
|
|
||||||
|
Future.microtask(() => context.read<GymLinkProvider>().setOnError(() {
|
||||||
|
const snackBar = SnackBar(
|
||||||
|
content: Text('Ошибка подключения'),
|
||||||
|
duration: Duration(seconds: 3), // Длительность отображения Snackbar
|
||||||
|
behavior: SnackBarBehavior
|
||||||
|
.fixed, // Поведение Snackbar (fixed или floating)
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||||
|
Future.delayed(const Duration(seconds: 3))
|
||||||
|
.then((value) => _setToken());
|
||||||
|
}));
|
||||||
|
|
||||||
|
Future.microtask(() async {
|
||||||
|
await _setToken();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _setToken() async {
|
||||||
|
final token = await getToken('eeb42dcb-8e5b-4f21-825a-3fc7ada43445', '123');
|
||||||
|
if (token != '') {
|
||||||
|
context.read<GymLinkProvider>().checkToken(token);
|
||||||
|
} else {
|
||||||
|
context.read<GymLinkProvider>().onError();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('GymLink Example App'),
|
title: const Text('GymLink Example App Gym 1'),
|
||||||
),
|
),
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
drawer: getDrawer(context),
|
drawer: getDrawer(context),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
const Text('test'),
|
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.abc),
|
icon: const Icon(Icons.colorize),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.read<GymLinkProvider>().onTokenReceived('token123');
|
context.read<GymLinkProvider>().changeTheme(
|
||||||
|
Random().nextInt(0xffffff + 1),
|
||||||
|
blackTheme: Random().nextBool());
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Expanded(
|
const Expanded(
|
||||||
@@ -104,13 +182,73 @@ class _ExamplePageState extends State<ExamplePage> {
|
|||||||
const Text('Bottom text')
|
const Text('Bottom text')
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
floatingActionButton: IconButton(
|
);
|
||||||
icon: const Icon(Icons.search),
|
}
|
||||||
onPressed: () {
|
}
|
||||||
context
|
|
||||||
.read<GymLinkProvider>()
|
class ExampleClub2Page extends StatefulWidget {
|
||||||
.changeTheme(Random().nextInt(0xffffff + 1));
|
const ExampleClub2Page({super.key});
|
||||||
},
|
|
||||||
|
@override
|
||||||
|
State<ExampleClub2Page> createState() => _ExampleClub2PageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ExampleClub2PageState extends State<ExampleClub2Page> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// Future.microtask(
|
||||||
|
// () => context.read<GymLinkProvider>().onTokenReceived('token123'));
|
||||||
|
Future.microtask(() => context
|
||||||
|
.read<GymLinkProvider>()
|
||||||
|
.setTheme(ThemeData.light(useMaterial3: true)));
|
||||||
|
|
||||||
|
Future.microtask(() => context.read<GymLinkProvider>().setOnError(() {
|
||||||
|
const snackBar = SnackBar(
|
||||||
|
content: Text('Ошибка подключения'),
|
||||||
|
duration: Duration(seconds: 3), // Длительность отображения Snackbar
|
||||||
|
behavior: SnackBarBehavior
|
||||||
|
.fixed, // Поведение Snackbar (fixed или floating)
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||||
|
Future.delayed(const Duration(seconds: 3))
|
||||||
|
.then((value) => _setToken());
|
||||||
|
}));
|
||||||
|
|
||||||
|
Future.microtask(() async {
|
||||||
|
await _setToken();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _setToken() async {
|
||||||
|
final token = await getToken('a8622a61-3142-487e-8db8-b6aebd4f04aa', '123');
|
||||||
|
context.read<GymLinkProvider>().changeTheme(0xFFAABCAB);
|
||||||
|
if (token != '') {
|
||||||
|
context.read<GymLinkProvider>().checkToken(token);
|
||||||
|
} else {
|
||||||
|
context.read<GymLinkProvider>().onError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('GymLink Example App Gym2'),
|
||||||
|
),
|
||||||
|
resizeToAvoidBottomInset: false,
|
||||||
|
drawer: getDrawer(context),
|
||||||
|
body: const Column(
|
||||||
|
children: [
|
||||||
|
Text('test'),
|
||||||
|
Expanded(
|
||||||
|
child: MyApp(),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 20,
|
||||||
|
),
|
||||||
|
Text('Bottom text')
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -126,8 +264,16 @@ class ExampleSecondPage extends StatelessWidget {
|
|||||||
title: const Text('GymLink Example App'),
|
title: const Text('GymLink Example App'),
|
||||||
),
|
),
|
||||||
drawer: getDrawer(context),
|
drawer: getDrawer(context),
|
||||||
body: const Center(
|
body: Center(
|
||||||
child: Text('Example page'),
|
child: TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
prefs.remove('token');
|
||||||
|
prefs.remove('history');
|
||||||
|
prefs.remove('cart');
|
||||||
|
prefs.remove('detail_history');
|
||||||
|
},
|
||||||
|
child: const Text('Clear')),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:gymlink_module_web/components/app_bar.dart';
|
import 'package:gymlink_module_web/components/app_bar.dart';
|
||||||
import 'package:gymlink_module_web/components/basket_item_card.dart';
|
import 'package:gymlink_module_web/components/basket_item_card.dart';
|
||||||
import 'package:gymlink_module_web/components/heading.dart';
|
import 'package:gymlink_module_web/components/heading.dart';
|
||||||
|
import 'package:gymlink_module_web/interfaces/items.dart';
|
||||||
|
import 'package:gymlink_module_web/pages/main.dart';
|
||||||
import 'package:gymlink_module_web/pages/order_confirmation.dart';
|
import 'package:gymlink_module_web/pages/order_confirmation.dart';
|
||||||
import 'package:gymlink_module_web/providers/cart.dart';
|
import 'package:gymlink_module_web/providers/cart.dart';
|
||||||
import 'package:gymlink_module_web/providers/main.dart';
|
import 'package:gymlink_module_web/tools/items.dart';
|
||||||
import 'package:gymlink_module_web/tools/prefs.dart';
|
import 'package:gymlink_module_web/tools/prefs.dart';
|
||||||
import 'package:gymlink_module_web/tools/routes.dart';
|
import 'package:gymlink_module_web/tools/routes.dart';
|
||||||
|
import 'package:gymlink_module_web/tools/text.dart';
|
||||||
import 'package:lazy_load_scrollview/lazy_load_scrollview.dart';
|
import 'package:lazy_load_scrollview/lazy_load_scrollview.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@@ -56,62 +59,71 @@ class BasketPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _BasketPageState extends State<BasketPage> {
|
class _BasketPageState extends State<BasketPage> {
|
||||||
List<Map<String, dynamic>> cartItems = [];
|
List<GymItem> cartItems = [];
|
||||||
int totalPrice = 0;
|
double totalPrice = 0;
|
||||||
|
List<GymItem> gymCart = [];
|
||||||
|
bool _isLoading = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
getCart().then((value) {
|
Future.microtask(() => getCart().then((value) async {
|
||||||
setState(() {
|
final itemIds =
|
||||||
cartItems = value.map((element) {
|
value.map((element) => element['id'] as String).toList();
|
||||||
final item = cart.firstWhere((e) => e['id'] == element['id']);
|
final items = await getItemsByIds(context, itemIds);
|
||||||
return {...item, 'count': element['count'] as int};
|
setState(() {
|
||||||
}).toList();
|
gymCart = items;
|
||||||
totalPrice = cartItems.fold(
|
cartItems = value.map((element) {
|
||||||
0,
|
final item = gymCart.firstWhere((e) => e.id == element['id']);
|
||||||
(sum, item) =>
|
item.localCount = element['count'] as int;
|
||||||
sum + int.parse(item['price']) * item['count'] as int);
|
return item;
|
||||||
});
|
}).toList();
|
||||||
});
|
totalPrice = cartItems.fold(
|
||||||
|
0, (sum, item) => sum + item.price * item.localCount);
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateCart() {
|
void _updateCart() {
|
||||||
Provider.of<CartProvider>(context, listen: false).updateCartLength();
|
Provider.of<CartProvider>(context, listen: false).updateCartLength();
|
||||||
Provider.of<GymLinkProvider>(context, listen: false).onTokenReceived('123');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeItem(String id) async {
|
void removeItem(String id) async {
|
||||||
final item = cartItems.firstWhere((element) => element['id'] == id);
|
final item = cartItems.firstWhere((element) => element.id == id);
|
||||||
bool toDelete = false;
|
bool toDelete = false;
|
||||||
setState(() {
|
setState(() {
|
||||||
if (item['count'] > 1) {
|
if (item.localCount > 1) {
|
||||||
item['count']--;
|
item.localCount--;
|
||||||
cartItems[cartItems.indexOf(item)]['count'] = item['count'];
|
cartItems[cartItems.indexOf(item)].localCount = item.localCount;
|
||||||
} else {
|
} else {
|
||||||
toDelete = true;
|
toDelete = true;
|
||||||
}
|
}
|
||||||
totalPrice = cartItems.fold(0,
|
totalPrice =
|
||||||
(sum, item) => sum + int.parse(item['price']) * item['count'] as int);
|
cartItems.fold(0, (sum, item) => sum + item.price * item.localCount);
|
||||||
});
|
});
|
||||||
if (toDelete) {
|
if (toDelete) {
|
||||||
await _deleteItemAlert(id, item['name']);
|
await _deleteItemAlert(id, item.title);
|
||||||
} else {
|
} else {
|
||||||
await removeItemFromCart(id);
|
await removeItemFromCart(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void addItem(String id) async {
|
void addItem(String id) async {
|
||||||
|
final item =
|
||||||
|
cartItems.firstWhere((element) => element.id == id, orElse: () {
|
||||||
|
final cartItem = gymCart.firstWhere((element) => element.id == id);
|
||||||
|
cartItem.localCount = 0;
|
||||||
|
return cartItem;
|
||||||
|
});
|
||||||
|
if (item.localCount + 1 > item.count) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
final item = cartItems.firstWhere((element) => element['id'] == id,
|
item.localCount++;
|
||||||
orElse: () => {
|
cartItems[cartItems.indexOf(item)].localCount = item.localCount;
|
||||||
...cart.firstWhere((element) => element['id'] == id),
|
totalPrice =
|
||||||
'count': 0
|
cartItems.fold(0, (sum, item) => sum + item.price * item.localCount);
|
||||||
});
|
|
||||||
item['count']++;
|
|
||||||
cartItems[cartItems.indexOf(item)]['count'] = item['count'];
|
|
||||||
totalPrice = cartItems.fold(0,
|
|
||||||
(sum, item) => sum + int.parse(item['price']) * item['count'] as int);
|
|
||||||
});
|
});
|
||||||
await addItemToCart(id);
|
await addItemToCart(id);
|
||||||
}
|
}
|
||||||
@@ -142,7 +154,9 @@ class _BasketPageState extends State<BasketPage> {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
removeItemFromCart(id);
|
removeItemFromCart(id);
|
||||||
setState(() {
|
setState(() {
|
||||||
cartItems.removeWhere((element) => element['id'] == id);
|
cartItems.removeWhere((element) => element.id == id);
|
||||||
|
totalPrice = cartItems.fold(
|
||||||
|
0, (sum, item) => sum + item.price * item.localCount);
|
||||||
});
|
});
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
_updateCart();
|
_updateCart();
|
||||||
@@ -224,76 +238,24 @@ class _BasketPageState extends State<BasketPage> {
|
|||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
const GymLinkHeader(title: "Корзина"),
|
const GymLinkHeader(title: "Корзина"),
|
||||||
cartItems.isEmpty
|
_isLoading
|
||||||
? Expanded(
|
? const Expanded(
|
||||||
child: Center(
|
child: Center(child: CircularProgressIndicator()))
|
||||||
child: Column(
|
: cartItems.isEmpty
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
? Expanded(
|
||||||
children: [
|
child: Center(
|
||||||
Text('Корзина пуста',
|
|
||||||
style: Theme.of(context).textTheme.bodyLarge),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () => Navigator.pop(context, true),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
|
||||||
shape: const RoundedRectangleBorder(
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.all(Radius.circular(50)),
|
|
||||||
),
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
),
|
|
||||||
child: const Text('Вернуться назад'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Expanded(
|
|
||||||
child: _buildRowOrCol(
|
|
||||||
context: context,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: LazyLoadScrollView(
|
|
||||||
onEndOfPage: _onLoad,
|
|
||||||
child: ListView.builder(
|
|
||||||
itemCount: cartItems.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final item = cartItems[index];
|
|
||||||
return BasketItemCard(
|
|
||||||
name: item['name'],
|
|
||||||
price: item['price'],
|
|
||||||
id: item['id'],
|
|
||||||
image: Image(
|
|
||||||
image: AssetImage('assets/${item['image']}'),
|
|
||||||
width: 50,
|
|
||||||
),
|
|
||||||
onTapPlus: () => addItem(item['id'].toString()),
|
|
||||||
onTapMinus: () {
|
|
||||||
removeItem(item['id'].toString());
|
|
||||||
},
|
|
||||||
quantity: item['count'].toString(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
_buildSpacer(),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsetsDirectional.symmetric(
|
|
||||||
horizontal: 10, vertical: 10),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text('Корзина пуста',
|
||||||
'Итого: $totalPrice',
|
style: Theme.of(context).textTheme.bodyLarge),
|
||||||
),
|
const SizedBox(height: 10),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () => Navigator.of(context).push(
|
onPressed: () => Navigator.pushAndRemoveUntil(
|
||||||
CustomPageRoute(
|
context,
|
||||||
builder: (context) =>
|
CustomPageRoute(
|
||||||
const OrderConfirmationPage(),
|
builder: (_) => const MainPage()),
|
||||||
),
|
(route) => route.isFirst),
|
||||||
),
|
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
@@ -302,27 +264,102 @@ class _BasketPageState extends State<BasketPage> {
|
|||||||
),
|
),
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
),
|
),
|
||||||
child: const Text('Оформить заказ'),
|
child: const Text('Вернуться назад'),
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: _clearCartAlert,
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
|
||||||
shape: const RoundedRectangleBorder(
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.all(Radius.circular(50))),
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
),
|
|
||||||
child: const Text('Очистить корзину'),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 50),
|
)
|
||||||
],
|
: Expanded(
|
||||||
),
|
child: _buildRowOrCol(
|
||||||
),
|
context: context,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: LazyLoadScrollView(
|
||||||
|
onEndOfPage: _onLoad,
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: cartItems.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = cartItems[index];
|
||||||
|
return BasketItemCard(
|
||||||
|
name: shortString(item.title),
|
||||||
|
price: item.price.toStringAsFixed(2),
|
||||||
|
id: item.id,
|
||||||
|
image: FutureBuilder(
|
||||||
|
future: precacheImage(
|
||||||
|
NetworkImage(item.images[0].url),
|
||||||
|
context),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState ==
|
||||||
|
ConnectionState.done) {
|
||||||
|
return Image(
|
||||||
|
image: NetworkImage(
|
||||||
|
item.images[0].url),
|
||||||
|
width: 50,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const CircularProgressIndicator();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
onTapPlus: () =>
|
||||||
|
addItem(item.id.toString()),
|
||||||
|
onTapMinus: () {
|
||||||
|
removeItem(item.id.toString());
|
||||||
|
},
|
||||||
|
quantity: item.localCount.toString(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// _buildSpacer(),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsetsDirectional.symmetric(
|
||||||
|
horizontal: 10, vertical: 10),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Итого: ${totalPrice.toStringAsFixed(2)} руб.',
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Navigator.of(context).push(
|
||||||
|
CustomPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
const OrderConfirmationPage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).primaryColor,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.all(Radius.circular(50)),
|
||||||
|
),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
child: const Text('Оформить заказ'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _clearCartAlert,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).primaryColor,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(50))),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
child: const Text('Очистить корзину'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// const SizedBox(width: 50),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,24 +1,27 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:carousel_slider/carousel_slider.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
import 'package:gymlink_module_web/components/app_bar.dart';
|
import 'package:gymlink_module_web/components/app_bar.dart';
|
||||||
import 'package:gymlink_module_web/components/heading.dart';
|
import 'package:gymlink_module_web/components/heading.dart';
|
||||||
|
import 'package:gymlink_module_web/interfaces/items.dart';
|
||||||
|
import 'package:gymlink_module_web/pages/basket.dart';
|
||||||
import 'package:gymlink_module_web/providers/cart.dart';
|
import 'package:gymlink_module_web/providers/cart.dart';
|
||||||
|
import 'package:gymlink_module_web/providers/main.dart';
|
||||||
|
import 'package:gymlink_module_web/tools/items.dart';
|
||||||
import 'package:gymlink_module_web/tools/prefs.dart';
|
import 'package:gymlink_module_web/tools/prefs.dart';
|
||||||
|
import 'package:gymlink_module_web/tools/routes.dart';
|
||||||
|
import 'package:gymlink_module_web/tools/text.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
//TODO: Сделать получение инфы через объект
|
|
||||||
class DetailPage extends StatefulWidget {
|
class DetailPage extends StatefulWidget {
|
||||||
final String name;
|
|
||||||
final String description;
|
|
||||||
final String price;
|
|
||||||
final String id;
|
final String id;
|
||||||
final Image image;
|
|
||||||
const DetailPage({
|
const DetailPage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.name,
|
|
||||||
required this.description,
|
|
||||||
required this.price,
|
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.image,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -28,10 +31,13 @@ class DetailPage extends StatefulWidget {
|
|||||||
class _DetailPageState extends State<DetailPage> {
|
class _DetailPageState extends State<DetailPage> {
|
||||||
bool isInCart = false;
|
bool isInCart = false;
|
||||||
int quantity = 0;
|
int quantity = 0;
|
||||||
|
GymItem? item;
|
||||||
|
String? categoryName;
|
||||||
|
final CarouselController _carouselController = CarouselController();
|
||||||
|
int _currentImage = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
|
||||||
getCart().then((value) {
|
getCart().then((value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
isInCart = value.any((element) => element['id'] == widget.id);
|
isInCart = value.any((element) => element['id'] == widget.id);
|
||||||
@@ -41,6 +47,40 @@ class _DetailPageState extends State<DetailPage> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
_getItem();
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _getItem() async {
|
||||||
|
final Uri url =
|
||||||
|
Uri.https('gymlink.freemyip.com', 'api/product/get/${widget.id}');
|
||||||
|
final response = await http.get(url, headers: {
|
||||||
|
'Authorization': 'Bearer ${context.read<GymLinkProvider>().token}',
|
||||||
|
});
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final data =
|
||||||
|
GymItem.fromJson(jsonDecode(utf8.decode(response.bodyBytes)));
|
||||||
|
setState(() {
|
||||||
|
item = data;
|
||||||
|
});
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
for (var element in item!.images) {
|
||||||
|
precacheImage(NetworkImage(element.url), context);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (mounted) {
|
||||||
|
getCategories(context).then((value) {
|
||||||
|
setState(() {
|
||||||
|
categoryName = value
|
||||||
|
.firstWhere(
|
||||||
|
(element) => element.id == (item!.categoryId),
|
||||||
|
orElse: () => GymCategory(id: item!.categoryId, name: ''),
|
||||||
|
)
|
||||||
|
.name;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRowOrCol(
|
Widget _buildRowOrCol(
|
||||||
@@ -48,12 +88,6 @@ class _DetailPageState extends State<DetailPage> {
|
|||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.spaceAround,
|
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.spaceAround,
|
||||||
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center}) {
|
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center}) {
|
||||||
// if (false && MediaQuery.of(context).size.width > 600) {
|
|
||||||
// return Row(
|
|
||||||
// mainAxisAlignment: mainAxisAlignment,
|
|
||||||
// crossAxisAlignment: crossAxisAlignment,
|
|
||||||
// children: children);
|
|
||||||
// }
|
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisAlignment: mainAxisAlignment,
|
mainAxisAlignment: mainAxisAlignment,
|
||||||
crossAxisAlignment: crossAxisAlignment,
|
crossAxisAlignment: crossAxisAlignment,
|
||||||
@@ -83,117 +117,245 @@ class _DetailPageState extends State<DetailPage> {
|
|||||||
child: const Text('Добавить в корзину'),
|
child: const Text('Добавить в корзину'),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Row(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
Row(
|
||||||
icon: const Icon(Icons.remove),
|
mainAxisSize: MainAxisSize.min,
|
||||||
onPressed: () async {
|
children: [
|
||||||
await removeItemFromCart(widget.id);
|
IconButton(
|
||||||
setState(() {
|
icon: const Icon(Icons.remove),
|
||||||
if (quantity > 1) {
|
onPressed: () async {
|
||||||
quantity--;
|
await removeItemFromCart(widget.id);
|
||||||
} else {
|
setState(() {
|
||||||
isInCart = false;
|
if (quantity > 1) {
|
||||||
quantity = 0;
|
quantity--;
|
||||||
}
|
} else {
|
||||||
});
|
isInCart = false;
|
||||||
if (mounted) {
|
quantity = 0;
|
||||||
context.read<CartProvider>().updateCartLength();
|
}
|
||||||
}
|
});
|
||||||
},
|
if (mounted) {
|
||||||
),
|
context.read<CartProvider>().updateCartLength();
|
||||||
const SizedBox(width: 10),
|
}
|
||||||
Text('$quantity'),
|
},
|
||||||
const SizedBox(width: 10),
|
),
|
||||||
IconButton(
|
const SizedBox(width: 10),
|
||||||
icon: const Icon(Icons.add),
|
Text('$quantity'),
|
||||||
onPressed: () async {
|
const SizedBox(width: 10),
|
||||||
await addItemToCart(widget.id);
|
IconButton(
|
||||||
setState(() {
|
icon: const Icon(Icons.add),
|
||||||
quantity++;
|
onPressed: () async {
|
||||||
});
|
if (item!.count > quantity) {
|
||||||
},
|
await addItemToCart(widget.id);
|
||||||
|
setState(() {
|
||||||
|
quantity++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 10),
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pushReplacement(context,
|
||||||
|
CustomPageRoute(builder: (context) => const BasketPage()));
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(50)),
|
||||||
|
),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
child: const Text('Открыть корзину'),
|
||||||
|
),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double _getAspectRatio() {
|
||||||
|
double width = MediaQuery.sizeOf(context).width;
|
||||||
|
double height = MediaQuery.sizeOf(context).height;
|
||||||
|
return max(width, height) / min(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: const GymLinkAppBar(),
|
appBar: const GymLinkAppBar(),
|
||||||
body: Column(mainAxisAlignment: MainAxisAlignment.start, children: [
|
body: item != null
|
||||||
GymLinkHeader(title: widget.name),
|
? Column(mainAxisAlignment: MainAxisAlignment.start, children: [
|
||||||
Expanded(
|
GymLinkHeader(title: shortString(item!.title, length: 20)),
|
||||||
child: SingleChildScrollView(
|
Expanded(
|
||||||
child: Padding(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(20),
|
child: Padding(
|
||||||
child: SizedBox(
|
padding: const EdgeInsets.all(20),
|
||||||
width: MediaQuery.sizeOf(context).width,
|
child: SizedBox(
|
||||||
// height: MediaQuery.sizeOf(context).height,
|
width: MediaQuery.sizeOf(context).width,
|
||||||
child: _buildRowOrCol(
|
child: _buildRowOrCol(
|
||||||
context: context,
|
context: context,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
widget.image,
|
item!.images.length > 1
|
||||||
Padding(
|
? Column(children: [
|
||||||
padding: const EdgeInsetsDirectional.all(30),
|
CarouselSlider.builder(
|
||||||
child: ConstrainedBox(
|
itemCount: item!.images.length,
|
||||||
constraints: const BoxConstraints(
|
itemBuilder: (context, index, realIdx) {
|
||||||
minWidth: 340,
|
return Center(
|
||||||
maxWidth: 340,
|
child: Image.network(
|
||||||
maxHeight: 600,
|
item!.images[index].url,
|
||||||
),
|
width: min(
|
||||||
child: Card(
|
550,
|
||||||
elevation: 4,
|
MediaQuery.sizeOf(context)
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
.width)),
|
||||||
shape: RoundedRectangleBorder(
|
);
|
||||||
borderRadius: BorderRadius.circular(16),
|
},
|
||||||
|
carouselController: _carouselController,
|
||||||
|
options: CarouselOptions(
|
||||||
|
enlargeCenterPage: true,
|
||||||
|
height: min(
|
||||||
|
MediaQuery.sizeOf(context).height -
|
||||||
|
100,
|
||||||
|
400),
|
||||||
|
enableInfiniteScroll: false,
|
||||||
|
onPageChanged: (index, reason) {
|
||||||
|
setState(() {
|
||||||
|
_currentImage = index;
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: item!.images
|
||||||
|
.asMap()
|
||||||
|
.entries
|
||||||
|
.map((entry) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => _carouselController
|
||||||
|
.animateToPage(entry.key),
|
||||||
|
child: Container(
|
||||||
|
width: 12.0,
|
||||||
|
height: 12.0,
|
||||||
|
margin: const EdgeInsets.symmetric(
|
||||||
|
vertical: 8.0, horizontal: 4.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: (Theme.of(context)
|
||||||
|
.brightness ==
|
||||||
|
Brightness.dark
|
||||||
|
? Colors.white
|
||||||
|
: Colors.black)
|
||||||
|
.withOpacity(
|
||||||
|
_currentImage == entry.key
|
||||||
|
? 0.9
|
||||||
|
: 0.4)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
: Image.network(
|
||||||
|
item!.images[0].url,
|
||||||
|
height: 400,
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
child: Text(
|
||||||
|
item!.title,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: SingleChildScrollView(
|
Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsetsDirectional.all(15),
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||||
child: ConstrainedBox(
|
child: Chip(
|
||||||
constraints: const BoxConstraints(
|
label: Text(categoryName != null
|
||||||
minHeight: 100,
|
? (categoryName == ""
|
||||||
),
|
? "Без категории"
|
||||||
child: Text(
|
: categoryName!)
|
||||||
widget.description,
|
: ''),
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
backgroundColor: Colors.white,
|
||||||
),
|
labelStyle:
|
||||||
|
const TextStyle(color: Colors.black),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Center(
|
||||||
),
|
child: MarkdownBody(
|
||||||
),
|
data: '### Остаток: _${item!.count}_',
|
||||||
Align(
|
)),
|
||||||
alignment: const AlignmentDirectional(0, -1),
|
item!.description != ''
|
||||||
child: Padding(
|
? Padding(
|
||||||
padding:
|
padding: const EdgeInsetsDirectional.all(30),
|
||||||
const EdgeInsetsDirectional.fromSTEB(0, 30, 0, 0),
|
child: ConstrainedBox(
|
||||||
child: Column(
|
constraints: const BoxConstraints(
|
||||||
mainAxisSize: MainAxisSize.min,
|
minWidth: 340,
|
||||||
children: [
|
maxWidth: 340,
|
||||||
Text(
|
maxHeight: 600,
|
||||||
'Стоимость ${widget.price}',
|
),
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
child: Card(
|
||||||
|
elevation: 4,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.scaffoldBackgroundColor,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsetsDirectional.all(
|
||||||
|
15),
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minHeight: 100,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
item!.description,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
Align(
|
||||||
|
alignment: const AlignmentDirectional(0, -1),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsetsDirectional.fromSTEB(
|
||||||
|
0, 30, 0, 0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Стоимость ${item!.price.toStringAsFixed(2)}руб.',
|
||||||
|
style:
|
||||||
|
Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
_buildButton()
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
_buildButton()
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
])
|
||||||
|
: const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ import 'dart:math';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gymlink_module_web/components/app_bar.dart';
|
import 'package:gymlink_module_web/components/app_bar.dart';
|
||||||
import 'package:gymlink_module_web/components/item_card.dart';
|
import 'package:gymlink_module_web/components/item_card.dart';
|
||||||
|
import 'package:gymlink_module_web/interfaces/items.dart';
|
||||||
import 'package:gymlink_module_web/pages/basket.dart';
|
import 'package:gymlink_module_web/pages/basket.dart';
|
||||||
import 'package:gymlink_module_web/pages/detail.dart';
|
import 'package:gymlink_module_web/pages/detail.dart';
|
||||||
import 'package:gymlink_module_web/pages/order_history.dart';
|
import 'package:gymlink_module_web/pages/order_history.dart';
|
||||||
import 'package:gymlink_module_web/providers/cart.dart';
|
import 'package:gymlink_module_web/providers/cart.dart';
|
||||||
import 'package:gymlink_module_web/providers/main.dart';
|
import 'package:gymlink_module_web/tools/items.dart';
|
||||||
import 'package:gymlink_module_web/tools/prefs.dart';
|
import 'package:gymlink_module_web/tools/prefs.dart';
|
||||||
import 'package:gymlink_module_web/tools/relative.dart';
|
|
||||||
import 'package:gymlink_module_web/tools/routes.dart';
|
import 'package:gymlink_module_web/tools/routes.dart';
|
||||||
|
import 'package:gymlink_module_web/tools/text.dart';
|
||||||
import 'package:lazy_load_scrollview/lazy_load_scrollview.dart';
|
import 'package:lazy_load_scrollview/lazy_load_scrollview.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
|
|
||||||
const List<Map<String, String>> testData = [
|
const List<Map<String, String>> testData = [
|
||||||
{
|
{
|
||||||
@@ -65,48 +65,74 @@ class MainPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _MainPageState extends State<MainPage> {
|
class _MainPageState extends State<MainPage> {
|
||||||
String searchText = '';
|
String searchText = '';
|
||||||
List<Map<String, String>> filteredData = [];
|
List<GymItem> filteredData = [];
|
||||||
int cartLength = 0;
|
int cartLength = 0;
|
||||||
|
int itemViewCount = 0;
|
||||||
|
bool isLoading = false;
|
||||||
|
bool isSearching = false;
|
||||||
|
List<GymCategory> categories = [];
|
||||||
|
GymCategory? selectedCategory;
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
final TextEditingController _searchField = TextEditingController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
filteredData = testData;
|
|
||||||
getCart().then((value) {
|
getCart().then((value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
cartLength = value.length;
|
cartLength = value.length;
|
||||||
});
|
});
|
||||||
}).whenComplete(() {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
getCategories(context).then((value) => setState(() {
|
||||||
|
categories = value;
|
||||||
Future<void> _goToPage() async {
|
_onSearch();
|
||||||
final Uri url = Uri.parse('https://google.com');
|
}));
|
||||||
if (!await launchUrl(url, webOnlyWindowName: '_blank')) {
|
|
||||||
throw 'Could not launch $url';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onLoad() async {
|
void _onLoad() async {
|
||||||
await Future.delayed(const Duration(milliseconds: 1000));
|
if (itemViewCount < filteredData.length) {
|
||||||
debugPrint('aye');
|
setState(() {
|
||||||
|
isLoading = true;
|
||||||
|
});
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
setState(() {
|
||||||
|
itemViewCount = min(filteredData.length, itemViewCount + 5);
|
||||||
|
isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _searchItems({String searchText = '', String categoryId = ''}) async {
|
||||||
|
setState(() {
|
||||||
|
isSearching = true;
|
||||||
|
});
|
||||||
|
final data =
|
||||||
|
await getItems(context, searchText: searchText, categoryId: categoryId);
|
||||||
|
setState(() {
|
||||||
|
filteredData = data;
|
||||||
|
itemViewCount = min(filteredData.length, 5);
|
||||||
|
});
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
for (var element in filteredData.sublist(0, itemViewCount)) {
|
||||||
|
precacheImage(NetworkImage(element.images[0].url), context);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setState(() {
|
||||||
|
isSearching = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSearch() {
|
void _onSearch() {
|
||||||
|
final categoryId = selectedCategory == null ? '' : selectedCategory!.id;
|
||||||
setState(() {
|
setState(() {
|
||||||
filteredData = testData
|
searchText = _searchField.text.trim().toLowerCase();
|
||||||
.where((element) => (element['name']!).contains(searchText))
|
|
||||||
.toList();
|
|
||||||
});
|
});
|
||||||
|
_searchItems(searchText: searchText, categoryId: categoryId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final cartL = context.watch<CartProvider>().cartLength;
|
final cartL = context.watch<CartProvider>().cartLength;
|
||||||
final onError = context.read<GymLinkProvider>().onError;
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: const GymLinkAppBar(),
|
appBar: const GymLinkAppBar(),
|
||||||
body: Column(
|
body: Column(
|
||||||
@@ -119,19 +145,22 @@ class _MainPageState extends State<MainPage> {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
onChanged: (value) => setState(() {
|
onChanged: (value) {
|
||||||
searchText = value;
|
searchText = value.trim().toLowerCase();
|
||||||
if (searchText == '') {
|
if (searchText == '') {
|
||||||
_onSearch();
|
_onSearch();
|
||||||
}
|
}
|
||||||
}),
|
},
|
||||||
|
controller: _searchField,
|
||||||
|
textInputAction: TextInputAction.search,
|
||||||
|
onSubmitted: (_) => _onSearch(),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Поиск',
|
hintText: 'Поиск',
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(50),
|
||||||
),
|
),
|
||||||
suffixIcon: Padding(
|
suffixIcon: Padding(
|
||||||
padding: const EdgeInsets.only(right: 8),
|
padding: const EdgeInsets.only(right: 5),
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: _onSearch,
|
onPressed: _onSearch,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
@@ -154,13 +183,12 @@ class _MainPageState extends State<MainPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
getSpacer(context: context, flex: 2),
|
// getSpacer(context: context, flex: 2),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 8,
|
width: 8,
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
onError();
|
|
||||||
Navigator.of(context).push(CustomPageRoute(
|
Navigator.of(context).push(CustomPageRoute(
|
||||||
builder: (context) => const HistoryPage(),
|
builder: (context) => const HistoryPage(),
|
||||||
));
|
));
|
||||||
@@ -188,46 +216,142 @@ class _MainPageState extends State<MainPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 60,
|
||||||
|
child: ListView.builder(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: categories.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final category = categories[index];
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
selectedCategory =
|
||||||
|
selectedCategory == category ? null : category;
|
||||||
|
});
|
||||||
|
_onSearch();
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10, vertical: 10),
|
||||||
|
child: Chip(
|
||||||
|
label: Text(category.name),
|
||||||
|
backgroundColor: selectedCategory == category
|
||||||
|
? Theme.of(context).primaryColor
|
||||||
|
: Colors.white,
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
color: selectedCategory == category
|
||||||
|
? Colors.white
|
||||||
|
: Colors.black),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: LazyLoadScrollView(
|
child: LazyLoadScrollView(
|
||||||
onEndOfPage: _onLoad,
|
onEndOfPage: _onLoad,
|
||||||
child: Stack(
|
isLoading: isLoading,
|
||||||
children: [
|
child: Scrollbar(
|
||||||
GridView.builder(
|
controller: _scrollController,
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
child: ListView(
|
||||||
crossAxisCount: min(
|
controller: _scrollController,
|
||||||
(MediaQuery.sizeOf(context).width ~/ 200).toInt(), 8),
|
children: [
|
||||||
),
|
filteredData.isEmpty &&
|
||||||
itemCount: filteredData.length,
|
(searchText != '' || selectedCategory != null) &&
|
||||||
itemBuilder: (context, index) {
|
!isSearching
|
||||||
final product = filteredData[index];
|
? const Center(child: Text('Ничего не найдено'))
|
||||||
return ProductCard(
|
: isSearching
|
||||||
imagePath: Image(
|
? const Center(child: CircularProgressIndicator())
|
||||||
image: Image.network(
|
: GridView.builder(
|
||||||
'https://rus-sport.net/upload/iblock/311/topb85ez18pq0aavohpa5zipk2sbfxll.jpg')
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
.image,
|
shrinkWrap: true,
|
||||||
width: 50,
|
gridDelegate:
|
||||||
),
|
SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
name: product['name']!,
|
crossAxisCount: min(
|
||||||
price: product['price']!,
|
(MediaQuery.sizeOf(context).width ~/
|
||||||
onTap: () => Navigator.of(context).push(
|
220)
|
||||||
CustomPageRoute(
|
.toInt(),
|
||||||
builder: (context) => DetailPage(
|
8),
|
||||||
name: product['name']!,
|
childAspectRatio: 0.8,
|
||||||
description: product['details']!,
|
mainAxisSpacing: 10.0,
|
||||||
price: product['price']!,
|
crossAxisSpacing: 20.0),
|
||||||
id: product['id']!,
|
itemCount: itemViewCount,
|
||||||
image: Image(
|
itemBuilder: (context, index) {
|
||||||
image:
|
final product = filteredData[index];
|
||||||
AssetImage('assets/${product['image']!}'),
|
return ProductCard(
|
||||||
width: 300),
|
imagePath: FutureBuilder(
|
||||||
|
future: precacheImage(
|
||||||
|
NetworkImage(product.images[0].url),
|
||||||
|
context),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState ==
|
||||||
|
ConnectionState.done) {
|
||||||
|
return Image(
|
||||||
|
image: NetworkImage(
|
||||||
|
product.images[0].url),
|
||||||
|
width: 120,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const CircularProgressIndicator();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
name: shortString(product.title),
|
||||||
|
price: product.price.toStringAsFixed(2),
|
||||||
|
onTap: () => Navigator.of(context).push(
|
||||||
|
CustomPageRoute(
|
||||||
|
builder: (context) => DetailPage(
|
||||||
|
id: product.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
itemViewCount > 0 && !isSearching
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
child: Center(
|
||||||
|
child: itemViewCount < filteredData.length
|
||||||
|
? !isLoading
|
||||||
|
? ElevatedButton(
|
||||||
|
onPressed: _onLoad,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).primaryColor,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(50)),
|
||||||
|
),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
fixedSize: const Size(180, 40),
|
||||||
|
),
|
||||||
|
child: const Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('Загрузить ещё'),
|
||||||
|
Spacer(),
|
||||||
|
Icon(Icons.arrow_downward),
|
||||||
|
Spacer()
|
||||||
|
],
|
||||||
|
))
|
||||||
|
: const CircularProgressIndicator()
|
||||||
|
: const Text(
|
||||||
|
'Конец списка',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: Color(0x88000000)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
),
|
: const SizedBox.shrink(),
|
||||||
);
|
],
|
||||||
},
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
import 'package:gymlink_module_web/components/app_bar.dart';
|
import 'package:gymlink_module_web/components/app_bar.dart';
|
||||||
import 'package:gymlink_module_web/components/heading.dart';
|
import 'package:gymlink_module_web/components/heading.dart';
|
||||||
import 'package:gymlink_module_web/components/order_confirm_item_card.dart';
|
import 'package:gymlink_module_web/components/order_confirm_item_card.dart';
|
||||||
|
import 'package:gymlink_module_web/interfaces/items.dart';
|
||||||
import 'package:gymlink_module_web/pages/order_history.dart';
|
import 'package:gymlink_module_web/pages/order_history.dart';
|
||||||
import 'package:gymlink_module_web/providers/cart.dart';
|
import 'package:gymlink_module_web/providers/cart.dart';
|
||||||
|
import 'package:gymlink_module_web/tools/history.dart';
|
||||||
|
import 'package:gymlink_module_web/tools/items.dart';
|
||||||
import 'package:gymlink_module_web/tools/prefs.dart';
|
import 'package:gymlink_module_web/tools/prefs.dart';
|
||||||
import 'package:gymlink_module_web/tools/routes.dart';
|
import 'package:gymlink_module_web/tools/routes.dart';
|
||||||
|
import 'package:gymlink_module_web/tools/text.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
@@ -56,33 +62,154 @@ class OrderConfirmationPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _OrderConfirmationPageState extends State<OrderConfirmationPage> {
|
class _OrderConfirmationPageState extends State<OrderConfirmationPage> {
|
||||||
List<Map<String, dynamic>> cartItems = [];
|
List<GymItem> cartItems = [];
|
||||||
int totalPrice = 0;
|
double totalPrice = 0;
|
||||||
|
List<GymItem> gymCart = [];
|
||||||
|
bool isAgree = false;
|
||||||
|
bool _isLoading = true;
|
||||||
|
final _emailController = TextEditingController();
|
||||||
|
final _addressController = TextEditingController();
|
||||||
|
final _nameController = TextEditingController();
|
||||||
|
|
||||||
|
Future<void> _addOrderToHistory() async {
|
||||||
|
String name = _nameController.text;
|
||||||
|
String email = _emailController.text;
|
||||||
|
String address = _addressController.text;
|
||||||
|
|
||||||
|
Set<String> supplierIdsSet = {};
|
||||||
|
for (final item in cartItems) {
|
||||||
|
supplierIdsSet.add(item.supplierId);
|
||||||
|
}
|
||||||
|
List<GymHistoryItemDetailProvider> providers = [];
|
||||||
|
for (final supplierId in supplierIdsSet) {
|
||||||
|
List<GymItem> items =
|
||||||
|
cartItems.where((e) => e.supplierId == supplierId).toList();
|
||||||
|
List<GymHistoryItemDetailItem> detailItems = [];
|
||||||
|
for (final item in items) {
|
||||||
|
detailItems.add(GymHistoryItemDetailItem(
|
||||||
|
id: item.id,
|
||||||
|
photo: item.images[0].url,
|
||||||
|
count: item.localCount,
|
||||||
|
price: item.price.toString(),
|
||||||
|
name: item.title,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
GymHistoryItemDetailProvider provider = GymHistoryItemDetailProvider(
|
||||||
|
id: supplierId,
|
||||||
|
name: items.first.supplierName,
|
||||||
|
items: detailItems,
|
||||||
|
// status: 'Не оплачен'
|
||||||
|
status: Random().nextBool()
|
||||||
|
? 'Не оплачен'
|
||||||
|
: Random().nextBool()
|
||||||
|
? 'Не оплачен'
|
||||||
|
: Random().nextBool()
|
||||||
|
? 'Сборка'
|
||||||
|
: 'Доставляется',
|
||||||
|
);
|
||||||
|
providers.add(provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
final order = GymHistoryItemDetail(
|
||||||
|
id: Random().nextInt(1000000).toString(),
|
||||||
|
receiver: name,
|
||||||
|
email: email,
|
||||||
|
address: address,
|
||||||
|
sum: totalPrice.toString(),
|
||||||
|
date: '',
|
||||||
|
providers: providers,
|
||||||
|
timestamp: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||||
|
);
|
||||||
|
await addToHistory(order);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
getCart().then((value) {
|
Future.microtask(() => getCart().then((value) async {
|
||||||
setState(() {
|
final itemIds =
|
||||||
cartItems = value.map((element) {
|
value.map((element) => element['id'] as String).toList();
|
||||||
final item = cart.firstWhere((e) => e['id'] == element['id']);
|
final items = await getItemsByIds(context, itemIds);
|
||||||
return {...item, 'count': element['count'] as int};
|
setState(() {
|
||||||
}).toList();
|
gymCart = items;
|
||||||
totalPrice = cartItems.fold(
|
cartItems = value.map((element) {
|
||||||
0,
|
final item = gymCart.firstWhere((e) => e.id == element['id']);
|
||||||
(sum, item) =>
|
item.localCount = element['count'] as int;
|
||||||
sum + int.parse(item['price']) * item['count'] as int);
|
return item;
|
||||||
});
|
}).toList();
|
||||||
});
|
totalPrice = cartItems.fold(
|
||||||
|
0, (sum, item) => sum + item.price * item.localCount);
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _goToPage() async {
|
Future<void> _goToPage() async {
|
||||||
final Uri url = Uri.parse('https://google.com');
|
final Uri url = Uri.parse('https://example.org');
|
||||||
if (!await launchUrl(url, webOnlyWindowName: '_blank')) {
|
if (!await launchUrl(url, webOnlyWindowName: '_blank')) {
|
||||||
throw 'Could not launch $url';
|
throw 'Could not launch $url';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _checkInputs() {
|
||||||
|
final email = _emailController.text;
|
||||||
|
final address = _addressController.text;
|
||||||
|
final name = _nameController.text;
|
||||||
|
|
||||||
|
if (!RegExp(r"^((?!\.)[\w\-_.]*[^.])(@\w+)(\.\w+(\.\w+)?[^.\W])$")
|
||||||
|
.hasMatch(email)) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Ошибка'),
|
||||||
|
content: const Text('Некорректный email'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('ОК'),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (address.isEmpty) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Ошибка'),
|
||||||
|
content: const Text('Адрес не может быть пустым'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('ОК'),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.isEmpty) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Ошибка'),
|
||||||
|
content: const Text('ФИО не может быть пустым'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('ОК'),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -94,24 +221,39 @@ class _OrderConfirmationPageState extends State<OrderConfirmationPage> {
|
|||||||
const GymLinkHeader(title: 'Оформление заказа'),
|
const GymLinkHeader(title: 'Оформление заказа'),
|
||||||
const MarkdownBody(data: '## Состав заказа:'),
|
const MarkdownBody(data: '## Состав заказа:'),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ConstrainedBox(
|
child: _isLoading
|
||||||
constraints: const BoxConstraints(maxHeight: 350),
|
? const Center(child: CircularProgressIndicator())
|
||||||
child: ListView.builder(
|
: ConstrainedBox(
|
||||||
itemCount: cartItems.length,
|
constraints: const BoxConstraints(maxHeight: 350),
|
||||||
itemBuilder: (context, index) {
|
child: ListView.builder(
|
||||||
final item = cartItems[index];
|
shrinkWrap: true,
|
||||||
return OrderConfirmItemCard(
|
itemCount: cartItems.length,
|
||||||
name: item['name'],
|
itemBuilder: (context, index) {
|
||||||
image: Image(
|
final item = cartItems[index];
|
||||||
image: AssetImage('assets/${item['image']}'),
|
return OrderConfirmItemCard(
|
||||||
width: 50,
|
name: shortString(item.title),
|
||||||
height: 50),
|
image: FutureBuilder(
|
||||||
price: double.parse(item['price']),
|
future: precacheImage(
|
||||||
count: item['count'],
|
NetworkImage(item.images[0].url), context),
|
||||||
);
|
builder: (context, snapshot) {
|
||||||
},
|
if (snapshot.connectionState ==
|
||||||
),
|
ConnectionState.done) {
|
||||||
),
|
return Image(
|
||||||
|
image: NetworkImage(item.images[0].url),
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const CircularProgressIndicator();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
price: item.price,
|
||||||
|
count: item.localCount,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 10,
|
height: 10,
|
||||||
@@ -119,9 +261,11 @@ class _OrderConfirmationPageState extends State<OrderConfirmationPage> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
MarkdownBody(data: '## Итого: $totalPrice'),
|
MarkdownBody(
|
||||||
|
data: '## Итого: ${totalPrice.toStringAsFixed(2)} руб.'),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
|
controller: _addressController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Адрес доставки',
|
hintText: 'Адрес доставки',
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
@@ -132,6 +276,7 @@ class _OrderConfirmationPageState extends State<OrderConfirmationPage> {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
|
controller: _emailController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Электронная почта',
|
hintText: 'Электронная почта',
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
@@ -143,6 +288,7 @@ class _OrderConfirmationPageState extends State<OrderConfirmationPage> {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
|
controller: _nameController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Получатель',
|
hintText: 'Получатель',
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
@@ -153,8 +299,10 @@ class _OrderConfirmationPageState extends State<OrderConfirmationPage> {
|
|||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
if (!_checkInputs()) return;
|
||||||
_goToPage();
|
_goToPage();
|
||||||
await clearCart();
|
await clearCart();
|
||||||
|
await _addOrderToHistory();
|
||||||
Provider.of<CartProvider>(context, listen: false)
|
Provider.of<CartProvider>(context, listen: false)
|
||||||
.updateCartLength();
|
.updateCartLength();
|
||||||
Navigator.of(context).pushAndRemoveUntil(
|
Navigator.of(context).pushAndRemoveUntil(
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gymlink_module_web/components/app_bar.dart';
|
import 'package:gymlink_module_web/components/app_bar.dart';
|
||||||
import 'package:gymlink_module_web/components/heading.dart';
|
import 'package:gymlink_module_web/components/heading.dart';
|
||||||
import 'package:gymlink_module_web/components/history_item_card.dart';
|
import 'package:gymlink_module_web/components/history_item_card.dart';
|
||||||
import 'package:gymlink_module_web/tools/relative.dart';
|
import 'package:gymlink_module_web/interfaces/items.dart';
|
||||||
|
import 'package:gymlink_module_web/tools/history.dart';
|
||||||
import 'package:lazy_load_scrollview/lazy_load_scrollview.dart';
|
import 'package:lazy_load_scrollview/lazy_load_scrollview.dart';
|
||||||
|
|
||||||
List<Map<String, String>> orders = [
|
List<Map<String, String>> orders = [
|
||||||
@@ -45,32 +47,24 @@ class HistoryPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _HistoryPageState extends State<HistoryPage> {
|
class _HistoryPageState extends State<HistoryPage> {
|
||||||
List<Map<String, String>> my_orders = [];
|
List<GymHistoryItem> my_orders = [];
|
||||||
late Timer _updateTimer;
|
late Timer _updateTimer;
|
||||||
|
bool _isLoading = true;
|
||||||
|
bool _isRefreshing = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
my_orders = orders;
|
|
||||||
ordersRefresh();
|
ordersRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ordersRefresh() {
|
void ordersRefresh() {
|
||||||
_updateTimer = Timer.periodic(const Duration(minutes: 1), _onRefresh);
|
_updateTimer = Timer.periodic(const Duration(minutes: 1), _onRefresh);
|
||||||
|
Future.microtask(() => _onRefresh(_updateTimer));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onLoad() async {
|
void _onLoad() async {
|
||||||
await Future.delayed(const Duration(milliseconds: 1000));
|
await Future.delayed(const Duration(milliseconds: 1000));
|
||||||
setState(() {
|
|
||||||
my_orders.add(
|
|
||||||
{
|
|
||||||
"image": "product.png",
|
|
||||||
"price": "120",
|
|
||||||
"id": "666666",
|
|
||||||
"date": "11.09.2001"
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -81,7 +75,12 @@ class _HistoryPageState extends State<HistoryPage> {
|
|||||||
|
|
||||||
Future<void> _onRefresh(Timer timer) async {
|
Future<void> _onRefresh(Timer timer) async {
|
||||||
await Future.delayed(const Duration(milliseconds: 1000));
|
await Future.delayed(const Duration(milliseconds: 1000));
|
||||||
debugPrint('refreshed');
|
var orders = await getHistory();
|
||||||
|
setState(() {
|
||||||
|
my_orders = orders;
|
||||||
|
_isLoading = false;
|
||||||
|
_isRefreshing = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -91,44 +90,100 @@ class _HistoryPageState extends State<HistoryPage> {
|
|||||||
body: Column(
|
body: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const GymLinkHeader(title: 'История заказов'),
|
const GymLinkHeader(
|
||||||
Expanded(
|
title: 'История заказов',
|
||||||
child: Row(
|
toMain: true,
|
||||||
children: [
|
),
|
||||||
Expanded(
|
const SizedBox(height: 5),
|
||||||
child: LazyLoadScrollView(
|
kIsWeb && !_isLoading
|
||||||
onEndOfPage: _onLoad,
|
? Center(
|
||||||
scrollOffset: 200,
|
child: ElevatedButton(
|
||||||
child: RefreshIndicator(
|
onPressed: () {
|
||||||
edgeOffset: 55,
|
setState(() => _isRefreshing = true);
|
||||||
onRefresh: () => _onRefresh(_updateTimer),
|
_onRefresh(_updateTimer);
|
||||||
child: Stack(
|
},
|
||||||
children: [
|
style: ElevatedButton.styleFrom(
|
||||||
ListView.builder(
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
itemCount: my_orders.length,
|
shape: const RoundedRectangleBorder(
|
||||||
itemBuilder: (context, index) {
|
borderRadius: BorderRadius.all(Radius.circular(50)),
|
||||||
final item = my_orders[index];
|
|
||||||
return HistoryItemCard(
|
|
||||||
id: item['id']!,
|
|
||||||
cost: item['price']!,
|
|
||||||
date: item['date']!,
|
|
||||||
image: Image(
|
|
||||||
image: AssetImage('assets/${item['image']!}'),
|
|
||||||
width: 50,
|
|
||||||
),
|
|
||||||
status: OrderStatus.completed,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
fixedSize: const Size(180, 40),
|
||||||
|
),
|
||||||
|
child: const Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('Обновить'),
|
||||||
|
Spacer(),
|
||||||
|
Icon(Icons.refresh),
|
||||||
|
Spacer()
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
_isRefreshing
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
_isLoading
|
||||||
|
? const Expanded(
|
||||||
|
child: Center(child: CircularProgressIndicator()))
|
||||||
|
: Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: LazyLoadScrollView(
|
||||||
|
onEndOfPage: _onLoad,
|
||||||
|
scrollOffset: 200,
|
||||||
|
child: RefreshIndicator(
|
||||||
|
edgeOffset: 55,
|
||||||
|
onRefresh: () => _onRefresh(_updateTimer),
|
||||||
|
child: my_orders.isEmpty
|
||||||
|
? const Center(child: Text('Нет заказов'))
|
||||||
|
: Stack(
|
||||||
|
children: [
|
||||||
|
ListView.builder(
|
||||||
|
itemCount: my_orders.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = my_orders[index];
|
||||||
|
return HistoryItemCard(
|
||||||
|
id: item.id,
|
||||||
|
cost: double.parse(item.sum)
|
||||||
|
.toStringAsFixed(2),
|
||||||
|
date: item.date,
|
||||||
|
image: FutureBuilder(
|
||||||
|
future: precacheImage(
|
||||||
|
NetworkImage(item.photo),
|
||||||
|
context),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState ==
|
||||||
|
ConnectionState.done) {
|
||||||
|
return Image(
|
||||||
|
image: NetworkImage(
|
||||||
|
item.photo),
|
||||||
|
width: 50,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const CircularProgressIndicator();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// my_orders.isEmpty
|
||||||
|
// ? const SizedBox.shrink()
|
||||||
|
// : getSpacer(context: context)
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
getSpacer(context: context)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
265
lib/pages/order_info.dart
Normal file
265
lib/pages/order_info.dart
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
|
import 'package:gymlink_module_web/components/app_bar.dart';
|
||||||
|
import 'package:gymlink_module_web/components/heading.dart';
|
||||||
|
import 'package:gymlink_module_web/components/order_detail_item_card.dart';
|
||||||
|
import 'package:gymlink_module_web/interfaces/items.dart';
|
||||||
|
import 'package:gymlink_module_web/tools/history.dart';
|
||||||
|
import 'package:gymlink_module_web/tools/text.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
final GymHistoryItemDetail item = GymHistoryItemDetail.fromJson({
|
||||||
|
"id": "12345",
|
||||||
|
"date": "01.01.1970",
|
||||||
|
"sum": "45000",
|
||||||
|
"pay_url": "https://example.org",
|
||||||
|
"receiver": "Иванов Иван Иванович",
|
||||||
|
"email": "a@a.ru",
|
||||||
|
"address": "г. Москва, ул. Пушкина, д. 17",
|
||||||
|
"providers": [
|
||||||
|
{
|
||||||
|
"id": "123",
|
||||||
|
"name": "Поставщик 1",
|
||||||
|
"status": "Доставлен",
|
||||||
|
"items": [
|
||||||
|
{"photo": "url", "id": "123", "count": 2, "price": "15000"},
|
||||||
|
{"photo": "url", "id": "123", "count": 2, "price": "15000"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "123",
|
||||||
|
"name": "Поставщик 1",
|
||||||
|
"status": "Доставляется",
|
||||||
|
"items": [
|
||||||
|
{"photo": "url", "id": "123", "count": 2, "price": "15000"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
class OrderInfoPage extends StatefulWidget {
|
||||||
|
final String id;
|
||||||
|
const OrderInfoPage({super.key, required this.id});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _OrderInfoPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OrderInfoPageState extends State<OrderInfoPage> {
|
||||||
|
GymHistoryItemDetail? detail;
|
||||||
|
final _scrollController = ScrollController();
|
||||||
|
late Timer _updateTimer;
|
||||||
|
bool _isRefreshing = false;
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_updateTimer = Timer.periodic(const Duration(minutes: 1), _onRefresh);
|
||||||
|
_onRefresh(_updateTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_updateTimer.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onRefresh(Timer timer) async {
|
||||||
|
return Future.delayed(const Duration(milliseconds: 1000), () async {
|
||||||
|
var orderInfo = await getHistoryDetail(widget.id);
|
||||||
|
setState(() {
|
||||||
|
detail = orderInfo;
|
||||||
|
_isRefreshing = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _goToPage() async {
|
||||||
|
if (detail?.payUrl != null) {
|
||||||
|
final Uri url = Uri.parse(detail?.payUrl ?? 'https://example.org');
|
||||||
|
if (!await launchUrl(url, webOnlyWindowName: '_blank')) {
|
||||||
|
throw 'Could not launch $url';
|
||||||
|
}
|
||||||
|
_onRefresh(_updateTimer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent() {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
GymLinkHeader(title: "Заказ #${detail?.id} от ${detail?.date}"),
|
||||||
|
Expanded(
|
||||||
|
child: RefreshIndicator(
|
||||||
|
onRefresh: () => _onRefresh(_updateTimer),
|
||||||
|
edgeOffset: 55,
|
||||||
|
child: Scrollbar(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: ListView(
|
||||||
|
controller: _scrollController,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
kIsWeb
|
||||||
|
? Center(
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_isRefreshing = true;
|
||||||
|
});
|
||||||
|
_onRefresh(_updateTimer);
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.all(Radius.circular(50)),
|
||||||
|
),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
fixedSize: const Size(180, 40),
|
||||||
|
),
|
||||||
|
child: const Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('Обновить'),
|
||||||
|
Spacer(),
|
||||||
|
Icon(Icons.refresh),
|
||||||
|
Spacer()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
_isRefreshing
|
||||||
|
? const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
ListView.builder(
|
||||||
|
itemCount: detail!.providers.length,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final provider = detail!.providers[index];
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsetsDirectional.symmetric(
|
||||||
|
vertical: 10,
|
||||||
|
horizontal: 5,
|
||||||
|
),
|
||||||
|
child: Card(
|
||||||
|
elevation: 3,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
MarkdownBody(data: '# ${provider.name}'),
|
||||||
|
MarkdownBody(
|
||||||
|
data: '## Статус: ${provider.status}'),
|
||||||
|
const MarkdownBody(data: '### Состав:'),
|
||||||
|
for (final item in provider.items)
|
||||||
|
OrderDetailCardItemCard(
|
||||||
|
image: FutureBuilder(
|
||||||
|
future: precacheImage(
|
||||||
|
NetworkImage(item.photo), context),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState ==
|
||||||
|
ConnectionState.done) {
|
||||||
|
return Image(
|
||||||
|
image: NetworkImage(item.photo),
|
||||||
|
width: 50,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const CircularProgressIndicator();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
name: shortString(item.name),
|
||||||
|
count: item.count,
|
||||||
|
price: double.parse(item.price),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 10,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 200,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsetsDirectional.symmetric(
|
||||||
|
horizontal: 5,
|
||||||
|
),
|
||||||
|
child: Card(
|
||||||
|
elevation: 4,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsetsDirectional.symmetric(
|
||||||
|
horizontal: 10, vertical: 20),
|
||||||
|
child: Column(
|
||||||
|
// mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
MarkdownBody(
|
||||||
|
data:
|
||||||
|
'## Итого: ${double.parse(detail!.sum).toStringAsFixed(2)} руб.'),
|
||||||
|
MarkdownBody(
|
||||||
|
data:
|
||||||
|
"### Адрес получателя: __${detail!.address}__"),
|
||||||
|
MarkdownBody(
|
||||||
|
data: '### Почта: __${detail!.email}__'),
|
||||||
|
MarkdownBody(
|
||||||
|
data: '### ФИО: __${detail!.receiver}__'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
detail?.payUrl == null
|
||||||
|
? const SizedBox.shrink()
|
||||||
|
: Center(
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await _goToPage();
|
||||||
|
await payOrder(detail!.id);
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).primaryColor,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(50)),
|
||||||
|
),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
fixedSize: const Size(180, 40),
|
||||||
|
),
|
||||||
|
child: const Text('Оплатить заказ'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: const GymLinkAppBar(),
|
||||||
|
body: detail == null
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: _buildContent(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,24 +6,22 @@ class GymLinkProvider with ChangeNotifier {
|
|||||||
bool get isLoading => _isLoading;
|
bool get isLoading => _isLoading;
|
||||||
bool _blackTheme = false;
|
bool _blackTheme = false;
|
||||||
bool get blackTheme => _blackTheme;
|
bool get blackTheme => _blackTheme;
|
||||||
|
String _token = '';
|
||||||
|
String get token => _token;
|
||||||
ThemeData _theme = myTheme;
|
ThemeData _theme = myTheme;
|
||||||
ThemeData get theme => _theme;
|
ThemeData get theme => _theme;
|
||||||
void Function() _onError = () => {};
|
void Function() _onError = () => {};
|
||||||
|
|
||||||
void Function() get onError => _onError;
|
void Function() get onError => _onError;
|
||||||
|
|
||||||
void onTokenReceived(String token) {
|
void checkToken(String token) {
|
||||||
if (token == 'token123') {
|
_token = token;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} else {
|
|
||||||
_isLoading = true;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void changeTheme(int color) {
|
void changeTheme(int color, {bool blackTheme = false}) {
|
||||||
_blackTheme = !_blackTheme;
|
_blackTheme = blackTheme;
|
||||||
_theme = getThemeData(Color(color), _blackTheme);
|
_theme = getThemeData(Color(color), _blackTheme);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -34,6 +32,11 @@ class GymLinkProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void setOnError(void Function() onError) {
|
void setOnError(void Function() onError) {
|
||||||
_onError = onError;
|
_onError = () {
|
||||||
|
_token = '';
|
||||||
|
_isLoading = true;
|
||||||
|
onError();
|
||||||
|
notifyListeners();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ class MyAppStateMobile extends State<MyApp> {
|
|||||||
: MaterialApp(
|
: MaterialApp(
|
||||||
title: 'GymLink Module',
|
title: 'GymLink Module',
|
||||||
theme: theme,
|
theme: theme,
|
||||||
|
themeMode: context.read<GymLinkProvider>().blackTheme
|
||||||
|
? ThemeMode.dark
|
||||||
|
: ThemeMode.light,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
home: const MainPage(),
|
home: const MainPage(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -48,13 +48,13 @@ class MyAppStateWeb extends State<MyApp> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@js.JSExport()
|
@js.JSExport()
|
||||||
void onTokenReceived(String token) {
|
void checkToken(String token) {
|
||||||
context.read<GymLinkProvider>().onTokenReceived(token);
|
context.read<GymLinkProvider>().checkToken(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
@js.JSExport()
|
@js.JSExport()
|
||||||
void changeColor(int color) {
|
void changeColor(int color, bool blackTheme) {
|
||||||
context.read<GymLinkProvider>().changeTheme(color);
|
context.read<GymLinkProvider>().changeTheme(color, blackTheme: blackTheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
@js.JSExport()
|
@js.JSExport()
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ ThemeData getThemeData(Color color, bool dark) {
|
|||||||
).copyWith(
|
).copyWith(
|
||||||
onPrimary: dark ? materialColor[600] : Colors.white,
|
onPrimary: dark ? materialColor[600] : Colors.white,
|
||||||
),
|
),
|
||||||
useMaterial3: true,
|
// useMaterial3: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
99
lib/tools/history.dart
Normal file
99
lib/tools/history.dart
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:gymlink_module_web/interfaces/items.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
Future<List<GymHistoryItem>> getHistory() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
String historyString = prefs.getString('history') ?? "[]";
|
||||||
|
List<GymHistoryItem> history = [];
|
||||||
|
for (var historyItem in jsonDecode(historyString) as List<dynamic>) {
|
||||||
|
history.add(GymHistoryItem.fromJson(historyItem));
|
||||||
|
}
|
||||||
|
history.sort((a, b) => b.timestamp.compareTo(a.timestamp));
|
||||||
|
return history;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addToHistory(GymHistoryItemDetail item) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
String historyString = prefs.getString('history') ?? "[]";
|
||||||
|
List<GymHistoryItem> history = [];
|
||||||
|
for (var historyItem in jsonDecode(historyString) as List<dynamic>) {
|
||||||
|
history.add(GymHistoryItem.fromJson(historyItem));
|
||||||
|
}
|
||||||
|
item.id = Random().nextInt(100000).toString();
|
||||||
|
String detailHistoryString = prefs.getString('detail_history') ?? "[]";
|
||||||
|
List<GymHistoryItemDetail> detailHistory = [];
|
||||||
|
for (var historyItem in jsonDecode(detailHistoryString) as List<dynamic>) {
|
||||||
|
detailHistory.add(GymHistoryItemDetail.fromJson(historyItem));
|
||||||
|
}
|
||||||
|
List<Map<String, dynamic>> providers = [];
|
||||||
|
for (final provider in item.providers) {
|
||||||
|
providers.add(provider.toJson());
|
||||||
|
}
|
||||||
|
var json = {
|
||||||
|
"id": item.id,
|
||||||
|
"date": DateTime.now()
|
||||||
|
.toLocal()
|
||||||
|
.toString()
|
||||||
|
.split(' ')[0]
|
||||||
|
.replaceAll('-', '.')
|
||||||
|
.split('.')
|
||||||
|
.reversed
|
||||||
|
.join('.'),
|
||||||
|
"sum": item.sum,
|
||||||
|
"pay_url": item.providers.where((e) => e.status == 'Не оплачен').isNotEmpty
|
||||||
|
? 'https://example.org'
|
||||||
|
: null,
|
||||||
|
"receiver": item.receiver,
|
||||||
|
"email": item.email,
|
||||||
|
"address": item.address,
|
||||||
|
"providers": providers,
|
||||||
|
"timestamp": DateTime.now().millisecondsSinceEpoch.toString(),
|
||||||
|
};
|
||||||
|
final detailHistoryItem = GymHistoryItemDetail.fromJson(json);
|
||||||
|
detailHistory.add(detailHistoryItem);
|
||||||
|
history.add(GymHistoryItem(
|
||||||
|
date: detailHistoryItem.date,
|
||||||
|
id: detailHistoryItem.id,
|
||||||
|
photo: detailHistoryItem.providers[0].items[0].photo,
|
||||||
|
sum: detailHistoryItem.sum,
|
||||||
|
timestamp: detailHistoryItem.timestamp,
|
||||||
|
));
|
||||||
|
prefs.setString('history', jsonEncode(history));
|
||||||
|
prefs.setString('detail_history', jsonEncode(detailHistory));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<GymHistoryItemDetail?> getHistoryDetail(String id) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
String historyString = prefs.getString('detail_history') ?? "[]";
|
||||||
|
for (var historyItem in jsonDecode(historyString) as List<dynamic>) {
|
||||||
|
if (GymHistoryItemDetail.fromJson(historyItem).id == id) {
|
||||||
|
return GymHistoryItemDetail.fromJson(historyItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> payOrder(String id) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
String historyString = prefs.getString('detail_history') ?? "[]";
|
||||||
|
List<GymHistoryItemDetail> history = [];
|
||||||
|
for (var historyItem in jsonDecode(historyString) as List<dynamic>) {
|
||||||
|
history.add(GymHistoryItemDetail.fromJson(historyItem));
|
||||||
|
}
|
||||||
|
List<GymHistoryItemDetail> newHistory = [];
|
||||||
|
for (final historyItem in history) {
|
||||||
|
if (historyItem.id == id) {
|
||||||
|
for (final provider in historyItem.providers) {
|
||||||
|
if (provider.status == 'Не оплачен') {
|
||||||
|
provider.status = 'Оплачен';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
historyItem.payUrl = null;
|
||||||
|
}
|
||||||
|
newHistory.add(historyItem);
|
||||||
|
}
|
||||||
|
prefs.setString('detail_history', jsonEncode(newHistory));
|
||||||
|
}
|
||||||
97
lib/tools/items.dart
Normal file
97
lib/tools/items.dart
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gymlink_module_web/interfaces/items.dart';
|
||||||
|
import 'package:gymlink_module_web/providers/main.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
Future<List<GymItem>> getItems(BuildContext context,
|
||||||
|
{String searchText = '', String categoryId = ''}) async {
|
||||||
|
final token = context.read<GymLinkProvider>().token;
|
||||||
|
if (token != '') {
|
||||||
|
final Uri url = Uri.https('gymlink.freemyip.com', 'api/product/get-list');
|
||||||
|
try {
|
||||||
|
final response = await http.post(url,
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer $token',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: jsonEncode({
|
||||||
|
"search": searchText,
|
||||||
|
"page": 0,
|
||||||
|
"pageSize": 0,
|
||||||
|
"filter": categoryId,
|
||||||
|
"direction": 1
|
||||||
|
}));
|
||||||
|
if (response.statusCode == 201) {
|
||||||
|
final items = ItemsDataResponse.fromRawJson(response.body).rows;
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
throw response.body;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('error: $e');
|
||||||
|
return await Future.delayed(
|
||||||
|
const Duration(seconds: 5), () => getItems(context));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.read<GymLinkProvider>().onError();
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<GymItem>> getItemsByIds(
|
||||||
|
BuildContext context, List<String> ids) async {
|
||||||
|
final token = context.read<GymLinkProvider>().token;
|
||||||
|
if (token != '') {
|
||||||
|
if (ids.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
final Uri url =
|
||||||
|
Uri.https('gymlink.freemyip.com', 'api/product/get-products');
|
||||||
|
try {
|
||||||
|
final response = await http.post(url,
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer $token',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: jsonEncode({"ids": ids}));
|
||||||
|
if (response.statusCode == 201) {
|
||||||
|
final data =
|
||||||
|
jsonDecode(utf8.decode(response.bodyBytes)) as List<dynamic>;
|
||||||
|
final items = data.map((e) => GymItem.fromJson(e)).toList();
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
throw response.body;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('error: $e');
|
||||||
|
return await Future.delayed(
|
||||||
|
const Duration(seconds: 5), () => getItemsByIds(context, ids));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.read<GymLinkProvider>().onError();
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<GymCategory>> getCategories(BuildContext context) async {
|
||||||
|
final token = context.read<GymLinkProvider>().token;
|
||||||
|
if (token != '') {
|
||||||
|
final Uri url = Uri.https(
|
||||||
|
'gymlink.freemyip.com', 'api/category/get-internal-categories');
|
||||||
|
try {
|
||||||
|
final response = await http.get(url, headers: {
|
||||||
|
'Authorization': 'Bearer $token',
|
||||||
|
});
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final categories = CategoryDataResponse.fromRawJson(response.body).rows;
|
||||||
|
return categories;
|
||||||
|
}
|
||||||
|
throw response.body;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('error: $e');
|
||||||
|
return await Future.delayed(
|
||||||
|
const Duration(seconds: 5), () => getCategories(context));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.read<GymLinkProvider>().onError();
|
||||||
|
return [];
|
||||||
|
}
|
||||||
9
lib/tools/text.dart
Normal file
9
lib/tools/text.dart
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
String shortString(String text, {int length = 10}) {
|
||||||
|
if (text.length > length) {
|
||||||
|
String shortText = text.substring(0, length);
|
||||||
|
return shortText +
|
||||||
|
(text.substring(0, length + 1).endsWith(' ') ? '' : '...');
|
||||||
|
} else {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
76
pubspec.lock
76
pubspec.lock
@@ -25,6 +25,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.1"
|
||||||
|
carousel_slider:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: carousel_slider
|
||||||
|
sha256: "9c695cc963bf1d04a47bd6021f68befce8970bcd61d24938e1fb0918cf5d9c42"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.2.1"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -69,10 +77,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: cupertino_icons
|
name: cupertino_icons
|
||||||
sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d
|
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.6"
|
version: "1.0.8"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -118,6 +126,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.1"
|
version: "0.7.1"
|
||||||
|
flutter_svg:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_svg
|
||||||
|
sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.10+1"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -152,6 +168,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.2"
|
version: "4.0.2"
|
||||||
|
lazy_load_scrollview:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: lazy_load_scrollview
|
||||||
|
sha256: "230c827d6f7ec5e461f0674ef332daae2f78190bf1e4cd84977e51de04b231e3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -232,6 +256,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.0"
|
version: "1.9.0"
|
||||||
|
path_parsing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_parsing
|
||||||
|
sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
path_provider_linux:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -256,6 +288,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.2.1"
|
||||||
|
petitparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: petitparser
|
||||||
|
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.2"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -477,6 +517,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.1"
|
version: "3.1.1"
|
||||||
|
vector_graphics:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_graphics
|
||||||
|
sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.11+1"
|
||||||
|
vector_graphics_codec:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_graphics_codec
|
||||||
|
sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.11+1"
|
||||||
|
vector_graphics_compiler:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_graphics_compiler
|
||||||
|
sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.11+1"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -517,6 +581,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.4"
|
version: "1.0.4"
|
||||||
|
xml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xml
|
||||||
|
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.5.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.3.3 <4.0.0"
|
dart: ">=3.3.3 <4.0.0"
|
||||||
flutter: ">=3.19.0"
|
flutter: ">=3.19.0"
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ dependencies:
|
|||||||
lazy_load_scrollview: ^1.3.0
|
lazy_load_scrollview: ^1.3.0
|
||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
flutter_svg: ^2.0.10+1
|
flutter_svg: ^2.0.10+1
|
||||||
|
carousel_slider: ^4.2.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<!--
|
<!--
|
||||||
If you are serving your web app in a path other than the root, change the
|
If you are serving your web app in a path other than the root, change the
|
||||||
href value below to reflect the base path you are serving from.
|
href value below to reflect the base path you are serving from.
|
||||||
|
|
||||||
@@ -14,54 +14,56 @@
|
|||||||
This is a placeholder for base href that will be replaced by the value of
|
This is a placeholder for base href that will be replaced by the value of
|
||||||
the `--base-href` argument provided to `flutter build`.
|
the `--base-href` argument provided to `flutter build`.
|
||||||
-->
|
-->
|
||||||
<base href="$FLUTTER_BASE_HREF">
|
<base href="$FLUTTER_BASE_HREF" />
|
||||||
|
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible" />
|
||||||
<meta name="description" content="A new Flutter project.">
|
<meta name="description" content="A new Flutter project." />
|
||||||
|
|
||||||
<!-- iOS meta tags & icons -->
|
<!-- iOS meta tags & icons -->
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||||
<meta name="apple-mobile-web-app-title" content="flutter_application_1">
|
<meta name="apple-mobile-web-app-title" content="flutter_application_1" />
|
||||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
<link rel="apple-touch-icon" href="icons/Icon-192.png" />
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
<link rel="icon" type="image/png" href="favicon.png" />
|
||||||
|
|
||||||
<title>flutter_application_1</title>
|
<title>flutter_application_1</title>
|
||||||
<link rel='stylesheet' href='css/styles.css'>
|
<link rel="stylesheet" href="css/styles.css" />
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json" />
|
||||||
|
|
||||||
<!-- <script>
|
<!-- <script>
|
||||||
// The value below is injected by flutter build, do not touch.
|
// The value below is injected by flutter build, do not touch.
|
||||||
const serviceWorkerVersion = null;
|
const serviceWorkerVersion = null;
|
||||||
</script> -->
|
</script> -->
|
||||||
<!-- This script adds the flutter initialization JS code -->
|
<!-- This script adds the flutter initialization JS code -->
|
||||||
<script src="flutter.js" defer></script>
|
<script src="flutter.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<button id='token'>Token btn</button>
|
<button id="token">Token btn</button>
|
||||||
<button id='colorChangeBtnRed'>Color btn Red</button>
|
<button id="token2">Token btn 2</button>
|
||||||
<button id='colorChangeBtnBlue'>Color btn Blue</button>
|
<button id="colorChangeBtnRed">Color btn Red</button>
|
||||||
<section class='contents'>
|
<button id="colorChangeBtnBlue">Color btn Blue</button>
|
||||||
<article>
|
<button id="clearBtn">Clear</button>
|
||||||
<div id="flutter_target"></div>
|
<section class="contents">
|
||||||
</article>
|
<article>
|
||||||
</section>
|
<div id="flutter_target"></div>
|
||||||
<script>
|
</article>
|
||||||
window.addEventListener('load', function(ev) {
|
</section>
|
||||||
let target = document.querySelector('#flutter_target');
|
<script>
|
||||||
_flutter.loader.loadEntrypoint({
|
window.addEventListener('load', function (ev) {
|
||||||
onEntrypointLoaded: async function (engineInitializer) {
|
let target = document.querySelector('#flutter_target');
|
||||||
let appRunner = await engineInitializer.initializeEngine({
|
_flutter.loader.loadEntrypoint({
|
||||||
hostElement: target,
|
onEntrypointLoaded: async function (engineInitializer) {
|
||||||
});
|
let appRunner = await engineInitializer.initializeEngine({
|
||||||
await appRunner.runApp();
|
hostElement: target,
|
||||||
}
|
});
|
||||||
})
|
await appRunner.runApp();
|
||||||
});
|
},
|
||||||
</script>
|
});
|
||||||
<script src='js/demo-js-interop.js' defer></script>
|
});
|
||||||
</body>
|
</script>
|
||||||
|
<script src="js/demo-js-interop.js" defer></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -7,37 +7,62 @@
|
|||||||
};
|
};
|
||||||
let appState = window._appState;
|
let appState = window._appState;
|
||||||
|
|
||||||
let btn = document.getElementById('token');
|
function getToken(token) {
|
||||||
btn.addEventListener('click', function () {
|
fetch('https://gymlink.freemyip.com/api/auth/authorize_client', {
|
||||||
appState.onTokenReceived('token123');
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
GymKey: token, // Just testing token
|
||||||
|
id: '123',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.catch(e => {
|
||||||
|
console.log(e);
|
||||||
|
setTimeout(getToken, 1000);
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data.payload) appState.checkToken(data.payload.token);
|
||||||
|
else {
|
||||||
|
console.log(data);
|
||||||
|
setTimeout(getToken, 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const btn = document.getElementById('token');
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
localStorage.clear();
|
||||||
|
getToken('eeb42dcb-8e5b-4f21-825a-3fc7ada43445');
|
||||||
|
});
|
||||||
|
|
||||||
|
const btn2 = document.getElementById('token2');
|
||||||
|
btn2.addEventListener('click', () => {
|
||||||
|
localStorage.clear();
|
||||||
|
getToken('a8622a61-3142-487e-8db8-b6aebd4f04aa');
|
||||||
});
|
});
|
||||||
|
|
||||||
let colorChangeBtnRed = document.getElementById('colorChangeBtnRed');
|
let colorChangeBtnRed = document.getElementById('colorChangeBtnRed');
|
||||||
colorChangeBtnRed.addEventListener('click', function () {
|
colorChangeBtnRed.addEventListener('click', function () {
|
||||||
var hexColor = '#FF0000'
|
var hexColor = '#FF0000'.substring(1);
|
||||||
.replace(
|
|
||||||
/^#?([a-f\d])([a-f\d])([a-f\d])$/i,
|
|
||||||
(m, r, g, b) => '#ff' + r + r + g + g + b + b
|
|
||||||
)
|
|
||||||
.substring(1);
|
|
||||||
var numColor = parseInt(hexColor, 16);
|
var numColor = parseInt(hexColor, 16);
|
||||||
appState.changeColor(numColor);
|
appState.changeColor(numColor, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
let colorChangeBtnBlue = document.getElementById('colorChangeBtnBlue');
|
let colorChangeBtnBlue = document.getElementById('colorChangeBtnBlue');
|
||||||
colorChangeBtnBlue.addEventListener('click', function () {
|
colorChangeBtnBlue.addEventListener('click', function () {
|
||||||
var hexColor = '#0000FF'
|
var hexColor = '#0000FF'.substring(1);
|
||||||
.replace(
|
|
||||||
/^#?([a-f\d])([a-f\d])([a-f\d])$/i,
|
|
||||||
(m, r, g, b) => '#ff' + r + r + g + g + b + b
|
|
||||||
)
|
|
||||||
.substring(1);
|
|
||||||
var numColor = parseInt(hexColor, 16);
|
var numColor = parseInt(hexColor, 16);
|
||||||
appState.changeColor(numColor);
|
appState.changeColor(numColor, false);
|
||||||
|
});
|
||||||
|
let clearBtn = document.getElementById('clearBtn');
|
||||||
|
clearBtn.addEventListener('click', function () {
|
||||||
|
localStorage.clear();
|
||||||
|
window.location.reload();
|
||||||
});
|
});
|
||||||
|
|
||||||
function onError() {
|
function onError() {
|
||||||
console.error('aboba');
|
console.error('Error');
|
||||||
}
|
}
|
||||||
|
|
||||||
appState.setOnError(onError);
|
appState.setOnError(onError);
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
|
||||||
<title>Прием платежа с помощью виджета ЮKassa</title>
|
|
||||||
|
|
||||||
<!--Подключение библиотеки для инициализации виджета ЮKassa-->
|
|
||||||
<script src="https://yookassa.ru/checkout-widget/v1/checkout-widget.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
Ниже отобразится платежная форма. Если вы еще не создавали платеж и не передавали токен для инициализации виджета, появится сообщение об ошибке.
|
|
||||||
|
|
||||||
<!--Контейнер, в котором будет отображаться платежная форма-->
|
|
||||||
<div id="payment-form"></div>
|
|
||||||
|
|
||||||
Данные банковской карты для оплаты в <b>тестовом магазине</b>:
|
|
||||||
|
|
||||||
- номер — <b>5555 5555 5555 4477</b>
|
|
||||||
- срок действия — <b>01/30</b> (или другая дата, больше текущей)
|
|
||||||
- CVC — <b>123</b> (или три любые цифры)
|
|
||||||
- код для прохождения 3-D Secure — <b>123</b> (или три любые цифры)
|
|
||||||
|
|
||||||
<a href=https://yookassa.ru/developers/payment-acceptance/testing-and-going-live/testing#test-bank-card>Другие тестовые банковские карты</a>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
//Инициализация виджета. Все параметры обязательные.
|
|
||||||
const checkout = new window.YooMoneyCheckoutWidget({
|
|
||||||
confirmation_token: 'ct-287e0c37-000f-5000-8000-16961d35b0fd', //Токен, который перед проведением оплаты нужно получить от ЮKassa
|
|
||||||
return_url: 'https://example.com/', //Ссылка на страницу завершения оплаты, это может быть любая ваша страница
|
|
||||||
|
|
||||||
//При необходимости можно изменить цвета виджета, подробные настройки см. в документации
|
|
||||||
//customization: {
|
|
||||||
//Настройка цветовой схемы, минимум один параметр, значения цветов в HEX
|
|
||||||
//colors: {
|
|
||||||
//Цвет акцентных элементов: кнопка Заплатить, выбранные переключатели, опции и текстовые поля
|
|
||||||
//control_primary: '#00BF96', //Значение цвета в HEX
|
|
||||||
|
|
||||||
//Цвет платежной формы и ее элементов
|
|
||||||
//background: '#F2F3F5' //Значение цвета в HEX
|
|
||||||
//}
|
|
||||||
//},
|
|
||||||
error_callback: function(error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//Отображение платежной формы в контейнере
|
|
||||||
checkout.render('payment-form');
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Reference in New Issue
Block a user