Compare commits

...

56 Commits

Author SHA1 Message Date
04ee6d1699 Add: gym themes 2024-06-07 14:57:40 +03:00
65c8f56e20 Fix: shorten text 2024-06-07 14:57:28 +03:00
0170505376 Fix: category colors and count elements 2024-06-04 23:13:21 +03:00
15105a7f33 Add: search by category 2024-06-04 16:58:53 +03:00
1e5b235a6c Enhancement: back button to main menu reload 2024-06-04 15:57:17 +03:00
eaa8b138a4 Add: category on detail page and searching by API 2024-06-04 15:29:41 +03:00
db39169907 Add: new item interfaces and category interface 2024-06-04 14:36:43 +03:00
c8965dab4e Add: Second club page 2024-06-04 12:59:35 +03:00
97664fdb5a Fix: Precaching images 2024-05-31 15:29:35 +03:00
cfa6ef9a67 Fix: index in carousel 2024-05-31 15:28:58 +03:00
7335c55703 Fix: image carousel on web 2024-05-31 15:07:52 +03:00
1eeff4209e Add: images slider 2024-05-31 14:55:24 +03:00
3b593ad733 Fix: Lazy loader on web and its indicator 2024-05-31 14:00:34 +03:00
0438a6feec Add: Getting items by its ids 2024-05-23 22:12:07 +03:00
d6f64465b3 Add: cart items by its ids 2024-05-23 02:04:37 +03:00
e57c7dc0ea Add: detail page from API 2024-05-22 22:14:24 +03:00
e7073cec67 Add: closing module on errors 2024-05-22 15:46:39 +03:00
80da7e9008 Fix: $ to руб 2024-05-22 15:46:24 +03:00
7907dcf6c2 Add: getting products from API 2024-05-22 15:46:09 +03:00
46ba11cd57 Fix: connection problems checks 2024-05-22 00:49:51 +03:00
d8e68f9b34 Add: get token from api 2024-05-22 00:41:44 +03:00
e95fb08e31 Rename: application name 2024-05-22 00:40:48 +03:00
28db4ce298 Add: Clear search 2024-05-21 23:50:30 +03:00
30fdc0a144 Fix: Floating btn hover elevation 2024-05-21 23:50:18 +03:00
9ac9813244 OnPay operations 2024-05-21 22:36:42 +03:00
986a9d9bd5 Error provider 2024-05-17 16:11:05 +03:00
c54176212a Add: CartProvider 2024-05-15 13:49:18 +03:00
e52357edf5 Added: Some TODOs 2024-05-15 02:28:02 +03:00
464f51238f Add: Lazy loading and refresh on pull down 2024-05-15 00:34:05 +03:00
baf85776e9 Add: logo 2024-05-15 00:33:14 +03:00
e177c81f8b Mobile adaptive updates 2024-05-13 15:28:44 +03:00
51b0cbe89b Added TODO 2024-05-12 14:43:51 +03:00
9f720bdf75 Fix providers 2024-05-12 14:43:16 +03:00
2c2221f7f1 "Steal" theme from example app 2024-05-12 13:53:29 +03:00
78e468cc29 Provider rename 2024-05-12 13:43:30 +03:00
6c4c2c4acd Mobile example app 2024-05-12 13:42:05 +03:00
75bfc7ea6b Some relative stuff 2024-05-11 23:27:46 +03:00
57ff8a59e8 AssetImage using 2024-05-11 22:48:46 +03:00
b4092837d2 Gradle SDK vers 2024-05-11 16:11:19 +03:00
0c11883b48 Basket confirmation page 2024-05-10 22:08:27 +03:00
4853f61da2 Mobile and web versions 2024-05-10 19:50:33 +03:00
9d92dfd145 Example theme change 2024-05-10 19:49:21 +03:00
4b16b74d15 Theme gen 2024-05-10 19:49:08 +03:00
fecc388e1c Right deleting items from cart 2024-05-07 16:51:58 +03:00
e4628e977f Added tODO 2024-05-03 16:00:49 +03:00
4bbe7fbc0b Clearing shopping cart 2024-05-03 13:59:06 +03:00
5c3da0964a Added order history cards 2024-05-03 13:58:46 +03:00
26f822e83a Some fixes 2024-05-02 22:56:21 +03:00
8805b2a9a0 Basket items 2024-05-02 22:56:08 +03:00
ff29598ec5 Adding items to cart 2024-05-02 17:02:43 +03:00
727c04d368 AppBar and Header components 2024-05-02 15:04:05 +03:00
0a491ca34b Package rename 2024-05-01 20:36:40 +03:00
16d0ddca78 Goods details page 2024-05-01 20:17:20 +03:00
f941b26224 Product card on main screen 2024-05-01 16:44:18 +03:00
c6520041a6 Search btn and borders 2024-04-30 16:16:13 +03:00
7f0cef4b23 Created screens 2024-04-30 15:58:53 +03:00
44 changed files with 3199 additions and 236 deletions

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"editor.formatOnSave": true
}

View File

