Compare commits

..

15 Commits

Author SHA1 Message Date
c9e777b6c7 feat:补充注释 2025-10-10 14:00:22 +08:00
d16cbb3127 feat:补充注释 2025-10-10 13:55:13 +08:00
14411709d5 feat:补充注释 2025-10-10 13:54:28 +08:00
9a095ad96f feat:补充注释和上传安卓签名文件 2025-10-10 13:45:06 +08:00
73b47f1780 feat:会员和个人中心 2025-10-10 11:11:16 +08:00
9c223bd2d1 fix:播放切换问题修复 2025-09-24 16:00:55 +08:00
5fc3edfbe4 fix:切换播放问题 2025-09-24 15:03:07 +08:00
def8b49505 fix:切换播放问题 2025-09-24 14:51:32 +08:00
ab042840b9 fix:首页布局问题 2025-09-24 11:34:52 +08:00
311e4fe06c feat:分类切换优化 2025-09-23 18:26:23 +08:00
5c00e4efef feat:分类切换优化 2025-09-23 17:19:33 +08:00
f22c893932 Merge branch 'android-no-pay'
# Conflicts:
#	lib/kt_pages/kt_short_video/logic.dart
2025-09-23 16:45:14 +08:00
bd65038cfc feat:首页搜索 2025-09-23 15:56:46 +08:00
8932eefb14 feat:反馈 2025-09-23 15:50:21 +08:00
ab2e1a49f9 feat:iOS一期 2025-09-23 15:09:18 +08:00
48 changed files with 3495 additions and 404 deletions

View File

@ -41,31 +41,45 @@ https://pub.dev/packages/flutter_native_splash
# 生成安卓打包证书
keytool -genkeypair -v -keystore kinetra_adehok_app.jks -keyalg RSA -keysize 2048 -validity 10000 -alias com.kinetra.adehok.app
口令:123456@nyxora
口令:123456
# 查看安卓证书指纹
keytool -list -v -keystore kinetra_adehok_app.jks
别名: com.kinetra.adehok.app
创建日期: 2025年8月20
创建日期: 2025年9月22
条目类型: PrivateKeyEntry
证书链长度: 1
证书[1]:
所有者: CN=zy, OU=qj, O=qj, L=cs, ST=hn, C=cn
发布者: CN=zy, OU=qj, O=qj, L=cs, ST=hn, C=cn
序列号: 45fb9110ec85ed35
生效时间: Wed Aug 20 13:44:03 CST 2025, 失效时间: Sun Jan 05 13:44:03 CST 2053
所有者: CN=qj, OU=qj, O=qj, L=cs, ST=hunan, C=cn
发布者: CN=qj, OU=qj, O=qj, L=cs, ST=hunan, C=cn
序列号: f98a0bc4e952426a
生效时间: Mon Sep 22 16:02:03 CST 2025, 失效时间: Fri Feb 07 16:02:03 CST 2053
证书指纹:
SHA1: E8:3F:DB:87:73:40:A6:0E:BA:43:C5:C5:42:62:D2:95:FA:E0:DA:21
SHA256: C5:0A:88:AA:B8:1E:C2:0A:6C:64:0E:99:18:4E:97:86:A0:C8:3F:2C:98:EE:E9:97:6A:A2:CC:B0:03:70:EE:56
SHA1: 73:61:84:4F:97:9C:EC:B6:5C:25:64:E9:98:51:2C:2E:67:07:1F:AC
SHA256: 73:17:E7:6B:6A:7A:E8:32:72:AE:60:A2:18:78:55:2C:8D:49:66:E3:3C:E9:7E:D6:F3:88:6A:FD:F5:50:33:C1
签名算法名称: SHA384withRSA
主体公共密钥算法: 2048 位 RSA 密钥
版本: 3
# 监听model修改自动生成
# 监听model修改自动生成(当前项目未使用)
dart run build_runner watch --delete-conflicting-outputs
# 打包
正式包: flutter build apk --release
aab的包: flutter build appbundle
iOS: flutter build ipa
给测试打apk包测试完成后给漆维上架谷歌需要打aab的包
apk包: flutter build apk --release
aab包: flutter build aab --release
iOS: flutter build ipa或者flutter build iOS 完成后在xcode点击工具栏Product选择archive
# 文件结构
flutter_kinetra
-android:安卓侧相关配置文件
-assets:图片、字体静态资源
-ios:ios侧相关配置文件
-lib:主要代码文件
-dio_client:dio接口请求类及接口地址
-kt_model:数据实体类
-kt_pages:ui页面
-kt_utils:工具类
-kt_widget:组件
main.dart:项目启动入口函数
pubspec.yaml:配置项目版本号、引入插件、字体和图片等静态资源

6
android/.gitignore vendored
View File

@ -9,6 +9,6 @@ GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
#key.properties
#**/*.keystore
#**/*.jks

4
android/key.properties Normal file
View File

@ -0,0 +1,4 @@
storePassword=123456
keyPassword=123456
keyAlias=com.kinetra.adehok.app
storeFile=../kinetra_adehok_app.jks

Binary file not shown.

BIN
assets/ic_apple.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 B

BIN
assets/ic_fb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
assets/ic_lock.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 853 B

BIN
assets/ic_order_record.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

BIN
assets/ic_polygon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

BIN
assets/lock_black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 B

BIN
assets/lock_white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
assets/login_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
assets/login_sheet_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

BIN
assets/month_text_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
assets/quarter_text_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
assets/recharge_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

BIN
assets/unlock_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

