diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index fcfd475..090689c 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,8 +1,20 @@ - + + + + + + + + + + + + android:hardwareAccelerated="true" + android:icon="@mipmap/launcher_icon"> _instance; + factory KtHttpClient() => _instance; late Dio _dio; - HttpClient._internal() { + KtHttpClient._internal() { _initDio(); } diff --git a/lib/kt_model/kt_history_video_bean.dart b/lib/kt_model/kt_history_video_bean.dart new file mode 100644 index 0000000..73bdc34 --- /dev/null +++ b/lib/kt_model/kt_history_video_bean.dart @@ -0,0 +1,134 @@ +import 'dart:convert'; + +/// id : 76 +/// short_id : 75 +/// name : "Mrs. Gu, your disguise has been exposed" +/// short_play_video_id : 3572 +/// description : "Since Xu Mumu married Gu Beichen, the family of three has been living their own small and happy life in a low-key manner. But Xu Mumu is not willing to be a full-time housewife from then on and is determined to create her own" +/// short_play_id : 76 +/// image_url : "https://static.wanmwl.com/image/4e123202511fcb209b0d.webp" +/// episode_total : 18 +/// current_episode : 1 +/// updated_at : "2025-07-04 06:19:21" +/// is_collect : 1 +/// category : ["Dominant CEO Romance","Sweet Romance "] + +KtHistoryVideoBean ktHistoryVideoBeanFromJson(String str) => + KtHistoryVideoBean.fromJson(json.decode(str)); +String ktHistoryVideoBeanToJson(KtHistoryVideoBean data) => + json.encode(data.toJson()); + +class KtHistoryVideoBean { + KtHistoryVideoBean({ + num? id, + num? shortId, + String? name, + num? shortPlayVideoId, + String? description, + num? shortPlayId, + String? imageUrl, + num? episodeTotal, + num? currentEpisode, + String? updatedAt, + num? isCollect, + List? category, + }) { + _id = id; + _shortId = shortId; + _name = name; + _shortPlayVideoId = shortPlayVideoId; + _description = description; + _shortPlayId = shortPlayId; + _imageUrl = imageUrl; + _episodeTotal = episodeTotal; + _currentEpisode = currentEpisode; + _updatedAt = updatedAt; + _isCollect = isCollect; + _category = category; + } + + KtHistoryVideoBean.fromJson(dynamic json) { + _id = json['id']; + _shortId = json['short_id']; + _name = json['name']; + _shortPlayVideoId = json['short_play_video_id']; + _description = json['description']; + _shortPlayId = json['short_play_id']; + _imageUrl = json['image_url']; + _episodeTotal = json['episode_total']; + _currentEpisode = json['current_episode']; + _updatedAt = json['updated_at']; + _isCollect = json['is_collect']; + _category = json['category'] != null ? json['category'].cast() : []; + } + num? _id; + num? _shortId; + String? _name; + num? _shortPlayVideoId; + String? _description; + num? _shortPlayId; + String? _imageUrl; + num? _episodeTotal; + num? _currentEpisode; + String? _updatedAt; + num? _isCollect; + List? _category; + KtHistoryVideoBean copyWith({ + num? id, + num? shortId, + String? name, + num? shortPlayVideoId, + String? description, + num? shortPlayId, + String? imageUrl, + num? episodeTotal, + num? currentEpisode, + String? updatedAt, + num? isCollect, + List? category, + }) => KtHistoryVideoBean( + id: id ?? _id, + shortId: shortId ?? _shortId, + name: name ?? _name, + shortPlayVideoId: shortPlayVideoId ?? _shortPlayVideoId, + description: description ?? _description, + shortPlayId: shortPlayId ?? _shortPlayId, + imageUrl: imageUrl ?? _imageUrl, + episodeTotal: episodeTotal ?? _episodeTotal, + currentEpisode: currentEpisode ?? _currentEpisode, + updatedAt: updatedAt ?? _updatedAt, + isCollect: isCollect ?? _isCollect, + category: category ?? _category, + ); + num? get id => _id; + num? get shortId => _shortId; + String? get name => _name; + num? get shortPlayVideoId => _shortPlayVideoId; + String? get description => _description; + num? get shortPlayId => _shortPlayId; + String? get imageUrl => _imageUrl; + num? get episodeTotal => _episodeTotal; + num? get currentEpisode => _currentEpisode; + String? get updatedAt => _updatedAt; + num? get isCollect => _isCollect; + List? get category => _category; + + set isCollect(num? value) => _isCollect = value; + + Map toJson() { + final map = {}; + map['id'] = _id; + map['short_id'] = _shortId; + map['name'] = _name; + map['short_play_video_id'] = _shortPlayVideoId; + map['description'] = _description; + map['short_play_id'] = _shortPlayId; + map['image_url'] = _imageUrl; + map['episode_total'] = _episodeTotal; + map['current_episode'] = _currentEpisode; + map['updated_at'] = _updatedAt; + map['is_collect'] = _isCollect; + map['category'] = _category; + return map; + } +} diff --git a/lib/kt_model/kt_home_category_bean.dart b/lib/kt_model/kt_home_category_bean.dart new file mode 100644 index 0000000..2f79b51 --- /dev/null +++ b/lib/kt_model/kt_home_category_bean.dart @@ -0,0 +1,37 @@ +import 'dart:convert'; + +/// category_name : "123" +/// category_id : 12 + +KtHomeCategoryBean ktHomeCategoryBeanFromJson(String str) => + KtHomeCategoryBean.fromJson(json.decode(str)); +String ktHomeCategoryBeanToJson(KtHomeCategoryBean data) => + json.encode(data.toJson()); + +class KtHomeCategoryBean { + KtHomeCategoryBean({String? categoryName, int? categoryId}) { + _categoryName = categoryName; + _categoryId = categoryId; + } + + KtHomeCategoryBean.fromJson(dynamic json) { + _categoryName = json['category_name']; + _categoryId = json['category_id']; + } + String? _categoryName; + int? _categoryId; + KtHomeCategoryBean copyWith({String? categoryName, int? categoryId}) => + KtHomeCategoryBean( + categoryName: categoryName ?? _categoryName, + categoryId: categoryId ?? _categoryId, + ); + String? get categoryName => _categoryName; + int? get categoryId => _categoryId; + + Map toJson() { + final map = {}; + map['category_name'] = _categoryName; + map['category_id'] = _categoryId; + return map; + } +} diff --git a/lib/kt_model/kt_video_detail_bean.dart b/lib/kt_model/kt_video_detail_bean.dart new file mode 100644 index 0000000..18429a6 --- /dev/null +++ b/lib/kt_model/kt_video_detail_bean.dart @@ -0,0 +1,779 @@ +import 'dart:convert'; + +/// business_model : "iap" +/// video_info : {"id":7132,"short_play_video_id":7132,"short_play_id":162,"short_id":11,"promise_view_ad":0,"video_url":"https://static.wanmwl.com/videom3u8/3f9afda335fd2616d901/3f9afda335fd2616d901.m3u8","coins":0,"vip_coins":0,"episode":1,"is_vip":2,"play_seconds":"0"} +/// shortPlayInfo : {"id":162,"short_id":11,"short_play_id":162,"name":"Madam President, Holding Multiple Positions ","description":"It is rumored outside that the prestigious president of Xu's Group is about to marry the daughter of the Xu family. Just because the name of the Xu family's daughter is Pearl, which is the same as his childhood sweetheart in ","process":2,"image_url":"https://static.wanmwl.com/eyJrZXkiOiJpbWFnZS80ZmRhZDg4OWIxNmM3MmQ2MjkwZC5qcGciLCJlZGl0cyI6eyJyZXNpemUiOnsiZml0IjoiY292ZXIifX19?sign=$23f3feca08db05081465b3677a6f671819b85d9f18ad94cc7236f3c89aada01ebe2de98adbca00c6f0c8c5b8621c5e4f6064afb617","horizontally_img":"https://static.wanmwl.com/image/ac2f87aad3384032215c.jpg","buy_type":1,"tag_type":"","all_coins":0,"collect_total":4,"watch_total":307,"episode_total":18,"search_click_total":0,"is_collect":true,"can_share_get_coin":true,"category":["Sweet Romance "]} +/// episodeList : [{"id":7132,"short_play_video_id":7132,"short_play_id":162,"short_id":11,"promise_view_ad":0,"video_url":"https://static.wanmwl.com/videom3u8/3f9afda335fd2616d901/3f9afda335fd2616d901.m3u8","coins":0,"vip_coins":0,"episode":1,"is_vip":2,"is_lock":false,"play_seconds":"0"},{"id":7140,"short_play_video_id":7140,"short_play_id":162,"short_id":11,"promise_view_ad":0,"video_url":"https://static.wanmwl.com/videom3u8/5c5860a67aab8fe3d89d/5c5860a67aab8fe3d89d.m3u8","coins":0,"vip_coins":0,"episode":2,"is_vip":2,"is_lock":false,"play_seconds":"0"},{"id":7136,"short_play_video_id":7136,"short_play_id":162,"short_id":11,"promise_view_ad":0,"video_url":"https://static.wanmwl.com/videom3u8/9f7c98f9c61fdf1bf5fc/9f7c98f9c61fdf1bf5fc.m3u8","coins":0,"vip_coins":0,"episode":3,"is_vip":2,"is_lock":false,"play_seconds":"0"},{"id":7137,"short_play_video_id":7137,"short_play_id":162,"short_id":11,"promise_view_ad":0,"video_url":"https://static.wanmwl.com/videom3u8/72c3555f90fcceb9aab4/72c3555f90fcceb9aab4.m3u8","coins":0,"vip_coins":0,"episode":4,"is_vip":2,"is_lock":false,"play_seconds":"0"},{"id":7133,"short_play_video_id":7133,"short_play_id":162,"short_id":11,"promise_view_ad":0,"video_url":"https://static.wanmwl.com/videom3u8/171bf35b0196bdf1e839/171bf35b0196bdf1e839.m3u8","coins":0,"vip_coins":0,"episode":5,"is_vip":2,"is_lock":false,"play_seconds":"0"},{"id":7135,"short_play_video_id":7135,"short_play_id":162,"short_id":11,"promise_view_ad":0,"video_url":"https://static.wanmwl.com/videom3u8/fff10f96153f53ddc7f6/fff10f96153f53ddc7f6.m3u8","coins":0,"vip_coins":0,"episode":6,"is_vip":2,"is_lock":false,"play_seconds":"0"},{"id":7134,"short_play_video_id":7134,"short_play_id":162,"short_id":11,"promise_view_ad":0,"video_url":"https://static.wanmwl.com/videom3u8/9a3ac7c5924b144b6222/9a3ac7c5924b144b6222.m3u8","coins":0,"vip_coins":0,"episode":7,"is_vip":2,"is_lock":false,"play_seconds":"0"},{"id":7139,"short_play_video_id":7139,"short_play_id":162,"short_id":11,"promise_view_ad":0,"video_url":"https://static.wanmwl.com/videom3u8/17927e0dd452d5a936d0/17927e0dd452d5a936d0.m3u8","coins":0,"vip_coins":0,"episode":8,"is_vip":2,"is_lock":false,"play_seconds":"0"},{"id":7138,"short_play_video_id":7138,"short_play_id":162,"short_id":11,"promise_view_ad":0,"video_url":"https://static.wanmwl.com/videom3u8/1d61d46d30292d0a7269/1d61d46d30292d0a7269.m3u8","coins":0,"vip_coins":0,"episode":9,"is_vip":2,"is_lock":false,"play_seconds":"0"},{"id":7141,"short_play_video_id":7141,"short_play_id":162,"short_id":11,"promise_view_ad":0,"video_url":"https://static.wanmwl.com/videom3u8/70ee4deb9bb176846534/70ee4deb9bb176846534.m3u8","coins":0,"vip_coins":0,"episode":10,"is_vip":2,"is_lock":false,"play_seconds":"0"},{"id":7146,"short_play_video_id":7146,"short_play_id":162,"short_id":11,"promise_view_ad":0,"video_url":"https://static.wanmwl.com/videom3u8/946ec0930ff6d1f9b36e/946ec0930ff6d1f9b36e.m3u8","coins":69,"vip_coins":0,"episode":11,"is_vip":2,"is_lock":true,"play_seconds":"0"},{"id":7144,"short_play_video_id":7144,"short_play_id":162,"short_id":11,"promise_view_ad":0,"video_url":"https://static.wanmwl.com/videom3u8/b92bed7933bbd5916f07/b92bed7933bbd5916f07.m3u8","coins":119,"vip_coins":0,"episode":12,"is_vip":2,"is_lock":true,"play_seconds":"0"},{"id":7142,"short_play_video_id":7142,"short_play_id":162,"short_id":11,"promise_view_ad":0,"video_url":"https://static.wanmwl.com/videom3u8/9679216680a679f7e6c2/9679216680a679f7e6c2.m3u8","coins":149,"vip_coins":0,"episode":13,"is_vip":2,"is_lock":true,"play_seconds":"0"},{"id":7143,"short_play_video_id":7143,"short_play_id":162,"short_id":11,"promise_view_ad":0,"video_url":"https://static.wanmwl.com/videom3u8/a73214d6aa17af4d639d/a73214d6aa17af4d639d.m3u8","coins":249,"vip_coins":0,"episode":14,"is_vip":2,"is_lock":true,"play_seconds":"0"},{"id":7147,"short_play_video_id":7147,"short_play_id":162,"short_id":11,"promise_view_ad":0,"video_url":"https://static.wanmwl.com/videom3u8/3a82aab4550ddc50cfe7/3a82aab4550ddc50cfe7.m3u8","coins":199,"vip_coins":0,"episode":15,"is_vip":2,"is_lock":true,"play_seconds":"0"},{"id":7149,"short_play_video_id":7149,"short_play_id":162,"short_id":11,"promise_view_ad":0,"video_url":"https://static.wanmwl.com/videom3u8/0b0781eefb331cc25fee/0b0781eefb331cc25fee.m3u8","coins":159,"vip_coins":0,"episode":16,"is_vip":2,"is_lock":true,"play_seconds":"0"},{"id":7145,"short_play_video_id":7145,"short_play_id":162,"short_id":11,"promise_view_ad":0,"video_url":"https://static.wanmwl.com/videom3u8/6c2d042f669e7bdaff3d/6c2d042f669e7bdaff3d.m3u8","coins":69,"vip_coins":0,"episode":17,"is_vip":2,"is_lock":true,"play_seconds":"0"},{"id":7148,"short_play_video_id":7148,"short_play_id":162,"short_id":11,"promise_view_ad":0,"video_url":"https://static.wanmwl.com/videom3u8/d6fd3ab315f161b9b980/d6fd3ab315f161b9b980.m3u8","coins":69,"vip_coins":0,"episode":18,"is_vip":2,"is_lock":true,"play_seconds":"0"}] +/// is_collect : true +/// show_share_coin : true +/// share_coin : 100 +/// install_coins : 1000 +/// revolution : 720 +/// user_level : "normal" +/// unlock_video_ad_count : 1 +/// discount : 80 + +VideoDetailBean videoDetailBeanBeanFromJson(String str) => + VideoDetailBean.fromJson(json.decode(str)); + +String videoDetailBeanBeanToJson(VideoDetailBean data) => + json.encode(data.toJson()); + +class VideoDetailBean { + VideoDetailBean({ + int? payMode, + String? businessModel, + VideoInfo? videoInfo, + ShortPlayInfo? shortPlayInfo, + List? episodeList, + List? checkPoint, + bool? isCollect, + bool? showShareCoin, + num? shareCoin, + num? installCoins, + dynamic revolution, + String? userLevel, + num? unlockVideoAdCount, + num? discount, + }) { + _payMode = payMode; + _businessModel = businessModel; + _videoInfo = videoInfo; + _shortPlayInfo = shortPlayInfo; + _episodeList = episodeList; + _checkPoint = checkPoint; + _isCollect = isCollect; + _showShareCoin = showShareCoin; + _shareCoin = shareCoin; + _installCoins = installCoins; + _revolution = revolution; + _userLevel = userLevel; + _unlockVideoAdCount = unlockVideoAdCount; + _discount = discount; + } + + VideoDetailBean.fromJson(dynamic json) { + _businessModel = json['business_model']; + _payMode = json['pay_mode']; + _videoInfo = json['video_info'] != null + ? VideoInfo.fromJson(json['video_info']) + : null; + _shortPlayInfo = json['shortPlayInfo'] != null + ? ShortPlayInfo.fromJson(json['shortPlayInfo']) + : null; + if (json['episodeList'] != null) { + _episodeList = []; + json['episodeList'].forEach((v) { + _episodeList?.add(Episode.fromJson(v)); + }); + } + _checkPoint = json['check_point'] != null + ? json['check_point'].cast() + : []; + _isCollect = json['is_collect']; + _showShareCoin = json['show_share_coin']; + _shareCoin = json['share_coin']; + _installCoins = json['install_coins']; + _revolution = json['revolution']; + _userLevel = json['user_level']; + _unlockVideoAdCount = json['unlock_video_ad_count']; + _discount = json['discount']; + } + + int? _payMode; + String? _businessModel; + VideoInfo? _videoInfo; + ShortPlayInfo? _shortPlayInfo; + List? _episodeList; + List? _checkPoint; + bool? _isCollect; + bool? _showShareCoin; + num? _shareCoin; + num? _installCoins; + dynamic _revolution; + String? _userLevel; + num? _unlockVideoAdCount; + num? _discount; + + VideoDetailBean copyWith({ + int? payMode, + String? businessModel, + VideoInfo? videoInfo, + ShortPlayInfo? shortPlayInfo, + List? episodeList, + List? checkPoint, + bool? isCollect, + bool? showShareCoin, + num? shareCoin, + num? installCoins, + dynamic revolution, + String? userLevel, + num? unlockVideoAdCount, + num? discount, + }) => VideoDetailBean( + payMode: payMode ?? _payMode, + businessModel: businessModel ?? _businessModel, + videoInfo: videoInfo ?? _videoInfo, + shortPlayInfo: shortPlayInfo ?? _shortPlayInfo, + episodeList: episodeList ?? _episodeList, + checkPoint: checkPoint ?? _checkPoint, + isCollect: isCollect ?? _isCollect, + showShareCoin: showShareCoin ?? _showShareCoin, + shareCoin: shareCoin ?? _shareCoin, + installCoins: installCoins ?? _installCoins, + revolution: revolution ?? _revolution, + userLevel: userLevel ?? _userLevel, + unlockVideoAdCount: unlockVideoAdCount ?? _unlockVideoAdCount, + discount: discount ?? _discount, + ); + + int? get payMode => _payMode; + + String? get businessModel => _businessModel; + + VideoInfo? get videoInfo => _videoInfo; + + ShortPlayInfo? get shortPlayInfo => _shortPlayInfo; + + List? get episodeList => _episodeList; + + List? get checkPoint => _checkPoint; + + bool? get isCollect => _isCollect; + + bool? get showShareCoin => _showShareCoin; + + num? get shareCoin => _shareCoin; + + num? get installCoins => _installCoins; + + dynamic get revolution => _revolution; + + String? get userLevel => _userLevel; + + num? get unlockVideoAdCount => _unlockVideoAdCount; + + num? get discount => _discount; + + // VideoDetailBean setters + set payMode(int? value) => _payMode = value; + + set businessModel(String? value) => _businessModel = value; + + set videoInfo(VideoInfo? value) => _videoInfo = value; + + set shortPlayInfo(ShortPlayInfo? value) => _shortPlayInfo = value; + + set episodeList(List? value) => _episodeList = value; + + set checkPoint(List? value) => _checkPoint = value; + + set isCollect(bool? value) => _isCollect = value; + + set showShareCoin(bool? value) => _showShareCoin = value; + + set shareCoin(num? value) => _shareCoin = value; + + set installCoins(num? value) => _installCoins = value; + + set revolution(dynamic value) => _revolution = value; + + set userLevel(String? value) => _userLevel = value; + + set unlockVideoAdCount(num? value) => _unlockVideoAdCount = value; + + set discount(num? value) => _discount = value; + + Map toJson() { + final map = {}; + map['pay_mode'] = _payMode; + map['business_model'] = _businessModel; + if (_videoInfo != null) { + map['video_info'] = _videoInfo?.toJson(); + } + if (_shortPlayInfo != null) { + map['shortPlayInfo'] = _shortPlayInfo?.toJson(); + } + if (_episodeList != null) { + map['episodeList'] = _episodeList?.map((v) => v.toJson()).toList(); + } + map['check_point'] = _checkPoint; + map['is_collect'] = _isCollect; + map['show_share_coin'] = _showShareCoin; + map['share_coin'] = _shareCoin; + map['install_coins'] = _installCoins; + map['revolution'] = _revolution; + map['user_level'] = _userLevel; + map['unlock_video_ad_count'] = _unlockVideoAdCount; + map['discount'] = _discount; + return map; + } +} + +/// id : 7132 +/// short_play_video_id : 7132 +/// short_play_id : 162 +/// short_id : 11 +/// promise_view_ad : 0 +/// video_url : "https://static.wanmwl.com/videom3u8/3f9afda335fd2616d901/3f9afda335fd2616d901.m3u8" +/// coins : 0 +/// vip_coins : 0 +/// episode : 1 +/// is_vip : 2 +/// is_lock : false +/// play_seconds : "0" + +Episode episodeListFromJson(String str) => Episode.fromJson(json.decode(str)); + +String episodeListToJson(Episode data) => json.encode(data.toJson()); + +class Episode { + Episode({ + num? id, + num? shortPlayVideoId, + num? shortPlayId, + num? shortId, + num? promiseViewAd, + String? videoUrl, + num? coins, + num? vipCoins, + int? episode, + num? isVip, + bool? isLock, + String? playSeconds, + }) { + _id = id; + _shortPlayVideoId = shortPlayVideoId; + _shortPlayId = shortPlayId; + _shortId = shortId; + _promiseViewAd = promiseViewAd; + _videoUrl = videoUrl; + _coins = coins; + _vipCoins = vipCoins; + _episode = episode; + _isVip = isVip; + _isLock = isLock; + _playSeconds = playSeconds; + } + + Episode.fromJson(dynamic json) { + _id = json['id']; + _shortPlayVideoId = json['short_play_video_id']; + _shortPlayId = json['short_play_id']; + _shortId = json['short_id']; + _promiseViewAd = json['promise_view_ad']; + _videoUrl = json['video_url']; + _coins = json['coins']; + _vipCoins = json['vip_coins']; + _episode = json['episode']; + _isVip = json['is_vip']; + _isLock = json['is_lock']; + _playSeconds = json['play_seconds']; + } + + num? _id; + num? _shortPlayVideoId; + num? _shortPlayId; + num? _shortId; + num? _promiseViewAd; + String? _videoUrl; + num? _coins; + num? _vipCoins; + int? _episode; + num? _isVip; + bool? _isLock; + String? _playSeconds; + + Episode copyWith({ + num? id, + num? shortPlayVideoId, + num? shortPlayId, + num? shortId, + num? promiseViewAd, + String? videoUrl, + num? coins, + num? vipCoins, + int? episode, + num? isVip, + bool? isLock, + String? playSeconds, + }) => Episode( + id: id ?? _id, + shortPlayVideoId: shortPlayVideoId ?? _shortPlayVideoId, + shortPlayId: shortPlayId ?? _shortPlayId, + shortId: shortId ?? _shortId, + promiseViewAd: promiseViewAd ?? _promiseViewAd, + videoUrl: videoUrl ?? _videoUrl, + coins: coins ?? _coins, + vipCoins: vipCoins ?? _vipCoins, + episode: episode ?? _episode, + isVip: isVip ?? _isVip, + isLock: isLock ?? _isLock, + playSeconds: playSeconds ?? _playSeconds, + ); + + num? get id => _id; + + num? get shortPlayVideoId => _shortPlayVideoId; + + num? get shortPlayId => _shortPlayId; + + num? get shortId => _shortId; + + num? get promiseViewAd => _promiseViewAd; + + String? get videoUrl => _videoUrl; + + num? get coins => _coins; + + num? get vipCoins => _vipCoins; + + int? get episode => _episode; + + num? get isVip => _isVip; + + bool? get isLock => _isLock; + + String? get playSeconds => _playSeconds; + + // Episode setters + set id(num? value) => _id = value; + + set shortPlayVideoId(num? value) => _shortPlayVideoId = value; + + set shortPlayId(num? value) => _shortPlayId = value; + + set shortId(num? value) => _shortId = value; + + set promiseViewAd(num? value) => _promiseViewAd = value; + + set videoUrl(String? value) => _videoUrl = value; + + set coins(num? value) => _coins = value; + + set vipCoins(num? value) => _vipCoins = value; + + set episode(int? value) => _episode = value; + + set isVip(num? value) => _isVip = value; + + set isLock(bool? value) => _isLock = value; + + set playSeconds(String? value) => _playSeconds = value; + + Map toJson() { + final map = {}; + map['id'] = _id; + map['short_play_video_id'] = _shortPlayVideoId; + map['short_play_id'] = _shortPlayId; + map['short_id'] = _shortId; + map['promise_view_ad'] = _promiseViewAd; + map['video_url'] = _videoUrl; + map['coins'] = _coins; + map['vip_coins'] = _vipCoins; + map['episode'] = _episode; + map['is_vip'] = _isVip; + map['is_lock'] = _isLock; + map['play_seconds'] = _playSeconds; + return map; + } +} + +/// id : 162 +/// short_id : 11 +/// short_play_id : 162 +/// name : "Madam President, Holding Multiple Positions " +/// description : "It is rumored outside that the prestigious president of Xu's Group is about to marry the daughter of the Xu family. Just because the name of the Xu family's daughter is Pearl, which is the same as his childhood sweetheart in " +/// process : 2 +/// image_url : "https://static.wanmwl.com/eyJrZXkiOiJpbWFnZS80ZmRhZDg4OWIxNmM3MmQ2MjkwZC5qcGciLCJlZGl0cyI6eyJyZXNpemUiOnsiZml0IjoiY292ZXIifX19?sign=$23f3feca08db05081465b3677a6f671819b85d9f18ad94cc7236f3c89aada01ebe2de98adbca00c6f0c8c5b8621c5e4f6064afb617" +/// horizontally_img : "https://static.wanmwl.com/image/ac2f87aad3384032215c.jpg" +/// buy_type : 1 +/// tag_type : "" +/// all_coins : 0 +/// collect_total : 4 +/// watch_total : 307 +/// episode_total : 18 +/// search_click_total : 0 +/// is_collect : true +/// can_share_get_coin : true +/// category : ["Sweet Romance "] + +ShortPlayInfo shortPlayInfoFromJson(String str) => + ShortPlayInfo.fromJson(json.decode(str)); + +String shortPlayInfoToJson(ShortPlayInfo data) => json.encode(data.toJson()); + +class ShortPlayInfo { + ShortPlayInfo({ + num? id, + num? shortId, + num? shortPlayId, + String? name, + String? description, + num? process, + String? imageUrl, + String? horizontallyImg, + num? buyType, + String? tagType, + num? allCoins, + num? collectTotal, + num? watchTotal, + num? episodeTotal, + num? searchClickTotal, + bool? isCollect, + bool? canShareGetCoin, + List? category, + }) { + _id = id; + _shortId = shortId; + _shortPlayId = shortPlayId; + _name = name; + _description = description; + _process = process; + _imageUrl = imageUrl; + _horizontallyImg = horizontallyImg; + _buyType = buyType; + _tagType = tagType; + _allCoins = allCoins; + _collectTotal = collectTotal; + _watchTotal = watchTotal; + _episodeTotal = episodeTotal; + _searchClickTotal = searchClickTotal; + _isCollect = isCollect; + _canShareGetCoin = canShareGetCoin; + _category = category; + } + + ShortPlayInfo.fromJson(dynamic json) { + _id = json['id']; + _shortId = json['short_id']; + _shortPlayId = json['short_play_id']; + _name = json['name']; + _description = json['description']; + _process = json['process']; + _imageUrl = json['image_url']; + _horizontallyImg = json['horizontally_img']; + _buyType = json['buy_type']; + _tagType = json['tag_type']; + _allCoins = json['all_coins']; + _collectTotal = json['collect_total']; + _watchTotal = json['watch_total']; + _episodeTotal = json['episode_total']; + _searchClickTotal = json['search_click_total']; + _isCollect = json['is_collect']; + _canShareGetCoin = json['can_share_get_coin']; + _category = json['category'] != null ? json['category'].cast() : []; + } + + num? _id; + num? _shortId; + num? _shortPlayId; + String? _name; + String? _description; + num? _process; + String? _imageUrl; + String? _horizontallyImg; + num? _buyType; + String? _tagType; + num? _allCoins; + num? _collectTotal; + num? _watchTotal; + num? _episodeTotal; + num? _searchClickTotal; + bool? _isCollect; + bool? _canShareGetCoin; + List? _category; + + ShortPlayInfo copyWith({ + num? id, + num? shortId, + num? shortPlayId, + String? name, + String? description, + num? process, + String? imageUrl, + String? horizontallyImg, + num? buyType, + String? tagType, + num? allCoins, + num? collectTotal, + num? watchTotal, + num? episodeTotal, + num? searchClickTotal, + bool? isCollect, + bool? canShareGetCoin, + List? category, + }) => ShortPlayInfo( + id: id ?? _id, + shortId: shortId ?? _shortId, + shortPlayId: shortPlayId ?? _shortPlayId, + name: name ?? _name, + description: description ?? _description, + process: process ?? _process, + imageUrl: imageUrl ?? _imageUrl, + horizontallyImg: horizontallyImg ?? _horizontallyImg, + buyType: buyType ?? _buyType, + tagType: tagType ?? _tagType, + allCoins: allCoins ?? _allCoins, + collectTotal: collectTotal ?? _collectTotal, + watchTotal: watchTotal ?? _watchTotal, + episodeTotal: episodeTotal ?? _episodeTotal, + searchClickTotal: searchClickTotal ?? _searchClickTotal, + isCollect: isCollect ?? _isCollect, + canShareGetCoin: canShareGetCoin ?? _canShareGetCoin, + category: category ?? _category, + ); + + num? get id => _id; + + num? get shortId => _shortId; + + num? get shortPlayId => _shortPlayId; + + String? get name => _name; + + String? get description => _description; + + num? get process => _process; + + String? get imageUrl => _imageUrl; + + String? get horizontallyImg => _horizontallyImg; + + num? get buyType => _buyType; + + String? get tagType => _tagType; + + num? get allCoins => _allCoins; + + num? get collectTotal => _collectTotal; + + num? get watchTotal => _watchTotal; + + num? get episodeTotal => _episodeTotal; + + num? get searchClickTotal => _searchClickTotal; + + bool? get isCollect => _isCollect; + + bool? get canShareGetCoin => _canShareGetCoin; + + List? get category => _category; + + // ShortPlayInfo setters + set id(num? value) => _id = value; + + set shortId(num? value) => _shortId = value; + + set shortPlayId(num? value) => _shortPlayId = value; + + set name(String? value) => _name = value; + + set description(String? value) => _description = value; + + set process(num? value) => _process = value; + + set imageUrl(String? value) => _imageUrl = value; + + set horizontallyImg(String? value) => _horizontallyImg = value; + + set buyType(num? value) => _buyType = value; + + set tagType(String? value) => _tagType = value; + + set allCoins(num? value) => _allCoins = value; + + set collectTotal(num? value) => _collectTotal = value; + + set watchTotal(num? value) => _watchTotal = value; + + set episodeTotal(num? value) => _episodeTotal = value; + + set searchClickTotal(num? value) => _searchClickTotal = value; + + set isCollect(bool? value) => _isCollect = value; + + set canShareGetCoin(bool? value) => _canShareGetCoin = value; + + set category(List? value) => _category = value; + + Map toJson() { + final map = {}; + map['id'] = _id; + map['short_id'] = _shortId; + map['short_play_id'] = _shortPlayId; + map['name'] = _name; + map['description'] = _description; + map['process'] = _process; + map['image_url'] = _imageUrl; + map['horizontally_img'] = _horizontallyImg; + map['buy_type'] = _buyType; + map['tag_type'] = _tagType; + map['all_coins'] = _allCoins; + map['collect_total'] = _collectTotal; + map['watch_total'] = _watchTotal; + map['episode_total'] = _episodeTotal; + map['search_click_total'] = _searchClickTotal; + map['is_collect'] = _isCollect; + map['can_share_get_coin'] = _canShareGetCoin; + map['category'] = _category; + return map; + } +} + +/// id : 7132 +/// short_play_video_id : 7132 +/// short_play_id : 162 +/// short_id : 11 +/// promise_view_ad : 0 +/// video_url : "https://static.wanmwl.com/videom3u8/3f9afda335fd2616d901/3f9afda335fd2616d901.m3u8" +/// coins : 0 +/// vip_coins : 0 +/// episode : 1 +/// is_vip : 2 +/// play_seconds : "0" + +VideoInfo videoInfoFromJson(String str) => VideoInfo.fromJson(json.decode(str)); + +String videoInfoToJson(VideoInfo data) => json.encode(data.toJson()); + +class VideoInfo { + VideoInfo({ + num? id, + num? shortPlayVideoId, + num? shortPlayId, + num? shortId, + num? promiseViewAd, + String? videoUrl, + num? coins, + num? vipCoins, + int? episode, + num? isVip, + String? playSeconds, + }) { + _id = id; + _shortPlayVideoId = shortPlayVideoId; + _shortPlayId = shortPlayId; + _shortId = shortId; + _promiseViewAd = promiseViewAd; + _videoUrl = videoUrl; + _coins = coins; + _vipCoins = vipCoins; + _episode = episode; + _isVip = isVip; + _playSeconds = playSeconds; + } + + VideoInfo.fromJson(dynamic json) { + _id = json['id']; + _shortPlayVideoId = json['short_play_video_id']; + _shortPlayId = json['short_play_id']; + _shortId = json['short_id']; + _promiseViewAd = json['promise_view_ad']; + _videoUrl = json['video_url']; + _coins = json['coins']; + _vipCoins = json['vip_coins']; + _episode = json['episode']; + _isVip = json['is_vip']; + _playSeconds = json['play_seconds']; + } + + num? _id; + num? _shortPlayVideoId; + num? _shortPlayId; + num? _shortId; + num? _promiseViewAd; + String? _videoUrl; + num? _coins; + num? _vipCoins; + int? _episode; + num? _isVip; + String? _playSeconds; + + VideoInfo copyWith({ + num? id, + num? shortPlayVideoId, + num? shortPlayId, + num? shortId, + num? promiseViewAd, + String? videoUrl, + num? coins, + num? vipCoins, + int? episode, + num? isVip, + String? playSeconds, + }) => VideoInfo( + id: id ?? _id, + shortPlayVideoId: shortPlayVideoId ?? _shortPlayVideoId, + shortPlayId: shortPlayId ?? _shortPlayId, + shortId: shortId ?? _shortId, + promiseViewAd: promiseViewAd ?? _promiseViewAd, + videoUrl: videoUrl ?? _videoUrl, + coins: coins ?? _coins, + vipCoins: vipCoins ?? _vipCoins, + episode: episode ?? _episode, + isVip: isVip ?? _isVip, + playSeconds: playSeconds ?? _playSeconds, + ); + + num? get id => _id; + + num? get shortPlayVideoId => _shortPlayVideoId; + + num? get shortPlayId => _shortPlayId; + + num? get shortId => _shortId; + + num? get promiseViewAd => _promiseViewAd; + + String? get videoUrl => _videoUrl; + + num? get coins => _coins; + + num? get vipCoins => _vipCoins; + + int? get episode => _episode; + + num? get isVip => _isVip; + + String? get playSeconds => _playSeconds; + + // VideoInfo setters + set id(num? value) => _id = value; + + set shortPlayVideoId(num? value) => _shortPlayVideoId = value; + + set shortPlayId(num? value) => _shortPlayId = value; + + set shortId(num? value) => _shortId = value; + + set promiseViewAd(num? value) => _promiseViewAd = value; + + set videoUrl(String? value) => _videoUrl = value; + + set coins(num? value) => _coins = value; + + set vipCoins(num? value) => _vipCoins = value; + + set episode(int? value) => _episode = value; + + set isVip(num? value) => _isVip = value; + + set playSeconds(String? value) => _playSeconds = value; + + Map toJson() { + final map = {}; + map['id'] = _id; + map['short_play_video_id'] = _shortPlayVideoId; + map['short_play_id'] = _shortPlayId; + map['short_id'] = _shortId; + map['promise_view_ad'] = _promiseViewAd; + map['video_url'] = _videoUrl; + map['coins'] = _coins; + map['vip_coins'] = _vipCoins; + map['episode'] = _episode; + map['is_vip'] = _isVip; + map['play_seconds'] = _playSeconds; + return map; + } +} diff --git a/lib/kt_pages/kt_explore/logic.dart b/lib/kt_pages/kt_explore/logic.dart new file mode 100644 index 0000000..31b3dbf --- /dev/null +++ b/lib/kt_pages/kt_explore/logic.dart @@ -0,0 +1,240 @@ +import 'package:flutter_kinetra/kt_pages/kt_explore/state.dart'; +import 'package:get/get.dart'; +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; + +import '../../dio_cilent/kt_apis.dart'; +import '../../dio_cilent/kt_request.dart'; +import '../../kt_model/kt_short_video_bean.dart'; +import '../../kt_widgets/kt_status_widget.dart'; + +class KtExploreLogic extends GetxController { + final state = KtExploreState(); + final EasyRefreshController refreshCtrl = EasyRefreshController( + controlFinishLoad: true, + ); + final PageController pageController = PageController(viewportFraction: .95); + final int preloadRange = 1; + bool isRefresh = false; + + @override + void onReady() { + super.onReady(); + fetchDiscoverList(); + } + + @override + void onClose() { + super.onClose(); + clearController(); + refreshCtrl.dispose(); + pageController.dispose(); + } + + clearController() { + for (var item in state.controllers) { + item?.pause(); + item?.dispose(); + } + state.controllers.clear(); + } + + fetchDiscoverList({bool refresh = false}) async { + state.loadStatus = KtLoadStatusType.loading; + + if (refresh) { + isRefresh = true; + state.curIndex = 1; + state.videoList.clear(); + clearController(); + } + + if (refresh) refreshCtrl.finishLoad(); + try { + ApiResponse res = await KtHttpClient().request( + KtApis.getRecommands, + queryParameters: {'current_page': state.curIndex, 'page_size': 20}, + method: HttpMethod.get, + ); + if (refresh) { + isRefresh = false; + update(); + } + + if (res.success) { + state.loadStatus = KtLoadStatusType.loadSuccess; + List list = [ + ...res.data['list'].map((item) => KtShortVideoBean.fromJson(item)), + ]; + state.videoList.addAll(list); + // if (videoId == -1) { + // video = state.videoList.first; + // videoId = state.videoList.first.id ?? -1; + // } + state.controllers = List.filled( + state.videoList.length, + null, + growable: true, + ); + + if (state.videoList.isNotEmpty && state.curIndex == 1) { + state.currentPage = 0; + _initializeController(0); + } + update(); + } else { + if (refresh) refreshCtrl.finishRefresh(IndicatorResult.fail); + state.loadStatus = KtLoadStatusType.loadFailed; + } + } catch (e) { + if (refresh) { + isRefresh = false; + update(); + } + state.loadStatus = KtLoadStatusType.loadFailed; + } + update(); + } + + // 切换剧集时处理视频状态 + Future onPageChanged(int index, {bool isToggle = false}) async { + if (index < 0 || index >= state.videoList.length) return; + + // 暂停当前视频 + if (state.controllers[state.currentPage]?.value.isPlaying ?? false) { + await state.controllers[state.currentPage]?.pause(); + } + + state.currentPage = index; + if (isToggle) { + // loadStatusType = LoadStatusType.loading; + // update(); + + await _initializeController(index); + // loadStatusType = LoadStatusType.loadSuccess; + update(); + WidgetsBinding.instance.addPostFrameCallback((_) { + pageController.jumpToPage(index); + }); + } + if (state.controllers[index] != null) { + state.controllers[index]?.play(); + } else { + if (!isToggle) await _initializeController(index); + state.controllers[index]?.play(); + } + // 预加载新的相邻视频,并释放多余控制器 + _preloadAdjacentVideos(); + update(); + // }); + } + + // 释放非当前、前后的视频控制器,减少内存占用 + void _releaseUnusedControllers() { + for (int i = 0; i < state.controllers.length; i++) { + if (i < state.currentPage - 1 || i > state.currentPage + 1) { + state.controllers[i]?.dispose(); + state.controllers[i] = null; + } + } + } + + // 初始化视频控制器 + Future _initializeController(int index) async { + if (index < 0 || index >= state.videoList.length) return; + if (state.controllers[index] != null) return; + final episode = state.videoList[index]; + final controller = VideoPlayerController.networkUrl( + Uri.parse(episode.videoInfo!.videoUrl!), + formatHint: VideoFormat.hls, + ); + + state.controllers[index] = controller; + + try { + await controller.initialize(); + if (index == state.currentPage) { + controller.play(); + update(); + } + controller.addListener(() { + if (state.currentPage == index) update(); + + if (controller.value.isCompleted && !controller.value.isBuffering) { + onPageChanged(index + 1, isToggle: true); + } + }); + } catch (e) { + // 可根据需要处理异常 + // UserUtil().reportErrorEvent( + // 'video initialize failed', + // UserUtil.videoError, + // errMsg: e.toString(), + // payData: episode.toJson(), + // shortPlayId: episode.shortPlayId ?? 0, + // shortPlayVideoId: episode.shortPlayVideoId ?? 0, + // ); + debugPrint('---err:$e'); + } + } + + // 预加载相邻视频 + void _preloadAdjacentVideos() { + if (state.currentPage > 0) _initializeController(state.currentPage - 1); + if (state.currentPage < state.videoList.length - 1) { + _initializeController(state.currentPage + 1); + } + _releaseUnusedControllers(); + } + + likeVideo(KtShortVideoBean video) { + if (video.isCollect ?? false) { + video.collectTotal = video.collectTotal! - 1; + if (video.collectTotal! < 0) { + video.collectTotal = 0; + } + KtHttpClient().request( + KtApis.deleteFavoriteVideo, + data: { + "short_play_id": video.shortPlayId, + "video_id": video.videoInfo!.id, + }, + ); + } else { + video.collectTotal = video.collectTotal! + 1; + KtHttpClient().request( + KtApis.collectVideo, + data: { + "short_play_id": video.shortPlayId, + "video_id": video.videoInfo!.id, + }, + ); + } + video.isCollect = !(video.isCollect ?? false); + update(); + } + + KtShortVideoBean get curVideo => state.videoList[state.currentPage]; + + setCollectVideo(int shortPlayId, {bool isCollect = true}) { + KtShortVideoBean? video = state.videoList.firstWhereOrNull( + (item) => item.shortPlayId == shortPlayId, + ); + if (video == null) return; + video.isCollect = isCollect; + video.collectTotal = video.collectTotal! + (isCollect ? 1 : -1); + update(); + } + + // + // Future onRefresh() async { + // state.currentPage = 0; + // state.curIndex = 1; + // // clearController(); + // // update(); + // WidgetsBinding.instance.addPostFrameCallback((_) async { + // await fetchDiscoverList(refresh: true); + // }); + // } +} diff --git a/lib/kt_pages/kt_explore/state.dart b/lib/kt_pages/kt_explore/state.dart new file mode 100644 index 0000000..2434ce5 --- /dev/null +++ b/lib/kt_pages/kt_explore/state.dart @@ -0,0 +1,13 @@ +import 'package:flutter_kinetra/kt_model/kt_short_video_bean.dart'; +import 'package:video_player/video_player.dart'; + +import '../../kt_widgets/kt_status_widget.dart'; + +class KtExploreState { + int curIndex = 1; + List videoList = []; + List controllers = []; + + int currentPage = 0; + KtLoadStatusType loadStatus = KtLoadStatusType.loading; +} diff --git a/lib/kt_pages/kt_explore/view.dart b/lib/kt_pages/kt_explore/view.dart index 403cd5d..4c12974 100644 --- a/lib/kt_pages/kt_explore/view.dart +++ b/lib/kt_pages/kt_explore/view.dart @@ -1,4 +1,16 @@ +import 'package:easy_refresh/easy_refresh.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_kinetra/kt_utils/kt_string_extend.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:video_player/video_player.dart'; +import 'package:visibility_detector/visibility_detector.dart'; + +import '../../kt_model/kt_short_video_bean.dart'; +import '../../kt_widgets/kt_network_image.dart'; +import '../../kt_widgets/kt_status_widget.dart'; +import '../kt_routes.dart'; +import 'logic.dart'; class KtExplorePage extends StatefulWidget { const KtExplorePage({super.key}); @@ -7,9 +19,256 @@ class KtExplorePage extends StatefulWidget { State createState() => _KtExplorePageState(); } -class _KtExplorePageState extends State { +class _KtExplorePageState extends State + with AutomaticKeepAliveClientMixin { + final logic = Get.put(KtExploreLogic()); + final state = Get.find().state; @override Widget build(BuildContext context) { - return const Scaffold(); + super.build(context); + return GetBuilder( + builder: (ctrl) { + if (state.loadStatus == KtLoadStatusType.loadNoData || + state.loadStatus == KtLoadStatusType.loadFailed) { + return KtStatusWidget( + type: KtErrorStatusType.nothingYet, + onPressed: logic.fetchDiscoverList, + ); + } + return Container( + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + padding: EdgeInsets.only( + top: ScreenUtil().statusBarHeight, + left: 18.w, + right: 18.w, + ), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('explore_bg.png'.ktIcon), + fit: BoxFit.fill, + ), + ), + child: Stack( + clipBehavior: Clip.none, + children: [ + Column( + children: [ + SizedBox(height: 40.w), + Expanded( + child: EasyRefresh( + controller: logic.refreshCtrl, + header: MaterialHeader(), + footer: MaterialFooter(), + onRefresh: () => logic.fetchDiscoverList(refresh: true), + child: PageView.builder( + scrollDirection: Axis.vertical, + itemCount: state.videoList.length, + controller: logic.pageController, + onPageChanged: (index) => logic.onPageChanged(index), + itemBuilder: (_, index) => + videoPlayerItem(index: index), + ), + ), + ), + ], + ), + Row( + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + Image.asset('ic_explore_ip.png'.ktIcon, width: 56.8.w), + Positioned( + top: 0.w, + right: -2.w, + child: Image.asset( + 'ic_explore_tv.png'.ktIcon, + width: 19.w, + ), + ), + ], + ), + SizedBox(width: 12.w), + Text( + "Unlock Your Next Binge\nObsession", + style: TextStyle( + fontSize: 18.sp, + color: Colors.black, + fontStyle: FontStyle.italic, + fontWeight: FontWeight.w800, + ), + ), + ], + ), + ], + ), + ); + }, + ); } + + Widget videoPlayerItem({int index = 0}) { + return GetBuilder( + builder: (ctrl) { + if (state.controllers.isEmpty) return Container(); + final VideoPlayerController? videoPlayerController = + state.controllers[index]; + if (videoPlayerController == null || + !videoPlayerController.value.isInitialized) { + return Stack( + children: [ + KtNetworkImage( + imageUrl: state.videoList[index].imageUrl!, + width: ScreenUtil().screenWidth - 36.w, + height: 540.w, + fit: BoxFit.cover, + borderRadius: BorderRadius.circular(16.w), + ), + Center(child: CircularProgressIndicator()), + ], + ); + } + KtShortVideoBean item = state.videoList[index]; + return Container( + margin: EdgeInsets.only(bottom: 18.w), + alignment: Alignment.topCenter, + child: GestureDetector( + onTap: () { + videoPlayerController.value.isPlaying + ? videoPlayerController.pause() + : videoPlayerController.play(); + setState(() {}); + }, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + Positioned.fill( + child: VisibilityDetector( + key: ValueKey( + 'discover-video-${state.videoList[index].id}', + ), + onVisibilityChanged: (info) { + if (info.visibleFraction > 0.85 && + state.currentPage == index && + !videoPlayerController.value.isPlaying) { + videoPlayerController.play(); + logic.update(); + } + + if (info.visibleFraction < 0.09 && + videoPlayerController.value.isPlaying) { + videoPlayerController.pause(); + logic.update(); + } + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(16.w), + child: SizedBox( + width: ScreenUtil().screenWidth - 36.w, + height: 540.w, + child: VideoPlayer(videoPlayerController), + ), + ), + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + width: ScreenUtil().screenWidth - 56.w, + margin: EdgeInsets.only(left: 12.w), + child: Text( + item.name ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 16.sp, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ), + SizedBox(height: 11.w), + GestureDetector( + onTap: () { + Get.toNamed( + KtRoutes.shortVideo, + arguments: { + 'shortPlayId': item.shortPlayId, + 'imageUrl': item.imageUrl, + 'isFromDiscover': true, + }, + )?.then((v) { + videoPlayerController.play(); + setState(() {}); + }); + }, + child: Container( + width: ScreenUtil().screenWidth - 60.w, + margin: EdgeInsets.symmetric(horizontal: 12.w), + padding: EdgeInsets.symmetric( + horizontal: 12.w, + vertical: 9.w, + ), + decoration: BoxDecoration( + color: Color(0xFF1E1E20).withValues(alpha: .3), + borderRadius: BorderRadius.circular(100), + ), + child: Row( + children: [ + Image.asset('ic_ep.png'.ktIcon, width: 16.w), + SizedBox(width: 6.w), + Text( + 'Watch the complete series', + style: TextStyle( + fontSize: 12.sp, + color: Colors.white, + fontWeight: FontWeight.w400, + ), + ), + const Spacer(), + Image.asset( + 'ic_right_white.png'.ktIcon, + width: 10.w, + ), + ], + ), + ), + ), + SizedBox(height: 16.w), + + // if (state.controllers[state.currentPage] != null) + // DrVideoProgressBar( + // controller: state.controllers[state.currentPage]!, + // width: ScreenUtil().screenWidth, + // ), + ], + ), + + if (!videoPlayerController.value.isPlaying) + Positioned( + top: 240.h, + child: GestureDetector( + onTap: () { + videoPlayerController.value.isPlaying + ? videoPlayerController.pause() + : videoPlayerController.play(); + + setState(() {}); + }, + child: Image.asset('ic_play.png'.ktIcon, width: 62.w), + ), + ), + ], + ), + ), + ); + }, + ); + } + + @override + bool get wantKeepAlive => true; } diff --git a/lib/kt_pages/kt_home/kt_search_page.dart b/lib/kt_pages/kt_home/kt_search_page.dart new file mode 100644 index 0000000..0749972 --- /dev/null +++ b/lib/kt_pages/kt_home/kt_search_page.dart @@ -0,0 +1,274 @@ +import 'dart:collection'; +import 'dart:convert'; +import 'dart:io'; +import 'package:flustars/flustars.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:flutter_kinetra/kt_pages/kt_routes.dart'; +import 'package:flutter_kinetra/kt_utils/kt_string_extend.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart' hide ScreenUtil; +import 'package:get/get.dart'; + +import '../../../dio_cilent/kt_apis.dart'; +import '../../../kt_utils/kt_keys.dart'; +import '../../../kt_utils/kt_utils.dart'; +import '../../../main.dart'; +import '../../kt_widgets/kt_status_widget.dart'; + +class KtSearchPage extends StatefulWidget { + const KtSearchPage({super.key}); + + @override + SignInActivityPageState createState() => SignInActivityPageState(); +} + +class SignInActivityPageState extends State with RouteAware { + InAppWebViewController? _webViewController; + late PullToRefreshController _webRefreshController; + late Map _userData; + KtLoadStatusType loadingStatus = KtLoadStatusType.loading; + + @override + void initState() { + super.initState(); + _initUserData(); + _initRefreshController(); + } + + void _initUserData() { + _userData = { + 'time_zone': KtUtils.getTimeZoneOffset(DateTime.now()), + 'type': Platform.isAndroid ? 'android' : 'ios', + 'lang': 'en', + // 'theme': 'theme_7', + 'token': SpUtil.getString(KtKeys.token) ?? '', + }; + debugPrint('-----userData:$_userData'); + } + + _initRefreshController() { + _webRefreshController = PullToRefreshController( + settings: PullToRefreshSettings(enabled: true), + onRefresh: () async { + if (Platform.isAndroid) { + _webViewController?.reload(); + } else if (Platform.isIOS) { + _webViewController?.loadUrl( + urlRequest: URLRequest(url: WebUri(KtApis.WEB_SITE_SEARCH)), + ); + } + }, + ); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + routeObserver.subscribe(this, ModalRoute.of(context)! as PageRoute); + } + + // 从别的路由回来 + @override + void didPopNext() { + super.didPopNext(); + _webViewController?.reload(); + } + + @override + void dispose() { + super.dispose(); + _webViewController?.dispose(); + } + + // 处理传递的消息 + void _handleWebMessage(String jsonS) async { + if (jsonS.isEmpty) return; + Map? webParams; + webParams = jsonDecode(jsonS); + if (webParams == null) { + debugPrint("没有获取到传递过来的参数"); + return; + } + Get.toNamed( + KtRoutes.shortVideo, + arguments: {'shortPlayId': webParams['short_play_id']}, + ); + } + + @override + Widget build(BuildContext context) { + return Container( + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + padding: EdgeInsets.only(top: ScreenUtil().statusBarHeight + 30.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('bg2.png'.ktIcon), + fit: BoxFit.fill, + ), + ), + child: Column( + children: [ + _buildAppBar(context), + Expanded( + child: Stack( + children: [ + InAppWebView( + pullToRefreshController: _webRefreshController, + initialSettings: InAppWebViewSettings( + cacheEnabled: false, + javaScriptEnabled: true, + alwaysBounceVertical: true, + allowsBackForwardNavigationGestures: true, + domStorageEnabled: false, + clearCache: true, + transparentBackground: true, + useShouldOverrideUrlLoading: false, + ), + // 注入两个handler + initialUserScripts: UnmodifiableListView([ + UserScript( + source: """ + window.AndroidInterface = { + getUserInfo: function() { + return window.flutter_inappwebview.callHandler('getUserInfo'); + }, + js2app: function(jsonS) { + return window.flutter_inappwebview.callHandler('js2app',jsonS); + } + }; + + """, + injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START, + ), + ]), + onWebViewCreated: (controller) async { + _webViewController = controller; + _webViewController?.addJavaScriptHandler( + handlerName: 'getUserInfo', + callback: (_) => jsonEncode(_userData), + ); + await _webViewController?.addWebMessageListener( + WebMessageListener( + jsObjectName: "openDetail", + allowedOriginRules: {"*"}, + onPostMessage: + (message, sourceOrigin, isMainFrame, replyProxy) { + print('----callback--jsonS:$message'); + if (message?.data != null) { + _handleWebMessage(message?.data); + } + }, + ), + ); + await _webViewController?.loadUrl( + urlRequest: URLRequest( + url: WebUri(KtApis.WEB_SITE_SEARCH), + ), + ); + }, + onLoadStart: (controller, url) { + setState(() { + loadingStatus = KtLoadStatusType.loading; + }); + }, + onLoadStop: (controller, url) async { + // await _webViewController?.evaluateJavascript( + // source: ''' + // document.body.style.backgroundColor = "transparent" + // document.style.backgroundColor = "transparent" + // ''', + // ); + if (Platform.isIOS) { + String userJsonStr = jsonEncode(_userData); + Future.delayed(const Duration(seconds: 1)).then((_) { + controller.evaluateJavascript( + source: + ''' + 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'); + }, + }; + """, + ); + } + setState(() { + loadingStatus = KtLoadStatusType.loadSuccess; + }); + _webRefreshController.endRefreshing(); + }, + onReceivedError: (controller, request, error) { + _webRefreshController.endRefreshing(); + Future.delayed(const Duration(milliseconds: 100)).then((_) { + setState(() { + loadingStatus = KtLoadStatusType.loadFailed; + }); + }); + }, + ), + _buildWidget(), + ], + ), + ), + ], + ), + ); + } + + Widget _buildAppBar(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: Image.asset('ic_back.png'.ktIcon, width: 10.w), + onPressed: () => Navigator.of(context).maybePop(), + ), + Text( + 'Search', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: Colors.black, + ), + ), + Container(width: 24.w), + ], + ); + } + + Widget _buildWidget() { + print('----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(); + } + } +} diff --git a/lib/kt_pages/kt_home/logic.dart b/lib/kt_pages/kt_home/logic.dart index 0c39054..b49e0a9 100644 --- a/lib/kt_pages/kt_home/logic.dart +++ b/lib/kt_pages/kt_home/logic.dart @@ -1,6 +1,7 @@ +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:flutter_kinetra/kt_model/kt_home_category_bean.dart'; import 'package:flutter_kinetra/kt_pages/kt_home/state.dart'; import 'package:get/get.dart'; -import 'package:pull_to_refresh/pull_to_refresh.dart'; import '../../dio_cilent/kt_apis.dart'; import '../../dio_cilent/kt_request.dart'; @@ -10,21 +11,39 @@ import '../../kt_widgets/kt_status_widget.dart'; class KtHomeLogic extends GetxController { final state = KtHomeState(); + final EasyRefreshController easyRefreshController = EasyRefreshController( + controlFinishRefresh: true, + controlFinishLoad: true, + ); + @override void onReady() { - // TODO: implement onReady super.onReady(); - getHomeInfo(); + refreshData(); } - getHomeInfo({RefreshController? refreshCtrl}) async { + refreshData() { + getHomeInfo(); + getMostSearchList(); + } + + loadMoreData() { + if (state.selDiscover == 0) { + easyRefreshController.finishLoad(IndicatorResult.noMore); + return; + } + state.pageIndex++; + getCategoryVideoList(); + } + + getHomeInfo() async { state.loadStatus = KtLoadStatusType.loading; try { - ApiResponse res = await HttpClient().request( + ApiResponse res = await KtHttpClient().request( KtApis.homeAllModules, method: HttpMethod.get, ); - refreshCtrl?.refreshCompleted(); + easyRefreshController.finishRefresh(); if (res.success) { state.loadStatus = KtLoadStatusType.loadSuccess; @@ -57,7 +76,7 @@ class KtHomeLogic extends GetxController { .toList(), ]; } else if (item['module_key'] == 'week_highest_recommend') { - state.trendingList = [ + state.hotList = [ ...item['data'] .map( (item) => @@ -65,13 +84,24 @@ class KtHomeLogic extends GetxController { ) .toList(), ]; - int halfLength = state.trendingList.length ~/ 2; - state.trendingTopList = state.trendingList.sublist(0, halfLength); - state.trendingBottomList = state.trendingList.sublist(halfLength); + } else if (item['module_key'] == 'category_navigation') { + state.categoryList = [ + ...item['data'] + .map( + (item) => KtHomeCategoryBean.fromJson( + item as Map, + ), + ) + .toList(), + ]; + if (state.selCategoryId == -1) { + state.selCategoryId = state.categoryList.first.categoryId ?? -1; + } + getCategoryVideoList(isRefresh: true); } }); - if (state.trendingList.isEmpty && + if (state.hotList.isEmpty && state.bannerList.isEmpty && state.arrivalList.isEmpty) { state.loadStatus = KtLoadStatusType.loadNoData; @@ -85,4 +115,57 @@ class KtHomeLogic extends GetxController { update(); } } + + getMostSearchList() async { + ApiResponse res = await KtHttpClient().request( + KtApis.searchHot, + method: HttpMethod.get, + ); + if (res.success) { + state.mostSearchedList = [ + ...res.data['list'].map((item) => KtShortVideoBean.fromJson(item)), + ]; + if (state.mostSearchedList.length > 3) { + state.mostSearchedList = state.mostSearchedList.sublist(0, 3); + } + update(['trend']); + } + } + + getCategoryVideoList({bool isRefresh = false}) async { + if (isRefresh) { + state.pageIndex = 1; + state.categoryVideoList.clear(); + update(); + } + Map params = { + 'current_page': state.pageIndex, + 'page_size': 20, + }; + if (state.selCategoryId != -1) { + params.putIfAbsent('category_id', () => state.selCategoryId); + } + + ApiResponse res = await KtHttpClient().request( + KtApis.homeVideoList, + method: HttpMethod.get, + queryParameters: params, + ); + easyRefreshController.finishRefresh(); + easyRefreshController.finishLoad(); + if (res.success) { + List list = [ + ...res.data['list'] + .map( + (item) => KtShortVideoBean.fromJson(item as Map), + ) + .toList(), + ]; + if (list.length < 20) { + easyRefreshController.finishLoad(IndicatorResult.noMore); + } + state.categoryVideoList.addAll(list); + update(); + } + } } diff --git a/lib/kt_pages/kt_home/state.dart b/lib/kt_pages/kt_home/state.dart index 8c64901..e555649 100644 --- a/lib/kt_pages/kt_home/state.dart +++ b/lib/kt_pages/kt_home/state.dart @@ -1,14 +1,23 @@ +import '../../kt_model/kt_home_category_bean.dart'; import '../../kt_model/kt_short_video_bean.dart'; import '../../kt_widgets/kt_status_widget.dart'; class KtHomeState { KtLoadStatusType loadStatus = KtLoadStatusType.loading; List topPickList = []; - List trendingList = []; - List trendingTopList = []; - List trendingBottomList = []; + List hotList = []; List arrivalList = []; List bannerList = []; + List mostSearchedList = []; + + List categoryList = []; + List categoryVideoList = []; + num selCategoryId = -1; + + int pageIndex = 1; + int selDiscover = 0; + String selType = 'Hot & Rising'; + List typeList = ['Hot & Rising', 'Top Charts', 'Fresh Drops']; bool showVideo = true; bool hasSubCoin = false; diff --git a/lib/kt_pages/kt_home/view.dart b/lib/kt_pages/kt_home/view.dart index 4c71d92..748fd08 100644 --- a/lib/kt_pages/kt_home/view.dart +++ b/lib/kt_pages/kt_home/view.dart @@ -1,9 +1,16 @@ +import 'package:easy_refresh/easy_refresh.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_kinetra/kt_model/kt_home_category_bean.dart'; import 'package:flutter_kinetra/kt_pages/kt_home/logic.dart'; +import 'package:flutter_kinetra/kt_pages/kt_routes.dart'; import 'package:flutter_kinetra/kt_utils/kt_string_extend.dart'; +import 'package:flutter_kinetra/kt_widgets/kt_status_widget.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; +import '../../kt_model/kt_short_video_bean.dart'; +import '../../kt_widgets/kt_network_image.dart'; + class KtHomePage extends StatefulWidget { const KtHomePage({super.key}); @@ -11,14 +18,26 @@ class KtHomePage extends StatefulWidget { State createState() => _KtHomePageState(); } -class _KtHomePageState extends State { +class _KtHomePageState extends State + with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { final logic = Get.put(KtHomeLogic()); final state = Get.find().state; + late TabController tabCtrl; + @override Widget build(BuildContext context) { + super.build(context); return GetBuilder( assignId: true, - builder: (logic) { + builder: (ctrl) { + if (state.loadStatus == KtLoadStatusType.loadFailed) { + return Center( + child: KtStatusWidget( + type: KtErrorStatusType.nothingYet, + onPressed: logic.refreshData, + ), + ); + } return Stack( children: [ Container( @@ -31,122 +50,19 @@ class _KtHomePageState extends State { fit: BoxFit.fill, ), ), - child: Column( - children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 15.w), - margin: EdgeInsets.only(top: 20.w), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - width: 235.w, - height: 22.w, - padding: EdgeInsets.only(right: 7.w), - alignment: Alignment.centerRight, - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage('home_top_slogan.png'.ktIcon), - fit: BoxFit.fill, - ), - ), - child: Text( - 'Get Hooked in Seconds', - style: TextStyle( - fontSize: 12.sp, - fontWeight: FontWeight.w600, - fontStyle: FontStyle.italic, - color: Colors.white, - ), - ), - ), - Image.asset('ic_search.png'.ktIcon, width: 34.w), - ], - ), - ), - Container( - width: ScreenUtil().screenWidth, - height: 129.h, - padding: EdgeInsets.fromLTRB(29.w, 32.w, 17.w, 15.h), - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage('home_top.png'.ktIcon), - fit: BoxFit.fill, - ), - ), - child: Row( - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Stack( - alignment: Alignment.bottomCenter, - children: [ - Image.asset( - 'text_bg.png'.ktIcon, - height: 17.w, - ), - Text( - 'Trend Cyclone', - style: TextStyle( - fontSize: 14.sp, - color: Color(0xFF1E1E20), - fontWeight: FontWeight.w800, - ), - ), - ], - ), - Image.asset('ic_right.png'.ktIcon, width: 10.w), - ], - ), - SizedBox(height: 5.w), - Text( - 'Everyone\'s Watching', - style: TextStyle( - fontSize: 12.sp, - fontWeight: FontWeight.w400, - color: Color(0xFFAEAEAE), - ), - ), - ], - ), - SizedBox(width: 17.w), - Container( - child: Column( - children: [ - Row( - children: [ - Image.asset( - 'ic_star.png'.ktIcon, - width: 14.w, - ), - SizedBox(width: 4.w), - - SizedBox( - width: 162.w, - child: Text( - 'rebirth: power and beauty', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 13.sp, - color: Colors.black, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - ], - ), - ), - ], - ), - ), - ], + child: EasyRefresh( + controller: logic.easyRefreshController, + header: MaterialHeader(), + footer: MaterialFooter(), + onRefresh: () => logic.refreshData(), + // onLoad: () => logic.loadMoreData(), + canLoadAfterNoMore: false, + child: NestedScrollView( + headerSliverBuilder: (context, _) => [ + SliverToBoxAdapter(child: topAdapterView()), + ], + body: discoverView(), + ), ), ), ], @@ -154,4 +70,1477 @@ class _KtHomePageState extends State { }, ); } + + Widget topAdapterView() { + return Column(children: [searchView(), topTrendView(), picksView()]); + } + + Widget searchView() { + return Container( + padding: EdgeInsets.symmetric(horizontal: 15.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + width: 235.w, + height: 22.w, + padding: EdgeInsets.only(right: 7.w), + alignment: Alignment.centerRight, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('home_top_slogan.png'.ktIcon), + fit: BoxFit.fill, + ), + ), + child: Text( + 'Get Hooked in Seconds', + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w600, + fontStyle: FontStyle.italic, + color: Colors.white, + ), + ), + ), + GestureDetector( + onTap: () => Get.toNamed(KtRoutes.search), + child: Image.asset('ic_search.png'.ktIcon, width: 34.w), + ), + ], + ), + ); + } + + Widget topTrendView() { + return GetBuilder( + id: 'trend', + builder: (ctrl) { + if (state.mostSearchedList.isEmpty) return Container(); + return Container( + width: ScreenUtil().screenWidth, + height: 129.h, + padding: EdgeInsets.fromLTRB(29.w, 24.w, 17.w, 15.h), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('home_top.png'.ktIcon), + fit: BoxFit.fill, + ), + ), + child: Row( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Stack( + alignment: Alignment.bottomCenter, + children: [ + Image.asset('text_bg.png'.ktIcon, height: 17.w), + Text( + 'Trend Cyclone', + style: TextStyle( + fontSize: 14.sp, + color: Color(0xFF1E1E20), + fontWeight: FontWeight.w800, + ), + ), + ], + ), + Image.asset('ic_right.png'.ktIcon, width: 10.w), + ], + ), + SizedBox(height: 5.w), + Text( + 'Everyone\'s Watching', + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w400, + color: Color(0xFFAEAEAE), + ), + ), + ], + ), + SizedBox(width: 17.w), + Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ...state.mostSearchedList.map( + (item) => GestureDetector( + onTap: () => Get.toNamed( + KtRoutes.shortVideo, + arguments: { + 'shortPlayId': item.shortPlayId, + 'imageUrl': item.imageUrl ?? '', + }, + ), + child: Container( + padding: EdgeInsets.symmetric(vertical: 4.w), + child: Row( + children: [ + Image.asset('ic_star.png'.ktIcon, width: 14.w), + SizedBox(width: 4.w), + SizedBox( + width: 162.w, + child: Text( + '${item.name}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13.sp, + color: Colors.black, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ), + ), + SizedBox(height: 2.w), + ], + ), + ], + ), + ); + }, + ); + } + + Widget picksView() { + return GetBuilder( + builder: (ctrl) { + if (state.bannerList.isEmpty && state.topPickList.isEmpty) { + return Container(); + } + return Container( + margin: EdgeInsets.only(bottom: 18.w, top: 10.w), + child: Row( + children: [ + SizedBox(width: 15.w), + if (state.bannerList.isNotEmpty) + Container( + width: 218.w, + height: 160.w, + padding: EdgeInsets.only(top: 3.w), + decoration: BoxDecoration( + border: Border.all(color: Colors.white, width: 4.w), + borderRadius: BorderRadius.circular(10.w), + gradient: LinearGradient( + colors: [Color(0xFFDAEEFE), Colors.white, Colors.white], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: Column( + children: [ + Row( + children: [ + SizedBox(width: 4.w), + Text( + 'Addictive Picks', + style: TextStyle( + fontSize: 15.sp, + color: Colors.black, + fontStyle: FontStyle.italic, + fontWeight: FontWeight.w800, + ), + ), + const Spacer(), + Image.asset('ic_right_arr.png'.ktIcon, width: 17.w), + SizedBox(width: 4.w), + ], + ), + SizedBox(height: 9.w), + SizedBox( + height: 117.w, + child: ListView.separated( + padding: EdgeInsets.only(left: 2.w), + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) => GestureDetector( + onTap: () => Get.toNamed( + KtRoutes.shortVideo, + arguments: { + 'shortPlayId': + state.bannerList[index].shortPlayId, + 'imageUrl': + state.bannerList[index].imageUrl ?? '', + }, + ), + child: KtNetworkImage( + imageUrl: state.bannerList[index].imageUrl ?? '', + borderRadius: BorderRadius.circular(6.w), + ), + ), + separatorBuilder: (_, __) => SizedBox(width: 5.w), + itemCount: state.bannerList.length, + ), + ), + ], + ), + ), + const Spacer(), + if (state.topPickList.isNotEmpty) + Stack( + children: [ + GestureDetector( + onTap: () => Get.toNamed( + KtRoutes.shortVideo, + arguments: { + 'shortPlayId': state.topPickList.first.shortPlayId, + 'imageUrl': state.topPickList.first.imageUrl ?? '', + }, + ), + child: Container( + width: 120.w, + height: 160.w, + decoration: BoxDecoration( + border: Border.all(color: Colors.white, width: 4.w), + borderRadius: BorderRadius.circular(10.w), + gradient: LinearGradient( + colors: [ + Color(0xFFDAEEFE), + Colors.white, + Colors.white, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: KtNetworkImage( + width: 112.w, + height: 152.w, + imageUrl: state.topPickList.first.imageUrl ?? '', + borderRadius: BorderRadius.circular(6.w), + ), + ), + ), + Container( + width: 104.w, + margin: EdgeInsets.only(top: 7.w, left: 8.w, right: 8.w), + child: Row( + children: [ + Text( + 'Top 1', + style: TextStyle( + fontSize: 15.sp, + color: Colors.black, + fontStyle: FontStyle.italic, + fontWeight: FontWeight.w800, + ), + ), + const Spacer(), + Image.asset('ic_right_arr.png'.ktIcon, width: 17.w), + ], + ), + ), + ], + ), + SizedBox(width: 15.w), + ], + ), + ); + }, + ); + } + + Widget discoverView() { + return GetBuilder( + builder: (ctrl) { + return Column( + children: [ + Stack( + alignment: state.selDiscover == 1 + ? Alignment.centerLeft + : Alignment.centerRight, + children: [ + Container( + width: double.infinity, + alignment: state.selDiscover == 0 + ? Alignment.centerLeft + : Alignment.centerRight, + child: Container( + width: 214.w, + height: 50.w, + margin: state.selDiscover == 0 + ? EdgeInsets.only(right: 15.w) + : EdgeInsets.only(left: 15.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + state.selDiscover == 0 + ? 'home_left_sel.png'.ktIcon + : 'home_right_sel.png'.ktIcon, + ), + fit: BoxFit.fill, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset('ip.png'.ktIcon, width: 32.w), + SizedBox(width: 8.w), + Stack( + alignment: Alignment.bottomCenter, + children: [ + Image.asset( + 'text_bg_discover.png'.ktIcon, + height: 17.w, + width: 70.w, + ), + Padding( + padding: EdgeInsets.only(bottom: 4.w), + child: Text( + state.selDiscover == 0 + ? 'Discover' + : 'Categories', + style: TextStyle( + fontSize: 16.sp, + color: Colors.black, + fontStyle: FontStyle.italic, + fontWeight: FontWeight.w800, + ), + ), + ), + ], + ), + ], + ), + ), + ), + GestureDetector( + onTap: () { + state.selDiscover = state.selDiscover == 0 ? 1 : 0; + setState(() {}); + }, + child: Container( + width: 168.w, + height: 50.w, + padding: EdgeInsets.only(top: 10.w), + margin: state.selDiscover == 0 + ? EdgeInsets.only(right: 15.w) + : EdgeInsets.only(left: 15.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + state.selDiscover == 1 + ? 'home_left.png'.ktIcon + : 'home_right.png'.ktIcon, + ), + fit: BoxFit.fill, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + state.selDiscover == 1 ? 'Discover' : 'Categories', + style: TextStyle( + fontSize: 16.sp, + color: Colors.black, + fontStyle: FontStyle.italic, + fontWeight: FontWeight.w800, + ), + ), + ], + ), + ), + ), + ], + ), + Expanded( + child: Container( + width: double.infinity, + color: Colors.white, + padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 10.w), + child: Column( + children: [ + state.selDiscover == 0 + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ...state.typeList.map( + (item) => GestureDetector( + onTap: () { + state.selType = item; + setState(() {}); + tabCtrl.animateTo( + state.typeList.indexOf(item), + ); + }, + child: Container( + width: + (ScreenUtil().screenWidth - 42.w) / 3, + padding: EdgeInsets.symmetric( + vertical: 8.w, + ), + alignment: Alignment.center, + decoration: BoxDecoration( + color: Color(0xFFF6F6F6), + borderRadius: BorderRadius.circular(8.w), + image: state.selType == item + ? DecorationImage( + image: AssetImage( + 'home_item_sel.png'.ktIcon, + ), + fit: BoxFit.fill, + ) + : null, + ), + child: Text( + item, + style: TextStyle( + fontSize: 14.sp, + color: Color(0xFF1E1E20), + fontWeight: state.selType == item + ? FontWeight.w600 + : FontWeight.w400, + ), + ), + ), + ), + ), + ], + ) + : categoryView(), + SizedBox(height: 14.w), + Expanded(child: bottomVideoView()), + ], + ), + ), + ), + ], + ); + }, + ); + } + + Widget categoryView() { + return GetBuilder( + id: "category-items", + builder: (ctrl) { + return SizedBox( + height: 35.w, + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + KtHomeCategoryBean item = state.categoryList[index]; + return GestureDetector( + onTap: () { + state.selCategoryId = item.categoryId!; + logic.getCategoryVideoList(isRefresh: true); + logic.update(['category-items']); + }, + child: Container( + width: (ScreenUtil().screenWidth - 42.w) / 3, + padding: EdgeInsets.symmetric(vertical: 8.w), + alignment: Alignment.center, + decoration: BoxDecoration( + color: Color(0xFFF6F6F6), + borderRadius: BorderRadius.circular(8.w), + image: state.selCategoryId == item.categoryId + ? DecorationImage( + image: AssetImage('home_item_sel.png'.ktIcon), + fit: BoxFit.fill, + ) + : null, + ), + child: Text( + item.categoryName ?? '', + style: TextStyle( + fontSize: 14.sp, + color: Color(0xFF1E1E20), + fontWeight: state.selCategoryId == item.categoryId + ? FontWeight.w600 + : FontWeight.w400, + ), + ), + ), + ); + }, + separatorBuilder: (_, __) => SizedBox(width: 6.w), + itemCount: state.categoryList.length, + ), + ); + }, + ); + } + + Widget bottomVideoView() { + if (state.selDiscover == 0) { + return Expanded( + child: TabBarView( + controller: tabCtrl, + physics: NeverScrollableScrollPhysics(), + children: [hotRisingView(), topChartView(), freshDropView()], + ), + ); + // return [hotRisingView(), topChartView(), freshDropView()][state.typeList + // .indexOf(state.selType)]; + } else { + return categoryVideoView(); + } + } + + Widget hotRisingView() { + return GetBuilder( + builder: (ctrl) { + return GridView.builder( + padding: EdgeInsets.only(bottom: 16.w), + shrinkWrap: true, + // physics: NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 10.w, + crossAxisSpacing: 7.w, + childAspectRatio: 169 / 219, + ), + itemCount: state.hotList.length, + itemBuilder: (BuildContext context, int index) { + KtShortVideoBean video = state.hotList[index]; + return GestureDetector( + onTap: () => Get.toNamed( + KtRoutes.shortVideo, + arguments: { + 'shortPlayId': video.shortPlayId, + 'imageUrl': video.imageUrl ?? '', + }, + ), + child: Container( + width: 169.w, + height: 219.w, + padding: EdgeInsets.all(10.w), + decoration: BoxDecoration( + color: Color(0xFFF5F5F5), + borderRadius: BorderRadius.circular(12.w), + ), + child: Stack( + clipBehavior: Clip.none, + children: [ + Container( + width: 149.w, + height: 199.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + ), + child: KtNetworkImage( + imageUrl: video.imageUrl ?? '', + borderRadius: BorderRadius.circular(6.w), + ), + ), + Positioned( + top: 4.w, + left: 4.w, + child: Image.asset('ic_fire.png'.ktIcon, width: 20.w), + ), + Positioned( + top: -10.w, + right: -11.w, + child: Image.asset( + 'ic_rise_right.png'.ktIcon, + width: 30.w, + ), + ), + Positioned( + bottom: -10.w, + left: -16.w, + child: Container( + width: 181.w, + height: 55.w, + alignment: Alignment.bottomCenter, + padding: EdgeInsets.only( + left: 10.w, + right: 14.w, + bottom: 6.w, + ), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('ic_rise_bottom.png'.ktIcon), + fit: BoxFit.fill, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + SizedBox( + width: 120.w, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + video.name ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w400, + color: Color(0xFF1E1E20), + ), + ), + Text( + video.category?.first ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10.sp, + fontWeight: FontWeight.w400, + color: Color(0xFF79C900), + ), + ), + ], + ), + ), + Image.asset('ic_rise_play.png'.ktIcon, width: 30.w), + ], + ), + ), + ), + ], + ), + ), + ); + }, + ); + }, + ); + } + + Widget topChartView() { + return GetBuilder( + builder: (ctrl) { + return ListView( + padding: EdgeInsets.zero, + children: [ + Container( + width: ScreenUtil().screenWidth - 30.w, + height: 224.w, + padding: EdgeInsets.all(10.w), + decoration: BoxDecoration( + image: DecorationImage(image: AssetImage('top_bg.png'.ktIcon)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + GestureDetector( + onTap: () => Get.toNamed( + KtRoutes.shortVideo, + arguments: { + 'shortPlayId': state.topPickList[1].shortPlayId, + 'imageUrl': state.topPickList[1].imageUrl ?? '', + }, + ), + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + Stack( + alignment: Alignment.bottomCenter, + children: [ + Container( + margin: EdgeInsets.only(bottom: 10.w), + child: Image.asset( + 'top_bottom_bg.png'.ktIcon, + width: 90.w, + height: 55.w, + ), + ), + Container( + width: 33.w, + height: 33.w, + alignment: Alignment.center, + margin: EdgeInsets.only(bottom: 12.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('top_2.png'.ktIcon), + fit: BoxFit.fill, + ), + ), + child: Text( + '2', + style: TextStyle( + fontSize: 12.sp, + color: Colors.black, + fontWeight: FontWeight.w800, + fontStyle: FontStyle.italic, + ), + ), + ), + ], + ), + Stack( + alignment: Alignment.bottomCenter, + clipBehavior: Clip.none, + children: [ + Container( + width: 80.w, + height: 107.w, + margin: EdgeInsets.only(bottom: 56.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.w), + border: Border.all( + width: 2.w, + color: Colors.white, + ), + ), + child: KtNetworkImage( + imageUrl: state.topPickList[1].imageUrl ?? '', + borderRadius: BorderRadius.circular(8.w), + ), + ), + Container( + width: 90.w, + height: 18.w, + margin: EdgeInsets.only(bottom: 56.w), + padding: EdgeInsets.symmetric( + vertical: 2.w, + horizontal: 4.w, + ), + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(100), + ), + child: Text( + state.topPickList[1].name ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10.sp, + color: Color(0xFF1E1E20), + ), + ), + ), + ], + ), + ], + ), + ), + GestureDetector( + onTap: () => Get.toNamed( + KtRoutes.shortVideo, + arguments: { + 'shortPlayId': state.topPickList.first.shortPlayId, + 'imageUrl': state.topPickList.first.imageUrl ?? '', + }, + ), + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + Stack( + alignment: Alignment.bottomCenter, + children: [ + Image.asset( + 'top_bottom_bg.png'.ktIcon, + width: 130.w, + height: 80.w, + ), + Container( + width: 46.w, + height: 46.w, + margin: EdgeInsets.only(bottom: 2.w), + alignment: Alignment.center, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('top_1.png'.ktIcon), + fit: BoxFit.fill, + ), + ), + child: Text( + '1', + style: TextStyle( + fontSize: 16.sp, + color: Colors.black, + fontWeight: FontWeight.w800, + fontStyle: FontStyle.italic, + ), + ), + ), + ], + ), + Stack( + alignment: Alignment.bottomCenter, + clipBehavior: Clip.none, + children: [ + Container( + width: 104.w, + height: 138.w, + margin: EdgeInsets.only(bottom: 66.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.w), + border: Border.all( + width: 2.w, + color: Colors.white, + ), + ), + child: KtNetworkImage( + imageUrl: + state.topPickList.first.imageUrl ?? '', + borderRadius: BorderRadius.circular(8.w), + ), + ), + Container( + width: 120.w, + height: 18.w, + margin: EdgeInsets.only(bottom: 66.w), + padding: EdgeInsets.symmetric( + vertical: 2.w, + horizontal: 4.w, + ), + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(100), + ), + child: Text( + state.topPickList.first.name ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10.sp, + color: Color(0xFF1E1E20), + ), + ), + ), + ], + ), + ], + ), + ), + GestureDetector( + onTap: () => Get.toNamed( + KtRoutes.shortVideo, + arguments: { + 'shortPlayId': state.topPickList[2].shortPlayId, + 'imageUrl': state.topPickList[2].imageUrl ?? '', + }, + ), + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + Stack( + alignment: Alignment.bottomCenter, + children: [ + Container( + margin: EdgeInsets.only(bottom: 10.w), + child: Image.asset( + 'top_bottom_bg.png'.ktIcon, + width: 90.w, + height: 55.w, + ), + ), + Container( + width: 33.w, + height: 33.w, + margin: EdgeInsets.only(bottom: 12.w), + alignment: Alignment.center, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('top_3.png'.ktIcon), + fit: BoxFit.fill, + ), + ), + child: Text( + '3', + style: TextStyle( + fontSize: 12.sp, + color: Colors.black, + fontWeight: FontWeight.w800, + fontStyle: FontStyle.italic, + ), + ), + ), + ], + ), + Stack( + alignment: Alignment.bottomCenter, + clipBehavior: Clip.none, + children: [ + Container( + width: 80.w, + height: 107.w, + margin: EdgeInsets.only(bottom: 56.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.w), + border: Border.all( + width: 2.w, + color: Colors.white, + ), + ), + child: KtNetworkImage( + imageUrl: state.topPickList[2].imageUrl ?? '', + borderRadius: BorderRadius.circular(8.w), + ), + ), + Container( + width: 90.w, + height: 18.w, + margin: EdgeInsets.only(bottom: 56.w), + padding: EdgeInsets.symmetric( + vertical: 2.w, + horizontal: 4.w, + ), + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(100), + ), + child: Text( + state.topPickList[2].name ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10.sp, + color: Color(0xFF1E1E20), + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + SizedBox(height: 10.w), + if (state.topPickList.length > 3) + Expanded( + child: ListView.separated( + padding: EdgeInsets.zero, + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: state.topPickList.length - 3, + itemBuilder: (BuildContext context, int index) { + KtShortVideoBean video = state.topPickList[index + 3]; + return GestureDetector( + onTap: () => Get.toNamed( + KtRoutes.shortVideo, + arguments: { + 'shortPlayId': video.shortPlayId, + 'imageUrl': video.imageUrl ?? '', + }, + ), + child: Container( + width: ScreenUtil().screenWidth - 30.w, + padding: EdgeInsets.all(10.w), + decoration: BoxDecoration( + color: Color(0xFFF5F5F5), + borderRadius: BorderRadius.circular(14.w), + ), + child: Row( + children: [ + Stack( + clipBehavior: Clip.none, + alignment: Alignment.topLeft, + children: [ + KtNetworkImage( + imageUrl: video.imageUrl ?? '', + width: 100.w, + height: 127.w, + borderRadius: BorderRadius.circular(12.w), + ), + Container( + width: 24.w, + height: 24.w, + alignment: Alignment.center, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('top_other.png'.ktIcon), + fit: BoxFit.fill, + ), + ), + child: Text( + '${index + 4}', + style: TextStyle( + fontSize: 12.sp, + color: Colors.black, + fontWeight: FontWeight.w800, + fontStyle: FontStyle.italic, + ), + ), + ), + ], + ), + SizedBox(width: 12.w), + SizedBox( + width: 200.w, + height: 127.w, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + video.name ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w500, + color: Color(0xFF1E1E20), + ), + ), + Text( + video.categoryList?.first.name ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w400, + color: Color(0xFF79C900), + ), + ), + Text( + video.description ?? '', + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10.sp, + fontWeight: FontWeight.w400, + color: Color(0xFF5E5E5E), + ), + ), + + Row( + children: [ + Container( + padding: EdgeInsets.symmetric( + horizontal: 14.w, + vertical: 7.w, + ), + decoration: BoxDecoration( + color: Color(0xFF1E1E20), + borderRadius: BorderRadius.circular( + 100, + ), + ), + child: Row( + children: [ + Image.asset( + 'ic_top_play.png'.ktIcon, + width: 16.w, + ), + SizedBox(width: 4.w), + Text( + 'Play', + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w500, + color: Color(0xFFA7F62F), + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ); + }, + separatorBuilder: (_, __) => SizedBox(height: 10.w), + ), + ), + ], + ); + }, + ); + } + + Widget freshDropView() { + return GetBuilder( + builder: (ctrl) { + return ListView.separated( + padding: EdgeInsets.only(bottom: 20.w), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: state.arrivalList.length, + itemBuilder: (BuildContext context, int index) { + KtShortVideoBean video = state.arrivalList[index]; + if (index == 0) { + return GestureDetector( + onTap: () => Get.toNamed( + KtRoutes.shortVideo, + arguments: { + 'shortPlayId': video.shortPlayId, + 'imageUrl': video.imageUrl ?? '', + }, + ), + child: Container( + width: ScreenUtil().screenWidth - 30.w, + decoration: BoxDecoration( + color: Color(0xFFFFFBCA), + borderRadius: BorderRadius.circular(14.w), + ), + child: Column( + children: [ + SizedBox(height: 10.w), + Row( + children: [ + SizedBox(width: 10.w), + Image.asset('ic_drop_dot.png'.ktIcon, width: 12.w), + SizedBox(width: 5.w), + Text( + 'Today\'s Drops', + style: TextStyle( + fontSize: 15.sp, + fontWeight: FontWeight.w700, + color: Colors.black, + fontStyle: FontStyle.italic, + ), + ), + Container( + height: 12.w, + width: 1.w, + color: Color(0xFFBFBFBF), + margin: EdgeInsets.symmetric(horizontal: 8.w), + ), + Text( + 'Daily updates, Stay tuned!', + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w400, + color: Color(0xFF616161), + ), + ), + ], + ), + SizedBox(height: 10.w), + Container( + padding: EdgeInsets.all(10.w), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: .6), + borderRadius: BorderRadius.circular(14.w), + ), + child: Row( + children: [ + KtNetworkImage( + imageUrl: video.imageUrl ?? '', + width: 108.w, + height: 144.w, + borderRadius: BorderRadius.circular(12.w), + ), + SizedBox(width: 12.w), + SizedBox( + width: 204.w, + height: 144.w, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + video.name ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w500, + color: Color(0xFF1E1E20), + ), + ), + SizedBox(height: 10.w), + Text( + video.description ?? '', + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10.sp, + fontWeight: FontWeight.w400, + color: Color(0xFF5E5E5E), + ), + ), + + Container( + margin: EdgeInsets.symmetric( + vertical: 11.w, + ), + height: 1.w, + color: Color( + 0xFFE8E4BD, + ).withValues(alpha: .7), + ), + Row( + children: [ + Container( + padding: EdgeInsets.symmetric( + horizontal: 14.w, + vertical: 7.w, + ), + decoration: BoxDecoration( + color: Color(0xFF1E1E20), + borderRadius: BorderRadius.circular( + 100, + ), + ), + child: Row( + children: [ + Image.asset( + 'ic_top_play.png'.ktIcon, + width: 16.w, + ), + SizedBox(width: 4.w), + Text( + 'Play', + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w500, + color: Color(0xFFA7F62F), + ), + ), + ], + ), + ), + const Spacer(), + Text( + video.category?.first ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w400, + color: Color(0xFF79C900), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + return GestureDetector( + onTap: () => Get.toNamed( + KtRoutes.shortVideo, + arguments: { + 'shortPlayId': video.shortPlayId, + 'imageUrl': video.imageUrl ?? '', + }, + ), + child: Container( + width: ScreenUtil().screenWidth - 30.w, + padding: EdgeInsets.symmetric(horizontal: 10.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14.w), + ), + child: Row( + children: [ + KtNetworkImage( + imageUrl: video.imageUrl ?? '', + width: 108.w, + height: 144.w, + borderRadius: BorderRadius.circular(12.w), + ), + SizedBox(width: 12.w), + SizedBox( + width: 204.w, + height: 144.w, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + video.name ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w500, + color: Color(0xFF1E1E20), + ), + ), + SizedBox(height: 10.w), + Text( + video.description ?? '', + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10.sp, + fontWeight: FontWeight.w400, + color: Color(0xFF5E5E5E), + ), + ), + + Container( + margin: EdgeInsets.symmetric(vertical: 11.w), + height: 1.w, + color: Color(0xFFE8E4BD).withValues(alpha: .7), + ), + Row( + children: [ + Container( + padding: EdgeInsets.symmetric( + horizontal: 14.w, + vertical: 7.w, + ), + decoration: BoxDecoration( + color: Color(0xFF1E1E20), + borderRadius: BorderRadius.circular(100), + ), + child: Row( + children: [ + Image.asset( + 'ic_top_play.png'.ktIcon, + width: 16.w, + ), + SizedBox(width: 4.w), + Text( + 'Play', + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w500, + color: Color(0xFFA7F62F), + ), + ), + ], + ), + ), + const Spacer(), + Text( + video.category?.first ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w400, + color: Color(0xFF79C900), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ); + }, + separatorBuilder: (_, __) => SizedBox(height: 10.w), + ); + }, + ); + } + + Widget categoryVideoView() { + return GetBuilder( + id: 'category-list', + builder: (ctrl) { + if (state.categoryVideoList.isEmpty) { + return KtStatusWidget( + type: KtErrorStatusType.nothingYet, + onPressed: logic.getCategoryVideoList, + ); + } + return GridView.builder( + padding: EdgeInsets.only(bottom: 16.w), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 7.w, + crossAxisSpacing: 7.w, + childAspectRatio: 169 / 238, + ), + itemCount: state.categoryVideoList.length, + itemBuilder: (BuildContext context, int index) { + KtShortVideoBean video = state.categoryVideoList[index]; + return GestureDetector( + onTap: () => Get.toNamed( + KtRoutes.shortVideo, + arguments: { + 'shortPlayId': video.shortPlayId, + 'imageUrl': video.imageUrl ?? '', + }, + ), + child: Container( + width: 169.w, + height: 219.w, + padding: EdgeInsets.fromLTRB(8.w, 8.w, 8.w, 0.w), + decoration: BoxDecoration( + color: Color(0xFFF5F5F5), + borderRadius: BorderRadius.circular(14.w), + ), + child: Column( + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + Container( + width: 153.w, + height: 203.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + ), + child: KtNetworkImage( + imageUrl: video.imageUrl ?? '', + borderRadius: BorderRadius.circular(6.w), + ), + ), + Positioned( + bottom: 5.w, + right: 5.w, + child: Image.asset( + 'ic_rise_play.png'.ktIcon, + width: 30.w, + ), + ), + ], + ), + SizedBox(height: 5.w), + SizedBox( + width: 151.w, + child: Center( + child: Text( + video.name ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12.sp, + color: Color(0xFF616161), + fontWeight: FontWeight.w400, + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); + }, + ); + } + + @override + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + tabCtrl = TabController(length: 3, vsync: this); + } + + @override + void dispose() { + tabCtrl.dispose(); + super.dispose(); + } } diff --git a/lib/kt_pages/kt_mine/kt_about_us_page.dart b/lib/kt_pages/kt_mine/kt_about_us_page.dart new file mode 100644 index 0000000..69d1fa0 --- /dev/null +++ b/lib/kt_pages/kt_mine/kt_about_us_page.dart @@ -0,0 +1,161 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_kinetra/kt_utils/kt_string_extend.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../../dio_cilent/kt_apis.dart'; +import '../../kt_utils/kt_device_info_utils.dart'; +import '../kt_webview_page.dart'; + +class KtAboutUsPage extends StatefulWidget { + const KtAboutUsPage({super.key}); + + @override + State createState() => _AboutUsPageState(); +} + +class _AboutUsPageState extends State { + String versionNum = ''; + String appName = 'Kinetra'; + final List serviceList = [ + {'title': 'Privacy Policy', 'url': KtApis.WEB_SITE_PRIVATE}, + {'title': 'User Agreement', 'url': KtApis.WEB_SITE_POLICY}, + {'title': 'About Us', 'url': KtApis.WEB_SITE_INDEX}, + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('bg2.png'.ktIcon), + fit: BoxFit.fill, + ), + ), + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + child: SafeArea( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: Image.asset('ic_back.png'.ktIcon, width: 10.w), + onPressed: () => Navigator.of(context).maybePop(), + ), + Text( + 'About', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w700, + color: Colors.black, + ), + ), + Container(width: 24.w), + ], + ), + SizedBox(height: 40.w), + ClipRRect( + borderRadius: BorderRadius.circular(20.r), + child: Image.asset( + 'ic_logo.png'.ktIcon, + width: 84.w, + fit: BoxFit.cover, + ), + ), + SizedBox(height: 7.w), + Text( + appName, + style: TextStyle( + fontSize: 18.sp, + color: Colors.black, + fontWeight: FontWeight.w700, + ), + ), + Text( + 'Version: $versionNum', + style: TextStyle(fontSize: 12.sp, color: Colors.black), + ), + SizedBox(height: 40.w), + ...serviceList.map( + (item) => Column( + children: [ + GestureDetector( + onTap: () { + if (item['title'] == 'About Us') { + _openUrl(item['url'].toString()); + return; + } + Get.to( + () => KtWebViewPage( + url: item['url'].toString(), + title: item['title'], + ), + ); + }, + child: Container( + color: Colors.transparent, + padding: EdgeInsets.symmetric(horizontal: 30.w), + child: Row( + children: [ + Text( + item['title'].toString(), + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + ), + const Spacer(), + Image.asset( + 'ic_right_arrow.png'.ktIcon, + width: 10.w, + ), + ], + ), + ), + ), + Container( + margin: EdgeInsets.symmetric( + horizontal: 30.w, + vertical: 20.w, + ), + color: Colors.black.withValues(alpha: .1), + width: double.infinity, + height: 1 / MediaQuery.of(context).devicePixelRatio, + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + + @override + void initState() { + initData(); + super.initState(); + } + + initData() { + versionNum = KtDeviceInfoUtil().appVersion ?? ''; + } + + void _openUrl(String url) async { + try { + final encodedUrl = Uri.encodeFull(url); + await launchUrl( + Uri.parse(encodedUrl), + mode: LaunchMode.externalApplication, + ); + } catch (e) { + debugPrint('---err:$e'); + } + } +} diff --git a/lib/kt_pages/kt_mine/kt_webview.dart b/lib/kt_pages/kt_mine/kt_webview.dart new file mode 100644 index 0000000..8035212 --- /dev/null +++ b/lib/kt_pages/kt_mine/kt_webview.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class KtKtWebViewPage extends StatefulWidget { + final String url; + final String? title; + + const KtKtWebViewPage({super.key, required this.url, this.title}); + + @override + State createState() => _KtWebViewPageState(); +} + +class _KtWebViewPageState extends State { + late final WebViewController _controller; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate( + onPageFinished: (_) => setState(() => _isLoading = false), + ), + ) + ..loadRequest(Uri.parse(widget.url)); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(widget.title ?? '')), + body: Stack( + children: [ + WebViewWidget(controller: _controller), + if (_isLoading) const Center(child: CircularProgressIndicator()), + ], + ), + ); + } +} diff --git a/lib/kt_pages/kt_mine/logic.dart b/lib/kt_pages/kt_mine/logic.dart index 859afdf..c43ca84 100644 --- a/lib/kt_pages/kt_mine/logic.dart +++ b/lib/kt_pages/kt_mine/logic.dart @@ -25,7 +25,7 @@ class KtMineLogic extends GetxController { getUserInfo() async { try { - ApiResponse res = await HttpClient().request( + ApiResponse res = await KtHttpClient().request( KtApis.getCustomerInfo, method: HttpMethod.get, ); diff --git a/lib/kt_pages/kt_mine/view.dart b/lib/kt_pages/kt_mine/view.dart index 1219ec9..9d5ef2c 100644 --- a/lib/kt_pages/kt_mine/view.dart +++ b/lib/kt_pages/kt_mine/view.dart @@ -1,8 +1,14 @@ import 'package:flutter/material.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'; +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_apis.dart'; +import '../kt_webview_page.dart'; class KtMinePage extends StatefulWidget { const KtMinePage({super.key}); @@ -12,6 +18,9 @@ class KtMinePage extends StatefulWidget { } class _KtMinePageState extends State { + final logic = Get.put(KtMineLogic()); + final state = Get.find().state; + @override Widget build(BuildContext context) { return Scaffold( @@ -25,6 +34,217 @@ class _KtMinePageState extends State { fit: BoxFit.fill, ), ), + child: GetBuilder( + builder: (ctrl) { + return Column( + children: [ + Text( + 'Profile', + style: TextStyle( + fontSize: 18.sp, + color: Colors.black, + fontWeight: FontWeight.w800, + fontStyle: FontStyle.italic, + ), + ), + Expanded( + child: SmartRefresher( + controller: logic.refreshController, + enablePullDown: true, + 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, + ), + ), + 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, + ), + SizedBox(width: 13.w), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + 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, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + SizedBox(height: 30.w), + settingsView(), + ], + ), + ), + ), + ], + ); + }, + ), + ), + ); + } + + Widget settingsView() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Settings & More', + style: TextStyle( + fontSize: 15.sp, + color: Colors.black, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 12.w), + Container( + width: ScreenUtil().screenWidth - 36.w, + padding: EdgeInsets.symmetric(horizontal: 12.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(18.w), + image: DecorationImage( + image: AssetImage('setting_bg.png'.ktIcon), + fit: BoxFit.fill, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + handleItem( + 'ic_privacy.png', + 'Privacy Policy', + () => Get.to( + () => KtWebViewPage( + url: KtApis.WEB_SITE_PRIVATE, + title: "Privacy Policy", + ), + ), + ), + handleItem( + 'ic_agreement.png', + 'User Agreement', + () => Get.to( + () => KtWebViewPage( + url: KtApis.WEB_SITE_POLICY, + title: "User Agreement", + ), + ), + ), + handleItem( + 'ic_help_center.png', + 'Help Center', + () => Get.toNamed(KtRoutes.search), + ), + handleItem( + 'ic_about_us.png', + 'About Us', + () => Get.toNamed(KtRoutes.aboutUs), + ), + ], + ), + ), + ], + ); + } + + Widget handleItem(String icon, String title, Function func) { + return GestureDetector( + onTap: () { + func.call(); + }, + child: Container( + padding: EdgeInsets.symmetric(vertical: 15.w), + color: Colors.transparent, + child: Row( + children: [ + Image.asset(icon.ktIcon, width: 20.w), + SizedBox(width: 10.w), + Text( + title, + style: TextStyle( + fontSize: 14.sp, + color: Colors.black, + fontWeight: FontWeight.w400, + ), + ), + const Spacer(), + Image.asset('ic_right_arrow.png'.ktIcon, width: 14.w), + ], + ), ), ); } diff --git a/lib/kt_pages/kt_my_list/kt_my_chest_page.dart b/lib/kt_pages/kt_my_list/kt_my_chest_page.dart new file mode 100644 index 0000000..1d0760e --- /dev/null +++ b/lib/kt_pages/kt_my_list/kt_my_chest_page.dart @@ -0,0 +1,158 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_kinetra/kt_model/kt_short_video_bean.dart'; +import 'package:flutter_kinetra/kt_utils/kt_string_extend.dart'; +import 'package:flutter_kinetra/kt_widgets/kt_status_widget.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:pull_to_refresh/pull_to_refresh.dart'; + +import '../../kt_widgets/kt_network_image.dart'; +import '../kt_routes.dart'; +import 'logic.dart'; + +class KtMyChestPage extends StatefulWidget { + const KtMyChestPage({super.key}); + + @override + State createState() => _KtMyChestPageState(); +} + +class _KtMyChestPageState extends State { + final logic = Get.put(MyListLogic()); + final state = Get.find().state; + + @override + void initState() { + super.initState(); + logic.getCollectList(refresh: true); + } + + @override + Widget build(BuildContext context) { + return Container( + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + padding: EdgeInsets.only(top: ScreenUtil().statusBarHeight), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('bg2.png'.ktIcon), + fit: BoxFit.fill, + ), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: Image.asset('ic_back.png'.ktIcon, width: 10.w), + onPressed: () => Navigator.of(context).maybePop(), + ), + Text( + 'My Treasure Chest', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: Colors.black, + ), + ), + Container(width: 24.w), + ], + ), + GetBuilder( + builder: (ctrl) { + if (state.loadStatus == KtLoadStatusType.loading) { + return Container(); + } + + if (state.favoriteList.isEmpty && + state.loadStatus != KtLoadStatusType.loading) { + return Container( + margin: EdgeInsets.only(top: 60.w), + child: KtStatusWidget( + type: KtErrorStatusType.nothingYet, + onPressed: logic.initData, + ), + ); + } + return Expanded( + child: SmartRefresher( + controller: logic.chestRefreshCtrl, + enablePullUp: true, + enablePullDown: true, + onRefresh: () => logic.getCollectList(refresh: true), + onLoading: () => logic.getCollectList(loadMore: true), + child: GridView.builder( + padding: EdgeInsets.only( + bottom: 16.w, + left: 15.w, + right: 15.w, + ), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + mainAxisSpacing: 18.w, + crossAxisSpacing: 6.w, + childAspectRatio: 111 / 168, + ), + itemCount: state.favoriteList.length, + itemBuilder: (BuildContext context, int index) { + KtShortVideoBean video = state.favoriteList[index]; + return GestureDetector( + onTap: () => Get.toNamed( + KtRoutes.shortVideo, + arguments: { + 'shortPlayId': video.shortPlayId, + 'imageUrl': video.imageUrl ?? '', + }, + ), + child: Column( + children: [ + Stack( + children: [ + KtNetworkImage( + imageUrl: video.imageUrl ?? '', + width: 111.w, + height: 148.w, + borderRadius: BorderRadius.circular(12.w), + ), + Positioned( + right: 4.w, + top: 4.w, + child: GestureDetector( + onTap: () => + logic.cancelCollect(video.shortPlayId!), + child: Image.asset( + 'ic_collect_sel.png'.ktIcon, + width: 28.w, + ), + ), + ), + ], + ), + SizedBox( + width: 111.w, + child: Text( + video.name ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12.sp, + color: Color(0xFF1E1E20), + fontWeight: FontWeight.w400, + ), + ), + ), + ], + ), + ); + }, + ), + ), + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/kt_pages/kt_my_list/logic.dart b/lib/kt_pages/kt_my_list/logic.dart new file mode 100644 index 0000000..160b5f6 --- /dev/null +++ b/lib/kt_pages/kt_my_list/logic.dart @@ -0,0 +1,185 @@ +import 'package:flutter_kinetra/kt_model/kt_history_video_bean.dart'; +import 'package:flutter_kinetra/kt_pages/kt_my_list/state.dart'; +import 'package:flutter_kinetra/kt_widgets/kt_dialog.dart'; +import 'package:get/get.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_short_video_bean.dart'; +import '../../kt_widgets/kt_status_widget.dart'; + +class MyListLogic extends GetxController { + final state = MyListState(); + + final refreshCtrl = RefreshController(); + final chestRefreshCtrl = RefreshController(); + + @override + onReady() { + super.onReady(); + initData(); + } + + @override + void onClose() { + super.onClose(); + refreshCtrl.dispose(); + chestRefreshCtrl.dispose(); + } + + initData() { + state.loadStatus = KtLoadStatusType.loading; + getCollectList(refresh: true); + getHistoryList(refresh: true); + } + + getCollectList({bool refresh = false, bool loadMore = false}) async { + state.loadStatus = KtLoadStatusType.loading; + if (refresh) { + state.curFavoriteIndex = 1; + state.favoriteList.clear(); + state.chestList.clear(); + } + if (loadMore) { + state.curFavoriteIndex++; + } + refreshCtrl.refreshCompleted(); + chestRefreshCtrl.refreshCompleted(); + try { + ApiResponse res = await KtHttpClient().request( + KtApis.getMyCollections, + queryParameters: { + 'current_page': state.curFavoriteIndex, + 'page_size': 20, + }, + method: HttpMethod.get, + ); + if (res.success) { + state.loadStatus = KtLoadStatusType.loadSuccess; + List list = [ + ...res.data['list'].map((item) => KtShortVideoBean.fromJson(item)), + ]; + if (list.length < 20) { + refreshCtrl.loadNoData(); + chestRefreshCtrl.loadNoData(); + } + state.favoriteList.addAll(list); + if (state.favoriteList.length > 3) { + state.chestList = state.favoriteList.sublist(0, 3); + } else { + state.chestList = state.favoriteList; + } + update(); + } else { + refreshCtrl.loadFailed(); + chestRefreshCtrl.loadFailed(); + } + } catch (e) { + state.loadStatus = KtLoadStatusType.loadFailed; + refreshCtrl.loadFailed(); + chestRefreshCtrl.loadFailed(); + update(); + } + } + + getHistoryList({bool refresh = false, bool loadMore = false}) async { + if (refresh) { + state.curHistoryIndex = 1; + state.historyList.clear(); + } + if (loadMore) { + state.curHistoryIndex++; + } + refreshCtrl.refreshCompleted(); + try { + ApiResponse res = await KtHttpClient().request( + KtApis.getMyHistoryList, + queryParameters: { + 'current_page': state.curHistoryIndex, + 'page_size': 20, + }, + method: HttpMethod.get, + ); + if (res.success) { + state.loadStatus = KtLoadStatusType.loadSuccess; + List list = [ + ...res.data['list'].map((item) => KtHistoryVideoBean.fromJson(item)), + ]; + if (list.length < 20) refreshCtrl.loadNoData(); + + state.historyList.addAll(list); + update(); + } else { + refreshCtrl.loadFailed(); + } + } catch (e) { + state.loadStatus = KtLoadStatusType.loadFailed; + update(); + } + } + + Future likeVideo(KtHistoryVideoBean video) async { + Map params = { + "short_play_id": video.shortPlayId, + "video_id": video.shortPlayVideoId, + }; + if (video.isCollect == 1) { + Get.dialog( + KtDialog( + title: 'Remove from your list?', + subTitle: 'This drama will be removed from your saved list.', + rightBtnText: 'Remove', + rightBtnIcon: 'ic_dialog_delete.png', + rightBtnFunc: () async { + ApiResponse res = await KtHttpClient().request( + KtApis.deleteFavoriteVideo, + data: params, + ); + if (res.success) { + video.isCollect = 0; + getCollectList(refresh: true); + update(); + } + }, + ), + ); + } else { + ApiResponse res = await KtHttpClient().request( + KtApis.collectVideo, + data: params, + ); + if (res.success) { + video.isCollect = 1; + getCollectList(refresh: true); + update(); + } + } + } + + cancelCollect(num id) async { + Get.dialog( + KtDialog( + title: 'Remove from your list?', + subTitle: 'This drama will be removed from your saved list.', + rightBtnText: 'Remove', + rightBtnIcon: 'ic_dialog_delete.png', + rightBtnFunc: () async { + ApiResponse res = await KtHttpClient().request( + KtApis.deleteFavoriteVideo, + queryParameters: {'short_play_id': id}, + ); + if (res.success) { + state.favoriteList.removeWhere((item) => id == item.shortPlayId); + state.chestList.removeWhere((item) => id == item.shortPlayId); + state.historyList + .firstWhereOrNull((item) => id == item.shortPlayId) + ?.isCollect = + 0; + update(); + } + }, + ), + ); + } +} diff --git a/lib/kt_pages/kt_my_list/state.dart b/lib/kt_pages/kt_my_list/state.dart new file mode 100644 index 0000000..f3b633e --- /dev/null +++ b/lib/kt_pages/kt_my_list/state.dart @@ -0,0 +1,14 @@ +import 'package:flutter_kinetra/kt_model/kt_short_video_bean.dart'; + +import '../../kt_model/kt_history_video_bean.dart'; +import '../../kt_widgets/kt_status_widget.dart'; + +class MyListState { + KtLoadStatusType loadStatus = KtLoadStatusType.loading; + + List favoriteList = []; + List chestList = []; + List historyList = []; + int curFavoriteIndex = 1; + int curHistoryIndex = 1; +} diff --git a/lib/kt_pages/kt_my_list/view.dart b/lib/kt_pages/kt_my_list/view.dart index 9bd83f5..8379b09 100644 --- a/lib/kt_pages/kt_my_list/view.dart +++ b/lib/kt_pages/kt_my_list/view.dart @@ -1,4 +1,16 @@ import 'package:flutter/material.dart'; +import 'package:flutter_kinetra/kt_model/kt_short_video_bean.dart'; +import 'package:flutter_kinetra/kt_pages/kt_my_list/kt_my_chest_page.dart'; +import 'package:flutter_kinetra/kt_utils/kt_string_extend.dart'; +import 'package:flutter_kinetra/kt_widgets/kt_status_widget.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:pull_to_refresh/pull_to_refresh.dart'; + +import '../../kt_model/kt_history_video_bean.dart'; +import '../../kt_widgets/kt_network_image.dart'; +import '../kt_routes.dart'; +import 'logic.dart'; class KtMyListPage extends StatefulWidget { const KtMyListPage({super.key}); @@ -8,8 +20,308 @@ class KtMyListPage extends StatefulWidget { } class _KtMyListPageState extends State { + final logic = Get.put(MyListLogic()); + final state = Get.find().state; + @override Widget build(BuildContext context) { - return const Scaffold(); + return GetBuilder( + builder: (ctrl) { + if (state.loadStatus == KtLoadStatusType.loading) return Container(); + return Container( + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + padding: EdgeInsets.only(top: kToolbarHeight), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('bg2.png'.ktIcon), + fit: BoxFit.fill, + ), + ), + child: state.chestList.isEmpty && state.historyList.isEmpty + ? KtStatusWidget( + type: KtErrorStatusType.nothingYet, + onPressed: logic.initData, + ) + : SmartRefresher( + controller: logic.refreshCtrl, + enablePullUp: true, + enablePullDown: true, + onRefresh: () => logic.initData(), + onLoading: () => logic.getHistoryList(loadMore: true), + child: Column( + children: [ + if (state.chestList.isNotEmpty) + Container( + width: ScreenUtil().screenWidth - 30.w, + padding: EdgeInsets.fromLTRB(6.w, 53.w, 6.w, 6.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('collect_bg.png'.ktIcon), + fit: BoxFit.fill, + ), + ), + child: Column( + children: [ + Row( + children: [ + SizedBox(width: 6.w), + Text( + 'My Treasure Chest', + style: TextStyle( + fontSize: 18.sp, + color: Colors.white, + fontWeight: FontWeight.w800, + fontStyle: FontStyle.italic, + ), + ), + const Spacer(), + GestureDetector( + onTap: () => Get.to(KtMyChestPage()), + child: Row( + children: [ + Text( + 'View All', + style: TextStyle( + fontSize: 12.sp, + color: Color(0xFFD6D6D6), + ), + ), + Image.asset( + 'ic_right_white.png'.ktIcon, + width: 10.w, + ), + SizedBox(width: 6.w), + ], + ), + ), + ], + ), + SizedBox(height: 15.w), + Container( + height: 145.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14.w), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + ...state.chestList.map( + (video) => Container( + margin: EdgeInsets.symmetric( + horizontal: 5.w, + ), + child: Stack( + children: [ + GestureDetector( + onTap: () => Get.toNamed( + KtRoutes.shortVideo, + arguments: { + 'shortPlayId': + video.shortPlayId, + 'imageUrl': + video.imageUrl ?? '', + }, + ), + child: KtNetworkImage( + imageUrl: video.imageUrl ?? '', + width: 100.w, + height: 133.w, + borderRadius: + BorderRadius.circular(6.w), + ), + ), + Positioned( + right: 4.w, + top: 4.w, + child: GestureDetector( + onTap: () => + logic.cancelCollect( + video.shortPlayId!, + ), + child: Image.asset( + 'ic_collect_sel.png'.ktIcon, + width: 28.w, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ], + ), + ), + SizedBox(height: 15.w), + Expanded( + child: Container( + width: ScreenUtil().screenWidth, + padding: EdgeInsets.all(15.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical( + top: Radius.circular(20.w), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Continue Watching', + style: TextStyle( + fontSize: 18.sp, + color: Color(0xFF1E1E20), + fontWeight: FontWeight.w800, + fontStyle: FontStyle.italic, + ), + ), + Expanded( + child: ListView.separated( + padding: EdgeInsets.only(top: 10.w), + // shrinkWrap: true, + // physics: NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + KtHistoryVideoBean video = + state.historyList[index]; + return GestureDetector( + onTap: () => Get.toNamed( + KtRoutes.shortVideo, + arguments: { + 'shortPlayId': video.shortPlayId, + 'imageUrl': video.imageUrl ?? '', + }, + ), + child: Container( + color: Colors.transparent, + child: Row( + children: [ + KtNetworkImage( + imageUrl: video.imageUrl ?? '', + width: 74.w, + height: 98.w, + borderRadius: + BorderRadius.circular(8.w), + ), + SizedBox(width: 13.w), + SizedBox( + width: 200.w, + height: 98.w, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.start, + children: [ + Text( + video.name ?? '', + maxLines: 1, + overflow: + TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13.sp, + color: Color(0xFF1E1E20), + fontWeight: + FontWeight.w500, + ), + ), + SizedBox(height: 6.w), + Text( + video.category?.first ?? '', + maxLines: 1, + overflow: + TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10.sp, + color: Color(0xFF79C900), + fontWeight: + FontWeight.w400, + ), + ), + SizedBox(height: 20.w), + Row( + children: [ + SizedBox( + width: 26.w, + height: 26.w, + child: CircularProgressIndicator( + value: + (video.currentEpisode ?? + 0) / + (video.episodeTotal! > + 0 + ? video + .episodeTotal! + : 1), + backgroundColor: + Color(0xFFD9D9D9), + valueColor: + AlwaysStoppedAnimation< + Color + >( + Color( + 0xFFA7F62F, + ), + ), + strokeWidth: 3.w, + ), + ), + SizedBox(width: 5.w), + Text( + "${((video.currentEpisode ?? 0) / (video.episodeTotal! > 0 ? video.episodeTotal! : 1) * 100).toStringAsFixed(0)}%", + style: TextStyle( + fontSize: 12.sp, + color: Color( + 0xFF1E1E20, + ), + fontWeight: + FontWeight.w400, + ), + ), + ], + ), + ], + ), + ), + const Spacer(), + GestureDetector( + onTap: () => + logic.likeVideo(video), + child: Column( + children: [ + Image.asset( + video.isCollect == 1 + ? 'ic_collect_sel.png' + .ktIcon + : 'ic_collect_unsel.png' + .ktIcon, + width: 32.w, + ), + ], + ), + ), + ], + ), + ), + ); + }, + separatorBuilder: (_, __) => + SizedBox(height: 12.w), + itemCount: state.historyList.length, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + }, + ); } } diff --git a/lib/kt_pages/kt_routes.dart b/lib/kt_pages/kt_routes.dart index 7279746..3c9a604 100644 --- a/lib/kt_pages/kt_routes.dart +++ b/lib/kt_pages/kt_routes.dart @@ -1,8 +1,11 @@ +import 'package:flutter_kinetra/kt_pages/kt_home/kt_search_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_store/view.dart'; +import 'kt_short_video/view.dart'; import 'kt_splash_page.dart'; import 'kt_webview_page.dart'; @@ -20,15 +23,21 @@ class KtRoutes { static const String webView = '/web_view'; static const String signInActivity = '/sign_in_activity'; static const String refill = '/refill'; + static const String aboutUs = '/about_us'; static final routes = [ GetPage(name: splash, page: () => const KtSplashPage()), GetPage(name: home, page: () => const KtMainPage()), - // GetPage(name: search, page: () => const SearchPage()), + GetPage(name: search, page: () => const KtSearchPage()), // GetPage(name: category, page: () => const CategoryPage()), - // GetPage(name: shortVideo, page: () => const ShortVideoPage(), preventDuplicates: false), + GetPage( + name: shortVideo, + page: () => const VideoPlayPage(), + preventDuplicates: false, + ), GetPage(name: store, page: () => KtStorePage()), GetPage(name: wallet, page: () => const WalletPage()), + GetPage(name: aboutUs, page: () => const KtAboutUsPage()), // GetPage(name: helpCenter, page: () => const HelpCenterPage()), // GetPage(name: helpCenterList, page: () => const HelpCenterListPage()), // GetPage(name: helpCenterDetail, page: () => const HelpCenterDetailPage()), diff --git a/lib/kt_pages/kt_short_video/logic.dart b/lib/kt_pages/kt_short_video/logic.dart new file mode 100644 index 0000000..7e83448 --- /dev/null +++ b/lib/kt_pages/kt_short_video/logic.dart @@ -0,0 +1,293 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_kinetra/kt_pages/kt_short_video/state.dart'; +import 'package:get/get.dart'; +import 'package:video_player/video_player.dart'; + +import '../../dio_cilent/kt_apis.dart'; +import '../../dio_cilent/kt_request.dart'; +import '../../kt_model/kt_video_detail_bean.dart'; +import '../../kt_widgets/kt_status_widget.dart'; + +class VideoPlayLogic extends GetxController { + final state = VideoPlayState(); + + late final PageController pageController; + List controllers = []; + + int currentIndex = 0; + bool _disposed = false; + + @override + void onInit() { + super.onInit(); + pageController = PageController(initialPage: currentIndex); + } + + @override + void onReady() { + super.onReady(); + _disposed = false; + state.shortPlayId = Get.arguments['shortPlayId']; + state.videoId = Get.arguments['videoId'] ?? 0; + state.imageUrl = Get.arguments['imageUrl'] ?? ''; + state.activityId = Get.arguments['activityId']; + state.isFromDiscover = Get.arguments['isFromDiscover'] ?? false; + fetchData(); + } + + @override + void onClose() { + _disposed = true; + clearCacheCtrl(); + super.onClose(); + } + + clearCacheCtrl() { + for (var controller in controllers) { + controller?.pause(); + controller?.dispose(); + } + controllers.clear(); + } + + Future fetchData({bool toPage = true}) async { + Map params = { + 'short_play_id': state.shortPlayId, + "video_id": state.videoId, + }; + if (state.activityId != null) { + params['activity_id'] = state.activityId; + } + clearCacheCtrl(); + try { + // LottieLoading.show(Get.context!); + ApiResponse res = await KtHttpClient().request( + KtApis.getVideoDetails, + method: HttpMethod.get, + queryParameters: params, + ); + if (res.success) { + state.loadStatus = KtLoadStatusType.loadSuccess; + state.video = VideoDetailBean.fromJson(res.data); + if (state.imageUrl == '') { + state.imageUrl = state.video!.shortPlayInfo?.imageUrl ?? ''; + } + if (state.videoId == 0) { + state.videoId = state.video?.videoInfo?.shortPlayVideoId ?? 0; + } + 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(); + controllers = List.filled( + state.episodeList.length, + null, + growable: true, + ); + // await _initializeController(currentIndex); + // _preloadAdjacentVideos(); + // if(currentIndex != 0){ + if (toPage) { + onPageChanged( + currentIndex, + isToggle: true, + isUploadHistorySeconds: false, + ); + } else { + _initializeController(currentIndex); + } + // } + + update(); + } else { + state.loadStatus = KtLoadStatusType.loadFailed; + update(); + } + // if (userLogic.state.userInfo.isVip == true) state.curUnlock = 9999; + update(); + } catch (e) { + // LottieLoading.close(); + state.loadStatus = KtLoadStatusType.loadFailed; + update(); + } + } + + void reportHistory() { + if (currentIndex < 0 || currentIndex >= state.episodeList.length) return; + Map params = { + "short_play_id": state.shortPlayId, + "video_id": state.episodeList[currentIndex].id, + }; + KtHttpClient().request(KtApis.createHistory, data: params); + } + + void reportActivity() { + Map params = { + 'short_play_id': state.shortPlayId, + "short_play_video_id": state.episodeList[currentIndex].shortPlayVideoId, + "activity_id": state.activityId, + }; + KtHttpClient().request(KtApis.activeAfterWatchingVideo, data: params); + } + + // 切换剧集时处理视频状态 + Future onPageChanged( + int index, { + bool isToggle = false, + bool isUploadHistorySeconds = true, + int type = 0, + }) async { + // EasyThrottle.throttle('page-change', Duration(seconds: 3), () async { + debugPrint('--$type-to-index:$index'); + if (index < 0 || index >= state.episodeList.length) return; + + // 暂停当前视频 + if (controllers[currentIndex]?.value.isPlaying ?? false) { + await controllers[currentIndex]?.pause(); + // if (isUploadHistorySeconds) uploadHistorySeconds(controllers[currentIndex]?.value.position.inMilliseconds ?? 0); + } + if (controllers[currentIndex]?.value.isCompleted ?? false) { + // if (isUploadHistorySeconds) uploadHistorySeconds(0); + if (state.activityId != null) reportActivity(); + } + if (isToggle) { + // loadStatusType = LoadStatusType.loading; + update(); + + await _initializeController(index); + // loadStatusType = LoadStatusType.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 (controllers[index] != null) { + // 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(); + // } + } + controllers[index]?.setPlaybackSpeed(state.currentSpeed); + // updateHomeVideo(); + // print('----curIndex:$currentIndex'); + // 预加载新的相邻视频,并释放多余控制器 + _preloadAdjacentVideos(); + update(); + reportHistory(); + // }); + } + + // 释放非当前、前后的视频控制器,减少内存占用 + void _releaseUnusedControllers() { + for (int i = 0; i < controllers.length; i++) { + if (i < currentIndex - 1 || i > currentIndex + 1) { + controllers[i]?.dispose(); + controllers[i] = null; + } + } + } + + // 初始化视频控制器 + Future _initializeController(int index) async { + if (index < 0 || index >= state.episodeList.length) return; + if (controllers[index] != null) return; + + final episode = state.episodeList[index]; + final controller = VideoPlayerController.networkUrl( + Uri.parse(episode.videoUrl!), + formatHint: VideoFormat.hls, + ); + + controllers[index] = controller; + + try { + await controller.initialize(); + // if (index == currentIndex && (episode.isLock == false || userLogic.state.userInfo.isVip == true)) { + if (index == currentIndex) { + controller.play(); + update(); + } + controller.setPlaybackSpeed(state.currentSpeed); + debugPrint('---seekTo:${episode.playSeconds}'); + controller.addListener(() { + if (currentIndex == index && !_disposed) update(); + if (currentIndex == state.episodeList.length - 1 && + (controllers.last?.value.isCompleted ?? false)) { + // showRecommendDialog(); + } + if (controller.value.isCompleted && !controller.value.isBuffering) { + onPageChanged(index + 1, isToggle: true); + } + }); + } catch (e) { + // 可根据需要处理异常 + // UserUtil().reportErrorEvent( + // 'video initialize failed', + // UserUtil.videoError, + // errMsg: e.toString(), + // payData: episode.toJson(), + // shortPlayId: episode.shortPlayId ?? 0, + // shortPlayVideoId: episode.shortPlayVideoId ?? 0, + // ); + debugPrint('---err:$e'); + } + } + + // 预加载相邻视频 + void _preloadAdjacentVideos() { + if (currentIndex > 0) _initializeController(currentIndex - 1); + if (currentIndex < state.episodeList.length - 1) { + _initializeController(currentIndex + 1); + } + _releaseUnusedControllers(); + } + + Future likeVideo() async { + if (state.video == null) return; + + Map params = { + "short_play_id": state.video?.shortPlayInfo?.shortPlayId, + "video_id": state.episodeList[currentIndex].id, + }; + if (state.video?.shortPlayInfo?.isCollect ?? false) { + await KtHttpClient().request(KtApis.deleteFavoriteVideo, data: params); + } else { + await KtHttpClient().request(KtApis.collectVideo, data: params); + } + + 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(); + } +} diff --git a/lib/kt_pages/kt_short_video/state.dart b/lib/kt_pages/kt_short_video/state.dart new file mode 100644 index 0000000..f792658 --- /dev/null +++ b/lib/kt_pages/kt_short_video/state.dart @@ -0,0 +1,17 @@ +import '../../kt_model/kt_video_detail_bean.dart'; +import '../../kt_widgets/kt_status_widget.dart'; + +class VideoPlayState { + String imageUrl = ''; + int shortPlayId = -1; + num videoId = -1; + int? activityId; + bool isFromDiscover = false; + VideoDetailBean? video; + List episodeList = []; + KtLoadStatusType loadStatus = KtLoadStatusType.loading; + + int currentVideoIndex = 0; + double currentSpeed = 1.0; + final List speedList = [0.75, 1.0, 1.25, 1.5, 2.0, 3.0]; +} diff --git a/lib/kt_pages/kt_short_video/view.dart b/lib/kt_pages/kt_short_video/view.dart new file mode 100644 index 0000000..6b9e796 --- /dev/null +++ b/lib/kt_pages/kt_short_video/view.dart @@ -0,0 +1,779 @@ +import 'dart:ui'; + +import 'package:flutter/material.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:video_player/video_player.dart'; +import 'package:visibility_detector/visibility_detector.dart'; + +import '../../kt_widgets/kt_network_image.dart'; +import '../../kt_widgets/kt_status_widget.dart'; +import '../../kt_widgets/kt_video_progress_bar.dart'; +import 'logic.dart'; + +class VideoPlayPage extends StatefulWidget { + const VideoPlayPage({super.key}); + + @override + State createState() => _VideoPlayPageState(); +} + +class _VideoPlayPageState extends State + with SingleTickerProviderStateMixin { + final logic = Get.put(VideoPlayLogic()); + final state = Get.find().state; + + @override + void initState() { + state.imageUrl = Get.arguments['imageUrl'] ?? ''; + super.initState(); + } + + // 选择倍速 + void _selectSpeed(double speed) { + state.currentSpeed = speed; + logic.controllers[logic.currentIndex]?.setPlaybackSpeed(speed); + } + + bool get isPause => + !((logic.controllers[logic.currentIndex]?.value.isPlaying ?? true) || + !(logic.controllers[logic.currentIndex]?.value.isBuffering ?? true)); + + bool get isAllOver => + logic.currentIndex == state.episodeList.length - 1 && + (logic.controllers[logic.currentIndex]?.value.isCompleted ?? false); + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: true, + onPopInvokedWithResult: (didPop, result) async { + // if (logic.controllers.isNotEmpty) { + // logic.uploadHistorySeconds(logic.controllers[logic.currentIndex]?.value.position.inMilliseconds ?? 0); + // } + if (didPop) return; + }, + child: Scaffold( + backgroundColor: Colors.white, + body: GetBuilder( + assignId: true, + builder: (ctrl) { + if (state.loadStatus == KtLoadStatusType.loadFailed) { + return SizedBox( + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + margin: EdgeInsets.only( + top: ScreenUtil().statusBarHeight, + ), + child: IconButton( + onPressed: () => Get.back(), + icon: Icon(Icons.arrow_back_outlined, size: 28), + ), + ), + SizedBox(height: 100.w), + KtStatusWidget( + type: KtErrorStatusType.loadFailed, + onPressed: logic.fetchData, + ), + ], + ), + ); + } + return Stack( + children: [ + KtNetworkImage( + imageUrl: state.imageUrl, + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + placeholder: Image.asset( + 'bg2.png'.ktIcon, + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.cover, + ), + ), + BackdropFilter( + filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), + child: Container( + color: Colors.black.withAlpha(30), + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + child: Center(child: CircularProgressIndicator()), + ), + ), + + // if (state.video == null) Center(child: CircularProgressIndicator(color: DrColors.mainColor)), + PageView.builder( + controller: logic.pageController, + scrollDirection: Axis.vertical, + onPageChanged: (index) => logic.onPageChanged(index, type: 1), + itemCount: state.episodeList.length, + itemBuilder: (context, index) => _videoPlayWidget(index), + ), + ], + ); + }, + ), + ), + ); + } + + Widget _videoPlayWidget(int index) { + if (logic.controllers.isEmpty) return Container(); + + final controller = logic.controllers[index]; + // final episode = state.episodeList[index]; + return Stack( + children: [ + // 视频播放器 + if (controller != null && controller.value.isInitialized && !isAllOver) + GestureDetector( + onTap: () { + // if (episode.isLock == true) return; + + controller.value.isPlaying + ? controller.pause() + : controller.play(); + setState(() {}); + }, + child: Stack( + alignment: Alignment.center, + children: [ + Positioned.fill( + child: FittedBox( + fit: BoxFit.cover, + child: VisibilityDetector( + key: Key('short-video-$index'), + onVisibilityChanged: (VisibilityInfo info) { + var visiblePercentage = info.visibleFraction * 100; + if (visiblePercentage > 20) { + // if (episode.isLock == true) return; + controller.play(); + } else { + controller.pause(); + } + logic.update(); + }, + child: SizedBox( + width: controller.value.size.width, + height: controller.value.size.height, + child: VideoPlayer(controller), + ), + ), + ), + ), + + // if (logic.loadStatusType == LoadStatusType.loading) + // Center(child: CircularProgressIndicator(color: DrColors.mainColor)), + if (!controller.value.isPlaying) + // if (!controller.value.isPlaying && logic.loadStatusType == LoadStatusType.loadSuccess) + Center( + child: Image.asset( + 'ic_play.png'.ktIcon, + width: 62.w, + height: 62.w, + ), + ), + ], + ), + ) + else + 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, + ), + ), + if (!isAllOver) Center(child: CircularProgressIndicator()), + ], + ), + + // 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, + left: 16.w, + child: GestureDetector( + onTap: () { + // if (state.isFromRecommend || state.recommendList.isEmpty) { + Get.back(); + // } else { + // logic.showRecommendDialog(); + // } + }, + child: Image.asset('ic_back_white.png'.ktIcon, width: 24.w), + ), + ), + // 底部控制器 + Positioned( + bottom: 0, + left: 0.w, + 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), + ], + ), + ), + 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, + ), + ), + ), + 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( + 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, + ), + ], + ), + ); + }, + ), + ], + ), + ], + ), + ), + ), + ], + ); + } + + showFeatureDialog() { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => StatefulBuilder( + builder: (context, dialogState) { + return Container( + height: 245.w, + padding: EdgeInsets.symmetric(horizontal: 16.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical(top: Radius.circular(12.w)), + image: DecorationImage( + image: AssetImage('ic_speed_bg.png'.ktIcon), + fit: BoxFit.fill, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 36.w), + GestureDetector( + onTap: () { + Get.back(); + showSpeedSelDialog(); + }, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 12.w, + vertical: 10.w, + ), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Color(0xFF03011B).withValues(alpha: .4), + Color(0xFF00000B).withValues(alpha: .4), + ], + ), + borderRadius: BorderRadius.circular(8.w), + ), + child: Row( + children: [ + Container( + margin: EdgeInsets.only(right: 5.w), + child: Image.asset( + 'ic_cur_speed.png'.ktIcon, + width: 24.w, + ), + ), + Text( + 'Playback Speed', + style: TextStyle( + fontSize: 12.sp, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + const Spacer(), + Text( + '${state.currentSpeed}x', + style: TextStyle( + fontSize: 12.sp, + color: Colors.white, + ), + ), + Container( + margin: EdgeInsets.only(left: 9.w), + child: Image.asset( + 'ic_right_grey.png'.ktIcon, + width: 8.w, + ), + ), + ], + ), + ), + ), + SizedBox(height: 18.w), + GestureDetector( + onTap: () { + Get.back(); + }, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 12.w, + vertical: 10.w, + ), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Color(0xFF03011B).withValues(alpha: .4), + Color(0xFF00000B).withValues(alpha: .4), + ], + ), + borderRadius: BorderRadius.circular(8.w), + ), + child: Row( + children: [ + Container( + margin: EdgeInsets.only(right: 5.w), + child: Image.asset( + 'ic_video_feedback.png'.ktIcon, + width: 16.w, + ), + ), + Text( + 'Feedback', + style: TextStyle( + fontSize: 12.sp, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + const Spacer(), + Container( + margin: EdgeInsets.only(left: 9.w), + child: Image.asset( + 'ic_right_grey.png'.ktIcon, + width: 8.w, + ), + ), + ], + ), + ), + ), + ], + ), + ); + }, + ), + ); + } + + showEpSelDialog() { + return Get.bottomSheet( + Container( + height: 550.w, + padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 10.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical(top: Radius.circular(12.w)), + image: DecorationImage( + image: AssetImage('ep_sel_bg.png'.ktIcon), + fit: BoxFit.fill, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + margin: EdgeInsets.only(top: 80.w), + child: Row( + children: [ + Image.asset('ip.png'.ktIcon, width: 38.w), + SizedBox(width: 6.w), + Container( + padding: EdgeInsets.symmetric( + vertical: 9.w, + horizontal: 14.w, + ), + decoration: BoxDecoration( + color: Color(0xFF1E1E20), + borderRadius: BorderRadius.circular(100), + ), + child: Row( + children: [ + Text( + 'Select Episode', + style: TextStyle( + fontSize: 15.sp, + fontWeight: FontWeight.w500, + color: Color(0xFFA7F62F), + ), + ), + Text( + '(All ${state.video?.shortPlayInfo?.episodeTotal} episodes)', + style: TextStyle( + fontSize: 12.sp, + color: Color(0xFFA7F62F), + ), + ), + ], + ), + ), + ], + ), + ), + Container( + margin: EdgeInsets.only(top: 16.w), + child: Row( + children: [ + KtNetworkImage( + imageUrl: state.video?.shortPlayInfo?.imageUrl ?? '', + width: 64.w, + height: 85.w, + borderRadius: BorderRadius.circular(6.w), + ), + SizedBox(width: 10.w), + SizedBox( + width: ScreenUtil().screenWidth - 105.w, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + state.video?.shortPlayInfo?.name ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 14.sp, + color: Color(0xFF1E1E20), + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 7.w), + Text( + state.video?.shortPlayInfo?.category?.first ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12.sp, + color: Color(0xFFA7F62F), + ), + ), + SizedBox(height: 9.w), + if (!KtUtils.isEmpty( + state.video?.shortPlayInfo?.description, + )) + SizedBox( + height: 40.w, + child: Text( + state.video?.shortPlayInfo?.description ?? '', + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10.sp, + color: Color(0xFF5E5E5E), + ), + ), + ), + ], + ), + ), + ], + ), + ), + SizedBox(height: 19.w), + Expanded( + child: GridView.builder( + padding: EdgeInsets.only(bottom: 16.w), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 5, + mainAxisSpacing: 10.w, + crossAxisSpacing: 10.w, + childAspectRatio: 61 / 50, + ), + itemCount: state.episodeList.length, + itemBuilder: (context, index) { + bool isCurrent = index == logic.currentIndex; + + 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; + // } + logic.onPageChanged(index, isToggle: true); + }, + child: Stack( + children: [ + Container( + decoration: BoxDecoration( + color: isCurrent ? Color(0xFFA7F62F) : Colors.white, + borderRadius: BorderRadius.circular(10.w), + border: isCurrent + ? Border.all( + color: Color(0xFF1E1E20), + width: 1.w, + ) + : null, + ), + child: Center( + child: Text( + '${index + 1}', + style: TextStyle( + color: Color(0xFF1E1E20), + fontSize: 14.sp, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + // 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, + // ), + // ), + // ), + ], + ), + ); + }, + ), + ), + ], + ), + ), + isScrollControlled: true, + ); + } + + showSpeedSelDialog() { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => StatefulBuilder( + builder: (context, dialogState) { + return Container( + height: 375.w, + padding: EdgeInsets.symmetric(horizontal: 15.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical(top: Radius.circular(12.w)), + image: DecorationImage( + image: AssetImage('ic_speed_bg.png'.ktIcon), + fit: BoxFit.fill, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 44.w), + Row( + children: [ + Container( + margin: EdgeInsets.only(right: 5.w), + child: Image.asset( + 'ic_cur_speed.png'.ktIcon, + width: 24.w, + ), + ), + Text( + 'Playback Speed', + style: TextStyle( + fontSize: 18.sp, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(width: 12.w), + Text( + '· ${state.currentSpeed}X', + style: TextStyle( + fontSize: 16.sp, + color: Color(0xFF36F2FF), + fontWeight: FontWeight.w500, + ), + ), + ], + ), + SizedBox(height: 15.w), + + Expanded( + child: ListView.separated( + shrinkWrap: true, + itemBuilder: (context, index) => GestureDetector( + onTap: () { + _selectSpeed(state.speedList[index]); + Get.back(); + }, + child: Container( + padding: EdgeInsets.all(12.w), + alignment: Alignment.centerLeft, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.w), + color: Colors.white.withValues(alpha: .16), + border: state.currentSpeed == state.speedList[index] + ? Border.all(width: 2.w, color: Color(0xFF36F2FF)) + : null, + ), + child: Text( + '${state.speedList[index]}x', + style: TextStyle( + fontSize: 14.sp, + color: Colors.white, + ), + ), + ), + ), + separatorBuilder: (_, __) => SizedBox(height: 10.w), + itemCount: state.speedList.length, + ), + ), + ], + ), + ); + }, + ), + ); + } +} diff --git a/lib/kt_utils/kt_user_utils.dart b/lib/kt_utils/kt_user_utils.dart index b7cb469..4c3163f 100644 --- a/lib/kt_utils/kt_user_utils.dart +++ b/lib/kt_utils/kt_user_utils.dart @@ -25,12 +25,12 @@ class KtUserUtil { register({bool toHome = true}) async { try { - ApiResponse res = await HttpClient().request(KtApis.register); + ApiResponse res = await KtHttpClient().request(KtApis.register); if (res.success) { KtRegisterBean data = KtRegisterBean.fromJson(res.data); SpUtil.putString(KtKeys.token, data.token ?? ''); - HttpClient().setAuthToken(data.token ?? ''); + KtHttpClient().setAuthToken(data.token ?? ''); if (toHome) Get.offNamed(KtRoutes.home); KtUserUtil().enterAppPost(); // FirebaseCommon.reportFirebaseToken(); @@ -65,12 +65,12 @@ class KtUserUtil { if (token == null) return; if (SpUtil.containsKey(KtKeys.token) ?? false) { EasyThrottle.throttle('enterAppPost', Duration(seconds: 1), () async { - await HttpClient().request( + await KtHttpClient().request( KtApis.enterTheApp, data: {'is_open_notice': isOpenNotice}, ); }); - // await HttpClient().request(KtApis.enterTheApp, data: {"is_open_notice": isOpenNotice}); + // await KtHttpClient().request(KtApis.enterTheApp, data: {"is_open_notice": isOpenNotice}); } } @@ -79,7 +79,7 @@ class KtUserUtil { if (token == null) return; EasyThrottle.throttle('onLinePost', Duration(seconds: 1), () async { - await HttpClient().request( + await KtHttpClient().request( KtApis.onLine, data: {'PostAuthorization': token ?? ''}, ); @@ -91,7 +91,7 @@ class KtUserUtil { if (token == null) return; EasyThrottle.throttle('offline', Duration(seconds: 1), () async { - await HttpClient().request( + await KtHttpClient().request( KtApis.leaveApp, data: {'PostAuthorization': token ?? ''}, ); @@ -103,12 +103,12 @@ class KtUserUtil { if (token == null) return; final permissionStatus = await Permission.notification.status; if (permissionStatus.isDenied || permissionStatus.isPermanentlyDenied) { - await HttpClient().request( + await KtHttpClient().request( KtApis.uploadNoticeStatus, data: {'is_open_notice': 0}, ); } else if (permissionStatus.isGranted) { - await HttpClient().request( + await KtHttpClient().request( KtApis.uploadNoticeStatus, data: {'is_open_notice': 1}, ); @@ -117,7 +117,7 @@ class KtUserUtil { // 上报firebase token reportFirebaseToken(String token) async { - await HttpClient().request( + await KtHttpClient().request( KtApis.reportFirebaseToken, data: {"fcm_token": token}, ); @@ -125,7 +125,7 @@ class KtUserUtil { // 上报firebase 消息 sendMessageReport(int id, String title) async { - await HttpClient().request( + await KtHttpClient().request( KtApis.sendMessageReport, data: {"message_id": id, "title": title}, ); @@ -161,7 +161,7 @@ class KtUserUtil { params.putIfAbsent('transaction_id', () => transactionId); if (extendData != null) params.addAll(extendData); params.putIfAbsent('error_msg', () => errMsg); - HttpClient().request(KtApis.reportEvent, data: params); + KtHttpClient().request(KtApis.reportEvent, data: params); } static String payCallback = 'pay_callback'; diff --git a/lib/kt_widgets/kt_dialog.dart b/lib/kt_widgets/kt_dialog.dart new file mode 100644 index 0000000..435d5b1 --- /dev/null +++ b/lib/kt_widgets/kt_dialog.dart @@ -0,0 +1,155 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_kinetra/kt_utils/kt_string_extend.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class KtDialog extends StatelessWidget { + final String title; + final String subTitle; + final String leftBtnText; + final String? leftBtnIcon; + final String rightBtnText; + final String? rightBtnIcon; + final String topIcon; + final Widget? topIconWidget; + final bool hasLeftBtn; + final double titleSize; + final VoidCallback? leftBtnFunc; + final VoidCallback? rightBtnFunc; + + const KtDialog({ + super.key, + required this.title, + required this.subTitle, + this.leftBtnText = 'Cancel', + this.leftBtnIcon, + this.rightBtnText = 'Confirm', + this.rightBtnIcon, + this.hasLeftBtn = true, + this.topIcon = 'dialog_star.png', + this.topIconWidget, + this.titleSize = 18, + this.leftBtnFunc, + this.rightBtnFunc, + }); + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + width: 315.w, + height: 367.w, + decoration: BoxDecoration( + image: DecorationImage(image: AssetImage('dialog_bg1.png'.ktIcon)), + ), + padding: EdgeInsets.fromLTRB(16.w, 94.w, 16.w, 16.w), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + title, + style: TextStyle( + fontSize: titleSize.sp, + color: Color(0xFF1E1E20), + fontWeight: FontWeight.w600, + ), + ), + // SizedBox(height: 20.w), + Image.asset(topIcon.ktIcon, width: 100.w), + // SizedBox(height: 10.w), + SizedBox( + width: 246.w, + child: Text( + subTitle, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15.sp, + color: Color(0xFF1C1C1C), + fontWeight: FontWeight.w500, + ), + ), + ), + SizedBox(height: 15.w), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: () { + Navigator.of(context).pop(); + leftBtnFunc?.call(); + }, + child: Container( + width: 120.w, + height: 48.w, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + border: Border.all(width: 1.w, color: Color(0xFFC5C5C5)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (leftBtnIcon != null) + Container( + margin: EdgeInsets.only(right: 4.w), + child: Image.asset( + leftBtnIcon!.ktIcon, + width: 16.w, + ), + ), + Text( + leftBtnText, + style: TextStyle( + fontSize: 14.sp, + color: Color(0xFFB9B9B9), + ), + ), + ], + ), + ), + ), + SizedBox(width: 20.w), + GestureDetector( + onTap: () { + Navigator.of(context).pop(); + rightBtnFunc?.call(); + }, + child: Container( + width: 120.w, + height: 48.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Color(0xFF1E1E20), + borderRadius: BorderRadius.circular(100), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (rightBtnIcon != null) + Container( + margin: EdgeInsets.only(right: 4.w), + child: Image.asset( + rightBtnIcon!.ktIcon, + width: 16.w, + ), + ), + Text( + rightBtnText, + style: TextStyle( + fontSize: 14.sp, + color: Color(0xFFA7F62F), + ), + ), + ], + ), + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/kt_widgets/kt_network_image.dart b/lib/kt_widgets/kt_network_image.dart new file mode 100644 index 0000000..028eee9 --- /dev/null +++ b/lib/kt_widgets/kt_network_image.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:flutter_kinetra/kt_utils/kt_string_extend.dart'; + +/// 带有缓存功能的网络图片组件 +class KtNetworkImage extends StatelessWidget { + final String imageUrl; + final double? width; + final double? height; + final BoxFit? fit; + final Widget? placeholder; + final Widget? errorWidget; + final BorderRadius? borderRadius; + + const KtNetworkImage({ + super.key, + required this.imageUrl, + this.width, + this.height, + this.fit = BoxFit.cover, + this.placeholder, + this.errorWidget, + this.borderRadius, + }); + + @override + Widget build(BuildContext context) { + // 如果URL为空,直接显示错误组件 + if (imageUrl.isEmpty) { + return errorWidget ?? _buildDefaultErrorWidget(); + } + + // 基础的CachedNetworkImage配置 + Widget imageWidget = CachedNetworkImage( + imageUrl: imageUrl, + width: width, + height: height, + fit: fit, + filterQuality: FilterQuality.high, + placeholder: (context, url) => placeholder ?? _buildDefaultPlaceholder(), + errorWidget: (context, url, error) => + errorWidget ?? _buildDefaultErrorWidget(), + cacheManager: CacheManager( + Config( + 'customImageCache', + stalePeriod: const Duration(days: 7), + maxNrOfCacheObjects: 100, + ), + ), + fadeInDuration: const Duration(milliseconds: 300), + fadeOutDuration: const Duration(milliseconds: 300), + ); + + // 如果设置了圆角,使用ClipRRect包裹 + if (borderRadius != null) { + imageWidget = ClipRRect(borderRadius: borderRadius!, child: imageWidget); + } + + return imageWidget; + } + + // 默认占位符组件 + Widget _buildDefaultPlaceholder() { + Widget imageWidget = Image.asset( + (width ?? 0) > (height ?? 0) + ? 'ic_image_hor.png'.ktIcon + : 'ic_image_ver.png'.ktIcon, + width: width, + height: height, + fit: fit, + ); + // 如果设置了圆角,使用ClipRRect包裹 + if (borderRadius != null) { + imageWidget = ClipRRect(borderRadius: borderRadius!, child: imageWidget); + } + return imageWidget; + } + + // 默认错误组件 + Widget _buildDefaultErrorWidget() { + Widget imageWidget = Image.asset( + (width ?? 0) > (height ?? 0) + ? 'ic_image_hor.png'.ktIcon + : 'ic_image_ver.png'.ktIcon, + width: width, + height: height, + fit: fit, + ); + // 如果设置了圆角,使用ClipRRect包裹 + if (borderRadius != null) { + imageWidget = ClipRRect(borderRadius: borderRadius!, child: imageWidget); + } + return imageWidget; + } +} diff --git a/lib/kt_widgets/kt_status_widget.dart b/lib/kt_widgets/kt_status_widget.dart index a41ede8..acfeae7 100644 --- a/lib/kt_widgets/kt_status_widget.dart +++ b/lib/kt_widgets/kt_status_widget.dart @@ -6,12 +6,12 @@ enum KtErrorStatusType { noNetwork, loadFailed, nothingYet, notFound } enum KtLoadStatusType { loading, loadSuccess, loadFailed, loadNoData } -class BadStatusWidget extends StatelessWidget { +class KtStatusWidget extends StatelessWidget { final KtErrorStatusType type; final String? message; final VoidCallback? onPressed; - const BadStatusWidget({ + const KtStatusWidget({ super.key, required this.type, this.message, @@ -24,19 +24,14 @@ class BadStatusWidget extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Stack( - alignment: Alignment.bottomCenter, - children: [ - Image.asset(_getIcon(type).ktIcon, width: 290.w, height: 268.h), - Text( - _getMessage(type, message), - style: TextStyle( - fontSize: 15.sp, - color: Color(0xFF1E1E20), - fontWeight: FontWeight.w500, - ), - ), - ], + Image.asset(_getIcon(type).ktIcon, width: 290.w, height: 268.h), + Text( + _getMessage(type, message), + style: TextStyle( + fontSize: 15.sp, + color: Color(0xFF1E1E20), + fontWeight: FontWeight.w500, + ), ), Text( @@ -82,26 +77,26 @@ class BadStatusWidget extends StatelessWidget { String _getMessage(KtErrorStatusType type, String? message) { switch (type) { case KtErrorStatusType.noNetwork: - return 'No Network'; + return 'Network Hiccup'; case KtErrorStatusType.loadFailed: - return 'Load Failed'; + return 'Error reported'; case KtErrorStatusType.nothingYet: return 'Nothing here yet'; case KtErrorStatusType.notFound: - return 'Not found'; + return 'No Results Found'; } } String _getSubMessage(KtErrorStatusType type, String? message) { switch (type) { case KtErrorStatusType.noNetwork: - return 'Unable to connect. Try again later.'; + return 'Check your WiFi or cellular data'; case KtErrorStatusType.loadFailed: - return 'We’re having trouble. Hang tight.'; + return 'Our team has been notified.'; case KtErrorStatusType.nothingYet: return 'Start exploring and add something!'; case KtErrorStatusType.notFound: - return 'Sorry, we couldn\'t find anything.'; + return 'Check spelling or use simpler keywords.'; } } } diff --git a/lib/kt_widgets/kt_video_progress_bar.dart b/lib/kt_widgets/kt_video_progress_bar.dart new file mode 100644 index 0000000..83b5605 --- /dev/null +++ b/lib/kt_widgets/kt_video_progress_bar.dart @@ -0,0 +1,136 @@ +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; + + final double? width; + final bool canSlide; + + const CustomVideoProgressBar({ + super.key, + required this.controller, + this.width, + this.canSlide = true, + }); + + @override + State createState() => _CustomVideoProgressBarState(); +} + +class _CustomVideoProgressBarState extends State { + bool _isDragging = false; + double? _dragValue; + + @override + void initState() { + super.initState(); + widget.controller.addListener(_updateState); + } + + @override + void dispose() { + widget.controller.removeListener(_updateState); + super.dispose(); + } + + void _updateState() { + if (!_isDragging) { + if (mounted) setState(() {}); + } + } + + String _formatDuration(Duration duration) { + final minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0'); + final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0'); + return '$minutes:$seconds'; + } + + @override + Widget build(BuildContext context) { + final Duration duration = widget.controller.value.duration; + final Duration position = _isDragging + ? Duration(milliseconds: _dragValue?.toInt() ?? 0) + : widget.controller.value.position; + + return Column( + children: [ + // 时间显示 + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + '${_formatDuration(position)}/', + style: TextStyle(color: Color(0xFFDFDFDF), fontSize: 10.sp), + ), + Text( + _formatDuration(duration), + style: TextStyle(color: Color(0xFFDFDFDF), fontSize: 10.sp), + ), + ], + ), + // 进度条 + SizedBox( + width: widget.width ?? ScreenUtil().screenWidth - 30.w, + child: SliderTheme( + data: SliderTheme.of(context).copyWith( + trackHeight: 2.0.w, + trackShape: UniformTrackShape(), + thumbShape: RoundSliderThumbShape(enabledThumbRadius: 6.0), + ), + child: Slider( + padding: EdgeInsets.zero, + min: 0, + max: duration.inMilliseconds.toDouble(), + value: position.inMilliseconds.toDouble().clamp( + 0, + duration.inMilliseconds.toDouble(), + ), + + onChangeStart: (_) { + if (!widget.canSlide) return; + _isDragging = true; + }, + onChangeEnd: (value) { + if (!widget.canSlide) return; + _isDragging = false; + widget.controller.seekTo(Duration(milliseconds: value.toInt())); + widget.controller.play(); + _dragValue = null; + }, + onChanged: (value) { + if (!widget.canSlide) return; + setState(() => _dragValue = value); + }, + activeColor: Colors.white, + inactiveColor: Colors.white.withValues(alpha: .3), + ), + ), + ), + ], + ); + } +} + +// 自定义轨道形状,确保选中和未选中部分高度一致 +class UniformTrackShape extends RoundedRectSliderTrackShape { + const UniformTrackShape(); + + @override + Rect getPreferredRect({ + required RenderBox parentBox, + Offset offset = Offset.zero, + required SliderThemeData sliderTheme, + bool isEnabled = false, + bool isDiscrete = false, + }) { + final double? trackHeight = sliderTheme.trackHeight; + final double trackLeft = offset.dx; + final double trackTop = + offset.dy + (parentBox.size.height - trackHeight!) / 2; + final double trackWidth = parentBox.size.width; + + return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight); + } +} diff --git a/lib/main.dart b/lib/main.dart index c3307d2..dd00b44 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -98,7 +98,7 @@ class _MyAppState extends State with WidgetsBindingObserver { return GetMaterialApp( title: 'Kinetra', navigatorKey: navigatorKey, - navigatorObservers: [routeObserver], + navigatorObservers: [routeObserver, BotToastNavigatorObserver()], getPages: KtRoutes.routes, initialRoute: KtRoutes.splash, // 注册路由监听器 diff --git a/pubspec.lock b/pubspec.lock index 603ec1b..8e2440d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -695,7 +695,7 @@ packages: source: hosted version: "0.6.0" flutter_launcher_icons: - dependency: "direct main" + dependency: "direct dev" description: name: flutter_launcher_icons sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" @@ -743,7 +743,7 @@ packages: source: hosted version: "1.0.2" flutter_native_splash: - dependency: "direct main" + dependency: "direct dev" description: name: flutter_native_splash sha256: "8321a6d11a8d13977fa780c89de8d257cce3d841eecfb7a4cadffcc4f12d82dc" @@ -1048,14 +1048,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "5.1.1" - lottie: - dependency: "direct main" - description: - name: lottie - sha256: c5fa04a80a620066c15cf19cc44773e19e9b38e989ff23ea32e5903ef1015950 - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.3.1" matcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7c92bc2..07a4b1c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,8 +25,6 @@ dependencies: webview_flutter: ^4.13.0 shared_preferences: ^2.5.3 flutter_cache_manager: ^3.3.2 - flutter_native_splash: ^2.4.6 - flutter_launcher_icons: ^0.14.4 flutter_card_swiper: ^7.0.2 fluttertoast: ^8.2.11 device_info_plus: ^11.5.0 @@ -67,7 +65,6 @@ dependencies: flustars: ^2.0.1 flutter_spinkit: ^5.2.1 bot_toast: ^4.1.3 - lottie: ^3.3.1 easy_refresh: ^3.4.0 @@ -76,13 +73,9 @@ dev_dependencies: flutter_test: sdk: flutter - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. flutter_lints: ^5.0.0 - + flutter_launcher_icons: ^0.14.4 + flutter_native_splash: ^2.4.6 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec @@ -103,7 +96,7 @@ flutter: # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: -# fonts: + fonts: # - family: AaHouDiHei # fonts: # - asset: assets/fonts/AaHouDiHei-Regular.ttf @@ -116,17 +109,40 @@ flutter: # weight: 600 # - asset: assets/fonts/MontserratAlternates-BlackItalic.ttf # weight: 800 -# - family: Inter -# fonts: -# - asset: assets/fonts/Inter-Regular-9.otf -# weight: 400 -# - asset: assets/fonts/Inter-Medium-8.otf -# weight: 500 -# - asset: assets/fonts/Inter-SemiBold-10.otf -# weight: 600 -# - asset: assets/fonts/Inter-Bold-4.otf -# weight: 700 + - family: Inter + fonts: + - asset: assets/fonts/Inter/Inter-Regular-9.otf + weight: 400 + - asset: assets/fonts/Inter/Inter-Medium-8.otf + weight: 500 + - asset: assets/fonts/Inter/Inter-SemiBold-10.otf + weight: 600 + - asset: assets/fonts/Inter/Inter-Bold-4.otf + weight: 700 # For details regarding fonts from package dependencies, # see https://flutter.dev/to/font-from-package +flutter_launcher_icons: + image_path: "assets/ic_logo.png" + android: "launcher_icon" + min_sdk_android: 21 # android min sdk min:16, default 21 + ios: true + remove_alpha_ios: true + +flutter_native_splash: + color: "#01060B" + image: assets/splash.png + color_dark: "#01060B" + image_dark: assets/splash.png + android: true + ios: true + web: false + fullscreen: true # 设置为全屏 + android_gravity: centerInside + android_12: + color: "#01060B" + image: assets/ic_logo.png + color_dark: "#01060B" + image_dark: assets/ic_logo.png + icon_background_color: "#01060B"