@@ -23,7 +23,7 @@ if (flutterVersionName == null) {
}
android {
namespace "com.example.flutter_application_1"
namespace "com.example.gym_app"
compileSdk flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
@@ -42,11 +42,11 @@ android {
defaultConfig {
// 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.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion
minSdkVersion 21
targetSdkVersion 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}

View File

@@ -1,6 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="flutter_application_1"
android:label="Example Gym App"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
@@ -10,6 +10,7 @@
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:enableOnBackInvokedCallback="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
@@ -30,6 +31,7 @@
android:name="flutterEmbedding"
android:value="2" />
</application>
<uses-permission android:name="android.permission.INTERNET" />
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility?hl=en and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.

View File

@@ -1,4 +1,4 @@
package com.example.flutter_application_1
package com.example.gym_app
import io.flutter.embedding.android.FlutterActivity

14
assets/icon.svg Normal file
View File

@@ -0,0 +1,14 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1930_2195)">
<path d="M13.2378 0.536444C13.2378 0.832951 13.5486 1.07336 13.9311 1.07336C14.9923 1.07336 17.0092 1.07336 18.0698 1.07336C18.4523 1.07336 18.7624 0.83289 18.7624 0.535822C18.7624 0.239315 18.4522 -3.74317e-05 18.0698 -3.74317e-05C17.0099 -3.74317e-05 14.9928 -3.74317e-05 13.9305 -3.74317e-05C13.5486 -3.74317e-05 13.2378 0.239938 13.2378 0.536444Z" fill="black"/>
<path d="M13.2378 31.4624C13.2378 31.7589 13.5486 32 13.9311 32C14.9935 32 17.0074 32 18.0685 31.9994C18.451 31.9994 18.7612 31.7589 18.7625 31.4624C18.7625 31.1658 18.4523 30.926 18.0698 30.926C17.0093 30.926 14.9929 30.926 13.9305 30.9254C13.5486 30.9254 13.2378 31.1658 13.2378 31.4624Z" fill="black"/>
<path d="M6.96338 24.3076C6.96338 25.2968 7.89411 26.0993 9.04154 26.0993C12.5199 26.0993 19.4814 26.0987 22.9585 26.0981C24.1059 26.0981 25.0366 25.2968 25.0366 24.3075C25.0366 23.3182 24.1053 22.5157 22.9585 22.5157C21.5944 22.5157 19.6919 22.5157 17.6761 22.5157C17.6761 21.4829 17.6761 12.2383 17.6761 9.4828C19.6925 9.4828 21.5944 9.4828 22.9591 9.4828C24.1059 9.4828 25.0366 8.68032 25.0366 7.69097C25.0366 6.70174 24.1053 5.90038 22.9585 5.90038C19.4801 5.90038 12.5186 5.90038 9.04154 5.90038C7.89354 5.90038 6.96338 6.7018 6.96338 7.69097C6.96338 8.68026 7.8929 9.4828 9.04154 9.4828C10.4056 9.4828 12.3075 9.4828 14.3238 9.4828C14.3238 12.2393 14.3238 21.4829 14.3238 22.5157C12.3075 22.5157 10.4055 22.5157 9.0409 22.5157C7.89296 22.5157 6.96338 23.3177 6.96338 24.3076Z" fill="black"/>
<path d="M9.73926 3.39964C9.73926 3.45817 9.73926 3.51601 9.73926 3.57504C9.73926 4.32036 10.358 4.92535 11.1235 4.92535C13.5327 4.92535 18.4675 4.92535 20.8766 4.92535C21.6415 4.92535 22.2621 4.32042 22.2621 3.5746C22.2621 3.51607 22.2621 3.45699 22.2621 3.39914C22.2621 2.65332 21.6434 2.04839 20.8778 2.04839C18.4693 2.04839 13.5339 2.04839 11.1246 2.04889C10.3592 2.04827 9.73926 2.65382 9.73926 3.39964Z" fill="black"/>
<path d="M9.73926 28.424C9.73926 28.4824 9.73926 28.5415 9.73926 28.6C9.73926 29.3458 10.358 29.9503 11.1235 29.9503C13.5327 29.9503 18.4675 29.9503 20.8766 29.9503C21.6422 29.9503 22.2621 29.3458 22.2621 28.6C22.2621 28.5415 22.2621 28.4824 22.2621 28.4246C22.2621 27.6788 21.6434 27.0744 20.8778 27.0744C18.4693 27.0744 13.5339 27.0744 11.1246 27.0744C10.3592 27.0737 9.73926 27.6782 9.73926 28.424Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_1930_2195">
<rect width="32" height="32" fill="white" transform="matrix(0 -1 1 0 0 32)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
assets/product.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 KiB

View File

@@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class GymLinkAppBar extends StatelessWidget implements PreferredSizeWidget {
const GymLinkAppBar({super.key});
@override
Widget build(BuildContext context) {
return AppBar(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
shadowColor: null,
automaticallyImplyLeading: false,
elevation: 0,
scrolledUnderElevation: 0,
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(right: 8),
child: SvgPicture.asset(
'assets/icon.svg',
width: 24,
height: 24,
semanticsLabel: 'GymLink Logo',
),
),
Align(
alignment: Alignment.centerRight,
child: Text(
'Powered by GymLink',
style: Theme.of(context).textTheme.titleSmall,
),
),
],
),
toolbarHeight: 30,
);
}
@override
Size get preferredSize => const Size.fromHeight(30);
}

View File

@@ -0,0 +1,95 @@
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 {
final String name;
final String price;
final String id;
final Image image;
final String quantity;
final VoidCallback onTapPlus;
final VoidCallback onTapMinus;
const BasketItemCard({
super.key,
required this.name,
required this.price,
required this.id,
required this.image,
required this.onTapPlus,
required this.quantity,
required this.onTapMinus,
});
@override
Widget build(BuildContext context) {
return Padding(
padding:
const EdgeInsetsDirectional.symmetric(horizontal: 10, vertical: 10),
child: ConstrainedBox(
constraints: const BoxConstraints(
minHeight: 100,
maxHeight: 200,
minWidth: 400,
maxWidth: 600,
),
child: GestureDetector(
onTap: () {
Navigator.of(context).push(
CustomPageRoute(builder: (context) => DetailPage(id: id)));
},
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: [
Text(
name,
style: Theme.of(context).textTheme.bodyLarge,
),
Text('$price руб.'),
],
)
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.remove),
onPressed: onTapMinus,
),
const SizedBox(width: 10),
Text(quantity),
const SizedBox(width: 10),
IconButton(
icon: const Icon(Icons.add),
onPressed: onTapPlus,
),
],
)
],
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,35 @@
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 {
final String title;
final bool toMain;
const GymLinkHeader({super.key, required this.title, this.toMain = false});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
Row(
children: [
IconButton(
onPressed: () => toMain
? Navigator.pushAndRemoveUntil(
context,
CustomPageRoute(
builder: (context) => const MainPage()),
(route) => route.isFirst)
: Navigator.pop(context),
icon: const Icon(Icons.arrow_back)),
Text(title, style: Theme.of(context).textTheme.titleLarge),
],
),
const Divider(thickness: 1, height: 0),
],
),
);
}
}

View File

@@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
enum OrderStatus { created, inProgress, completed, canceled }
Map<OrderStatus, String> orderStatusMap = {
OrderStatus.created: 'Создан',
OrderStatus.inProgress: 'В обработке',
OrderStatus.completed: 'Завершен',
OrderStatus.canceled: 'Отменен',
};
class HistoryItemCard extends StatelessWidget {
final String id;
final String cost;
final String date;
final Image image;
final OrderStatus status;
const HistoryItemCard({
super.key,
required this.id,
required this.cost,
required this.date,
required this.status,
required this.image,
});
@override
Widget build(BuildContext context) {
return Padding(
padding:
const EdgeInsetsDirectional.symmetric(horizontal: 10, vertical: 10),
child: ConstrainedBox(
constraints: const BoxConstraints(
minHeight: 100,
maxHeight: 200,
minWidth: 600,
maxWidth: 800,
),
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.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
image,
const SizedBox(width: 20),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MarkdownBody(
data: '### Заказ **№$id** от $date',
),
MarkdownBody(
data: 'Статус: **${orderStatusMap[status]}**'),
MarkdownBody(data: 'Сумма: **$cost руб.**'),
],
)
],
),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
class ProductCard extends StatelessWidget {
final Image imagePath;
final String name;
final String price;
final VoidCallback onTap;
const ProductCard({
super.key,
required this.imagePath,
required this.name,
required this.price,
required this.onTap,
});
double getCardHeight({required BuildContext context}) {
if (MediaQuery.of(context).size.width > 600) {
return 200;
}
return 100;
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: 80, maxHeight: getCardHeight(context: context)),
child: Card(
elevation: 3,
color: Theme.of(context).scaffoldBackgroundColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
imagePath,
const SizedBox(height: 16),
Text(
name,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
'$price руб.',
style: Theme.of(context).textTheme.titleSmall,
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
class OrderConfirmItemCard extends StatelessWidget {
final String name;
final int count;
final double price;
final Image image;
const OrderConfirmItemCard({
super.key,
required this.name,
required this.count,
required this.price,
required this.image,
});
@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: [
Text(
name,
style: Theme.of(context).textTheme.bodyLarge,
),
Text(
'${price.toStringAsFixed(2)} руб. x $count = ${(price * count).toStringAsFixed(2)} руб.'),
],
)
],
),
MarkdownBody(data: '# X$count')
],
),
),
),
),
);
}
}

150
lib/interfaces/items.dart Normal file
View File

@@ -0,0 +1,150 @@
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;
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,
});
factory GymItem.fromRawJson(String str) => GymItem.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory GymItem.fromJson(Map<String, dynamic> json) => 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))),
);
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())),
};
}
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,
};
}

View File