BIN
assets/year_text_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
002E1F1EA059FEE73E55E03C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D1AEBA8280B01EA417C5A4C5 /* Pods_Runner.framework */; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
2E31D4A42E82814000481906 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2E31D4A32E82814000481906 /* StoreKit.framework */; };
2F3E2165998010C253A6A3AB /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 31F0B6F62A10C414CCB11B56 /* Pods_RunnerTests.framework */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
@ -44,6 +45,7 @@
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
2E31D4A32E82814000481906 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
31F0B6F62A10C414CCB11B56 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@ -73,6 +75,7 @@
buildActionMask = 2147483647;
files = (
002E1F1EA059FEE73E55E03C /* Pods_Runner.framework in Frameworks */,
2E31D4A42E82814000481906 /* StoreKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -145,6 +148,7 @@
A643DB2437D6E4CE3EFAE3C0 /* Frameworks */ = {
isa = PBXGroup;
children = (
2E31D4A32E82814000481906 /* StoreKit.framework */,
D1AEBA8280B01EA417C5A4C5 /* Pods_Runner.framework */,
31F0B6F62A10C414CCB11B56 /* Pods_RunnerTests.framework */,
);

View File

@ -1,7 +1,7 @@
//
class KtApis {
static const String baseUrl = "https://api-csjiuxi.csjiuxi.com/kinet";
// static const String baseUrl = "https://test1-api.guyantv.com";
// static const String baseUrl = "https://api-pdaroll.pdaroll.com/panda";
static const String homeAllModules = "/home/all-modules";
static const String newShortPlay = "/newShortPlay";
@ -24,7 +24,7 @@ class KtApis {
static const String uploadW2a = "/w2aSelfAttribution";
static const String activeAfterWatchingVideo = "/activeAfterWatchingVideo";
static String W2A_PREFIX = "[QJ]";
static String W2A_NAME = "pandaloom";
static String W2A_NAME = "kinetra";
static String adjustToken = "z44550428xz4";
//
@ -93,7 +93,7 @@ class KtApis {
static String WEB_SITE_FEEDBACK_DETAIL = "${WEB_SITE_HOST}pages/leave/detail";
///
static String WEB_SITE_WALLET = "${WEB_SITE_HOST}pages/leave/detail";
static String WEB_SITE_WALLET = "${WEB_SITE_HOST}pages/order/kinetra";
///
static String WEB_SITE_SEARCH = "${WEB_SITE_HOST}pages/search/kinetra";

View File

@ -7,6 +7,7 @@ import 'package:flustars/flustars.dart';
import '../kt_utils/kt_device_info_utils.dart';
import '../kt_utils/kt_keys.dart';
///
class RequestInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {

View File

@ -0,0 +1,741 @@
import 'dart:convert';
import 'package:in_app_purchase/in_app_purchase.dart';
/// id : 93
/// status : "enable"
/// price : "59.99"
/// coins : 6000
/// send_coins : 6000
/// buy_type : "coins"
/// sort : 12
/// description : ""
/// vip_type : ""
/// title : "59.99 for coins"
/// brief : ""
/// created_at : "2025-04-15 07:01:47"
/// origin_price : "0.00"
/// backhaul_price : "59.99"
/// ios_template_id : "coins_59.99"
/// android_template_id : "coins_59.99"
/// currency : "US$"
/// updated_at : "2025-07-02 06:48:47"
/// translate_key : ""
/// platform : "all"
/// lang_id : 0
/// corner_marker : "Popularity"
/// first_promotion_price : "0.00"
/// version : 2
/// user_level : "high"
/// vip_days : 0
/// user_promise_level : ""
/// use_once : 0
/// send_coin_ttl : 1
/// backhaul_percent : 100
/// pay_template_id : 2
/// deleted_at : ""
/// pay_product_id : 14
/// ext_info : "{\"size\": \"big\", \"backhaul_percent_conf\": []}"
/// factor : "1.00"
/// short_type : ""
/// vip_type_key : ""
/// auto_sub : "Automatic renewal, cancel at any time"
/// size : "big"
KtGoodsBean ktGoodsBeanFromJson(String str) => KtGoodsBean.fromJson(json.decode(str));
String ktGoodsBeanToJson(KtGoodsBean data) => json.encode(data.toJson());
class KtGoodsBean {
KtGoodsBean({
int? id,
String? status,
String? price,
int? coins,
int? sendCoins,
String? buyType,
int? sort,
String? description,
String? vipType,
String? title,
String? brief,
String? createdAt,
String? originPrice,
String? backhaulPrice,
String? iosTemplateId,
String? androidTemplateId,
String? currency,
String? updatedAt,
String? translateKey,
String? platform,
int? langId,
String? cornerMarker,
String? firstPromotionPrice,
int? version,
String? userLevel,
int? vipDays,
String? userPromiseLevel,
int? useOnce,
int? sendCoinTtl,
int? backhaulPercent,
int? payTemplateId,
String? deletedAt,
int? payProductId,
int? isFirstBuy,
int? isDiscount,
int? discountType,
ExtInfo? extInfo,
String? factor,
String? shortType,
String? vipTypeKey,
String? autoSub,
String? size,
String? orderCode,
String? transactionId,
String? serverVerificationData,
ProductDetails? productDetails,
}) {
_id = id;
_status = status;
_price = price;
_coins = coins;
_sendCoins = sendCoins;
_buyType = buyType;
_sort = sort;
_description = description;
_vipType = vipType;
_title = title;
_brief = brief;
_createdAt = createdAt;
_originPrice = originPrice;
_backhaulPrice = backhaulPrice;
_iosTemplateId = iosTemplateId;
_androidTemplateId = androidTemplateId;
_currency = currency;
_updatedAt = updatedAt;
_translateKey = translateKey;
_platform = platform;
_langId = langId;
_cornerMarker = cornerMarker;
_firstPromotionPrice = firstPromotionPrice;
_version = version;
_userLevel = userLevel;
_vipDays = vipDays;
_userPromiseLevel = userPromiseLevel;
_useOnce = useOnce;
_sendCoinTtl = sendCoinTtl;
_backhaulPercent = backhaulPercent;
_payTemplateId = payTemplateId;
_deletedAt = deletedAt;
_payProductId = payProductId;
_discountType = discountType;
_isDiscount = isDiscount;
_isFirstBuy = isFirstBuy;
_extInfo = extInfo;
_factor = factor;
_shortType = shortType;
_vipTypeKey = vipTypeKey;
_autoSub = autoSub;
_size = size;
_orderCode = orderCode;
_transactionId = transactionId;
_serverVerificationData = serverVerificationData;
_productDetails = productDetails;
}
KtGoodsBean.fromJson(dynamic json) {
_id = json['id'];
_status = json['status'];
_price = json['price'];
_coins = json['coins'];
_sendCoins = json['send_coins'];
_buyType = json['buy_type'];
_sort = json['sort'];
_description = json['description'];
_vipType = json['vip_type'];
_title = json['title'];
_brief = json['brief'];
_createdAt = json['created_at'];
_originPrice = json['origin_price'];
_backhaulPrice = json['backhaul_price'];
_iosTemplateId = json['ios_template_id'];
_androidTemplateId = json['android_template_id'];
_currency = json['currency'];
_updatedAt = json['updated_at'];
_translateKey = json['translate_key'];
_platform = json['platform'];
_langId = json['lang_id'];
_cornerMarker = json['corner_marker'];
_firstPromotionPrice = json['first_promotion_price'];
_version = json['version'];
_userLevel = json['user_level'];
_vipDays = json['vip_days'];
_userPromiseLevel = json['user_promise_level'];
_useOnce = json['use_once'];
_sendCoinTtl = json['send_coin_ttl'];
_backhaulPercent = json['backhaul_percent'];
_payTemplateId = json['pay_template_id'];
_deletedAt = json['deleted_at'];
_payProductId = json['pay_product_id'];
_discountType = json['discount_type'];
_isDiscount = json['is_discount'];
_isFirstBuy = json['is_first_buy'];
_extInfo = json['ext_info'] != null ? ExtInfo.fromJson(json['ext_info']) : null;
_factor = json['factor'];
_shortType = json['short_type'];
_vipTypeKey = json['vip_type_key'];
_autoSub = json['auto_sub'];
_size = json['size'];
_orderCode = json['orderCode'];
_transactionId = json['transactionId'];
_serverVerificationData = json['serverVerificationData'];
_productDetails = json['productDetails'];
}
int? _id;
String? _status;
String? _price;
int? _coins;
int? _sendCoins;
String? _buyType;
int? _sort;
String? _description;
String? _vipType;
String? _title;
String? _brief;
String? _createdAt;
String? _originPrice;
String? _backhaulPrice;
String? _iosTemplateId;
String? _androidTemplateId;
String? _currency;
String? _updatedAt;
String? _translateKey;
String? _platform;
int? _langId;
String? _cornerMarker;
String? _firstPromotionPrice;
int? _version;
String? _userLevel;
int? _vipDays;
String? _userPromiseLevel;
int? _useOnce;
int? _sendCoinTtl;
int? _backhaulPercent;
int? _payTemplateId;
String? _deletedAt;
int? _payProductId;
int? _discountType;
int? _isDiscount;
int? _isFirstBuy;
ExtInfo? _extInfo;
String? _factor;
String? _shortType;
String? _vipTypeKey;
String? _autoSub;
String? _size;
String? _orderCode;
String? _transactionId;
String? _serverVerificationData;
ProductDetails? _productDetails;
KtGoodsBean copyWith({
int? id,
String? status,
String? price,
int? coins,
int? sendCoins,
String? buyType,
int? sort,
String? description,
String? vipType,
String? title,
String? brief,
String? createdAt,
String? originPrice,
String? backhaulPrice,
String? iosTemplateId,
String? androidTemplateId,
String? currency,
String? updatedAt,
String? translateKey,
String? platform,
int? langId,
String? cornerMarker,
String? firstPromotionPrice,
int? version,
String? userLevel,
int? vipDays,
String? userPromiseLevel,
int? useOnce,
int? sendCoinTtl,
int? backhaulPercent,
int? payTemplateId,
String? deletedAt,
int? payProductId,
int? discountType,
int? isDiscount,
int? isFirstBuy,
ExtInfo? extInfo,
String? factor,
String? shortType,
String? vipTypeKey,
String? autoSub,
String? size,
String? orderCode,
String? transactionId,
String? serverVerificationData,
ProductDetails? productDetails,
}) => KtGoodsBean(
id: id ?? _id,
status: status ?? _status,
price: price ?? _price,
coins: coins ?? _coins,
sendCoins: sendCoins ?? _sendCoins,
buyType: buyType ?? _buyType,
sort: sort ?? _sort,
description: description ?? _description,
vipType: vipType ?? _vipType,
title: title ?? _title,
brief: brief ?? _brief,
createdAt: createdAt ?? _createdAt,
originPrice: originPrice ?? _originPrice,
backhaulPrice: backhaulPrice ?? _backhaulPrice,
iosTemplateId: iosTemplateId ?? _iosTemplateId,
androidTemplateId: androidTemplateId ?? _androidTemplateId,
currency: currency ?? _currency,
updatedAt: updatedAt ?? _updatedAt,
translateKey: translateKey ?? _translateKey,
platform: platform ?? _platform,
langId: langId ?? _langId,
cornerMarker: cornerMarker ?? _cornerMarker,
firstPromotionPrice: firstPromotionPrice ?? _firstPromotionPrice,
version: version ?? _version,
userLevel: userLevel ?? _userLevel,
vipDays: vipDays ?? _vipDays,
userPromiseLevel: userPromiseLevel ?? _userPromiseLevel,
useOnce: useOnce ?? _useOnce,
sendCoinTtl: sendCoinTtl ?? _sendCoinTtl,
backhaulPercent: backhaulPercent ?? _backhaulPercent,
payTemplateId: payTemplateId ?? _payTemplateId,
deletedAt: deletedAt ?? _deletedAt,
payProductId: payProductId ?? _payProductId,
discountType: discountType ?? _discountType,
isDiscount: isDiscount ?? _isDiscount,
isFirstBuy: isFirstBuy ?? _isFirstBuy,
extInfo: extInfo ?? _extInfo,
factor: factor ?? _factor,
shortType: shortType ?? _shortType,
vipTypeKey: vipTypeKey ?? _vipTypeKey,
autoSub: autoSub ?? _autoSub,
size: size ?? _size,
orderCode: orderCode ?? _orderCode,
transactionId: transactionId ?? _transactionId,
serverVerificationData: serverVerificationData ?? _serverVerificationData,
productDetails: productDetails ?? _productDetails,
);
int? get id => _id;
String? get status => _status;
String? get price => _price;
int? get coins => _coins;
int? get sendCoins => _sendCoins;
String? get buyType => _buyType;
int? get sort => _sort;
String? get description => _description;
String? get vipType => _vipType;
String? get title => _title;
String? get brief => _brief;
String? get createdAt => _createdAt;
String? get originPrice => _originPrice;
String? get backhaulPrice => _backhaulPrice;
String? get iosTemplateId => _iosTemplateId;
String? get androidTemplateId => _androidTemplateId;
String? get currency => _currency;
String? get updatedAt => _updatedAt;
String? get translateKey => _translateKey;
String? get platform => _platform;
int? get langId => _langId;
String? get cornerMarker => _cornerMarker;
String? get firstPromotionPrice => _firstPromotionPrice;
int? get version => _version;
String? get userLevel => _userLevel;
int? get vipDays => _vipDays;
String? get userPromiseLevel => _userPromiseLevel;
int? get useOnce => _useOnce;
int? get sendCoinTtl => _sendCoinTtl;
int? get backhaulPercent => _backhaulPercent;
int? get payTemplateId => _payTemplateId;
String? get deletedAt => _deletedAt;
int? get payProductId => _payProductId;
int? get discountType => _discountType;
int? get isDiscount => _isDiscount;
int? get isFirstBuy => _isFirstBuy;
ExtInfo? get extInfo => _extInfo;
String? get factor => _factor;
String? get shortType => _shortType;
String? get vipTypeKey => _vipTypeKey;
String? get autoSub => _autoSub;
String? get size => _size;
String? get orderCode => _orderCode;
String? get transactionId => _transactionId;
String? get serverVerificationData => _serverVerificationData;
ProductDetails? get productDetails => _productDetails;
// set方法补充
set id(int? value) => _id = value;
set status(String? value) => _status = value;
set price(String? value) => _price = value;
set coins(int? value) => _coins = value;
set sendCoins(int? value) => _sendCoins = value;
set buyType(String? value) => _buyType = value;
set sort(int? value) => _sort = value;
set description(String? value) => _description = value;
set vipType(String? value) => _vipType = value;
set title(String? value) => _title = value;
set brief(String? value) => _brief = value;
set createdAt(String? value) => _createdAt = value;
set originPrice(String? value) => _originPrice = value;
set backhaulPrice(String? value) => _backhaulPrice = value;
set iosTemplateId(String? value) => _iosTemplateId = value;
set androidTemplateId(String? value) => _androidTemplateId = value;
set currency(String? value) => _currency = value;
set updatedAt(String? value) => _updatedAt = value;
set translateKey(String? value) => _translateKey = value;
set platform(String? value) => _platform = value;
set langId(int? value) => _langId = value;
set cornerMarker(String? value) => _cornerMarker = value;
set firstPromotionPrice(String? value) => _firstPromotionPrice = value;
set version(int? value) => _version = value;
set userLevel(String? value) => _userLevel = value;
set vipDays(int? value) => _vipDays = value;
set userPromiseLevel(String? value) => _userPromiseLevel = value;
set useOnce(int? value) => _useOnce = value;
set sendCoinTtl(int? value) => _sendCoinTtl = value;
set backhaulPercent(int? value) => _backhaulPercent = value;
set payTemplateId(int? value) => _payTemplateId = value;
set deletedAt(String? value) => _deletedAt = value;
set payProductId(int? value) => _payProductId = value;
set discountType(int? value) => _discountType = value;
set isDiscount(int? value) => _isDiscount = value;
set isFirstBuy(int? value) => _isFirstBuy = value;
set extInfo(ExtInfo? value) => _extInfo = value;
set factor(String? value) => _factor = value;
set shortType(String? value) => _shortType = value;
set vipTypeKey(String? value) => _vipTypeKey = value;
set autoSub(String? value) => _autoSub = value;
set size(String? value) => _size = value;
set orderCode(String? value) => _orderCode = value;
set transactionId(String? value) => _transactionId = value;
set serverVerificationData(String? value) => _serverVerificationData = value;
set productDetails(ProductDetails? value) => _productDetails = value;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['id'] = _id;
map['status'] = _status;
map['price'] = _price;
map['coins'] = _coins;
map['send_coins'] = _sendCoins;
map['buy_type'] = _buyType;
map['sort'] = _sort;
map['description'] = _description;
map['vip_type'] = _vipType;
map['title'] = _title;
map['brief'] = _brief;
map['created_at'] = _createdAt;
map['origin_price'] = _originPrice;
map['backhaul_price'] = _backhaulPrice;
map['ios_template_id'] = _iosTemplateId;
map['android_template_id'] = _androidTemplateId;
map['currency'] = _currency;
map['updated_at'] = _updatedAt;
map['translate_key'] = _translateKey;
map['platform'] = _platform;
map['lang_id'] = _langId;
map['corner_marker'] = _cornerMarker;
map['first_promotion_price'] = _firstPromotionPrice;
map['version'] = _version;
map['user_level'] = _userLevel;
map['vip_days'] = _vipDays;
map['user_promise_level'] = _userPromiseLevel;
map['use_once'] = _useOnce;
map['send_coin_ttl'] = _sendCoinTtl;
map['backhaul_percent'] = _backhaulPercent;
map['pay_template_id'] = _payTemplateId;
map['deleted_at'] = _deletedAt;
map['pay_product_id'] = _payProductId;
map['discount_type'] = _discountType;
map['is_discount'] = _isDiscount;
map['is_first_buy'] = _isFirstBuy;
if (_extInfo != null) {
map['ext_info'] = _extInfo?.toJson();
}
map['factor'] = _factor;
map['short_type'] = _shortType;
map['vip_type_key'] = _vipTypeKey;
map['auto_sub'] = _autoSub;
map['size'] = _size;
map['serverVerificationData'] = _serverVerificationData;
map['transactionId'] = _transactionId;
map['orderCode'] = _orderCode;
// map['productDetails'] = _productDetails;
return map;
}
}
/// size : "small"
/// extra_coins : 0
/// max_total_coins : 0
/// max_total_coins_pop : 0
/// is_vip_show : false
/// receive_coins_rate : 85
/// extra_day_coins : 85
/// sub_coins_txt_list : ["",""]
/// backhaul_percent_conf : [{"episode_num":50,"backhaul_percent":50},{"episode_num":80,"backhaul_percent":80}]
ExtInfo extInfoFromJson(String str) => ExtInfo.fromJson(json.decode(str));
String extInfoToJson(ExtInfo data) => json.encode(data.toJson());
class ExtInfo {
ExtInfo({
String? size,
int? maxTotalCoins,
int? maxTotalCoinsPop,
int? extraCoins,
bool? isVipShow = false,
String? receiveCoinsRate,
int? extraDayCoins,
bool? coinsWinBackShow,
List<String>? subCoinsTxtList,
List<BackhaulPercentConf>? backhaulPercentConf,
}) {
_size = size;
_maxTotalCoins = maxTotalCoins;
_maxTotalCoinsPop = maxTotalCoinsPop;
_extraCoins = extraCoins;
_isVipShow = isVipShow;
_receiveCoinsRate = receiveCoinsRate;
_extraDayCoins = extraDayCoins;
_coinsWinBackShow = coinsWinBackShow;
_subCoinsTxtList = subCoinsTxtList;
_backhaulPercentConf = backhaulPercentConf;
}
ExtInfo.fromJson(dynamic json) {
_size = json['size'];
_maxTotalCoins = json['max_total_coins'];
_maxTotalCoinsPop = json['max_total_coins_pop'];
_isVipShow = json['is_vip_show'];
_receiveCoinsRate = json['receive_coins_rate'];
_extraDayCoins = json['extra_day_coins'];
_extraCoins = json['extra_coins'];
_coinsWinBackShow = json['coins_win_back_show'];
_subCoinsTxtList = json['sub_coins_txt_list'] != null ? json['sub_coins_txt_list'].cast<String>() : [];
if (json['backhaul_percent_conf'] != null) {
_backhaulPercentConf = [];
json['backhaul_percent_conf'].forEach((v) {
_backhaulPercentConf?.add(BackhaulPercentConf.fromJson(v));
});
}
}
String? _size;
int? _extraCoins;
int? _maxTotalCoins;
int? _maxTotalCoinsPop;
String? _receiveCoinsRate;
int? _extraDayCoins;
bool? _isVipShow;
bool? _coinsWinBackShow;
List<String>? _subCoinsTxtList;
List<BackhaulPercentConf>? _backhaulPercentConf;
ExtInfo copyWith({
String? size,
int? extraCoins,
int? maxTotalCoins,
int? maxTotalCoinsPop,
String? receiveCoinsRate,
int? extraDayCoins,
bool? coinsWinBackShow,
bool? isVipShow,
List<String>? subCoinsTxtList,
List<BackhaulPercentConf>? backhaulPercentConf,
}) => ExtInfo(
size: size ?? _size,
maxTotalCoins: maxTotalCoins ?? _maxTotalCoins,
maxTotalCoinsPop: maxTotalCoinsPop ?? _maxTotalCoinsPop,
receiveCoinsRate: receiveCoinsRate ?? _receiveCoinsRate,
extraDayCoins: extraDayCoins ?? _extraDayCoins,
isVipShow: isVipShow ?? _isVipShow,
extraCoins: extraCoins ?? _extraCoins,
coinsWinBackShow: coinsWinBackShow ?? _coinsWinBackShow,
subCoinsTxtList: subCoinsTxtList ?? _subCoinsTxtList,
backhaulPercentConf: backhaulPercentConf ?? _backhaulPercentConf,
);
String? get size => _size;
int? get extraCoins => _extraCoins;
int? get maxTotalCoins => _maxTotalCoins;
int? get maxTotalCoinsPop => _maxTotalCoinsPop;
String? get receiveCoinsRate => _receiveCoinsRate;
int? get extraDayCoins => _extraDayCoins;
bool? get coinsWinBackShow => _coinsWinBackShow;
bool? get isVipShow => _isVipShow;
List<String>? get subCoinsTxtList => _subCoinsTxtList;
List<BackhaulPercentConf>? get backhaulPercentConf => _backhaulPercentConf;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['size'] = _size;
map['max_total_coins'] = _maxTotalCoins;
map['max_total_coins_pop'] = _maxTotalCoinsPop;
map['receive_coins_rate'] = _receiveCoinsRate;
map['extra_day_coins'] = _extraDayCoins;
map['is_vip_show'] = _isVipShow;
map['extra_coins'] = _extraCoins;
map['coins_win_back_show'] = _coinsWinBackShow;
map['sub_coins_txt_list'] = _subCoinsTxtList;
if (_backhaulPercentConf != null) {
map['backhaul_percent_conf'] = _backhaulPercentConf?.map((v) => v.toJson()).toList();
}
return map;
}
}
/// episode_num : 50
/// backhaul_percent : 50
BackhaulPercentConf backhaulPercentConfFromJson(String str) => BackhaulPercentConf.fromJson(json.decode(str));
String backhaulPercentConfToJson(BackhaulPercentConf data) => json.encode(data.toJson());
class BackhaulPercentConf {
BackhaulPercentConf({int? episodeNum, int? backhaulPercent}) {
_episodeNum = episodeNum;
_backhaulPercent = backhaulPercent;
}
BackhaulPercentConf.fromJson(dynamic json) {
_episodeNum = json['episode_num'];
_backhaulPercent = json['backhaul_percent'];
}
int? _episodeNum;
int? _backhaulPercent;
BackhaulPercentConf copyWith({int? episodeNum, int? backhaulPercent}) =>
BackhaulPercentConf(episodeNum: episodeNum ?? _episodeNum, backhaulPercent: backhaulPercent ?? _backhaulPercent);
int? get episodeNum => _episodeNum;
int? get backhaulPercent => _backhaulPercent;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['episode_num'] = _episodeNum;
map['backhaul_percent'] = _backhaulPercent;
return map;
}
}

View File

@ -0,0 +1,32 @@
import 'dart:convert';
/// customer_id : ""
/// token : ""
KtLoginBean ktLoginBeanFromJson(String str) => KtLoginBean.fromJson(json.decode(str));
String ktLoginBeanToJson(KtLoginBean data) => json.encode(data.toJson());
class KtLoginBean {
KtLoginBean({String? customerId, String? token}) {
_customerId = customerId;
_token = token;
}
KtLoginBean.fromJson(dynamic json) {
_customerId = json['customer_id'];
_token = json['token'];
}
String? _customerId;
String? _token;
KtLoginBean copyWith({String? customerId, String? token}) =>
KtLoginBean(customerId: customerId ?? _customerId, token: token ?? _token);
String? get customerId => _customerId;
String? get token => _token;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['customer_id'] = _customerId;
map['token'] = _token;
return map;
}
}

View File

@ -0,0 +1,38 @@
import 'dart:convert';
/// order_code : "O202507111000016870c46c4c3fd"
/// money : "9.99"
/// is_backhaul : 2
KtOrderBean ktOrderBeanFromJson(String str) => KtOrderBean.fromJson(json.decode(str));
String ktOrderBeanToJson(KtOrderBean data) => json.encode(data.toJson());
class KtOrderBean {
KtOrderBean({String? orderCode, String? money, int? isBackhaul}) {
_orderCode = orderCode;
_money = money;
_isBackhaul = isBackhaul;
}
KtOrderBean.fromJson(dynamic json) {
_orderCode = json['order_code'];
_money = json['money'];
_isBackhaul = json['is_backhaul'];
}
String? _orderCode;
String? _money;
int? _isBackhaul;
KtOrderBean copyWith({String? orderCode, String? money, int? isBackhaul}) =>
KtOrderBean(orderCode: orderCode ?? _orderCode, money: money ?? _money, isBackhaul: isBackhaul ?? _isBackhaul);
String? get orderCode => _orderCode;
String? get money => _money;
int? get isBackhaul => _isBackhaul;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['order_code'] = _orderCode;
map['money'] = _money;
map['is_backhaul'] = _isBackhaul;
return map;
}
}

View File

@ -0,0 +1,159 @@
import 'dart:convert';
import 'kt_goods_bean.dart';
/// sort : ["list_coins","list_sub_vip"]
/// list_vip : [{"id":93,"status":"enable","price":"59.99","coins":6000,"send_coins":6000,"buy_type":"coins","sort":12,"description":"","vip_type":"","title":"59.99 for coins","brief":"","created_at":"2025-04-15 07:01:47","origin_price":"0.00","backhaul_price":"59.99","ios_template_id":"coins_59.99","android_template_id":"coins_59.99","currency":"US$","updated_at":"2025-07-02 06:48:47","translate_key":"","platform":"all","lang_id":0,"corner_marker":"Popularity","first_promotion_price":"0.00","version":2,"user_level":"high","vip_days":0,"user_promise_level":"","use_once":0,"send_coin_ttl":1,"backhaul_percent":100,"pay_template_id":2,"deleted_at":"","pay_product_id":14,"ext_info":"{\"size\": \"big\", \"backhaul_percent_conf\": []}","factor":"1.00","short_type":"","vip_type_key":"","auto_sub":"Automatic renewal, cancel at any time","size":"big"}]
/// list_coins : [{"id":93,"status":"enable","price":"59.99","coins":6000,"send_coins":6000,"buy_type":"coins","sort":12,"description":"","vip_type":"","title":"59.99 for coins","brief":"","created_at":"2025-04-15 07:01:47","origin_price":"0.00","backhaul_price":"59.99","ios_template_id":"coins_59.99","android_template_id":"coins_59.99","currency":"US$","updated_at":"2025-07-02 06:48:47","translate_key":"","platform":"all","lang_id":0,"corner_marker":"Popularity","first_promotion_price":"0.00","version":2,"user_level":"high","vip_days":0,"user_promise_level":"","use_once":0,"send_coin_ttl":1,"backhaul_percent":100,"pay_template_id":2,"deleted_at":"","pay_product_id":14,"ext_info":"{\"size\": \"big\", \"backhaul_percent_conf\": []}","factor":"1.00","short_type":"","vip_type_key":"","auto_sub":"Automatic renewal, cancel at any time","size":"big"}]
/// list_sub_vip : [{"id":104,"status":"enable","price":"49.99","coins":0,"send_coins":4500,"buy_type":"sub_vip","sort":5,"description":"Unlimited access to all series for 1 month (Ad-free)","vip_type":"month","title":"Monthly membership","brief":"Monthly membership","created_at":"2025-04-15 07:30:47","origin_price":"0.00","backhaul_price":"24.99","ios_template_id":"sub.m_49.99","android_template_id":"sub.m_49.99","currency":"US$","updated_at":"2025-06-20 07:27:23","translate_key":"","platform":"all","lang_id":0,"corner_marker":"Popularity","first_promotion_price":"0.00","version":2,"user_level":"high","vip_days":0,"user_promise_level":"null","use_once":0,"send_coin_ttl":35,"backhaul_percent":100,"pay_template_id":2,"deleted_at":"null","pay_product_id":25,"ext_info":"{\"size\": \"\", \"backhaul_percent_conf\": []}","factor":"1.00","short_type":"Monthly","vip_type_key":"month","auto_sub":"Automatic renewal, cancel at any time","size":"small"}]
/// list_retrieve : [{"id":93,"status":"enable","price":"59.99","coins":6000,"send_coins":6000,"buy_type":"coins","sort":12,"description":"","vip_type":"","title":"59.99 for coins","brief":"","created_at":"2025-04-15 07:01:47","origin_price":"0.00","backhaul_price":"59.99","ios_template_id":"coins_59.99","android_template_id":"coins_59.99","currency":"US$","updated_at":"2025-07-02 06:48:47","translate_key":"","platform":"all","lang_id":0,"corner_marker":"Popularity","first_promotion_price":"0.00","version":2,"user_level":"high","vip_days":0,"user_promise_level":"","use_once":0,"send_coin_ttl":1,"backhaul_percent":100,"pay_template_id":2,"deleted_at":"","pay_product_id":14,"ext_info":"{\"size\": \"big\", \"backhaul_percent_conf\": []}","factor":"1.00","short_type":"","vip_type_key":"","auto_sub":"Automatic renewal, cancel at any time","size":"big"}]
KtStoreBean ktStoreBeanFromJson(String str) => KtStoreBean.fromJson(json.decode(str));
String ktStoreBeanToJson(KtStoreBean data) => json.encode(data.toJson());
class KtStoreBean {
KtStoreBean({
int? showType,
int? payMode,
List<String>? sort,
List<KtGoodsBean>? listVip,
List<KtGoodsBean>? listCoins,
List<KtGoodsBean>? listSubCoins,
List<KtGoodsBean>? listSubVip,
List<KtGoodsBean>? listRetrieve,
}) {
_sort = sort;
_payMode = payMode;
_showType = showType;
_listVip = listVip;
_listCoins = listCoins;
_listSubCoins = listSubCoins;
_listSubVip = listSubVip;
_listRetrieve = listRetrieve;
}
KtStoreBean.fromJson(dynamic json) {
_showType = json['show_type'];
_payMode = json['pay_mode'];
_sort = json['sort'] != null ? json['sort'].cast<String>() : [];
if (json['list_vip'] != null) {
_listVip = [];
json['list_vip'].forEach((v) {
_listVip?.add(KtGoodsBean.fromJson(v));
});
}
if (json['list_coins'] != null) {
_listCoins = [];
json['list_coins'].forEach((v) {
_listCoins?.add(KtGoodsBean.fromJson(v));
});
}
if (json['list_sub_coins'] != null) {
_listSubCoins = [];
json['list_sub_coins'].forEach((v) {
_listSubCoins?.add(KtGoodsBean.fromJson(v));
});
}
if (json['list_sub_vip'] != null) {
_listSubVip = [];
json['list_sub_vip'].forEach((v) {
_listSubVip?.add(KtGoodsBean.fromJson(v));
});
}
if (json['list_retrieve'] != null) {
_listRetrieve = [];
json['list_retrieve'].forEach((v) {
_listRetrieve?.add(KtGoodsBean.fromJson(v));
});
}
}
int? _payMode;
int? _showType;
List<String>? _sort;
List<KtGoodsBean>? _listVip;
List<KtGoodsBean>? _listCoins;
List<KtGoodsBean>? _listSubCoins;
List<KtGoodsBean>? _listSubVip;
List<KtGoodsBean>? _listRetrieve;
KtStoreBean copyWith({
int? payMode,
int? showType,
List<String>? sort,
List<KtGoodsBean>? listVip,
List<KtGoodsBean>? listCoins,
List<KtGoodsBean>? listSubCoins,
List<KtGoodsBean>? listSubVip,
List<KtGoodsBean>? listRetrieve,
}) => KtStoreBean(
payMode: payMode ?? _payMode,
showType: showType ?? _showType,
sort: sort ?? _sort,
listVip: listVip ?? _listVip,
listCoins: listCoins ?? _listCoins,
listSubVip: listSubVip ?? _listSubVip,
listSubCoins: listSubCoins ?? _listSubCoins,
listRetrieve: listRetrieve ?? _listRetrieve,
);
int? get payMode => _payMode;
int? get showType => _showType;
List<String>? get sort => _sort;
List<KtGoodsBean>? get listVip => _listVip;
List<KtGoodsBean>? get listCoins => _listCoins;
List<KtGoodsBean>? get listSubCoins => _listSubCoins;
List<KtGoodsBean>? get listSubVip => _listSubVip;
List<KtGoodsBean>? get listRetrieve => _listRetrieve;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['show_type'] = _showType;
map['pay_mode'] = _payMode;
map['sort'] = _sort;
if (_listVip != null) {
map['list_vip'] = _listVip?.map((v) => v.toJson()).toList();
}
if (_listCoins != null) {
map['list_coins'] = _listCoins?.map((v) => v.toJson()).toList();
}
if (_listSubVip != null) {
map['list_sub_vip'] = _listSubVip?.map((v) => v.toJson()).toList();
}
if (_listSubCoins != null) {
map['list_sub_coins'] = _listSubCoins?.map((v) => v.toJson()).toList();
}
if (_listRetrieve != null) {
map['list_retrieve'] = _listRetrieve?.map((v) => v.toJson()).toList();
}
return map;
}
}
KtGoodsBean listRetrieveFromJson(String str) => KtGoodsBean.fromJson(json.decode(str));
String listRetrieveToJson(KtGoodsBean data) => json.encode(data.toJson());
KtGoodsBean listSubVipFromJson(String str) => KtGoodsBean.fromJson(json.decode(str));
String listSubVipToJson(KtGoodsBean data) => json.encode(data.toJson());
KtGoodsBean listSubCoinsFromJson(String str) => KtGoodsBean.fromJson(json.decode(str));
String listSubCoinsToJson(KtGoodsBean data) => json.encode(data.toJson());
KtGoodsBean listCoinsFromJson(String str) => KtGoodsBean.fromJson(json.decode(str));
String listCoinsToJson(KtGoodsBean data) => json.encode(data.toJson());
KtGoodsBean listVipFromJson(String str) => KtGoodsBean.fromJson(json.decode(str));
String listVipToJson(KtGoodsBean data) => json.encode(data.toJson());

