Files
keyboard/keyBoard/Class/Shop/VC/KBSkinDetailVC.m
2025-12-17 20:43:16 +08:00

385 lines
14 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// KBSkinDetailVC.m
// keyBoard
#import "KBSkinDetailVC.h"
#import <Masonry/Masonry.h>
#import "UIColor+Extension.h"
#import "UICollectionViewLeftAlignedLayout.h"
#import "KBSkinDetailHeaderCell.h"
#import "KBSkinTagsContainerCell.h"
#import "KBSkinCardCell.h"
#import "KBSkinSectionTitleCell.h"
#import "KBSkinBottomActionView.h"
#import "KBShopVM.h"
#import "KBShopThemeTagModel.h"
#import "KBShopThemeModel.h"
#import "KBHUD.h"
#import "KBSkinService.h"
static NSString * const kHeaderCellId = @"kHeaderCellId";
static NSString * const kTagsContainerCellId = @"kTagsContainerCellId";
static NSString * const kSectionTitleCellId = @"kSectionTitleCellId";
static NSString * const kGridCellId = @"kGridCellId"; // 直接复用 KBSkinCardCell
typedef NS_ENUM(NSInteger, KBSkinDetailSection) {
KBSkinDetailSectionHeader = 0,
KBSkinDetailSectionTags,
KBSkinDetailSectionTitle,
KBSkinDetailSectionGrid,
KBSkinDetailSectionCount
};
@interface KBSkinDetailVC () <UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
@property (nonatomic, strong) UICollectionView *collectionView; // 主列表
@property (nonatomic, strong) KBSkinBottomActionView *bottomBar; // 底部操作条
@property (nonatomic, copy) NSArray<NSString *> *tags; // 标签数据
@property (nonatomic, copy) NSArray<KBShopThemeModel *> *recommendedThemes; // 底部网格数据
@property (nonatomic, strong) KBShopVM *shopVM;
@property (nonatomic, strong, nullable) KBShopThemeDetailModel *detailModel;
//@property (nonatomic, assign) BOOL isProcessingAction;
@end
@implementation KBSkinDetailVC
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
self.tags = @[];
self.recommendedThemes = @[];
// 1. 列表
[self.view addSubview:self.collectionView];
// 2. 底部操作条(左右 15高 45可点击
[self.view addSubview:self.bottomBar];
[self.bottomBar mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.view).offset(15);
make.right.equalTo(self.view).offset(-15);
make.height.mas_equalTo([KBSkinBottomActionView preferredHeight]);
make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom).offset(-10);
}];
// 列表底部距离操作条顶部 10
[self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(self.view);
make.top.equalTo(self.view).offset(KB_NAV_TOTAL_HEIGHT);
make.bottom.equalTo(self.bottomBar.mas_top).offset(-10);
}];
[self updateBottomBarAppearance];
[self fetchThemeDetailIfNeeded];
[self fetchRecommendedThemes];
}
#pragma mark - UICollectionView DataSource
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
return KBSkinDetailSectionCount;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
switch (section) {
case KBSkinDetailSectionHeader: return 1; // 顶部大卡片
case KBSkinDetailSectionTags: return self.tags.count > 0 ? 1 : 0;
case KBSkinDetailSectionTitle: return 1; // 标题
case KBSkinDetailSectionGrid: return self.recommendedThemes.count; // 2 列网格
}
return 0;
}
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
switch (indexPath.section) {
case KBSkinDetailSectionHeader: {
KBSkinDetailHeaderCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kHeaderCellId forIndexPath:indexPath];
[cell configWithDetail:self.detailModel];
return cell;
}
case KBSkinDetailSectionTags: {
KBSkinTagsContainerCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kTagsContainerCellId forIndexPath:indexPath];
[cell configWithTags:self.tags];
return cell;
}
case KBSkinDetailSectionTitle: {
KBSkinSectionTitleCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kSectionTitleCellId forIndexPath:indexPath];
[cell config:@"Recommended Skin"];
return cell;
}
case KBSkinDetailSectionGrid: {
KBSkinCardCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kGridCellId forIndexPath:indexPath];
KBShopThemeModel *model = self.recommendedThemes[indexPath.item];
NSString *priceText = [self priceTextForTheme:model];
[cell configWithTitle:model.themeName ?: @""
imageURL:model.themePreviewImageUrl
price:priceText];
return cell;
}
}
return [UICollectionViewCell new];
}
#pragma mark - UICollectionView Delegate
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.section != KBSkinDetailSectionGrid) { return; }
if (indexPath.item >= self.recommendedThemes.count) { return; }
KBShopThemeModel *model = self.recommendedThemes[indexPath.item];
NSString *nextThemeId = [model.themeId stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if (nextThemeId.length == 0) { return; }
if ([nextThemeId isEqualToString:self.themeId ?: @""]) { return; }
// 不跳转新页面:直接切换 themeId重新请求详情并刷新当前页面
self.themeId = nextThemeId;
self.detailModel = nil;
self.tags = @[];
[self updateBottomBarAppearance];
[self.collectionView setContentOffset:CGPointZero animated:NO];
[self.collectionView reloadData];
[self fetchThemeDetailIfNeeded];
}
#pragma mark - UICollectionView DelegateFlowLayout
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
CGFloat W = collectionView.bounds.size.width;
CGFloat insetLR = 16.0;
CGFloat contentW = W - insetLR * 2;
switch (indexPath.section) {
case KBSkinDetailSectionHeader: {
// 高度 = 图片 0.58W + 文案与上下留白≈56
CGFloat h = KBFit(264) + 30;
return CGSizeMake(contentW, h);
}
case KBSkinDetailSectionTags: {
CGFloat h = [KBSkinTagsContainerCell heightForTags:self.tags width:W];
return CGSizeMake(contentW, MAX(h, 0.1));
}
case KBSkinDetailSectionTitle: {
return CGSizeMake(contentW, 44);
}
case KBSkinDetailSectionGrid: {
// 2 列
CGFloat spacing = 12.0;
CGFloat itemW = floor((contentW - spacing) / 2.0);
// CGFloat itemH = itemW * 0.75 + 56;
return CGSizeMake(itemW, KBFit(197));
}
}
return CGSizeZero;
}
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
// 控制各区上下留白,缩小 Header 与 Tags 之间的间距
switch (section) {
case KBSkinDetailSectionHeader:
// 顶部与导航之间 12底部稍微缩小
return UIEdgeInsetsMake(12, 16, 4, 16);
case KBSkinDetailSectionTags:
// 与 Header 更贴近一些
return UIEdgeInsetsMake(14, 16, 5, 16);
default:
return UIEdgeInsetsMake(0, 16, 12, 16);
}
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section {
return 12.0;
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section {
// 网格区需要这个间距,其它区也保持统一
return 12.0;
}
#pragma mark - Lazy
- (UICollectionView *)collectionView {
if (!_collectionView) {
UICollectionViewFlowLayout *layout = [UICollectionViewFlowLayout new];
layout.scrollDirection = UICollectionViewScrollDirectionVertical;
_collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
_collectionView.backgroundColor = [UIColor whiteColor];
_collectionView.dataSource = self;
_collectionView.delegate = self;
// 注册 cell
[_collectionView registerClass:KBSkinDetailHeaderCell.class forCellWithReuseIdentifier:kHeaderCellId];
[_collectionView registerClass:KBSkinTagsContainerCell.class forCellWithReuseIdentifier:kTagsContainerCellId];
[_collectionView registerClass:KBSkinSectionTitleCell.class forCellWithReuseIdentifier:kSectionTitleCellId];
[_collectionView registerClass:KBSkinCardCell.class forCellWithReuseIdentifier:kGridCellId];
}
return _collectionView;
}
#pragma mark - Lazy (Bottom Bar)
- (KBSkinBottomActionView *)bottomBar {
if (!_bottomBar) {
_bottomBar = [[KBSkinBottomActionView alloc] init];
// 中文注释:配置文案与图标,可根据业务传入金币图/价格
[_bottomBar configWithTitle:@"Download" price:@"20" icon:nil];
KBWeakSelf
_bottomBar.tapHandler = ^{
// 示例:点击下载/购买
// [KBHUD showText:@"点击了下载"];
// TODO: 在此处触发下载/购买逻辑
[weakSelf handleDownloadAction];
};
}
return _bottomBar;
}
#pragma mark - Actions
- (void)handleDownloadAction {
// if (self.isProcessingAction) { return; }
if (self.themeId.length == 0) {
[KBHUD showInfo:KBLocalized(@"主题信息缺失")];
return;
}
if (!self.detailModel) {
[KBHUD showInfo:KBLocalized(@"正在加载主题详情")];
return;
}
if (self.detailModel.isPurchased) {
[self requestDownload];
} else {
[self purchaseCurrentTheme];
}
}
- (void)purchaseCurrentTheme {
// if (self.isProcessingAction) { return; }
// self.isProcessingAction = YES;
[KBHUD show];
__weak typeof(self) weakSelf = self;
[self.shopVM purchaseThemeWithId:self.themeId completion:^(BOOL success, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
// weakSelf.isProcessingAction = NO;
[KBHUD dismiss];
if (error || !success) {
NSString *msg = error.localizedDescription ?: KBLocalized(@"购买失败");
[KBHUD showInfo:msg];
return;
}
weakSelf.detailModel.isPurchased = YES;
[weakSelf updateBottomBarAppearance];
[weakSelf requestDownload];
});
}];
}
- (void)requestDownload {
// if (self.isProcessingAction) { return; }
// self.isProcessingAction = YES;
[KBHUD show];
NSMutableDictionary *skin = [NSMutableDictionary dictionary];
if (!skin[@"id"] && self.detailModel.themeId) {
skin[@"id"] = self.detailModel.themeId;
}
if (!skin[@"name"] && self.detailModel.themeName) {
skin[@"name"] = self.detailModel.themeName;
}
skin[@"zip_url"] = self.detailModel.themeDownloadUrl ? self.detailModel.themeDownloadUrl : @"";
if (self.detailModel.themePreviewImageUrl.length > 0) {
skin[@"preview"] = self.detailModel.themePreviewImageUrl;
}
[[KBSkinService shared] applySkinWithJSON:skin
fromViewController:self
mode:KBSkinSourceModeRemoteZip
completion:^(BOOL success) {
if (success) {
[KBHUD showSuccess:KBLocalized(@"已开始下载")];
} else {
[KBHUD showInfo:KBLocalized(@"下载失败")];
}
}];
}
- (void)updateBottomBarAppearance {
BOOL purchased = self.detailModel.isPurchased;
if (purchased) {
self.bottomBar.titleText = KBLocalized(@"Download again");
self.bottomBar.showsPrice = NO;
} else {
NSString *price = self.detailModel ? [NSString stringWithFormat:@"%.2f", self.detailModel.themePrice] : @"0";
self.bottomBar.titleText = KBLocalized(@"Download");
self.bottomBar.priceText = price;
self.bottomBar.showsPrice = YES;
UIImage *coin = [UIImage imageNamed:@"shop_jb_icon"];
if (coin) {
self.bottomBar.iconImage = coin;
}
}
}
- (void)fetchThemeDetailIfNeeded {
if (self.themeId.length == 0) {
NSLog(@"[KBSkinDetailVC] themeId is empty, skip detail request");
return;
}
__weak typeof(self) weakSelf = self;
[self.shopVM fetchThemeDetailWithId:self.themeId completion:^(KBShopThemeDetailModel * _Nullable detail, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error) {
NSLog(@"[KBSkinDetailVC] fetch detail failed: %@", error);
return;
}
if (!detail) {
NSLog(@"[KBSkinDetailVC] theme detail is empty");
return;
}
weakSelf.detailModel = detail;
NSMutableArray<NSString *> *tagNames = [NSMutableArray array];
for (KBShopThemeTagModel *tag in detail.themeTag) {
if (tag.label.length) {
[tagNames addObject:tag.label];
}
}
weakSelf.tags = tagNames.copy;
[weakSelf updateBottomBarAppearance];
[weakSelf.collectionView reloadData];
});
}];
}
- (KBShopVM *)shopVM {
if (!_shopVM) {
_shopVM = [[KBShopVM alloc] init];
}
return _shopVM;
}
#pragma mark - Data
- (void)fetchRecommendedThemes {
__weak typeof(self) weakSelf = self;
[self.shopVM fetchRecommendedThemesWithCompletion:^(NSArray<KBShopThemeModel *> * _Nullable themes, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error) {
NSLog(@"[KBSkinDetailVC] fetch recommended failed: %@", error);
return;
}
weakSelf.recommendedThemes = themes ?: @[];
NSIndexSet *sections = [NSIndexSet indexSetWithIndex:KBSkinDetailSectionGrid];
[weakSelf.collectionView reloadSections:sections];
});
}];
}
- (NSString *)priceTextForTheme:(KBShopThemeModel *)model {
// if (model.isFree) {
// return KBLocalized(@"Free");
// }
if (model.themePrice > 0.0) {
return [NSString stringWithFormat:@"%.2f", model.themePrice];
}
return @"0";
}
@end