@@ -1,226 +1,25 @@
import 'dart:async';
import 'dart:js_interop' as js;
import 'dart:js_interop_unsafe' as js_util;
import 'package:flutter/material.dart';
import 'package:gymlink_module_web/providers/main.dart';
import 'package:gymlink_module_web/states/web.dart';
import 'package:provider/provider.dart';
void main() {
runApp(const MyApp());
runApp(const MyAppWithProvider());
}
enum DemoScreen { counter, textField }
class MyApp extends StatefulWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
State<MyApp> createState() => _MyAppState();
State<MyApp> createState() => MyAppStateWeb();
}
@js.JSExport()
class _MyAppState extends State<MyApp> {
final _streanController = StreamController<void>.broadcast();
DemoScreen _currentDemoScreen = DemoScreen.counter;
int _counterScreenCount = 0;
bool _isLoading = true;
@override
void initState() {
super.initState();
final export = js.createJSInteropWrapper(this);
js.globalContext['_appState'] = export;
js.globalContext.callMethod('_stateSet'.toJS);
}
@override
void dispose() {
_streanController.close();
super.dispose();
}
@js.JSExport()
void increment() {
if (_currentDemoScreen == DemoScreen.counter) {
setState(() {
_counterScreenCount++;
_streanController.add(null);
});
}
}
@js.JSExport()
void addHandler(void Function() handler) {
_streanController.stream.listen((event) {
handler();
});
}
@js.JSExport()
int get count => _counterScreenCount;
class MyAppWithProvider extends StatelessWidget {
const MyAppWithProvider({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Aboba app',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.red),
),
debugShowCheckedModeBanner: false,
home: demoScreenRouter(_currentDemoScreen),
);
}
@js.JSExport()
void onTokenReceived(String token) {
if (token == 'token123') {
setState(() {
_isLoading = false;
});
}
}
Widget demoScreenRouter(DemoScreen which) {
switch (which) {
case DemoScreen.counter:
return CounterDemo(
title: 'Counter',
numToDisplay: _counterScreenCount,
incrementHandler: increment,
isLoading: _isLoading);
case DemoScreen.textField:
return const TextFieldDemo(title: 'Nasdfs');
}
}
@js.JSExport()
void changeDemoScreenTo(String screenString) {
setState(() {
switch (screenString) {
case 'counter':
_currentDemoScreen = DemoScreen.counter;
break;
case 'textField':
_currentDemoScreen = DemoScreen.textField;
break;
}
});
}
}
class CounterDemo extends StatefulWidget {
final String title;
final int numToDisplay;
final VoidCallback incrementHandler;
final bool isLoading;
const CounterDemo(
{super.key,
required this.title,
required this.numToDisplay,
required this.incrementHandler,
required this.isLoading});
@override
State<CounterDemo> createState() => _CounterDemoState();
}
class _CounterDemoState extends State<CounterDemo> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: widget.isLoading
? null
: AppBar(
title: Row(
children: [
Expanded(
child: TextField(
decoration: InputDecoration(
hintText: 'Search',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
suffixIcon: const Icon(
Icons.search,
color: Colors.blue,
),
),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(0),
shape: const CircleBorder(
side: BorderSide(
color: Colors.blue,
width: 2,
),
),
),
child: const Icon(
Icons.shopping_basket,
color: Colors.white,
size: 36,
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(0),
shape: const CircleBorder(
side: BorderSide(
color: Colors.blue,
width: 2,
),
),
),
child: const Icon(
Icons.history,
color: Colors.white,
size: 36,
),
),
],
),
),
body: widget.isLoading
? const Center(child: CircularProgressIndicator())
: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Здесь будут товары',
),
],
),
),
);
}
}
class TextFieldDemo extends StatelessWidget {
const TextFieldDemo({super.key, required this.title});
final String title;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: const Center(
child: Padding(
padding: EdgeInsets.all(14.0),
child: TextField(
maxLines: null,
decoration: InputDecoration(border: OutlineInputBorder()),
),
),
),
);
return ChangeNotifierProvider(
create: (_) => GymLinkProvider(), child: const MyApp());
}
}

13
lib/main_mobile.dart Normal file
View File

@@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
import 'package:gymlink_module_web/states/mobile.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => MyAppStateMobile();
}

259
lib/mobile_example.dart Normal file
View File

@@ -0,0 +1,259 @@
import 'dart:convert';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:gymlink_module_web/main_mobile.dart';
import 'package:gymlink_module_web/providers/main.dart';
import 'package:http/http.dart' as http;
import 'package:provider/provider.dart';
void main() {
runApp(const MyExampleApp());
}
Future<String> getToken(String token, String clientId) async {
debugPrint(token);
var url = Uri.http('gymlink.freemyip.com:8080', '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 {
const MyExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'GymLink Example App',
debugShowCheckedModeBanner: false,
home: const ExampleMainPage(),
theme: ThemeData.light(useMaterial3: true),
);
}
}
class ExamplePage extends StatefulWidget {
const ExamplePage({super.key});
@override
State<ExamplePage> createState() => _ExamplePageState();
}
Widget getDrawer(BuildContext context) => Drawer(
child: Column(
children: [
const DrawerHeader(child: Text('Drawer Header')),
ListTile(
leading: const Icon(Icons.home),
title: const Text('Home'),
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const ExampleMainPage(),
),
),
),
ListTile(
leading: const Icon(Icons.sell),
title: const Text('Club 2'),
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ChangeNotifierProvider(
create: (_) => GymLinkProvider(),
child: Consumer<GymLinkProvider>(
builder: (_, value, __) => const ExampleClub2Page(),
),
),
),
),
),
ListTile(
leading: const Icon(Icons.search),
title: const Text('Example page'),
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => const ExampleSecondPage(),
)),
),
],
),
);
class ExampleMainPage extends StatelessWidget {
const ExampleMainPage({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => GymLinkProvider(),
child: Consumer<GymLinkProvider>(
builder: (_, value, __) => const ExamplePage(),
));
}
}
class _ExamplePageState extends State<ExamplePage> {
@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('eeb42dcb-8e5b-4f21-825a-3fc7ada43445', '123');
if (token != '') {
context.read<GymLinkProvider>().onTokenReceived(token);
} else {
context.read<GymLinkProvider>().onError();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('GymLink Example App Gym 1'),
),
resizeToAvoidBottomInset: false,
drawer: getDrawer(context),
body: const Column(
children: [
Text('test'),
Expanded(
child: MyApp(),
),
SizedBox(
height: 20,
),
Text('Bottom text')
],
),
floatingActionButton: IconButton(
icon: const Icon(Icons.search),
onPressed: () {
context.read<GymLinkProvider>().changeTheme(
Random().nextInt(0xffffff + 1),
blackTheme: Random().nextBool());
},
),
);
}
}
class ExampleClub2Page extends StatefulWidget {
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>().onTokenReceived(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')
],
),
floatingActionButton: IconButton(
icon: const Icon(Icons.search),
onPressed: () {
// context.read<GymLinkProvider>().changeTheme(0xFFAABCAB);
},
),
);
}
}
class ExampleSecondPage extends StatelessWidget {
const ExampleSecondPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('GymLink Example App'),
),
drawer: getDrawer(context),
body: const Center(
child: Text('Example page'),
),
);
}
}

351
lib/pages/basket.dart Normal file
View File