View File

@ -21,6 +21,7 @@ class KtExploreLogic extends GetxController {
final PageController pageController = PageController(viewportFraction: .95);
final int preloadRange = 1;
bool isRefresh = false;
final mainLogic = Get.find<KtMainLogic>();
@override
void onReady() {
@ -123,10 +124,10 @@ class KtExploreLogic extends GetxController {
});
}
if (state.controllers[index] != null) {
state.controllers[index]?.play();
if (mainLogic.curIndex == 1) state.controllers[index]?.play();
} else {
if (!isToggle) await _initializeController(index);
state.controllers[index]?.play();
if (mainLogic.curIndex == 1) state.controllers[index]?.play();
}
//
_preloadAdjacentVideos();
@ -165,13 +166,16 @@ class KtExploreLogic extends GetxController {
try {
await controller.initialize();
if (index == state.currentPage) {
final mainLogic = Get.find<KtMainLogic>();
if(mainLogic.curIndex == 1) controller.play();
if (index == state.currentPage && mainLogic.curIndex == 1) {
controller.play();
update();
}
controller.addListener(() {
if (state.currentPage == index) update();
if (state.currentPage == index && mainLogic.curIndex == 1) {
update();
} else {
controller.pause();
}
if (controller.value.isCompleted && !controller.value.isBuffering) {
onPageChanged(index + 1, isToggle: true);

View File

@ -154,7 +154,7 @@ class SignInActivityPageState extends State<KtSearchPage> with RouteAware {
allowedOriginRules: {"*"},
onPostMessage:
(message, sourceOrigin, isMainFrame, replyProxy) {
print('----callback--jsonS:$message');
debugPrint('----callback--jsonS:$message');
if (message?.data != null) {
_handleWebMessage(message?.data);
}
@ -247,7 +247,7 @@ class SignInActivityPageState extends State<KtSearchPage> with RouteAware {
}
Widget _buildWidget() {
print('----loadStatus:$loadingStatus');
debugPrint('----loadStatus:$loadingStatus');
switch (loadingStatus) {
case KtLoadStatusType.loading:
return Center(child: CircularProgressIndicator());

View File

@ -12,20 +12,21 @@ import 'package:get/get.dart';
import '../../../dio_cilent/kt_apis.dart';
import '../../../kt_utils/kt_keys.dart';
import '../../../kt_utils/kt_utils.dart';
import '../../../kt_widgets/kt_status_widget.dart';
import '../../../main.dart';
class WalletPage extends StatefulWidget {
const WalletPage({super.key});
class KtOrderRecordPage extends StatefulWidget {
const KtOrderRecordPage({super.key});
@override
SignInActivityPageState createState() => SignInActivityPageState();
KtOrderRecordPageState createState() => KtOrderRecordPageState();
}
class SignInActivityPageState extends State<WalletPage> with RouteAware {
class KtOrderRecordPageState extends State<KtOrderRecordPage> with RouteAware {
InAppWebViewController? _webViewController;
late PullToRefreshController _webRefreshController;
late Map<String, String> _userData;
// LoadStatusType loadingStatus = LoadStatusType.loading;
KtLoadStatusType loadingStatus = KtLoadStatusType.loading;
@override
void initState() {
@ -39,10 +40,10 @@ class SignInActivityPageState extends State<WalletPage> with RouteAware {
'time_zone': KtUtils.getTimeZoneOffset(DateTime.now()),
'type': Platform.isAndroid ? 'android' : 'ios',
'lang': 'en',
'theme': 'theme_7',
// 'theme': 'theme_7',
'token': SpUtil.getString(KtKeys.token) ?? '',
};
print('-----userData:${_userData}');
debugPrint('-----userData:$_userData');
}
_initRefreshController() {
@ -108,32 +109,24 @@ class SignInActivityPageState extends State<WalletPage> with RouteAware {
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
// appBar: AppBar(
// leading: Container(
// padding: EdgeInsets.only(left: 15.w, bottom: 6.w),
// child: IconButton(
// icon: Image.asset('ic_back.png'.ktIcon, width: 24.w),
// onPressed: () => Navigator.of(context).maybePop(),
// ),
// ),
// ),
body: Container(
width: ScreenUtil().screenWidth,
height: ScreenUtil().screenHeight,
padding: EdgeInsets.only(top: ScreenUtil().statusBarHeight + 20.w),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('bg1.png'.ktIcon),
fit: BoxFit.fill,
),
return Container(
width: ScreenUtil().screenWidth,
height: ScreenUtil().screenHeight,
padding: EdgeInsets.only(top: ScreenUtil().statusBarHeight + 20.w),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('bg1.png'.ktIcon),
fit: BoxFit.fill,
),
child: Container(
margin: EdgeInsets.only(top: 16.w),
child: Stack(
children: [
InAppWebView(
),
child: Container(
margin: EdgeInsets.only(top: 16.w),
child: Stack(
clipBehavior: Clip.none,
children: [
Container(
margin: EdgeInsets.only(top: 20.w),
child: InAppWebView(
pullToRefreshController: _webRefreshController,
initialSettings: InAppWebViewSettings(
cacheEnabled: false,
@ -167,37 +160,26 @@ class SignInActivityPageState extends State<WalletPage> with RouteAware {
handlerName: 'getUserInfo',
callback: (_) => jsonEncode(_userData),
);
if (Platform.isIOS) {
// ios
await _webViewController?.addWebMessageListener(
WebMessageListener(
jsObjectName: "js2app",
allowedOriginRules: {"*"},
onPostMessage:
(message, sourceOrigin, isMainFrame, replyProxy) {
if (message?.data != null) {
_handleWebMessage(message?.data);
}
},
),
);
} else if (Platform.isAndroid) {
_webViewController?.addJavaScriptHandler(
handlerName: "js2app",
callback: (jsonS) {
if (jsonS.isNotEmpty) {
_handleWebMessage(jsonS.toString());
}
},
);
}
await _webViewController?.addWebMessageListener(
WebMessageListener(
jsObjectName: "goStore",
allowedOriginRules: {"*"},
onPostMessage:
(message, sourceOrigin, isMainFrame, replyProxy) {
debugPrint('----callback--jsonS:$message');
if (message != null) {
Get.toNamed(KtRoutes.store);
}
},
),
);
await _webViewController?.loadUrl(
urlRequest: URLRequest(url: WebUri(KtApis.WEB_SITE_WALLET)),
);
},
onLoadStart: (controller, url) {
setState(() {
// loadingStatus = LoadStatusType.loading;
loadingStatus = KtLoadStatusType.loading;
});
},
onLoadStop: (controller, url) async {
@ -213,25 +195,25 @@ class SignInActivityPageState extends State<WalletPage> with RouteAware {
controller.evaluateJavascript(
source:
'''
if(typeof window.receiveDataFromNative === 'function') {
window.receiveDataFromNative($userJsonStr);
}
''',
if(typeof window.receiveDataFromNative === 'function') {
window.receiveDataFromNative($userJsonStr);
}
''',
);
});
} else if (Platform.isAndroid) {
await controller.evaluateJavascript(
source: """
window.AndroidInterface = {
getUserInfo: async function () {
return window.flutter_inappwebview.callHandler('getUserInfo');
},
};
""",
window.AndroidInterface = {
getUserInfo: async function () {
return window.flutter_inappwebview.callHandler('getUserInfo');
},
};
""",
);
}
setState(() {
// loadingStatus = LoadStatusType.loadSuccess;
loadingStatus = KtLoadStatusType.loadSuccess;
});
_webRefreshController.endRefreshing();
},
@ -239,16 +221,68 @@ class SignInActivityPageState extends State<WalletPage> with RouteAware {
_webRefreshController.endRefreshing();
Future.delayed(const Duration(milliseconds: 100)).then((_) {
setState(() {
// loadingStatus = LoadStatusType.loadFailed;
loadingStatus = KtLoadStatusType.loadFailed;
});
});
},
),
// _buildWidget(),
],
),
),
_buildWidget(),
Positioned(left: 0,right: 0, child: _buildAppBar()),
],
),
),
);
}
Widget _buildAppBar() {
return SizedBox(
width: ScreenUtil().screenWidth,
height: 40.w,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: Image.asset('ic_back.png'.ktIcon, width: 8.w),
onPressed: () => Navigator.of(context).maybePop(),
),
Text(
'My Wallet',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: Colors.black,
),
),
Container(width: 24.w),
],
),
);
}
Widget _buildWidget() {
debugPrint('----loadStatus:$loadingStatus');
switch (loadingStatus) {
case KtLoadStatusType.loading:
return Center(child: CircularProgressIndicator());
case KtLoadStatusType.loadFailed:
return KtStatusWidget(
type: KtErrorStatusType.noNetwork,
onPressed: () {
_webViewController?.loadUrl(
urlRequest: URLRequest(url: WebUri(KtApis.WEB_SITE_SEARCH)),
);
},
);
case KtLoadStatusType.loadNoData:
return KtStatusWidget(
type: KtErrorStatusType.notFound,
onPressed: () {
_webViewController?.reload();
},
);
default:
return const SizedBox.shrink();
}
}
}

View File

@ -0,0 +1,529 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_kinetra/kt_pages/kt_mine/kt_store/state.dart';
import 'package:get/get.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import '../../../dio_cilent/kt_apis.dart';
import '../../../dio_cilent/kt_request.dart';
import '../../../kt_model/kt_goods_bean.dart';
import '../../../kt_model/kt_order_bean.dart';
import '../../../kt_model/kt_store_bean.dart';
import '../../../kt_utils/kt_device_info_utils.dart';
import '../../../kt_utils/kt_iap_util.dart';
import '../../../kt_utils/kt_purchase_restore_utils.dart';
import '../../../kt_utils/kt_toast_utils.dart';
import '../../../kt_utils/kt_utils.dart';
import '../logic.dart';
class KtStoreLogic extends GetxController {
final state = KtStoreState();
late StreamSubscription<List<PurchaseDetails>> _streamSubscription;
final RefreshController refreshController = RefreshController();
final String appKey = 'kinetra';
List<PurchaseDetails> purchaseList = [];
Function? func;
bool isCoinDialog = false;
bool isRaminVipDialog = false; //
bool isCoinDialogOpen = false;
//
bool coinsModalEasyClose = false;
bool forcedRecharge = false;
String closeLabel = '';
Function? pauseVideoFunc;
Function? coinDialogCallback;
@override
onReady() {
getStoreInfo();
initPurchaseListener();
super.onReady();
}
@override
void onClose() {
KtInAppPurchaseUtil.cancelStreamSubscription(_streamSubscription);
super.onClose();
}
/// store商品列表
getStoreInfo() async {
EasyLoading.show(
status: 'Searching...',
maskType: EasyLoadingMaskType.black,
dismissOnTap: false,
);
try {
ApiResponse res = await KtHttpClient().request(
KtApis.paySettingsV4,
method: HttpMethod.get,
);
EasyLoading.dismiss();
refreshController.refreshCompleted();
if (res.success) {
state.storeBean = KtStoreBean.fromJson(res.data);
update();
// initStore();
}
} catch (e) {
EasyLoading.dismiss();
}
}
initStore() async {
EasyLoading.show(status: 'Searching...');
bool isAvailable = await KtInAppPurchaseUtil.isAvailable();
if (!isAvailable) {
return KtToastUtils.showError('In App purchase is not available');
}
// ID
List<String> productIds = [];
// vipId
List<String> vipIds = [];
if (Platform.isIOS) {
productIds = (state.storeBean.listCoins ?? [])
.map((item) => '$appKey.${item.iosTemplateId}')
.toList();
vipIds = (state.storeBean.listSubVip ?? [])
.map((item) => '$appKey.${item.iosTemplateId}')
.toList();
} else {
productIds = state.storeBean.listCoins!
.map((item) => item.androidTemplateId ?? '')
.toList();
vipIds = state.storeBean.listSubVip!
.map((item) => item.androidTemplateId ?? '')
.toList();
}
if (productIds.isEmpty && vipIds.isEmpty) {
EasyLoading.dismiss();
KtToastUtils.showError('Pay item is empty');
return;
}
ProductDetailsResponse productDetailsResponse =
await KtInAppPurchaseUtil.queryProducts(<String>{
...vipIds,
...productIds,
});
if (productDetailsResponse.error != null) {
EasyLoading.dismiss();
debugPrint(
"----productDetailsResponse.error${productDetailsResponse.error}",
);
KtToastUtils.showError(productDetailsResponse.error!.message);
return;
}
debugPrint('---未找到的商品: ${productDetailsResponse.notFoundIDs.join(', ')}');
debugPrint(
'----productDetailsResponse:${productDetailsResponse.productDetails.length}',
);
if (productDetailsResponse.productDetails.isEmpty) {
EasyLoading.dismiss();
KtToastUtils.showError('Query store is empty');
return;
}
if (Platform.isIOS) {
state.storeBean.listCoins!.removeWhere(
(a) => productDetailsResponse.productDetails.every((b) {
if (b.id == '$appKey.${a.iosTemplateId}') {
a.productDetails = b;
return false;
}
return true;
}),
);
state.storeBean.listSubVip!.removeWhere(
(a) => productDetailsResponse.productDetails.every((b) {
if (b.id == '$appKey.${a.iosTemplateId}') {
a.productDetails = b;
return false;
}
return true;
}),
);
} else if (Platform.isAndroid) {
state.storeBean.listCoins!.removeWhere(
(a) => productDetailsResponse.productDetails.every((b) {
if (b.id == a.androidTemplateId) {
a.productDetails = b;
return false;
}
return true;
}),
);
state.storeBean.listSubVip!.removeWhere(
(a) => productDetailsResponse.productDetails.every((b) {
if (b.id == a.androidTemplateId) {
a.productDetails = b;
return false;
}
return true;
}),
);
}
state.storeBean.listCoins?.sort((a, b) => a.size!.compareTo(b.size!));
state.refillCoinList =
state.storeBean.listCoins
?.where((item) => item.size == 'spread')
.toList() ??
[];
EasyLoading.dismiss();
update();
}
///
initPurchaseListener() {
_streamSubscription = KtInAppPurchaseUtil.purchaseStream.listen(
(List<PurchaseDetails> purchaseDetailsList) {
purchaseList = purchaseDetailsList;
_listenToPurchaseUpdated(purchaseDetailsList);
},
onDone: () {
_streamSubscription.cancel();
},
onError: (err) {
debugPrint('---stream-err:$err');
},
);
}
_listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) async {
for (var purchaseDetails in purchaseDetailsList) {
debugPrint(
'---listen-purchaseDetails:${purchaseDetailsList.length} ${purchaseDetails.productID} ${purchaseDetails.status} ${purchaseDetails.pendingCompletePurchase}',
);
if (purchaseDetails.status == PurchaseStatus.pending) {
//
debugPrint('Purchase is pending: ${purchaseDetails.productID}');
KtInAppPurchaseUtil.completePurchase(purchaseDetails);
} else if (purchaseDetails.status == PurchaseStatus.purchased ||
purchaseDetails.status == PurchaseStatus.restored) {
//
debugPrint(
'Purchase successful: ${purchaseDetails.productID} ${purchaseDetails.status} ${purchaseDetails.pendingCompletePurchase}',
);
try {
KtGoodsBean? temp =
// isCoinDialog
// ? state.vipCoinStoreBean
// : isRaminVipDialog
// ? state.remainVipDialogBean
// :
[
...(state.storeBean.listCoins ?? []),
...(state.storeBean.listSubVip ?? []),
].firstWhereOrNull(
(item) => item.productDetails?.id == purchaseDetails.productID,
);
if (temp == null) {
// UserUtil().reportErrorEvent(
// 'platform pay failed',
// UserUtil.payError,
// payData: {'id': purchaseDetails.productID, 'status': purchaseDetails.status},
// errMsg: '未找到匹配商品',
// );
KtToastUtils.showError('There were some problems with the payment');
return;
}
KtGoodsBean? goods = KtGoodsBean.fromJson(temp.toJson());
goods.transactionId = purchaseDetails.purchaseID;
goods.serverVerificationData =
purchaseDetails.verificationData.serverVerificationData;
EasyLoading.dismiss();
if (KtUtils.isNotEmpty(goods.orderCode)) {
bool isSuccess = await verifyPay(goods);
if (isSuccess) KtInAppPurchaseUtil.consumeIfNeeded(purchaseDetails);
}
} catch (e) {
debugPrint('--purchase-success-err:$e');
}
KtInAppPurchaseUtil.completePurchase(purchaseDetails);
} else if (purchaseDetails.status == PurchaseStatus.error) {
//
debugPrint('Purchase failed: ${purchaseDetails.error?.message}');
EasyLoading.dismiss();
KtGoodsBean? goods =
[
...(state.storeBean.listCoins ?? []),
...(state.storeBean.listSubVip ?? []),
].firstWhereOrNull(
(item) => item.productDetails?.id == purchaseDetails.productID,
);
// UserUtil().reportErrorEvent(
// 'platform pay failed',
// UserUtil.payError,
// orderCode: goods?.orderCode ?? '',
// errMsg: purchaseDetails.error?.toString(),
// );
debugPrint(
'-----itemAlreadyOwned:${purchaseDetails.error?.message.contains('itemAlreadyOwned')}',
);
if (purchaseDetails.error?.message.contains('itemAlreadyOwned') ??
false) {
KtInAppPurchaseUtil.completePurchase(purchaseDetails, isRetry: true);
// KtGoodsBean? goods = [
// ...(state.storeBean.listCoins ?? []),
// ...(state.storeBean.listSubVip ?? []),
// ].firstWhereOrNull((item) => item.productDetails?.id == purchaseDetails.productID);
// if (goods?.productDetails != null && count <= 2) {
// count++;
// KtInAppPurchaseUtil.buy(goods!.productDetails!);
// }
} else {
KtToastUtils.showError('There were some problems with the payment');
}
} else if (purchaseDetails.status == PurchaseStatus.canceled) {
//
debugPrint('Purchase canceled: ${purchaseDetails.productID}');
EasyLoading.dismiss();
KtToastUtils.showToast('User cancel');
// KtGoodsBean? goods = [
// ...(state.storeBean.listCoins ?? []),
// ...(state.storeBean.listSubVip ?? []),
// ].firstWhereOrNull((item) => item.productDetails?.id == purchaseDetails.productID);
// UserUtil().reportErrorEvent('user pay canceled', "pay_cancel", orderCode: goods?.orderCode ?? '');
}
KtInAppPurchaseUtil.completePurchase(purchaseDetails, isRetry: true);
}
}
buyGoods(KtGoodsBean payItem, {num? shortPlayId, num? videoId}) {
if (payItem.buyType == 'sub_coins') {
// showSubCoinCheckDialog(payItem, shortPlayId: shortPlayId, videoId: videoId);
} else {
createOrder(payItem, shortPlayId: shortPlayId, videoId: videoId);
}
}
createOrder(KtGoodsBean goods, {num? shortPlayId, num? videoId}) async {
EasyLoading.show(status: 'Paying...', maskType: EasyLoadingMaskType.black);
// Map<String, dynamic> params = {"pay_setting_id": goods.id!};
Map<String, dynamic> params = {
"pay_setting_id": goods.id!,
"is_discount": 0,
};
// if (Platform.isIOS && params["is_discount"] == 1) {
// final product = goods.productDetails as AppStoreProductDetails;
// params.putIfAbsent('product_discount', () => product.skProduct.discounts.first.identifier!);
// }
if (shortPlayId != null) {
params.putIfAbsent('short_play_id', () => shortPlayId);
state.shortPlayId = shortPlayId;
}
if (videoId != null) {
params.putIfAbsent('video_id', () => videoId);
state.videoId = videoId;
}
ApiResponse res = await KtHttpClient().request(
KtApis.createOrder,
data: params,
);
EasyLoading.dismiss();
if (res.success) {
if (res.data['code'] == 30007) {
KtToastUtils.showError(
isCoinDialog
? 'You\'ve already subscribed to this coin pack'
: 'You are already VIP!',
);
return;
}
KtOrderBean order = KtOrderBean.fromJson(res.data);
if (KtUtils.isEmpty(order.orderCode) ||
KtUtils.isEmpty(goods.productDetails))
return;
goods.orderCode = order.orderCode;
state.orderCode = order.orderCode;
// if (isRaminVipDialog) state.remainVipDialogBean.orderCode = order.orderCode;
try {
// if (goods.discountType != 0) {
// if (Platform.isIOS) {
// if (goods.discountType == 2) {
// final product = goods.productDetails as AppStoreProductDetails;
// final purchaseParamSk2 = AppStorePurchaseParam(
// productDetails: goods.productDetails!,
// applicationUserName: order.discount!.signData!.applicationUsername!,
// discount: SKPaymentDiscountWrapper(
// identifier: product.skProduct.discounts.first.identifier!,
// keyIdentifier: order.discount!.signData!.keyIdentifier!,
// nonce: order.discount!.signData!.nonce!,
// signature: order.discount!.signData!.signature!,
// timestamp: order.discount!.signData!.timestamp!,
// ),
// );
// debugPrint('----purchaseParamSk2 identifier:${purchaseParamSk2.discount?.identifier}');
// await KtInAppPurchaseUtil.buyDiscount(purchaseParamSk2);
// } else {
// await KtInAppPurchaseUtil.buy(goods.productDetails!, consumable: goods.buyType == 'coins');
// }
// } else if (Platform.isAndroid) {
// if (goods.productDetails! is GooglePlayProductDetails) {
// final offer =
// (goods.productDetails! as GooglePlayProductDetails).productDetails.subscriptionOfferDetails!.first;
//
// debugPrint('----offertoken:${offer.offerIdToken}');
// final purchaseParam = GooglePlayPurchaseParam(
// productDetails: goods.productDetails!,
// offerToken: offer.offerIdToken,
// );
// await KtInAppPurchaseUtil.buyDiscount(purchaseParam);
// }
// }
// } else {
// if (Platform.isAndroid) {
// if (goods.productDetails! is GooglePlayProductDetails) {
// if ((goods.productDetails! as GooglePlayProductDetails).productDetails.subscriptionOfferDetails != null) {
// List<SubscriptionOfferDetailsWrapper> offerList =
// (goods.productDetails! as GooglePlayProductDetails).productDetails.subscriptionOfferDetails!;
// final offerIdToken = offerList.length > 1 ? offerList[1].offerIdToken : offerList[0].offerIdToken;
//
// final purchaseParam = GooglePlayPurchaseParam(
// productDetails: goods.productDetails!,
// offerToken: offerIdToken,
// );
// await KtInAppPurchaseUtil.buyDiscount(purchaseParam);
// } else {
// await KtInAppPurchaseUtil.buy(goods.productDetails!, consumable: goods.buyType == 'coins');
// }
// } else {
// await KtInAppPurchaseUtil.buy(goods.productDetails!, consumable: goods.buyType == 'coins');
// }
// } else {
// await KtInAppPurchaseUtil.buy(goods.productDetails!, consumable: goods.buyType == 'coins');
// }
// }
await KtInAppPurchaseUtil.buy(
goods.productDetails!,
consumable: goods.buyType == 'coins',
);
} catch (e) {
EasyLoading.dismiss();
KtToastUtils.showError(
'There were some problems with the payment,Please try again!',
);
debugPrint('---e:$e');
// UserUtil().reportErrorEvent(
// 'platform pay timeout',
// UserUtil.payPlatformTimeout,
// orderCode: goods.orderCode,
// errMsg: e.toString(),
// payData: params,
// );
KtInAppPurchaseUtil.clearFailedPurchases();
for (var item in purchaseList) {
if (item.productID == goods.productDetails?.id) {
KtInAppPurchaseUtil.completePurchase(item);
}
}
}
} else {
EasyLoading.dismiss();
KtToastUtils.showError('Failed to create order');
goods.orderCode = null;
}
}
///
Future<bool> verifyPay(
KtGoodsBean goods, {
bool isRestore = false,
bool isAuto = false,
}) async {
String url = Platform.isAndroid
? KtApis.createGooglePay
: KtApis.createApplePay;
final deviceInfo = KtDeviceInfoUtil();
String transactionId = goods.transactionId ?? "";
String serverVerificationData = goods.serverVerificationData ?? "";
Map<String, dynamic> params = {
'pkg_name': deviceInfo.packageName ?? "",
'order_code': goods.orderCode ?? "",
'product_id': Platform.isAndroid
? goods.androidTemplateId
: goods.iosTemplateId,
'show_money': goods.price,
'pay_setting_id': goods.id.toString(),
'transaction_id': transactionId,
'purchases_token': serverVerificationData,
};
// if (isRestore) {
// UserUtil().reportErrorEvent(
// 'pay restore',
// UserUtil.payRestore,
// type: isAuto ? 'auto' : 'manual',
// orderCode: goods.orderCode,
// transactionId: transactionId,
// payData: params,
// );
// }
try {
ApiResponse res = await KtHttpClient().request(url, data: params);
if (res.success && res.data['status'] == 'success') {
if (!isRestore) {
KtToastUtils.showSuccess(placeholder: 'Pay Success');
Future.delayed(Duration(seconds: 1), () => getStoreInfo());
}
Get.put(KtMineLogic()).getUserInfo();
func?.call();
// coinDialogCallback?.call();
// if (isCoinDialog) {
// Get.back();
// }
///todo:线
// if (Platform.isAndroid) KtPurchaseRestoreUtil().cacheFailedGoods(goods);
KtPurchaseRestoreUtil().removeGoods(goods);
cleanFunc();
return true;
}
coinDialogCallback?.call();
// UserUtil().reportErrorEvent(
// 'pay callback failed',
// UserUtil.payCallback,
// orderCode: goods.orderCode,
// transactionId: transactionId,
// errMsg: params.toString(),
// );
if (!isRestore) KtPurchaseRestoreUtil().cacheFailedGoods(goods);
cleanFunc();
return false;
} catch (e) {
coinDialogCallback?.call();
if (!isRestore) KtPurchaseRestoreUtil().cacheFailedGoods(goods);
debugPrint('---e:$e');
cleanFunc();
return false;
}
}
cleanFunc() {
func = null;
// coinDialogCallback = null;
// pauseVideoFunc = null;
// if (isCoinDialog) isCoinDialog = false;
}
restorePay() {}
}

View File

@ -0,0 +1,17 @@
import '../../../kt_model/kt_goods_bean.dart';
import '../../../kt_model/kt_store_bean.dart';
class KtStoreState {
KtStoreBean storeBean = KtStoreBean();
List vipTypes = ['Monthly', 'Weekly', 'Quarterly', 'Yearly'];
List<KtGoodsBean> vipList = [];
List<KtGoodsBean> coinList = [];
List<KtGoodsBean> spreadCoinList = [];
List<KtGoodsBean> refillCoinList = [];
int selVip = 0;
int selCoin = 0;
String? orderCode;
num? shortPlayId;
num? videoId;
}

View File

@ -1,4 +1,11 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter_kinetra/kt_pages/kt_mine/kt_store/logic.dart';
import 'package:flutter_kinetra/kt_utils/kt_string_extend.dart';
import 'package:flutter_kinetra/kt_widgets/kt_store_widget.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
class KtStorePage extends StatefulWidget {
const KtStorePage({super.key});
@ -8,10 +15,82 @@ class KtStorePage extends StatefulWidget {
}
class _KtStorePageState extends State<KtStorePage> {
final logic = Get.put(KtStoreLogic());
final state = Get.find<KtStoreLogic>().state;
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Text('Store', style: TextStyle(color: Colors.black)),
return Container(
color: Color(0xFF1E1E20),
padding: EdgeInsets.fromLTRB(15.w, ScreenUtil().statusBarHeight, 15.w, 0),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
child: Image.asset('ic_back_white.png'.ktIcon, width: 24.w),
onTap: () => Navigator.of(context).maybePop(),
),
Text(
'Store',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
GestureDetector(
onTap: () {
EasyThrottle.throttle(
'restore-tap',
Duration(seconds: 3),
() => logic.restorePay(),
);
},
child: Text(
'ReStore',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
),
],
),
Expanded(
child: SmartRefresher(
controller: logic.refreshController,
physics: const ClampingScrollPhysics(),
enablePullUp: false,
enablePullDown: true,
onRefresh: logic.getStoreInfo,
child: SingleChildScrollView(
child: Column(
children: [
GetBuilder<KtStoreLogic>(
assignId: true,
builder: (logic) {
return KtStoreWidget(
store: state.storeBean,
onItemTap: (goods) {
EasyThrottle.throttle(
'buy',
Duration(seconds: 3),
() => logic.buyGoods(goods),
);
},
);
},
),
],
),
),
),
),
],
),
);
}
}

View File

@ -1,13 +1,25 @@
import 'package:flustars/flustars.dart';
import 'dart:io';
import 'package:flustars/flustars.dart' hide ScreenUtil;
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_facebook_auth/flutter_facebook_auth.dart';
import 'package:flutter_kinetra/dio_cilent/kt_apis.dart';
import 'package:flutter_kinetra/kt_pages/kt_mine/state.dart';
import 'package:flutter_kinetra/kt_utils/kt_string_extend.dart';
import 'package:flutter_kinetra/kt_utils/kt_utils.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import '../../dio_cilent/kt_request.dart';
import '../../kt_model/kt_login_bean.dart';
import '../../kt_model/kt_user_info.dart';
import '../../kt_utils/kt_keys.dart';
import '../../kt_utils/kt_toast_utils.dart';
import '../../kt_widgets/kt_dialog.dart';
import '../kt_routes.dart';
import '../kt_webview_page.dart';
class KtMineLogic extends GetxController {
final state = KtMineState();
@ -39,4 +51,237 @@ class KtMineLogic extends GetxController {
refreshController.refreshCompleted();
}
}
showLoginDialog() {
Get.bottomSheet(
isScrollControlled: true,
Container(
height: 510.w,
width: ScreenUtil().screenWidth,
padding: EdgeInsets.fromLTRB(0.w, 68.w, 0.w, 20.w),
decoration: BoxDecoration(
// borderRadius: BorderRadius.vertical(top: Radius.circular(12.w)),
image: DecorationImage(
image: AssetImage('login_sheet_bg.png'.ktIcon),
fit: BoxFit.fill,
),
),
child: Column(
children: [
Image.asset('login_bg.png'.ktIcon, width: 322.w),
SizedBox(height: 14.w),
Text(
'Welcome to LimeTale',
style: TextStyle(
fontSize: 20.sp,
color: Color(0xFF1C1C1C),
fontWeight: FontWeight.w700,
fontStyle: FontStyle.italic,
),
),
const Spacer(),
GestureDetector(
onTap: loginWithFacebook,
child: Container(
width: ScreenUtil().screenWidth - 30.w,
padding: EdgeInsets.fromLTRB(35.w, 13.w, 30.w, 13.w),
decoration: BoxDecoration(
border: Border.all(width: 1.w, color: Color(0xFF1E1E20)),
borderRadius: BorderRadius.circular(100),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Image.asset('ic_fb.png'.ktIcon, width: 20.w),
Text(
'Login with Facebook',
style: TextStyle(
fontSize: 14.sp,
color: Color(0xFF1C1C1C),
),
),
Image.asset('ic_right_arrow.png'.ktIcon, width: 14.w),
],
),
),
),
if (Platform.isIOS)
GestureDetector(
child: Container(
width: ScreenUtil().screenWidth - 30.w,
margin: EdgeInsets.only(top: 20.w),
padding: EdgeInsets.fromLTRB(35.w, 13.w, 30.w, 13.w),
decoration: BoxDecoration(
color: Color(0xFF1C1C1C),
borderRadius: BorderRadius.circular(100),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Image.asset('ic_apple.png'.ktIcon, width: 20.w),
Text(
'Login with Facebook',
style: TextStyle(fontSize: 14.sp, color: Colors.white),
),
Image.asset('ic_right_white.png'.ktIcon, width: 14.w),
],
),
),
),
const Spacer(),
Column(
children: [
Text(
'By logging in you agree to:',
style: TextStyle(fontSize: 12.sp, color: Color(0xFF777777)),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
onTap: () => Get.to(
() => KtWebViewPage(
url: KtApis.WEB_SITE_POLICY,
title: "User Agreement",
),
),
child: Text(
'User Agreement',
style: TextStyle(
fontSize: 12.sp,
decoration: TextDecoration.underline, // 线
color: Color(0xFF777777),
),
),
),
Text(
' & ',
style: TextStyle(
fontSize: 12.sp,
color: Color(0xFF777777),
),
),
GestureDetector(
onTap: () => Get.to(
() => KtWebViewPage(
url: KtApis.WEB_SITE_PRIVATE,
title: "Privacy Policy",
),
),
child: Text(
'Privacy Policy',
style: TextStyle(
fontSize: 12.sp,
decoration: TextDecoration.underline, // 线
color: Color(0xFF777777),
),
),
),
],
),
],
),
],
),
),
);
}
loginWithFacebook() async {
final LoginResult result = await FacebookAuth.instance.login(
permissions: ['email', 'public_profile'],
loginBehavior: LoginBehavior.nativeWithFallback,
);
if (result.status == LoginStatus.success) {
try {
EasyLoading.show(status: '', dismissOnTap: true);
final userData = await FacebookAuth.instance.getUserData();
final String facebookAvatar =
userData['picture']?['data']?['url'] ?? "";
final String facebookId = userData['id'] ?? "";
final String facebookEmail = userData['email'] ?? "";
final String facebookName = userData['name'] ?? "";
loginRequest(
facebookAvatar,
facebookEmail,
facebookName,
'facebook',
facebookId,
);
} catch (e) {
EasyLoading.dismiss();
}
} else {
KtToastUtils.showError('Login failed');
debugPrint('---facebook failed-:${result.status} ${result.message}');
}
}
loginRequest(String avatar, email, familyName, platform, thirdId) async {
Map<String, dynamic> params = {
"avator": avatar,
"email": email,
"family_name": familyName,
"platform": platform,
"third_id": thirdId,
};
ApiResponse res = await KtHttpClient().request(KtApis.login, data: params);
EasyLoading.dismiss();
if (res.success) {
debugPrint('----old token"${SpUtil.getString(KtKeys.token)}');
String? token = SpUtil.getString(KtKeys.token);
await KtHttpClient().request(
KtApis.leaveApp,
data: {'PostAuthorization': token ?? ''},
);
await KtHttpClient().request(
KtApis.onLine,
data: {'PostAuthorization': token ?? ''},
);
KtLoginBean data = KtLoginBean.fromJson(res.data);
SpUtil.putString(KtKeys.token, data.token ?? '');
KtHttpClient().setAuthToken(data.token ?? '');
debugPrint('----new token"${SpUtil.getString(KtKeys.token)}');
await KtHttpClient().request(
KtApis.enterTheApp,
data: {'is_open_notice': 0},
);
getUserInfo();
// Get.offAllNamed(KtRoutes.home);
} else {
KtToastUtils.showError('Login Failed');
}
}
checkSignOut() {
Get.dialog(
KtDialog(
topIcon: 'dialog_logout.png',
title: 'Log out?',
subTitle: 'Sign in again to access favorites',
leftBtnText: 'Log Out',
leftBtnFunc: signOut,
rightBtnText: 'Stay',
),
);
}
signOut() async {
String? token = SpUtil.getString(KtKeys.token);
await KtHttpClient().request(
KtApis.leaveApp,
data: {'PostAuthorization': token ?? ''},
);
ApiResponse res = await KtHttpClient().request(KtApis.signOut);
if (res.success) {
KtToastUtils.showSuccess(placeholder: 'Logout Success');
// await UserUtil().register(toHome: false);
getUserInfo();
}
}
}

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_kinetra/kt_pages/kt_mine/insert_web/kt_order_record_page.dart';
import 'package:flutter_kinetra/kt_pages/kt_mine/logic.dart';
import 'package:flutter_kinetra/kt_pages/kt_routes.dart';
import 'package:flutter_kinetra/kt_utils/kt_string_extend.dart';
@ -54,97 +55,201 @@ class _KtMinePageState extends State<KtMinePage> {
onRefresh: logic.getUserInfo,
child: Column(
children: [
Container(
width: ScreenUtil().screenWidth - 30.w,
margin: EdgeInsets.only(top: 22.w),
padding: EdgeInsets.fromLTRB(6.w, 13.w, 6.w, 6.w),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('ic_mine_top_bg.png'.ktIcon),
fit: BoxFit.fill,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Welcome Back, visitor!',
style: TextStyle(
fontSize: 12.sp,
color: Colors.white,
fontWeight: FontWeight.w500,
Stack(
clipBehavior: Clip.none,
children: [
Container(
width: ScreenUtil().screenWidth - 30.w,
margin: EdgeInsets.only(top: 10.w),
padding: EdgeInsets.fromLTRB(6.w, 13.w, 6.w, 6.w),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(
'ic_mine_top_bg.png'.ktIcon,
),
fit: BoxFit.fill,
),
),
Container(
padding: EdgeInsets.symmetric(
vertical: 17.w,
horizontal: 13.w,
),
margin: EdgeInsets.only(top: 13.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14.w),
),
child: Row(
children: [
Image.asset(
'ic_avatar.png'.ktIcon,
width: 56.w,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'Coins',
style: TextStyle(
fontSize: 12.sp,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
SizedBox(width: 5.w),
Image.asset(
'ic_coin.png'.ktIcon,
width: 15.w,
),
SizedBox(width: 2.w),
Text(
'${(state.userInfo.coinLeftTotal ?? 0) + (state.userInfo.sendCoinLeftTotal ?? 0)}',
style: TextStyle(
fontSize: 14.sp,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
Container(
width: 1.w,
height: 10.w,
margin: EdgeInsets.symmetric(
horizontal: 15.w,
),
color: Colors.white.withValues(
alpha: .3,
),
),
Text(
'Donate',
style: TextStyle(
fontSize: 12.sp,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
SizedBox(width: 5.w),
Image.asset(
'ic_coin.png'.ktIcon,
width: 15.w,
),
SizedBox(width: 2.w),
Text(
'${state.userInfo.sendCoinLeftTotal ?? 0}',
style: TextStyle(
fontSize: 14.sp,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
],
),
Container(
padding: EdgeInsets.symmetric(
vertical: 17.w,
horizontal: 13.w,
),
SizedBox(width: 13.w),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
margin: EdgeInsets.only(top: 8.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14.w),
),
child: Row(
children: [
Stack(
alignment: Alignment.bottomCenter,
Image.asset(
'ic_avatar.png'.ktIcon,
width: 56.w,
),
SizedBox(width: 13.w),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Image.asset(
'text_bg_discover.png'.ktIcon,
height: 17.w,
width: 55.w,
),
Padding(
padding: EdgeInsets.only(
bottom: 2.w,
),
child: Text(
KtUtils.isEmpty(
state.userInfo.familyName,
)
? 'Visitor'
: state
.userInfo
.familyName ??
'',
style: TextStyle(
fontFamily: 'PingFang SC',
fontSize: 18.sp,
color: Colors.black,
fontWeight: FontWeight.w500,
Stack(
alignment: Alignment.bottomCenter,
children: [
Image.asset(
'text_bg_discover.png'.ktIcon,
height: 17.w,
width: 55.w,
),
Padding(
padding: EdgeInsets.only(
bottom: 2.w,
),
child: Text(
KtUtils.isEmpty(
state
.userInfo
.familyName,
)
? 'Visitor'
: state
.userInfo
.familyName ??
'',
style: TextStyle(
fontFamily: 'PingFang SC',
fontSize: 18.sp,
color: Colors.black,
fontWeight:
FontWeight.w500,
),
),
),
],
),
Text(
'ID: ${state.userInfo.customerId}',
style: TextStyle(
fontFamily: 'PingFang SC',
fontSize: 12.sp,
color: Color(0xFF5E5E5E),
fontWeight: FontWeight.w400,
),
),
],
),
Text(
'ID: ${state.userInfo.customerId}',
style: TextStyle(
fontFamily: 'PingFang SC',
fontSize: 12.sp,
color: Color(0xFF5E5E5E),
fontWeight: FontWeight.w400,
const Spacer(),
GestureDetector(
onTap: () {
logic.isLogin
? logic.checkSignOut()
: logic.showLoginDialog();
},
child: Container(
width: 78.w,
alignment: Alignment.center,
padding: EdgeInsets.symmetric(
vertical: 11.w,
),
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(100),
border: Border.all(
width: 1.w,
color: Color(0xFFC0C0C0),
),
),
child: Text(
logic.isLogin
? 'Log out'
: 'Log in',
style: TextStyle(
fontSize: 12.sp,
color: Color(0xFFC0C0C0),
),
),
),
),
],
),
],
),
),
],
),
],
),
),
Positioned(
right: 0,
top: -28.w,
child: Image.asset(
'ic_mine_ip.png'.ktIcon,
width: 80.w,
height: 84.w,
),
),
],
),
SizedBox(height: 30.w),
SizedBox(height: 15.w),
vipView(),
SizedBox(height: 20.w),
settingsView(),
],
),
@ -158,6 +263,124 @@ class _KtMinePageState extends State<KtMinePage> {
);
}
Widget vipView() {
return GestureDetector(
onTap: () {
Get.toNamed(KtRoutes.store);
},
child: Container(
width: ScreenUtil().screenWidth - 30.w,
padding: EdgeInsets.only(left: 20.w, top: 15.w, bottom: 12.w),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('mine_vip_bg.png'.ktIcon),
fit: BoxFit.fill,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'VIP & Coins',
style: TextStyle(
color: Colors.black,
fontSize: 16.sp,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
),
),
Text(
'Exclusive Deals Inside',
style: TextStyle(
color: const Color(0xFF1E1E20).withValues(alpha: .5),
fontSize: 10.sp,
fontFamily: 'Inter',
fontWeight: FontWeight.w400,
),
),
],
),
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 3.w),
margin: EdgeInsets.only(left: 50.w),
decoration: BoxDecoration(
color: Color(0xFFA7F62F),
borderRadius: BorderRadius.circular(100),
border: Border.all(color: Color(0xFF487800)),
),
child: Row(
children: [
Text(
'GO',
style: TextStyle(
color: Colors.black,
fontSize: 14.sp,
fontFamily: 'Inter',
fontWeight: FontWeight.w900,
),
),
SizedBox(width: 3.w),
Image.asset('ic_polygon.png'.ktIcon, width: 6.w),
],
),
),
],
),
SizedBox(height: 8.w),
Container(
width: 208.w,
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 7.w),
decoration: BoxDecoration(
color: const Color(0x7FF2FFEA),
borderRadius: BorderRadius.circular(100),
border: Border.all(color: Colors.white, width: 1.w),
),
child: Row(
children: [
Image.asset('no_ad.png'.ktIcon, width: 12.w),
SizedBox(width: 2.w),
Text(
'Ad-Free',
style: TextStyle(fontSize: 10.sp, color: Color(0xFF5E5E5E)),
),
Container(
height: 8.w,
width: 1.w,
margin: EdgeInsets.symmetric(horizontal: 8.w),
color: Color(0xFFE6E6E6),
),
Image.asset('full_access.png'.ktIcon, width: 12.w),
SizedBox(width: 2.w),
Text(
'Full Access',
style: TextStyle(fontSize: 10.sp, color: Color(0xFF5E5E5E)),
),
Container(
height: 8.w,
width: 1.w,
margin: EdgeInsets.symmetric(horizontal: 8.w),
color: Color(0xFFE6E6E6),
),
Image.asset('ic_hd.png'.ktIcon, width: 12.w),
SizedBox(width: 2.w),
Text(
'HD',
style: TextStyle(fontSize: 10.sp, color: Color(0xFF5E5E5E)),
),
],
),
),
],
),
),
);
}
Widget settingsView() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -184,6 +407,11 @@ class _KtMinePageState extends State<KtMinePage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
handleItem(
'ic_order_record.png',
'Order Record',
() => Get.to(() => KtOrderRecordPage()),
),
handleItem(
'ic_privacy.png',
'Privacy Policy',

View File

@ -1,8 +1,8 @@
import 'package:flutter_kinetra/kt_pages/kt_home/kt_search_page.dart';
import 'package:flutter_kinetra/kt_pages/kt_mine/insert_web/kt_order_record_page.dart';
import 'package:get/get_navigation/src/routes/get_route.dart';
import 'kt_main_page/view.dart';
import 'kt_mine/insert_web/wallet_page.dart';
import 'kt_mine/kt_about_us_page.dart';
import 'kt_mine/kt_help_center/kt_help_center_detail_page.dart';
import 'kt_mine/kt_help_center/kt_help_center_list_page.dart';
@ -12,6 +12,7 @@ import 'kt_short_video/view.dart';
import 'kt_splash_page.dart';
import 'kt_webview_page.dart';
///
class KtRoutes {
static const String splash = '/';
static const String home = '/main';
@ -39,7 +40,7 @@ class KtRoutes {
preventDuplicates: false,
),
GetPage(name: store, page: () => KtStorePage()),
GetPage(name: wallet, page: () => const WalletPage()),
GetPage(name: wallet, page: () => const KtOrderRecordPage()),
GetPage(name: aboutUs, page: () => const KtAboutUsPage()),
GetPage(name: helpCenter, page: () => const KtHelpCenterPage()),
GetPage(name: helpCenterList, page: () => const KtHelpCenterListPage()),

View File

@ -1,7 +1,13 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_kinetra/kt_pages/kt_mine/logic.dart';
import 'package:flutter_kinetra/kt_pages/kt_short_video/state.dart';
import 'package:flutter_kinetra/kt_utils/kt_string_extend.dart';
import 'package:flutter_kinetra/kt_utils/kt_toast_utils.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:video_player/video_player.dart';
@ -10,15 +16,19 @@ import '../../dio_cilent/kt_request.dart';
import '../../kt_model/kt_video_detail_bean.dart';
import '../../kt_utils/kt_device_info_utils.dart';
import '../../kt_widgets/kt_status_widget.dart';
import '../../kt_widgets/kt_store_widget.dart';
import '../kt_mine/kt_store/logic.dart';
class VideoPlayLogic extends GetxController {
final state = VideoPlayState();
late final PageController pageController;
List<VideoPlayerController?> controllers = [];
KtLoadStatusType videoStatus = KtLoadStatusType.loading;
KtLoadStatusType loadStatusType = KtLoadStatusType.loadSuccess;
int currentIndex = 0;
bool _disposed = false;
final userLogic = Get.put(KtMineLogic());
@override
void onInit() {
@ -63,7 +73,6 @@ class VideoPlayLogic extends GetxController {
}
clearCacheCtrl();
try {
// LottieLoading.show(Get.context!);
ApiResponse res = await KtHttpClient().request(
KtApis.getVideoDetails,
method: HttpMethod.get,
@ -80,21 +89,17 @@ class VideoPlayLogic extends GetxController {
}
state.episodeList = state.video?.episodeList ?? [];
if (toPage) currentIndex = (state.video?.videoInfo?.episode ?? 1) - 1;
// for (var video in state.episodeList) {
// if (video.isLock == true) {
// state.curUnlock = state.episodeList.indexOf(video);
// break;
// }
// }
// reportHistory();
for (var video in state.episodeList) {
if (video.isLock == true) {
state.curUnlock = state.episodeList.indexOf(video);
break;
}
}
controllers = List<VideoPlayerController?>.filled(
state.episodeList.length,
null,
growable: true,
);
// await _initializeController(currentIndex);
// _preloadAdjacentVideos();
// if(currentIndex != 0){
if (toPage) {
onPageChanged(
currentIndex,
@ -104,17 +109,15 @@ class VideoPlayLogic extends GetxController {
} else {
_initializeController(currentIndex);
}
// }
update();
} else {
state.loadStatus = KtLoadStatusType.loadFailed;
update();
}
// if (userLogic.state.userInfo.isVip == true) state.curUnlock = 9999;
if (userLogic.state.userInfo.isVip == true) state.curUnlock = 9999;
update();
} catch (e) {
// LottieLoading.close();
state.loadStatus = KtLoadStatusType.loadFailed;
update();
}
@ -152,42 +155,49 @@ class VideoPlayLogic extends GetxController {
//
if (controllers[currentIndex]?.value.isPlaying ?? false) {
await controllers[currentIndex]?.pause();
// if (isUploadHistorySeconds) uploadHistorySeconds(controllers[currentIndex]?.value.position.inMilliseconds ?? 0);
if (isUploadHistorySeconds) {
uploadHistorySeconds(
controllers[currentIndex]?.value.position.inMilliseconds ?? 0,
);
}
}
if (controllers[currentIndex]?.value.isCompleted ?? false) {
// if (isUploadHistorySeconds) uploadHistorySeconds(0);
if (isUploadHistorySeconds) uploadHistorySeconds(0);
if (state.activityId != null) reportActivity();
}
if (isToggle) {
// loadStatusType = LoadStatusType.loading;
loadStatusType = KtLoadStatusType.loading;
update();
await _initializeController(index);
// loadStatusType = LoadStatusType.loadSuccess;
loadStatusType = KtLoadStatusType.loadSuccess;
update();
WidgetsBinding.instance.addPostFrameCallback((_) {
pageController.jumpToPage(index);
});
}
currentIndex = index;
// if (state.episodeList[index].isLock == true) {
// controllers[index]?.seekTo(Duration(seconds: 0));
// controllers[index]?.pause();
// update();
// // showUnlockDialog();
// //
// return;
// }
if (state.episodeList[index].isLock == true) {
controllers[index]?.seekTo(Duration(seconds: 0));
controllers[index]?.pause();
update();
// EasyThrottle.throttle(
// 'show-buy-dialog',
// Duration(milliseconds: 3000),
// () => showStoreDialog(state.episodeList[index].coins ?? 0),
// );
return;
}
if (controllers[index] != null) {
// if (state.curUnlock > index || userLogic.state.userInfo.isVip == true) {
controllers[index]?.play();
// }
if (state.curUnlock > index || userLogic.state.userInfo.isVip == true) {
controllers[index]?.play();
}
} else {
if (!isToggle) await _initializeController(index);
// if (state.curUnlock > index || userLogic.state.userInfo.isVip == true) {
controllers[index]?.play();
// }
if (state.curUnlock > index || userLogic.state.userInfo.isVip == true) {
controllers[index]?.play();
}
}
controllers[index]?.setPlaybackSpeed(state.currentSpeed);
// updateHomeVideo();
@ -231,8 +241,7 @@ class VideoPlayLogic extends GetxController {
try {
await controller.initialize();
// if (index == currentIndex && (episode.isLock == false || userLogic.state.userInfo.isVip == true)) {
if (index == currentIndex) {
if (index == currentIndex && (episode.isLock == false || userLogic.state.userInfo.isVip == true)) {
controller.play();
update();
}
@ -271,6 +280,15 @@ class VideoPlayLogic extends GetxController {
_releaseUnusedControllers();
}
void uploadHistorySeconds(int playSeconds) {
Map<String, dynamic> params = {
"short_play_id": state.shortPlayId,
"video_id": state.episodeList[currentIndex].id,
"play_seconds": playSeconds > 0 ? playSeconds : 0,
};
KtHttpClient().request(KtApis.uploadHistorySeconds, data: params);
}
Future<void> likeVideo() async {
if (state.video == null) return;
@ -286,18 +304,128 @@ class VideoPlayLogic extends GetxController {
state.video?.shortPlayInfo?.isCollect =
!(state.video?.shortPlayInfo?.isCollect ?? false);
// update(['video-like']);
// if (state.isFromDiscover) {
// try {
// final discoverLogic = Get.put(DiscoverLogic());
// discoverLogic.setCollectVideo(
// state.video!.shortPlayInfo.shortPlayId!,
// isCollect: state.video!.shortPlayInfo.isCollect!,
// );
// } catch (e) {
// debugPrint('---err:$e');
// }
// }
update();
}
//
buyVideo(num videoId, num coins, {bool toRecharge = false}) async {
ApiResponse res = await KtHttpClient().request(
KtApis.buyVideo,
data: {"short_play_id": state.shortPlayId, "video_id": videoId},
);
if (res.success) {
String status = res.data['status'];
if (status == 'success') {
int index = state.episodeList.indexWhere((item) => item.id == videoId);
state.episodeList[index].isLock = false;
if (index != state.episodeList.length - 1) {
state.curUnlock = index + 1;
}
update();
onPageChanged(index, isToggle: true);
final mineLogic = Get.put(KtMineLogic());
mineLogic.getUserInfo();
} else if (res.data['status'] == 'not_enough') {
KtToastUtils.showError('Coin not enough');
if (toRecharge) showStoreDialog(coins);
// if (toRecharge) state.detailBean?.payMode == 1 ? showBuyCoinDialog(coins) : showVipBuyDialog(coins);
} else if (status == 'jump') {
KtToastUtils.showError('Cannot jump episode');
}
} else {
KtToastUtils.showError('Purchase failed');
}
}
showStoreDialog(num coins) {
Get.put(KtStoreLogic());
// EasyThrottle.throttle('restore-short', Duration(seconds: 3), () => BuyUtils.restorePay(showTips: false));
Get.bottomSheet(
isScrollControlled: true,
Container(
width: ScreenUtil().screenWidth,
height: ScreenUtil().screenHeight - kToolbarHeight,
padding: EdgeInsets.fromLTRB(15.w, 72.w, 15.w, 10.w),
decoration: BoxDecoration(
image: DecorationImage(image: AssetImage('recharge_bg.png'.ktIcon)),
),
child: SingleChildScrollView(
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Balance',
style: TextStyle(fontSize: 12.sp, color: Colors.black),
),
Image.asset('ic_coin.png'.ktIcon, width: 20.w),
GetBuilder<KtMineLogic>(
builder: (logic) {
return Text(
'${(logic.state.userInfo.coinLeftTotal ?? 0) + (logic.state.userInfo.sendCoinLeftTotal ?? 0)}',
style: TextStyle(fontSize: 13.sp, color: Colors.black),
);
},
),
SizedBox(width: 2.w),
Image.asset('ic_coin.png'.ktIcon, width: 16.w),
Container(
width: 1.w,
height: 10.w,
margin: EdgeInsets.symmetric(horizontal: 18.w),
color: Color(0xFF5E5E5E),
),
Text(
'Unlock:',
style: TextStyle(fontSize: 12.sp, color: Colors.black),
),
Image.asset('ic_coin.png'.ktIcon, width: 20.w),
SizedBox(width: 2.w),
Text(
'$coins',
style: TextStyle(
fontSize: 13.sp,
color: Colors.black,
fontWeight: FontWeight.w500,
),
),
],
),
GetBuilder<KtStoreLogic>(
builder: (logic) {
return KtStoreWidget(
store: logic.state.storeBean,
isStoreDialog: true,
onItemTap: (goods) async {
logic.func = () {
Get.back();
fetchData(toPage: false);
buyVideo(
state.episodeList[currentIndex].id!,
coins,
toRecharge: false,
);
// checkVipCoinSub();
};
EasyThrottle.throttle(
'buy',
Duration(seconds: 3),
() async => await logic.buyGoods(
goods,
shortPlayId: state.shortPlayId,
videoId: state.episodeList[currentIndex].id,
),
);
},
);
},
),
],
),
),
),
);
}
}

View File

@ -5,6 +5,7 @@ class VideoPlayState {
String imageUrl = '';
int shortPlayId = -1;
num videoId = -1;
int curUnlock = 999;
int? activityId;
bool isFromDiscover = false;
VideoDetailBean? video;

View File

@ -1,6 +1,7 @@
import 'dart:io';
import 'dart:ui';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter_kinetra/kt_utils/kt_string_extend.dart';
import 'package:flutter_kinetra/kt_utils/kt_utils.dart';
@ -10,6 +11,7 @@ import 'package:video_player/video_player.dart';
import 'package:visibility_detector/visibility_detector.dart';
import '../../kt_model/kt_video_detail_bean.dart';
import '../../kt_utils/kt_toast_utils.dart';
import '../../kt_widgets/kt_network_image.dart';
import '../../kt_widgets/kt_status_widget.dart';
import '../../kt_widgets/kt_video_progress_bar.dart';
@ -130,15 +132,14 @@ class _VideoPlayPageState extends State<VideoPlayPage>
if (logic.controllers.isEmpty) return Container();
final controller = logic.controllers[index];
// final episode = state.episodeList[index];
final episode = state.episodeList[index];
return Stack(
children: [
//
if (controller != null && controller.value.isInitialized && !isAllOver)
GestureDetector(
onTap: () {
// if (episode.isLock == true) return;
if (episode.isLock == true) return;
controller.value.isPlaying
? controller.pause()
: controller.play();
@ -155,7 +156,7 @@ class _VideoPlayPageState extends State<VideoPlayPage>
onVisibilityChanged: (VisibilityInfo info) {
var visiblePercentage = info.visibleFraction * 100;
if (visiblePercentage > 20) {
// if (episode.isLock == true) return;
if (episode.isLock == true) return;
controller.play();
} else {
controller.pause();
@ -202,61 +203,101 @@ class _VideoPlayPageState extends State<VideoPlayPage>
if (!isAllOver) Center(child: CircularProgressIndicator()),
],
),
if (controller != null && controller.value.isInitialized && !isAllOver)
GestureDetector(
onTap: () {
// if (episode.isLock == true) return;
controller.value.isPlaying ? controller.pause() : controller.play();
setState(() {});
},
child: Container(
width: ScreenUtil().screenWidth,
height: ScreenUtil().screenHeight,
color: Colors.transparent,
),
GestureDetector(
onTap: () {
if (episode.isLock == true) return;
(controller?.value.isPlaying ?? true)
? controller?.pause()
: controller?.play();
setState(() {});
},
child: Container(
width: ScreenUtil().screenWidth,
height: ScreenUtil().screenHeight,
color: Colors.transparent,
),
),
if (episode.isLock == true)
Stack(
children: [
KtNetworkImage(
width: ScreenUtil().screenWidth,
height: ScreenUtil().screenHeight,
imageUrl: state.imageUrl,
placeholder: Image.asset(
'bg2.png'.ktIcon,
width: ScreenUtil().screenWidth,
height: ScreenUtil().screenHeight,
fit: BoxFit.cover,
),
),
Container(
width: ScreenUtil().screenWidth,
height: ScreenUtil().screenHeight,
color: Colors.black.withValues(alpha: .75),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('lock_white.png'.ktIcon, width: 50.w),
SizedBox(height: 26.w),
GestureDetector(
onTap: () {
EasyThrottle.throttle(
'unlock',
Duration(seconds: 2),
() => logic.buyVideo(
episode.id!,
episode.coins ?? 0,
toRecharge: true,
),
);
},
child: Container(
width: 280.w,
padding: EdgeInsets.only(top: 17.w, bottom: 28.w),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('unlock_bg.png'.ktIcon),
fit: BoxFit.fill,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('lock_black.png'.ktIcon, width: 20.w),
SizedBox(width: 3.w),
Text(
state.curUnlock == index
? 'Unlocking costs ${episode.coins ?? 0}'
: 'Prev.locked',
style: TextStyle(
fontSize: 14.sp,
color: Color(0xFF1E1E20),
),
),
Image.asset('ic_coin.png'.ktIcon, width: 18.7.w),
],
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Balance:${logic.userLogic.state.userInfo.coinLeftTotal ?? 0} ",
style: TextStyle(
fontSize: 12.sp,
color: Color(0xFFF5F5F5),
),
),
Image.asset('ic_coin.png'.ktIcon, width: 11.2.w),
],
),
],
),
),
],
),
// if (episode.isLock == true)
// Container(
// width: ScreenUtil().screenWidth,
// height: ScreenUtil().screenHeight,
// color: Colors.black.withValues(alpha: .75),
// child: Center(
// child: GestureDetector(
// onTap: () {
// EasyThrottle.throttle(
// 'unlock',
// Duration(seconds: 2),
// () => logic.buyVideo(
// episode.id!,
// episode.coins ?? 0,
// toRecharge: true,
// ),
// );
// },
// child: Container(
// width: 260.w,
// padding: EdgeInsets.symmetric(vertical: 17.w),
// decoration: MyStyles.mainBorder(width: 1, radius: 50),
// child: Row(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Image.asset('ic_unlock_lock.png'.ktIcon, width: 20.w),
// SizedBox(width: 3.w),
// Text(
// state.curUnlock == index
// ? 'Unlocking costs ${episode.coins ?? 0} coins'
// : 'Prev.locked',
// style: TextStyle(
// fontSize: 14.sp,
// color: DrColors.mainWhite,
// ),
// ),
// ],
// ),
// ),
// ),
// ),
// ),
//
Positioned(
top: ScreenUtil().statusBarHeight + 10.w,
@ -276,102 +317,105 @@ class _VideoPlayPageState extends State<VideoPlayPage>
Positioned(
bottom: 0,
left: 0.w,
child: Platform.isAndroid
? SafeArea(child: bottomView(index))
: bottomView(index),
),
],
);
}
Widget bottomView(int index) {
return Container(
width: ScreenUtil().screenWidth,
padding: EdgeInsets.fromLTRB(15.w, 40.w, 15.w, 50.w),
alignment: Alignment.bottomCenter,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF001D1F).withValues(alpha: 0), Color(0xFF001D1F)],
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: ScreenUtil().screenWidth - 100.w,
child: Text(
state.video?.shortPlayInfo?.name ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 16.sp,
color: Colors.white,
fontWeight: FontWeight.w500,
child: Container(
width: ScreenUtil().screenWidth,
padding: EdgeInsets.fromLTRB(15.w, 40.w, 15.w, 40.w),
alignment: Alignment.bottomCenter,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFF001D1F).withValues(alpha: 0),
Color(0xFF001D1F),
],
),
),
),
if (logic.controllers[index] != null)
CustomVideoProgressBar(
controller: logic.controllers[index]!,
width: ScreenUtil().screenWidth,
),
SizedBox(height: 16.w),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
onTap: showEpSelDialog,
child: Container(
width: 300.w,
padding: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 9.w,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50.w),
color: Colors.white.withValues(alpha: .1),
),
child: Row(
children: [
Image.asset('ic_ep.png'.ktIcon, width: 16.sp),
SizedBox(width: 6.w),
Text(
'EP.${index + 1}',
style: TextStyle(fontSize: 13.sp, color: Colors.white),
),
const Spacer(),
Text(
'All ${state.video?.shortPlayInfo?.episodeTotal ?? 0} Episodes',
style: TextStyle(fontSize: 13.sp, color: Colors.white),
),
],
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: ScreenUtil().screenWidth - 100.w,
child: Text(
state.video?.shortPlayInfo?.name ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 16.sp,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
),
GetBuilder<VideoPlayLogic>(
id: 'video-like',
builder: (c) {
return GestureDetector(
onTap: () => logic.likeVideo(),
child: Column(
children: [
Image.asset(
state.video?.shortPlayInfo?.isCollect == true
? 'ic_collect_sel.png'.ktIcon
: 'ic_collect_unsel.png'.ktIcon,
width: 32.w,
if (logic.controllers[index] != null)
CustomVideoProgressBar(
controller: logic.controllers[index]!,
width: ScreenUtil().screenWidth,
),
SizedBox(height: 16.w),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
onTap: showEpSelDialog,
child: Container(
width: 300.w,
padding: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 9.w,
),
],
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50.w),
color: Colors.white.withValues(alpha: .1),
),
child: Row(
children: [
Image.asset('ic_ep.png'.ktIcon, width: 16.sp),
SizedBox(width: 6.w),
Text(
'EP.${index + 1}',
style: TextStyle(
fontSize: 13.sp,
color: Colors.white,
),
),
const Spacer(),
Text(
'All ${state.video?.shortPlayInfo?.episodeTotal ?? 0} Episodes',
style: TextStyle(
fontSize: 13.sp,
color: Colors.white,
),
),
],
),
),
),
);
},
),
],
GetBuilder<VideoPlayLogic>(
id: 'video-like',
builder: (c) {
return GestureDetector(
onTap: () => logic.likeVideo(),
child: Column(
children: [
Image.asset(
state.video?.shortPlayInfo?.isCollect == true
? 'ic_collect_sel.png'.ktIcon
: 'ic_collect_unsel.png'.ktIcon,
width: 32.w,
),
],
),
);
},
),
],
),
],
),
),
],
),
),
],
);
}
@ -634,11 +678,13 @@ class _VideoPlayPageState extends State<VideoPlayPage>
return GestureDetector(
onTap: () {
Get.back();
// bool? beforeEpUnlock = index - 2 <= 0 ? false : state.episodeList[index - 2].isLock;
// if (beforeEpUnlock ?? false) {
// ToastUtils.show('Unable to unlock episodes');
// return;
// }
bool? beforeEpUnlock = index - 1 <= 0
? false
: state.episodeList[index - 1].isLock;
if (beforeEpUnlock ?? false) {
KtToastUtils.showError('Unable to unlock episodes');
return;
}
logic.onPageChanged(index, isToggle: true);
},
child: Stack(
@ -665,27 +711,15 @@ class _VideoPlayPageState extends State<VideoPlayPage>
),
),
),
// if (state.episodeList[episode - 1].isLock == true)
// Positioned(
// right: 0,
// top: 0,
// child: Container(
// width: 20.w,
// height: 12.w,
// decoration: BoxDecoration(
// color: ColorResource.mainYellow,
// borderRadius: BorderRadius.only(
// topRight: Radius.circular(6.w),
// bottomLeft: Radius.circular(6.w),
// ),
// ),
// child: Image.asset(
// 'ic_video_lock.png'.icon,
// width: 10.w,
// height: 10.w,
// ),
// ),
// ),
if (state.episodeList[index].isLock == true)
Positioned(
right: 5.w,
top: 5.w,
child: Image.asset(
'ic_lock.png'.ktIcon,
width: 12.w,
),
),
],
),
);

View File

@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:package_info_plus/package_info_plus.dart';
//
class KtDeviceInfoUtil {
static final KtDeviceInfoUtil _instance = KtDeviceInfoUtil._internal();

View File

@ -0,0 +1,127 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:in_app_purchase_android/in_app_purchase_android.dart';
import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart';
import 'package:in_app_purchase_storekit/store_kit_wrappers.dart';
//
class KtInAppPurchaseUtil {
static final InAppPurchase _iap = InAppPurchase.instance;
///
static Future<ProductDetailsResponse> queryProducts(Set<String> productIds) async {
return await _iap.queryProductDetails(productIds);
}
/// /
static Future<void> buy(ProductDetails product, {bool consumable = false}) async {
final purchaseParam = PurchaseParam(productDetails: product);
if (consumable) {
await _iap.buyConsumable(purchaseParam: purchaseParam, autoConsume: Platform.isIOS);
} else {
await _iap.buyNonConsumable(purchaseParam: purchaseParam);
}
}
///
static Stream<List<PurchaseDetails>> get purchaseStream => _iap.purchaseStream;
///
static Future<void> completePurchase(PurchaseDetails purchase, {bool isRetry = false}) async {
if (purchase.pendingCompletePurchase) {
if (isRetry) {
for (int i = 0; i < 3; i++) {
try {
await _iap.completePurchase(purchase);
return;
} catch (e) {
debugPrint('---err:${e}');
}
}
} else {
await _iap.completePurchase(purchase);
}
}
// if (Platform.isAndroid) {
// final InAppPurchaseAndroidPlatformAddition addition =
// InAppPurchasePlatformAddition.instance! as InAppPurchaseAndroidPlatformAddition;
// if (purchase.productID.contains('coins')) {
// await addition.consumePurchase(purchase);
// }
// QueryPurchaseDetailsResponse response = await addition.queryPastPurchases();
// for (GooglePlayPurchaseDetails purchaseDetails in response.pastPurchases) {
// _iap.completePurchase(purchaseDetails);
// }
// } else {
// if (purchase.pendingCompletePurchase) {
// await _iap.completePurchase(purchase);
// }
// }
}
static Future<void> consumeIfNeeded(PurchaseDetails purchaseDetails) async {
if (purchaseDetails is GooglePlayPurchaseDetails) {
final InAppPurchaseAndroidPlatformAddition addition =
InAppPurchasePlatformAddition.instance! as InAppPurchaseAndroidPlatformAddition;
await addition.consumePurchase(purchaseDetails);
}
}
///
static Future<bool> isAvailable() async {
return await _iap.isAvailable();
}
///
static Future<void> restorePurchases() async {
await _iap.restorePurchases();
}
///
static bool isPurchaseSuccess(PurchaseDetails purchase) {
return purchase.status == PurchaseStatus.purchased || purchase.status == PurchaseStatus.restored;
}
///
static void cancelStreamSubscription(StreamSubscription? sub) {
sub?.cancel();
}
///
// static void completeFailed() {
// _iap.clearPendingPurchases();
// }
// @visibleForTesting
static Future<void> clearFailedPurchases() async {
if (Platform.isIOS || Platform.isMacOS) {
final wrapper = SKPaymentQueueWrapper();
final transactions = await wrapper.transactions();
for (final transaction in transactions) {
// if (transaction.transactionState == SKPaymentTransactionStateWrapper.failed) {
await wrapper.finishTransaction(transaction);
// }
}
}
}
/// purchaseStream
static Future<List<PurchaseDetails>> getPendingPurchases() async {
final available = await isAvailable();
if (!available) return [];
final List<PurchaseDetails> pending = [];
//
final completer = Completer<List<PurchaseDetails>>();
final sub = purchaseStream.listen((purchases) {
pending.addAll(purchases.where((p) => p.status == PurchaseStatus.pending));
completer.complete(pending);
});
final result = await completer.future;
await sub.cancel();
return result;
}
}

View File

@ -0,0 +1,49 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import '../kt_model/kt_goods_bean.dart';
///restore工具类
class KtPurchaseRestoreUtil {
static final KtPurchaseRestoreUtil _instance = KtPurchaseRestoreUtil._internal();
factory KtPurchaseRestoreUtil() => _instance;
KtPurchaseRestoreUtil._internal();
static const String _cacheKey = 'restore_goods_list';
/// goods到本地缓存
Future<void> cacheFailedGoods(KtGoodsBean goods) async {
final prefs = await SharedPreferences.getInstance();
List<String> goodsList = prefs.getStringList(_cacheKey) ?? [];
goodsList.add(json.encode(goods.toJson()));
print('-----goods.toJson:${goods.toJson()}');
await prefs.setStringList(_cacheKey, goodsList);
}
/// goods列表
Future<List<KtGoodsBean>> getCachedGoodsList() async {
final prefs = await SharedPreferences.getInstance();
List<String> goodsList = prefs.getStringList(_cacheKey) ?? [];
return goodsList.map((e) => KtGoodsBean.fromJson(json.decode(e))).toList();
}
///
Future<void> clearCachedGoods() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_cacheKey);
}
/// goods
Future<void> removeGoods(KtGoodsBean goods) async {
final prefs = await SharedPreferences.getInstance();
List<String> goodsList = prefs.getStringList(_cacheKey) ?? [];
goodsList.removeWhere((e) {
final g = KtGoodsBean.fromJson(json.decode(e));
return g.id == goods.id;
});
await prefs.setStringList(_cacheKey, goodsList);
}
}

View File

@ -2,14 +2,14 @@ import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
//toast提示工具类
final class KtToastUtils {
KtToastUtils._();
static CancelFunc showError(String msg) {
return showToast(
msg,
icon: Icon(Icons.error, size: 13, color: Colors.white),
icon: Icon(Icons.error, size: 13, color: Colors.black),
);
}

View File

@ -1,4 +1,13 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:in_app_purchase_android/billing_client_wrappers.dart';
import 'package:in_app_purchase_android/in_app_purchase_android.dart';
import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart';
//
class KtUtils {
///
static String getTimeZoneOffset(DateTime dateTime) {
Duration offset = dateTime.timeZoneOffset;
String sign = offset.isNegative ? '-' : '+';
@ -18,4 +27,60 @@ class KtUtils {
static bool isNotEmpty(dynamic value) {
return !isEmpty(value);
}
///iOS内购优惠价格
static String getDiscountPrice(ProductDetails? product, {bool showDiscount = false}) {
String price = product?.price ?? '';
if (product == null) return price;
if (Platform.isIOS) {
if (product is AppStoreProductDetails) {
debugPrint("skProduct优惠价格: ${product.skProduct.price}");
for (final discount in product.skProduct.discounts) {
debugPrint("优惠ID: ${discount.identifier}");
debugPrint("优惠价格: ${discount.price}");
debugPrint("priceLocale: ${discount.priceLocale.currencySymbol}-${discount.priceLocale.currencyCode}");
debugPrint("优惠周期: ${discount.subscriptionPeriod.numberOfUnits}");
debugPrint("支付模式: ${discount.paymentMode}"); // freeTrial, payAsYouGo, payUpFront
if (isNotEmpty(discount.price)) price = discount.priceLocale.currencySymbol + discount.price;
break;
}
}
} else if (Platform.isAndroid) {
if (product is GooglePlayProductDetails) {
for (SubscriptionOfferDetailsWrapper? offer in product.productDetails.subscriptionOfferDetails ?? []) {
debugPrint('Base plan ID: ${offer?.basePlanId}');
debugPrint('Offer ID: ${offer?.offerId}');
if (offer?.pricingPhases.isEmpty ?? true) continue;
for (PricingPhaseWrapper phase in offer!.pricingPhases) {
debugPrint('价格: ${phase.formattedPrice}');
debugPrint('周期: ${phase.billingPeriod}');
debugPrint('周期内收费次数: ${phase.recurrenceMode}');
if (isNotEmpty(phase.formattedPrice)) {
price = phase.formattedPrice;
if (showDiscount) return price;
}
}
}
}
}
return price;
}
static Map<String, dynamic> filterVipType(String? type) {
if (isEmpty(type)) {
return {};
}
List<Map<String, dynamic>> vipTypes = [
{'type': 'week', 'name': 'Weekly', 'shortName': 'week'},
{'type': 'month', 'name': 'Monthly', 'shortName': 'month'},
{'type': 'three_months', 'name': 'Quarterly', 'shortName': 'quarter'},
{'type': 'yearly', 'name': 'Yearly', 'shortName': 'year'},
{'type': 'year', 'name': 'Yearly', 'shortName': 'year'},
];
final filterType = vipTypes.where((item) => item['type'] == type).cast<Map<String, dynamic>?>().firstOrNull;
if (filterType != null) {
return filterType;
}
return {};
}
}

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_kinetra/kt_utils/kt_string_extend.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
//app弹窗封装
class KtDialog extends StatelessWidget {
final String title;
final String subTitle;
@ -66,7 +67,7 @@ class KtDialog extends StatelessWidget {
style: TextStyle(
fontSize: 15.sp,
color: Color(0xFF1C1C1C),
fontWeight: FontWeight.w500,
fontWeight: FontWeight.w400,
),
),
),

View File

@ -2,10 +2,13 @@ import 'package:flutter/material.dart';
import 'package:flutter_kinetra/kt_utils/kt_string_extend.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
///
enum KtErrorStatusType { noNetwork, loadFailed, nothingYet, notFound }
///loading状态枚举
enum KtLoadStatusType { loading, loadSuccess, loadFailed, loadNoData }
///
class KtStatusWidget extends StatelessWidget {
final KtErrorStatusType type;
final String? message;

View File

@ -0,0 +1,550 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_kinetra/kt_model/kt_goods_bean.dart';
import 'package:flutter_kinetra/kt_utils/kt_string_extend.dart';
import 'package:flutter_kinetra/kt_utils/kt_utils.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../kt_model/kt_store_bean.dart';
///
class KtStoreWidget extends StatefulWidget {
final KtStoreBean store;
final bool onlyCoins;
final bool isStoreDialog;
final void Function(KtGoodsBean goods)? onItemTap; //
const KtStoreWidget({
super.key,
required this.store,
this.onItemTap,
this.isStoreDialog = false,
this.onlyCoins = false,
});
@override
State<KtStoreWidget> createState() => _KtStoreWidgetState();
}
class _KtStoreWidgetState extends State<KtStoreWidget> {
int selVip = -1;
int selCoin = -1;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: initWidget(),
);
}
Widget descView() {
return Text(
'''
1. Coins are virtual items and cannot be refunded. Use it for this product. \n
2. Gold coins will never expire, the reward coins will expire 24 hours a day.\n
3. Coins will be used first when unlocking episodes. If the amount is insufficient, reward coins will automatically be used. \n
4. The purchase has not been credited, click <Restore> to refresh. \n
5. For other questions, contact us via Profile>Help &feedback.\n
''',
style: TextStyle(fontSize: 12.sp, color: Color(0xFF848484), height: 0.95),
);
}
List<Widget> initWidget() {
// if (widget.onlyCoins) return [coinList()];
List<Widget> widgets = [SizedBox(height: 10.w)];
//
// int coinsIndex = widget.store.sort?.indexOf('list_coins') ?? 0;
// int vipIndex = widget.store.sort?.indexOf('list_sub_vip') ?? 0;
// if (coinsIndex < vipIndex) {
widgets.addAll([coinList(), SizedBox(height: 15.w), vipList()]);
// } else {
// widgets.addAll([vipList(), SizedBox(height: 15.w), coinList()]);
// }
widgets.addAll([
if (widget.store.payMode == 0 && widget.store.showType == 0)
SizedBox(height: 15.w),
descView(),
]);
return widgets;
}
List<KtGoodsBean> get smallList =>
widget.store.listCoins!.where((item) => item.size == 'small').toList();
List<KtGoodsBean> get bigList =>
widget.store.listCoins!.where((item) => item.size == 'big').toList();
Widget coinList() {
if (widget.store.listCoins?.isEmpty ?? true) return Container();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!widget.onlyCoins)
Row(
children: [
Text(
'Go Coins',
style: TextStyle(
fontSize: 14.sp,
color: widget.isStoreDialog
? Color(0xFF1E1E20)
: Colors.white,
fontWeight: FontWeight.w600,
),
),
Text(
' | Limited-time coin packs',
style: TextStyle(
fontSize: 14.sp,
color: widget.isStoreDialog
? Color(0xFF1E1E20)
: Colors.white,
fontWeight: FontWeight.w200,
),
),
],
),
SizedBox(height: 10.w),
// big
// if (!onlyNewSubCoins)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
...bigList.map((item) {
return GestureDetector(
onTap: () {
selCoin = item.id ?? -1;
setState(() {});
widget.onItemTap?.call(item); //
},
child: Stack(
children: [
Container(
width: 167.w,
padding: EdgeInsets.fromLTRB(4.w, 17.w, 5.w, 3.w),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('coin_big_bg.png'.ktIcon),
fit: BoxFit.fill,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('ic_coin.png'.ktIcon, width: 22.4.w),
Text(
'${item.coins ?? 0}',
style: TextStyle(
fontSize: 20.sp,
color: Color(0xFF1E1E20),
fontWeight: FontWeight.w500,
),
),
SizedBox(width: 4.w),
if ((item.sendCoins ?? 0) > 0)
Text(
'+${item.sendCoins}',
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
color: Color(0xFFAD8502),
),
),
],
),
SizedBox(height: 6.w),
Container(
width: 160.w,
alignment: Alignment.center,
padding: EdgeInsets.symmetric(vertical: 6.w),
decoration: BoxDecoration(
color: Color(0xFF1E1E20),
borderRadius: BorderRadius.circular(8.w),
),
child: Text(
'${item.productDetails?.price ?? 0}',
style: TextStyle(
fontSize: 13.sp,
color: Color(0xFFFFD321),
fontWeight: FontWeight.w700,
fontStyle: FontStyle.italic,
),
),
),
],
),
),
if (!item.cornerMarker.isNullString)
Positioned(
top: 4.w,
left: 4.w,
child: Image.asset('ic_fire.png'.ktIcon, width: 16.w),
),
if ((item.sendCoins ?? 0) > 0)
Positioned(
right: 0,
top: 0,
child: Container(
width: 42.w,
height: 16.w,
padding: EdgeInsets.symmetric(horizontal: 3.w),
alignment: Alignment.center,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment(0.50, 0.00),
end: Alignment(0.50, 1.00),
colors: [
const Color(0xFFFD9A68),
const Color(0xFFFF538D),
],
),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8.w),
topRight: Radius.circular(8.w),
bottomLeft: Radius.circular(8.w),
),
),
child: Text(
'+${((item.sendCoins ?? 0) / (item.coins ?? 1) * 100).toInt()}%',
style: TextStyle(
fontSize: 10.sp,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
);
}),
],
),
SizedBox(height: 10.w),
// if (!widget.onlyCoins) subCoinList(),
// small
// if (!onlyNewSubCoins)
GridView.builder(
shrinkWrap: true,
padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 10.w,
crossAxisSpacing: 10.w,
childAspectRatio: 108 / 90,
),
itemBuilder: (_, index) {
var item = smallList[index];
return GestureDetector(
onTap: () {
selCoin = item.id ?? -1;
setState(() {});
widget.onItemTap?.call(item); //
},
child: Stack(
children: [
Container(
width: 108.w,
padding: EdgeInsets.fromLTRB(5.w, 14.w, 5.w, 4.w),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('coin_small_bg.png'.ktIcon),
fit: BoxFit.fill,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'ic_coin.png'.ktIcon,
width: 22.4.w,
),
Text(
'${item.coins ?? 0}',
style: TextStyle(
fontSize: 20.sp,
color: Color(0xFF1E1E20),
fontWeight: FontWeight.w500,
height: 0.8,
),
),
],
),
if ((item.sendCoins ?? 0) > 0)
Text(
'+${item.sendCoins}',
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
color: Color(0xFF499C00),
),
),
],
),
const Spacer(),
Container(
alignment: Alignment.center,
padding: EdgeInsets.symmetric(vertical: 4.w),
decoration: BoxDecoration(
color: Color(0xFF1E1E20),
borderRadius: BorderRadius.circular(8.w),
),
child: Text(
'${item.productDetails?.price ?? 0}',
style: TextStyle(
fontSize: 13.sp,
color: Color(0xFF80FF00),
fontWeight: FontWeight.w700,
fontStyle: FontStyle.italic,
),
),
),
],
),
),
if (!item.cornerMarker.isNullString)
Positioned(
top: 4.w,
left: 4.w,
child: Image.asset('ic_fire.png'.ktIcon, width: 16.w),
),
if ((item.sendCoins ?? 0) > 0)
Positioned(
right: 0,
top: 0,
child: Container(
width: 42.w,
height: 16.w,
padding: EdgeInsets.symmetric(horizontal: 3.w),
alignment: Alignment.center,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment(0.50, 0.00),
end: Alignment(0.50, 1.00),
colors: [
const Color(0xFFFD9A68),
const Color(0xFFFF538D),
],
),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8.w),
topRight: Radius.circular(8.w),
bottomLeft: Radius.circular(8.w),
),
),
child: Text(
'+${((item.sendCoins ?? 0) / (item.coins ?? 1) * 100).toInt()}%',
style: TextStyle(
fontSize: 10.sp,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
);
},
itemCount: smallList.length,
),
],
);
}
Widget vipList() {
if (widget.store.listSubVip?.isEmpty ?? true) return Container();
if (widget.store.payMode == 1) return Container();
final colors = {
"month": Color(0xFF00679A),
"week": Color(0xFF3D6D00),
"quarter": Color(0xFF5C5FB5),
"year": Color(0xFFAE6F00),
};
final textColors = {
"month": Color(0xFFBDEBFF),
"week": Color(0xFFE7FCCA),
"quarter": Color(0xFFDBDDFF),
"year": Color(0xFFFFF2DA),
};
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Go VIP Premium | Auto renew, cancel anytime ',
style: TextStyle(
fontSize: 14.sp,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 10.w),
ListView.separated(
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (_, index) {
KtGoodsBean item = widget.store.listSubVip![index];
return GestureDetector(
onTap: () {
selVip = item.id ?? -1;
setState(() {});
widget.onItemTap?.call(item); //
},
child: Container(
width: ScreenUtil().screenWidth - 30.w,
padding: EdgeInsets.fromLTRB(12.w, 19.w, 12.w, 5.w),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('vip_${item.vipTypeKey}_bg.png'.ktIcon),
fit: BoxFit.fill,
),
border: selVip == item.id
? Border.all(color: Colors.red, width: 1.5.w)
: null,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${item.shortType ?? ''} VIP',
style: TextStyle(
fontSize: 14.sp,
color: Color(0xFF2A0428),
fontWeight: FontWeight.w500,
),
),
item.discountType != 0
? RichText(
text: TextSpan(
children: [
TextSpan(
text: Platform.isAndroid
? item.productDetails?.price
: KtUtils.getDiscountPrice(
item.productDetails,
),
style: TextStyle(
fontSize: 26.sp,
color: Color(0xFF2A0428),
fontStyle: FontStyle.italic,
),
),
TextSpan(
text: Platform.isIOS
? item.productDetails?.price
: KtUtils.getDiscountPrice(
item.productDetails,
),
style: TextStyle(
fontSize: 16.sp,
color: Color(0xFFC2C2C2),
decoration:
TextDecoration.lineThrough, // 线
),
),
],
),
)
: RichText(
text: TextSpan(
children: [
// TextSpan(
// text: item.productDetails?.currencySymbol ?? '',
// style: TextStyle(fontSize: 16.sp, color: ColorResource.mainBlack),
// ),
TextSpan(
text: Platform.isIOS
? item.productDetails?.price
: KtUtils.getDiscountPrice(
item.productDetails,
),
style: TextStyle(
fontSize: 26.sp,
color: Color(0xFF2A0428),
fontStyle: FontStyle.italic,
),
),
TextSpan(
text:
'/${KtUtils.filterVipType(item.vipType ?? '')['shortName']}',
style: TextStyle(
fontSize: 16.sp,
color: Colors.black,
),
),
],
),
),
SizedBox(height: 4.w),
SizedBox(height: 2.w),
Row(
children: [
Text(
'Unlimited access to all series',
// item.description ?? '',
style: TextStyle(
fontSize: 10.sp,
color: Color(0xFF99889B),
),
),
const Spacer(),
if ((item.sendCoins ?? 0) > 0)
Stack(
alignment: Alignment.topCenter,
children: [
Container(
padding: EdgeInsets.only(top: 8.w),
child: Image.asset(
'${KtUtils.filterVipType(item.vipType ?? '')['shortName']}_text_bg.png'.ktIcon,
height: 10.w,
),
),
Row(
children: [
Text(
'+Extra ${item.sendCoins}',
style: TextStyle(
fontSize: 12.sp,
color: Color(0xFF1E1E20),
fontWeight: FontWeight.w500,
),
),
SizedBox(width: 2.w),
Image.asset(
'ic_coin.png'.ktIcon,
width: 15.w,
),
],
),
],
),
],
),
],
),
),
);
},
separatorBuilder: (_, __) => SizedBox(height: 10.w),
itemCount: widget.store.listSubVip?.length ?? 0,
),
],
);
}
}

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:video_player/video_player.dart';
///
class CustomVideoProgressBar extends StatefulWidget {
final VideoPlayerController controller;

View File

@ -10,7 +10,6 @@ import 'package:wakelock_plus/wakelock_plus.dart';
import 'kt_pages/kt_routes.dart';
import 'kt_utils/kt_device_info_utils.dart';
import 'kt_utils/kt_keys.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();

View File

@ -123,6 +123,7 @@ flutter:
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package
# app图标配置
flutter_launcher_icons:
image_path: "assets/ic_logo.png"
android: "launcher_icon"
@ -130,6 +131,7 @@ flutter_launcher_icons:
ios: true
remove_alpha_ios: true
# app启动图配置
flutter_native_splash:
color: "#01060B"
image: assets/splash.png