@@ -0,0 +1,351 @@
import 'package:flutter/material.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/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/providers/cart.dart';
import 'package:gymlink_module_web/tools/items.dart';
import 'package:gymlink_module_web/tools/prefs.dart';
import 'package:gymlink_module_web/tools/routes.dart';
import 'package:lazy_load_scrollview/lazy_load_scrollview.dart';
import 'package:provider/provider.dart';
List<Map<String, dynamic>> cart = [
{
"name": "Протеин",
"image": "product.png",
"price": "120",
"details": "Test details",
"id": "34fa3126-bfaf-5dec-8f4a-b246c097ef73"
},
{
"name": "Протеин",
"image": "product.png",
"price": "150",
"details": "Test details",
"id": "34a26e82-7656-5e98-a44a-c2d01d0b1ad1123"
},
{
"name": "Протеин",
"image": "product.png",
"price": "250",
"details": "Test details",
"id": "4fb204b7-3f9e-52a2-bed1-415c00a31a37123"
},
{
"name": "Протеин",
"image": "product.png",
"price": "300",
"details": "Test details",
"id": "09b2f5bb-683e-5c39-ae89-b8e152fa8bcf123"
},
{
"name": "Протеин",
"image": "product.png",
"price": "100",
"details": "Test details",
"id": "cd1b6817-db94-5394-be1d-af88af79749f123"
}
];
class BasketPage extends StatefulWidget {
const BasketPage({super.key});
@override
State<BasketPage> createState() => _BasketPageState();
}
class _BasketPageState extends State<BasketPage> {
List<GymItem> cartItems = [];
double totalPrice = 0;
List<GymItem> gymCart = [];
bool _isLoading = true;
@override
void initState() {
super.initState();
Future.microtask(() => getCart().then((value) async {
final itemIds =
value.map((element) => element['id'] as String).toList();
final items = await getItemsByIds(context, itemIds);
setState(() {
gymCart = items;
cartItems = value.map((element) {
final item = gymCart.firstWhere((e) => e.id == element['id']);
item.localCount = element['count'] as int;
return item;
}).toList();
totalPrice = cartItems.fold(
0, (sum, item) => sum + item.price * item.localCount);
_isLoading = false;
});
}));
}
void _updateCart() {
Provider.of<CartProvider>(context, listen: false).updateCartLength();
}
void removeItem(String id) async {
final item = cartItems.firstWhere((element) => element.id == id);
bool toDelete = false;
setState(() {
if (item.localCount > 1) {
item.localCount--;
cartItems[cartItems.indexOf(item)].localCount = item.localCount;
} else {
toDelete = true;
}
totalPrice =
cartItems.fold(0, (sum, item) => sum + item.price * item.localCount);
});
if (toDelete) {
await _deleteItemAlert(id, item.title);
} else {
await removeItemFromCart(id);
}
}
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(() {
item.localCount++;
cartItems[cartItems.indexOf(item)].localCount = item.localCount;
totalPrice =
cartItems.fold(0, (sum, item) => sum + item.price * item.localCount);
});
await addItemToCart(id);
}
Future<void> _deleteItemAlert(String id, String name) async {
return showDialog<void>(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Удаление из корзины'),
content: SingleChildScrollView(
child: ListBody(
children: [
Text('Вы действительно хотите убрать "$name" из корзины?'),
],
),
),
actions: [
TextButton(
child: const Text('Отмена'),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('Удалить'),
onPressed: () {
removeItemFromCart(id);
setState(() {
cartItems.removeWhere((element) => element.id == id);
});
if (mounted) {
_updateCart();
}
Navigator.of(context).pop();
},
),
],
);
},
);
}
Future<void> _clearCartAlert() async {
return showDialog<void>(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Очистка корзины'),
content: const SingleChildScrollView(
child: ListBody(
children: [
Text('Вы действительно хотите очистить корзину?'),
],
),
),
actions: [
TextButton(
child: const Text('Отмена'),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('Очистить'),
onPressed: () {
clearCart();
setState(() {
cartItems = [];
});
if (mounted) {
_updateCart();
}
Navigator.of(context).pop();
},
),
],
);
},
);
}
Widget _buildRowOrCol(
{required BuildContext context, required List<Widget> children}) {
if (MediaQuery.of(context).size.width > 600) {
return Row(children: children);
}
return Column(children: children);
}
Widget _buildSpacer() {
if (MediaQuery.of(context).size.width > 600) {
return const Spacer();
}
return const SizedBox(height: 10);
}
void _onLoad() async {
await Future.delayed(const Duration(microseconds: 1000));
setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const GymLinkAppBar(),
body: Column(
children: [
const GymLinkHeader(title: "Корзина"),
_isLoading
? const Expanded(
child: Center(child: CircularProgressIndicator()))
: cartItems.isEmpty
? Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Корзина пуста',
style: Theme.of(context).textTheme.bodyLarge),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () => Navigator.pushAndRemoveUntil(
context,
CustomPageRoute(
builder: (_) => const MainPage()),
(route) => route.isFirst),
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.title,
price: item.price.toStringAsFixed(2),
id: item.id,
image: Image(
image: NetworkImage(item.images[0].url),
width: 50,
),
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),
],
),
),
],
),
);
}
}

366
lib/pages/detail.dart Normal file
View File

@@ -0,0 +1,366 @@
import 'dart:convert';
import 'dart:math';
import 'package:carousel_slider/carousel_slider.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/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/main.dart';
import 'package:gymlink_module_web/tools/items.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';
class DetailPage extends StatefulWidget {
final String id;
const DetailPage({
super.key,
required this.id,
});
@override
State<DetailPage> createState() => _DetailPageState();
}
class _DetailPageState extends State<DetailPage> {
bool isInCart = false;
int quantity = 0;
GymItem? item;
String? categoryName;
final CarouselController _carouselController = CarouselController();
int _currentImage = 0;
@override
void initState() {
getCart().then((value) {
setState(() {
isInCart = value.any((element) => element['id'] == widget.id);
if (isInCart) {
quantity = value
.firstWhere((element) => element['id'] == widget.id)['count'];
}
});
});
_getItem();
super.initState();
}
Future<void> _getItem() async {
final Uri url =
Uri.http('gymlink.freemyip.com:8080', '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(
{required List<Widget> children,
required BuildContext context,
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.spaceAround,
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center}) {
// if (false && MediaQuery.of(context).size.width > 600) {
// return Row(
// mainAxisAlignment: mainAxisAlignment,
// crossAxisAlignment: crossAxisAlignment,
// children: children);
// }
return Column(
mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: crossAxisAlignment,
children: children);
}
Widget _buildButton() {
if (!isInCart) {
return ElevatedButton(
onPressed: () async {
await addItemToCart(widget.id);
setState(() {
isInCart = true;
quantity = 1;
});
if (mounted) {
context.read<CartProvider>().updateCartLength();
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(50)),
),
foregroundColor: Colors.white,
padding: const EdgeInsetsDirectional.fromSTEB(34, 10, 34, 10)),
child: const Text('Добавить в корзину'),
);
} else {
return Column(
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.remove),
onPressed: () async {
await removeItemFromCart(widget.id);
setState(() {
if (quantity > 1) {
quantity--;
} else {
isInCart = false;
quantity = 0;
}
});
if (mounted) {
context.read<CartProvider>().updateCartLength();
}
},
),
const SizedBox(width: 10),
Text('$quantity'),
const SizedBox(width: 10),
IconButton(
icon: const Icon(Icons.add),
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
Widget build(BuildContext context) {
return Scaffold(
appBar: const GymLinkAppBar(),
body: item != null
? Column(mainAxisAlignment: MainAxisAlignment.start, children: [
GymLinkHeader(title: shortString(item!.title, length: 20)),
Expanded(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(20),
child: SizedBox(
width: MediaQuery.sizeOf(context).width,
// height: MediaQuery.sizeOf(context).height,
child: _buildRowOrCol(
context: context,
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
item!.images.length > 1
? Column(children: [
CarouselSlider.builder(
itemCount: item!.images.length,
itemBuilder: (context, index, realIdx) {
return Center(
child: Image.network(
item!.images[index].url,
width: min(
550,
MediaQuery.sizeOf(context)
.width)),
);
},
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,
),
),
Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Chip(
label: Text(categoryName != null
? (categoryName == ""
? "Без категории"
: categoryName!)
: ''),
backgroundColor: Colors.white,
),
),
),
Center(
child: MarkdownBody(
data: '### Отстаток: _${item!.count}_',
)),
item!.description != ''
? Padding(
padding: const EdgeInsetsDirectional.all(30),
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 340,
maxWidth: 340,
maxHeight: 600,
),
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()
],
),
),
),
],
),
),
),
),
),
])
: const Center(
child: CircularProgressIndicator(),
),
);
}
}

379
lib/pages/main.dart Normal file
View File

@@ -0,0 +1,379 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:gymlink_module_web/components/app_bar.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/detail.dart';
import 'package:gymlink_module_web/pages/order_history.dart';
import 'package:gymlink_module_web/providers/cart.dart';
import 'package:gymlink_module_web/tools/items.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/text.dart';
import 'package:lazy_load_scrollview/lazy_load_scrollview.dart';
import 'package:provider/provider.dart';
const List<Map<String, String>> testData = [
{
"name": "Протеин 2",
"image": "product.png",
"price": "120",
"details": "Test details",
"id": "34fa3126-bfaf-5dec-8f4a-b246c097ef73"
},
{
"name": "Протеин",
"image": "product.png",
"price": "150",
"details":
"that name factory say string eaten order harbor easier said tone now floor nest it comfortable such difficulty labor bridge fact market women badly chamber heading forest allow shirt possibly story strip elephant extra even joy lungs than low discussion barn rapidly evidence is stream crew let more sold bag river triangle court slept knowledge flat package research balloon station underline careful market better make curious secret boy poor captured creature harder public tool ring subject charge planet tone scientist piece page stone support bush way feathers summer describe back should said complex song giant his that name factory say string eaten order harbor easier said tone now floor nest it comfortable such difficulty labor bridge fact market women badly chamber heading forest allow shirt possibly story strip elephant extra even joy lungs than low discussion barn rapidly evidence is stream crew let more sold bag river triangle court slept knowledge flat package research balloon station underline careful market better make curious secret boy poor captured creature harder public tool ring subject charge planet tone scientist piece page stone support bush way feathers summer describe back should said complex song giant his that name factory say string eaten order harbor easier said tone now floor nest it comfortable such difficulty labor bridge fact market women badly chamber heading forest allow shirt possibly story strip elephant extra even joy lungs than low discussion barn rapidly evidence is stream crew let more sold bag river triangle court slept knowledge flat package research balloon station underline careful market better make curious secret boy poor captured creature harder public tool ring subject charge planet tone scientist piece page stone support bush way feathers summer describe back should said complex song giant his that name factory say string eaten order harbor easier said tone now floor nest it comfortable such difficulty labor bridge fact market women badly chamber heading forest allow shirt possibly story strip elephant extra even joy lungs than low discussion barn rapidly evidence is stream crew let more sold bag river triangle court slept knowledge flat package research balloon station underline careful market better make curious secret boy poor captured creature harder public tool ring subject charge planet tone scientist piece page stone support bush way feathers summer describe back should said complex song giant his",
"id": "34a26e82-7656-5e98-a44a-c2d01d0b1ad1123"
},
{
"name": "Протеин",
"image": "product.png",
"price": "250",
"details": "Test details",
"id": "4fb204b7-3f9e-52a2-bed1-415c00a31a37123"
},
{
"name": "Протеин",
"image": "product.png",
"price": "300",
"details": "Test details",
"id": "09b2f5bb-683e-5c39-ae89-b8e152fa8bcf123"
},
{
"name": "Протеин",
"image": "product.png",
"price": "100",
"details": "Test details",
"id": "cd1b6817-db94-5394-be1d-af88af79749f123"
}
];
class MainPage extends StatefulWidget {
const MainPage({
super.key,
});
@override
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
String searchText = '';
List<GymItem> filteredData = [];
int cartLength = 0;
int itemViewCount = 0;
bool isLoading = false;
bool isSearching = false;
List<GymCategory> categories = [];
GymCategory? selectedCategory;
@override
void initState() {
super.initState();
getCart().then((value) {
setState(() {
cartLength = value.length;
});
});
getCategories(context).then((value) => setState(() {
categories = value;
_onSearch();
}));
}
void _onLoad() async {
if (itemViewCount < filteredData.length) {
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);
debugPrint(itemViewCount.toString());
});
WidgetsBinding.instance.addPostFrameCallback((_) {
for (var element in filteredData.sublist(0, itemViewCount)) {
precacheImage(NetworkImage(element.images[0].url), context);
}
});
setState(() {
isSearching = false;
});
}
void _onSearch() async {
final categoryId = selectedCategory == null ? '' : selectedCategory!.id;
_searchItems(searchText: searchText, categoryId: categoryId);
}
@override
Widget build(BuildContext context) {
final cartL = context.watch<CartProvider>().cartLength;
return Scaffold(
appBar: const GymLinkAppBar(),
body: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Expanded(
child: TextField(
onChanged: (value) => setState(() {
searchText = value.trim().toLowerCase();
if (searchText == '') {
_onSearch();
}
}),
textInputAction: TextInputAction.search,
onSubmitted: (_) => _onSearch(),
decoration: InputDecoration(
hintText: 'Поиск',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
suffixIcon: Padding(
padding: const EdgeInsets.only(right: 8),
child: ElevatedButton(
onPressed: _onSearch,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 0,
),
minimumSize:
const Size(50, kMinInteractiveDimension),
backgroundColor: Theme.of(context).primaryColor,
shape: const CircleBorder(),
),
child: const Icon(
Icons.search,
color: Colors.white,
size: 24,
),
),
),
),
),
),
getSpacer(context: context, flex: 2),
const SizedBox(
width: 8,
),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(CustomPageRoute(
builder: (context) => const HistoryPage(),
));
},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(0),
minimumSize: const Size(50, kMinInteractiveDimension),
backgroundColor: Theme.of(context).primaryColor,
shape: const CircleBorder(
side: BorderSide(
color: Colors.black,
width: 1,
),
),
),
child: const Icon(
Icons.history,
color: Colors.white,
size: 24,
),
),
const SizedBox(
width: 10,
)
],
),
),
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(
child: LazyLoadScrollView(
onEndOfPage: _onLoad,
isLoading: isLoading,
child: Scrollbar(
child: ListView(
children: [
filteredData.isEmpty &&
(searchText != '' || selectedCategory != null) &&
!isSearching
? const Center(child: Text('Ничего не найдено'))
: isSearching
? const Center(child: CircularProgressIndicator())
: GridView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: min(
(MediaQuery.sizeOf(context).width ~/
200)
.toInt(),
8),
childAspectRatio: 1.0),
itemCount: itemViewCount,
itemBuilder: (context, index) {
final product = filteredData[index];
return ProductCard(
imagePath: Image(
image:
Image.network(product.images[0].url)
.image,
width: 50,
),
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,
),
child: const Text('Загрузить ещё'),
)
: const CircularProgressIndicator()
: const Text(
'Конец списка',
style: TextStyle(
fontSize: 10,
color: Color(0x88000000)),
),
),
)
: const SizedBox.shrink(),
],
),
),
),
),
],
),
floatingActionButton: SizedBox(
height: 80,
width: 80,
child: FittedBox(
child: Stack(
children: [
FloatingActionButton(
onPressed: () => Navigator.of(context).push(CustomPageRoute(
builder: (context) => const BasketPage(),
)),
highlightElevation: 0,
hoverColor: Colors.transparent,
focusColor: Colors.transparent,
hoverElevation: 0,
focusElevation: 0,
splashColor: Colors.transparent,
backgroundColor: Colors.transparent,
elevation: 0,
child: CircleAvatar(
radius: 25,
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.white,
child: const Icon(Icons.shopping_cart_outlined)),
),
cartL > 0
? Positioned(
right: -3,
bottom: 0,
child: Card(
color: Colors.red,
child: SizedBox(
width: 20,
child: Center(
child: Text(
cartL.toString(),
style: const TextStyle(color: Colors.white),
),
),
),
),
)
: const SizedBox.shrink(),
],
),
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.startFloat,
);
}
}

View File

@@ -0,0 +1,204 @@
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_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/providers/cart.dart';
import 'package:gymlink_module_web/tools/items.dart';
import 'package:gymlink_module_web/tools/prefs.dart';
import 'package:gymlink_module_web/tools/routes.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
List<Map<String, dynamic>> cart = [
{
"name": "Протеин",
"image": "product.png",
"price": "120",
"details": "Test details",
"id": "34fa3126-bfaf-5dec-8f4a-b246c097ef73"
},
{
"name": "Протеин",
"image": "product.png",
"price": "150",
"details": "Test details",
"id": "34a26e82-7656-5e98-a44a-c2d01d0b1ad1123"
},
{
"name": "Протеин",
"image": "product.png",
"price": "250",
"details": "Test details",
"id": "4fb204b7-3f9e-52a2-bed1-415c00a31a37123"
},
{
"name": "Протеин",
"image": "product.png",
"price": "300",
"details": "Test details",
"id": "09b2f5bb-683e-5c39-ae89-b8e152fa8bcf123"
},
{
"name": "Протеин",
"image": "product.png",
"price": "100",
"details": "Test details",
"id": "cd1b6817-db94-5394-be1d-af88af79749f123"
}
];
class OrderConfirmationPage extends StatefulWidget {
const OrderConfirmationPage({super.key});
@override
State<OrderConfirmationPage> createState() => _OrderConfirmationPageState();
}
class _OrderConfirmationPageState extends State<OrderConfirmationPage> {
List<GymItem> cartItems = [];
double totalPrice = 0;
List<GymItem> gymCart = [];
bool isAgree = false;
bool _isLoading = true;
@override
void initState() {
super.initState();
Future.microtask(() => getCart().then((value) async {
final itemIds =
value.map((element) => element['id'] as String).toList();
final items = await getItemsByIds(context, itemIds);
setState(() {
gymCart = items;
cartItems = value.map((element) {
final item = gymCart.firstWhere((e) => e.id == element['id']);
item.localCount = element['count'] as int;
return item;
}).toList();
totalPrice = cartItems.fold(
0, (sum, item) => sum + item.price * item.localCount);
_isLoading = false;
});
}));
}
Future<void> _goToPage() async {
final Uri url = Uri.parse('https://google.com');
if (!await launchUrl(url, webOnlyWindowName: '_blank')) {
throw 'Could not launch $url';
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const GymLinkAppBar(),
resizeToAvoidBottomInset: false,
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const GymLinkHeader(title: 'Оформление заказа'),
const MarkdownBody(data: '## Состав заказа:'),
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 350),
child: ListView.builder(
shrinkWrap: true,
itemCount: cartItems.length,
itemBuilder: (context, index) {
final item = cartItems[index];
return OrderConfirmItemCard(
name: item.title,
image: Image(
image: NetworkImage(item.images[0].url),
width: 50,
height: 50),
price: item.price,
count: item.localCount,
);
},
),
),
),
const SizedBox(
height: 10,
),
Expanded(
child: Column(
children: [
MarkdownBody(
data: '## Итого: ${totalPrice.toStringAsFixed(2)} руб.'),
Expanded(
child: TextField(
decoration: InputDecoration(
hintText: 'Адрес доставки',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
),
Expanded(
child: TextField(
decoration: InputDecoration(
hintText: 'Электронная почта',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
keyboardType: TextInputType.emailAddress,
),
),
Expanded(
child: TextField(
decoration: InputDecoration(
hintText: 'Получатель',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
),
CheckboxListTile(
title: const Text('Согласен с обаботкой персональных данных'),
value: isAgree,
onChanged: (value) {
setState(() {
isAgree = value!;
});
},
),
ElevatedButton(
onPressed: () async {
_goToPage();
await clearCart();
Provider.of<CartProvider>(context, listen: false)
.updateCartLength();
Navigator.of(context).pushAndRemoveUntil(
CustomPageRoute(
builder: (context) => const HistoryPage()),
(route) => route.isFirst);
},
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: 20),
],
),
),
],
),
);
}
}

View File

@@ -0,0 +1,139 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:gymlink_module_web/components/app_bar.dart';
import 'package:gymlink_module_web/components/heading.dart';
import 'package:gymlink_module_web/components/history_item_card.dart';
import 'package:gymlink_module_web/tools/relative.dart';
import 'package:lazy_load_scrollview/lazy_load_scrollview.dart';
List<Map<String, String>> orders = [
{"image": "product.png", "price": "120", "id": "66", "date": "11.09.2001"},
{
"image": "product.png",
"price": "150",
"id": "56",
"date": "11.09.2001",
},
{
"image": "product.png",
"price": "250",
"id": "98",
"date": "11.09.2001",
},
{
"image": "product.png",
"price": "300",
"id": "50",
"date": "11.09.2001",
},
{
"image": "product.png",
"price": "100",
"id": "30",
"date": "11.09.2001",
}
];
class HistoryPage extends StatefulWidget {
const HistoryPage({
super.key,
});
@override
State<HistoryPage> createState() => _HistoryPageState();
}
class _HistoryPageState extends State<HistoryPage> {
List<Map<String, String>> my_orders = [];
late Timer _updateTimer;
@override
void initState() {
super.initState();
my_orders = orders;
ordersRefresh();
}
void ordersRefresh() {
_updateTimer = Timer.periodic(const Duration(minutes: 1), _onRefresh);
}
void _onLoad() async {
await Future.delayed(const Duration(milliseconds: 1000));
setState(() {
my_orders.add(
{
"image": "product.png",
"price": "120",
"id": "666666",
"date": "11.09.2001"
},
);
});
}
@override
void dispose() {
_updateTimer.cancel();
super.dispose();
}
Future<void> _onRefresh(Timer timer) async {
await Future.delayed(const Duration(milliseconds: 1000));
debugPrint('refreshed');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const GymLinkAppBar(),
body: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const GymLinkHeader(
title: 'История заказов',
toMain: true,
),
Expanded(
child: Row(
children: [
Expanded(
child: LazyLoadScrollView(
onEndOfPage: _onLoad,
scrollOffset: 200,
child: RefreshIndicator(
edgeOffset: 55,
onRefresh: () => _onRefresh(_updateTimer),
child: Stack(
children: [
ListView.builder(
itemCount: my_orders.length,
itemBuilder: (context, index) {
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,
);
},
),
],
),
),
),
),
getSpacer(context: context)
],
),
),
],
),
);
}
}

26
lib/pages/order_pay.dart Normal file
View File

@@ -0,0 +1,26 @@
import 'dart:ui_web' as ui;
import 'package:flutter/material.dart';
import 'package:universal_html/html.dart';
class OrderPayPage extends StatelessWidget {
const OrderPayPage({super.key});
@override
Widget build(BuildContext context) {
registerHtmlElementView();
return const HtmlElementView(
viewType: 'payment-html',
);
}
void registerHtmlElementView() {
ui.platformViewRegistry.registerViewFactory(
'payment-html',
(int viewId) => IFrameElement()
..src = 'payment.html'
..style.width = '100%'
..style.height = '500px'
..style.border = 'none');
}
}

20
lib/providers/cart.dart Normal file
View File

@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'package:gymlink_module_web/tools/prefs.dart';
//TODO: Возможно нужно дорабатывать
class CartProvider extends ChangeNotifier {
int _cartLength = 0;
int get cartLength => _cartLength;
CartProvider() {
updateCartLength();
}
void updateCartLength() {
getCart().then((value) {
_cartLength = value.length;
notifyListeners();
});
}
}

49
lib/providers/main.dart Normal file
View File

@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:gymlink_module_web/theme.dart';
class GymLinkProvider with ChangeNotifier {
bool _isLoading = true;
bool get isLoading => _isLoading;
bool _blackTheme = false;
bool get blackTheme => _blackTheme;
String _token = '';
String get token => _token;
ThemeData _theme = myTheme;
ThemeData get theme => _theme;
void Function() _onError = () => {};
void Function() get onError => _onError;
void onTokenReceived(String token) {
_token = token;
_isLoading = false;
notifyListeners();
// if (token == 'token123') {
// _isLoading = false;
// notifyListeners();
// } else {
// _isLoading = true;
// notifyListeners();
// }
}
void changeTheme(int color, {bool blackTheme = false}) {
_blackTheme = blackTheme;
_theme = getThemeData(Color(color), _blackTheme);
notifyListeners();
}
void setTheme(ThemeData theme) {
_theme = theme;
notifyListeners();
}
void setOnError(void Function() onError) {
_onError = () {
_token = '';
_isLoading = true;
onError();
notifyListeners();
};
}
}

29
lib/states/mobile.dart Normal file
View File

@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:gymlink_module_web/main_mobile.dart';
import 'package:gymlink_module_web/pages/main.dart';
import 'package:gymlink_module_web/providers/cart.dart';
import 'package:gymlink_module_web/providers/main.dart';
import 'package:provider/provider.dart';
class MyAppStateMobile extends State<MyApp> {
@override
Widget build(BuildContext context) {
return Consumer<GymLinkProvider>(
builder: (context, provider, __) {
final theme = provider.theme;
final isLoading = provider.isLoading;
return ChangeNotifierProvider(
create: (_) => CartProvider(),
builder: (context, __) => isLoading
? const Center(child: CircularProgressIndicator())
: MaterialApp(
title: 'GymLink Module',
theme: theme,
debugShowCheckedModeBanner: false,
home: const MainPage(),
),
);
},
);
}
}

64
lib/states/web.dart Normal file
View File

@@ -0,0 +1,64 @@
import 'dart:async';
import 'dart:js_interop' as js;
import 'dart:js_interop_unsafe' as js_util;
import 'package:flutter/material.dart';
import 'package:gymlink_module_web/main.dart';
import 'package:gymlink_module_web/pages/main.dart';
import 'package:gymlink_module_web/providers/cart.dart';
import 'package:gymlink_module_web/providers/main.dart';
import 'package:gymlink_module_web/theme.dart';
import 'package:provider/provider.dart';
@js.JSExport()
class MyAppStateWeb extends State<MyApp> {
final _streamController = StreamController<void>.broadcast();
ThemeData theme = myTheme;
bool black_theme = false;
@override
void initState() {
super.initState();
final export = js.createJSInteropWrapper(this);
js.globalContext['_appState'] = export;
js.globalContext.callMethod('_stateSet'.toJS);
}
@override
void dispose() {
_streamController.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = context.watch<GymLinkProvider>().theme;
final isLoading = context.watch<GymLinkProvider>().isLoading;
return ChangeNotifierProvider(
create: (_) => CartProvider(),
child: isLoading
? const Center(child: CircularProgressIndicator())
: MaterialApp(
title: 'GymLink Module',
theme: theme,
debugShowCheckedModeBanner: false,
home: const MainPage(),
),
);
}
@js.JSExport()
void onTokenReceived(String token) {
context.read<GymLinkProvider>().onTokenReceived(token);
}
@js.JSExport()
void changeColor(int color) {
context.read<GymLinkProvider>().changeTheme(color);
}
@js.JSExport()
void setOnError(void Function() onError) {
context.read<GymLinkProvider>().setOnError(onError);
}
}

39
lib/theme.dart Normal file
View File

@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
final ThemeData myTheme = ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: getMaterialColor(const Color(0x007d85ff))));
ThemeData getThemeData(Color color, bool dark) {
final MaterialColor materialColor = getMaterialColor(color);
return ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: materialColor,
brightness: dark ? Brightness.dark : Brightness.light,
).copyWith(
onPrimary: dark ? materialColor[600] : Colors.white,
),
useMaterial3: true,
);
}
MaterialColor getMaterialColor(Color color) {
final int red = color.red;
final int green = color.green;
final int blue = color.blue;
final Map<int, Color> shades = {
50: Color.fromRGBO(red, green, blue, .1),
100: Color.fromRGBO(red, green, blue, .2),
200: Color.fromRGBO(red, green, blue, .3),
300: Color.fromRGBO(red, green, blue, .4),
400: Color.fromRGBO(red, green, blue, .5),
500: Color.fromRGBO(red, green, blue, .6),
600: Color.fromRGBO(red, green, blue, .7),
700: Color.fromRGBO(red, green, blue, .8),
800: Color.fromRGBO(red, green, blue, .9),
900: Color.fromRGBO(red, green, blue, 1),
};
return MaterialColor(color.value, shades);
}

98
lib/tools/items.dart Normal file
View File

@@ -0,0 +1,98 @@
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.http('gymlink.freemyip.com:8080', '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.http('gymlink.freemyip.com:8080', '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.http(
'gymlink.freemyip.com:8080', '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 [];
}

47
lib/tools/prefs.dart Normal file
View File

@@ -0,0 +1,47 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
Future<void> addItemToCart(String id) async {
final prefs = await SharedPreferences.getInstance();
String cartString = prefs.getString('cart') ?? "[]";
List<Map<String, dynamic>> cart =
List<Map<String, dynamic>>.from(jsonDecode(cartString) as List<dynamic>);
final index = cart.indexWhere((element) => element['id'] == id);
if (index == -1) {
cart.add({'id': id, 'count': 1});
} else {
cart[index]['count'] = cart[index]['count']! + 1;
}
prefs.setString('cart', jsonEncode(cart));
}
Future<List<Map<String, dynamic>>> getCart() async {
final prefs = await SharedPreferences.getInstance();
String cartString = prefs.getString('cart') ?? "[]";
List<Map<String, dynamic>> cart =
List<Map<String, dynamic>>.from(jsonDecode(cartString) as List<dynamic>);
return cart;
}
Future<void> removeItemFromCart(String id) async {
final prefs = await SharedPreferences.getInstance();
String cartString = prefs.getString('cart') ?? "[]";
List<Map<String, dynamic>> cart =
List<Map<String, dynamic>>.from(jsonDecode(cartString) as List<dynamic>);
cart.removeWhere((element) => element['id'] == id && element['count'] == 1);
for (final item in cart) {
if (item['id'] == id) {
item['count'] = item['count']! - 1;
if (item['count'] == 0) {
cart.remove(item);
}
}
}
prefs.setString('cart', jsonEncode(cart));
}
Future<void> clearCart() async {
final prefs = await SharedPreferences.getInstance();
prefs.setString('cart', "[]");
}

11
lib/tools/relative.dart Normal file
View File

@@ -0,0 +1,11 @@
import 'package:flutter/material.dart';
Widget getSpacer(
{required BuildContext context, int flex = 1, double width = 10}) {
if (MediaQuery.of(context).size.width > 600) {
return Spacer(
flex: flex,
);
}
return SizedBox(width: width);
}

8
lib/tools/routes.dart Normal file
View File

@@ -0,0 +1,8 @@
import 'package:flutter/material.dart';
class CustomPageRoute extends MaterialPageRoute {
CustomPageRoute({builder}) : super(builder: builder);
@override
Duration get transitionDuration => const Duration(milliseconds: 0);
}

9
lib/tools/text.dart Normal file
View 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;
}
}

View File

@@ -6,6 +6,10 @@
#include "generated_plugin_registrant.h"
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
}

View File

@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@@ -5,6 +5,10 @@
import FlutterMacOS
import Foundation
import shared_preferences_foundation
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}

View File

@@ -1,6 +1,14 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
args:
dependency: transitive
description:
name: args
sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
url: "https://pub.dev"
source: hosted
version: "2.5.0"
async:
dependency: transitive
description:
@@ -17,6 +25,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@@ -25,6 +41,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.0"
charcode:
dependency: transitive
description:
name: charcode
sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306
url: "https://pub.dev"
source: hosted
version: "1.3.1"
clock:
dependency: transitive
description:
@@ -41,14 +65,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.18.0"
csslib:
dependency: transitive
description:
name: csslib
sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.dev"
source: hosted
version: "1.0.6"
version: "1.0.8"
fake_async:
dependency: transitive
description:
@@ -57,6 +89,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.1"
ffi:
dependency: transitive
description:
name: ffi
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
file:
dependency: transitive
description:
name: file
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
flutter:
dependency: "direct main"
description: flutter
@@ -70,11 +118,64 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.2"
flutter_markdown:
dependency: "direct main"
description:
name: flutter_markdown
sha256: "9921f9deda326f8a885e202b1e35237eadfc1345239a0f6f0f1ff287e047547f"
url: "https://pub.dev"
source: hosted
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:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
html:
dependency: transitive
description:
name: html
sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a"
url: "https://pub.dev"
source: hosted
version: "0.15.4"
http:
dependency: "direct main"
description:
name: http
sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@@ -107,6 +208,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.0"
markdown:
dependency: transitive
description:
name: markdown
sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051
url: "https://pub.dev"
source: hosted
version: "7.2.2"
matcher:
dependency: transitive
description:
@@ -131,6 +240,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.11.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
path:
dependency: transitive
description:
@@ -139,6 +256,126 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
url: "https://pub.dev"
source: hosted
version: "6.0.2"
platform:
dependency: transitive
description:
name: platform
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
url: "https://pub.dev"
source: hosted
version: "3.1.4"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
provider:
dependency: "direct main"
description:
name: provider
sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
url: "https://pub.dev"
source: hosted
version: "6.1.2"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
url: "https://pub.dev"
source: hosted
version: "2.2.3"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c"
url: "https://pub.dev"
source: hosted
version: "2.3.5"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
sky_engine:
dependency: transitive
description: flutter
@@ -192,6 +429,118 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.1"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
url: "https://pub.dev"
source: hosted
version: "1.3.2"
universal_html:
dependency: "direct main"
description:
name: universal_html
sha256: "56536254004e24d9d8cfdb7dbbf09b74cf8df96729f38a2f5c238163e3d58971"
url: "https://pub.dev"
source: hosted
version: "2.2.4"
universal_io:
dependency: transitive
description:
name: universal_io
sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e"
url: "https://pub.dev"
source: hosted
version: "6.2.6"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775"
url: "https://pub.dev"
source: hosted
version: "6.3.1"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5"
url: "https://pub.dev"
source: hosted
version: "6.2.5"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811
url: "https://pub.dev"
source: hosted
version: "3.1.1"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234
url: "https://pub.dev"
source: hosted
version: "3.1.0"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@@ -208,5 +557,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "13.0.0"
web:
dependency: transitive
description:
name: web
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
url: "https://pub.dev"
source: hosted
version: "0.5.1"
win32:
dependency: transitive
description:
name: win32
sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb"
url: "https://pub.dev"
source: hosted
version: "5.5.0"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
url: "https://pub.dev"
source: hosted
version: "1.0.4"
xml:
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev"
source: hosted
version: "6.5.0"
sdks:
dart: ">=3.3.3 <4.0.0"
flutter: ">=3.19.0"

View File

@@ -1,5 +1,5 @@
name: flutter_application_1
description: "A new Flutter project."
name: gymlink_module_web
description: "GymLink Flutter Web Module."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
@@ -31,10 +31,16 @@ dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.6
url_launcher: ^6.2.6
shared_preferences: ^2.2.3
flutter_markdown: ^0.7.1
http: ^1.2.1
universal_html: ^2.2.4
provider: ^6.1.2
lazy_load_scrollview: ^1.3.0
cupertino_icons: ^1.0.8
flutter_svg: ^2.0.10+1
carousel_slider: ^4.2.1
dev_dependencies:
flutter_test:
@@ -57,6 +63,10 @@ flutter:
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
assets:
- assets/logo.png
- assets/product.png
- assets/icon.svg
# To add assets to your application, add an assets section, like this:
# assets:

View File

@@ -1,7 +1,7 @@
#flutter_target {
border: 1px solid #aaa;
width: 320px;
height: 480px;
width: 80vw;
height: 60vh;
border-radius: 0px;
transition: all 150ms ease-in;
align-self: center;

View File

@@ -42,6 +42,8 @@
</head>
<body>
<button id='token'>Token btn</button>
<button id='colorChangeBtnRed'>Color btn Red</button>
<button id='colorChangeBtnBlue'>Color btn Blue</button>
<section class='contents'>
<article>
<div id="flutter_target"></div>

View File

@@ -1,16 +1,70 @@
(function(){
"use strict";
(function () {
'use strict';
window._stateSet = function () {
window._stateSet = function() {
console.log("Call _stateSet only once");
window._stateSet = function () {
console.log('Call _stateSet only once');
};
let appState = window._appState;
function getToken() {
fetch(
'http://gymlink.freemyip.com:8080/api/auth/authorize_client',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
GymKey: 'eeb42dcb-8e5b-4f21-825a-3fc7ada43445', // Just testing token
id: '123',
}),
}
)
.then(res => res.json())
.catch(e => {
console.log(e);
setTimeout(getToken, 1000);
})
.then(data => {
if (data.payload)
appState.onTokenReceived(data.payload.token);
else {
console.log(data);
setTimeout(getToken, 1000);
}
});
}
let btn = document.getElementById('token');
btn.addEventListener('click', function() {
appState.onTokenReceived('token123');
})
}
}());
btn.addEventListener('click', getToken);
let colorChangeBtnRed = document.getElementById('colorChangeBtnRed');
colorChangeBtnRed.addEventListener('click', function () {
var hexColor = '#FF0000'
.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);
appState.changeColor(numColor);
});
let colorChangeBtnBlue = document.getElementById('colorChangeBtnBlue');
colorChangeBtnBlue.addEventListener('click', function () {
var hexColor = '#0000FF'
.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);
appState.changeColor(numColor);
});
function onError() {
console.error('aboba');
}
appState.setOnError(onError);
};
})();

51
web/payment.html Normal file
View File

@@ -0,0 +1,51 @@
<!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>

View File

@@ -6,6 +6,9 @@
#include "generated_plugin_registrant.h"
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}

View File

@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
url_launcher_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST