2025-11-11 14:56:57 +08:00
|
|
|
|
//
|
|
|
|
|
|
// KBPersonInfoVC.m
|
|
|
|
|
|
// keyBoard
|
|
|
|
|
|
//
|
|
|
|
|
|
// Created by Mac on 2025/11/11.
|
|
|
|
|
|
// 个人资料
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
|
|
#import "KBPersonInfoVC.h"
|
|
|
|
|
|
#import <Masonry/Masonry.h>
|
|
|
|
|
|
#import "KBPersonInfoItemCell.h"
|
2025-11-11 15:13:43 +08:00
|
|
|
|
#import <PhotosUI/PhotosUI.h>
|
2025-11-11 15:28:22 +08:00
|
|
|
|
#import "LSTPopView.h"
|
|
|
|
|
|
#import "KBChangeNicknamePopView.h"
|
2025-11-11 15:55:52 +08:00
|
|
|
|
#import "KBGenderPickerPopView.h"
|
2025-12-03 13:31:02 +08:00
|
|
|
|
#import "KBMyVM.h"
|
2026-02-27 14:49:46 +08:00
|
|
|
|
#import "KBAlert.h"
|
2026-02-28 14:50:27 +08:00
|
|
|
|
#import "KBCancelAccountVC.h"
|
2026-03-02 09:19:06 +08:00
|
|
|
|
#import "KBLocalizationManager.h"
|
|
|
|
|
|
#import "KBConfig.h"
|
|
|
|
|
|
#import "KBSkinInstallBridge.h"
|
|
|
|
|
|
#import "KBInputProfileManager.h"
|
|
|
|
|
|
|
|
|
|
|
|
static NSInteger const kKBPersonInfoRowNickname = 0;
|
|
|
|
|
|
static NSInteger const kKBPersonInfoRowGender = 1;
|
|
|
|
|
|
static NSInteger const kKBPersonInfoRowLanguage = 2;
|
|
|
|
|
|
static NSInteger const kKBPersonInfoRowUserID = 3;
|
|
|
|
|
|
|
|
|
|
|
|
typedef void(^KBInputProfileSelectHandler)(NSString *languageCode, NSString *layoutVariant, NSString *profileId);
|
|
|
|
|
|
|
|
|
|
|
|
@interface KBKeyboardLayoutSelectVC : BaseViewController <UITableViewDelegate, UITableViewDataSource>
|
|
|
|
|
|
@property (nonatomic, strong) BaseTableView *tableView;
|
|
|
|
|
|
@property (nonatomic, strong) UIButton *confirmButton;
|
|
|
|
|
|
@property (nonatomic, copy) NSDictionary *languageConfig;
|
|
|
|
|
|
@property (nonatomic, copy) NSString *selectedLayoutVariant;
|
|
|
|
|
|
@property (nonatomic, copy) NSString *selectedProfileId;
|
|
|
|
|
|
@property (nonatomic, copy) NSString *pendingLayoutVariant;
|
|
|
|
|
|
@property (nonatomic, copy) NSString *pendingProfileId;
|
|
|
|
|
|
@property (nonatomic, copy) KBInputProfileSelectHandler onSelect;
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
|
|
@implementation KBKeyboardLayoutSelectVC
|
|
|
|
|
|
|
|
|
|
|
|
- (void)viewDidLoad {
|
|
|
|
|
|
[super viewDidLoad];
|
|
|
|
|
|
/// 1:控件初始化
|
|
|
|
|
|
[self setupUI];
|
|
|
|
|
|
/// 2:初始化选中态
|
|
|
|
|
|
[self prepareDefaultSelection];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)setupUI {
|
|
|
|
|
|
self.kb_titleLabel.text = KBLocalized(@"Choose Layout");
|
|
|
|
|
|
self.view.backgroundColor = [UIColor colorWithHex:0xF8F8F8];
|
|
|
|
|
|
[self.view addSubview:self.tableView];
|
|
|
|
|
|
[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.left.right.equalTo(self.view);
|
|
|
|
|
|
make.top.equalTo(self.view).offset(KB_NAV_TOTAL_HEIGHT + 10);
|
|
|
|
|
|
make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom).offset(-72);
|
|
|
|
|
|
}];
|
|
|
|
|
|
[self.view addSubview:self.confirmButton];
|
|
|
|
|
|
[self.confirmButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.left.equalTo(self.view).offset(16);
|
|
|
|
|
|
make.right.equalTo(self.view).offset(-16);
|
|
|
|
|
|
make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom).offset(-12);
|
|
|
|
|
|
make.height.mas_equalTo(50);
|
|
|
|
|
|
}];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)prepareDefaultSelection {
|
|
|
|
|
|
NSArray *layouts = [self.languageConfig[@"layouts"] isKindOfClass:NSArray.class] ? self.languageConfig[@"layouts"] : @[];
|
|
|
|
|
|
NSDictionary *matched = nil;
|
|
|
|
|
|
for (NSDictionary *layout in layouts) {
|
|
|
|
|
|
NSString *variant = [layout[@"variant"] isKindOfClass:NSString.class] ? layout[@"variant"] : @"";
|
|
|
|
|
|
if (self.selectedLayoutVariant.length > 0 && [variant isEqualToString:self.selectedLayoutVariant]) {
|
|
|
|
|
|
matched = layout;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!matched) {
|
|
|
|
|
|
matched = layouts.firstObject;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.pendingLayoutVariant = [matched[@"variant"] isKindOfClass:NSString.class] ? matched[@"variant"] : @"";
|
|
|
|
|
|
self.pendingProfileId = [matched[@"profileId"] isKindOfClass:NSString.class] ? matched[@"profileId"] : @"";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
|
|
|
|
|
NSArray *layouts = [self.languageConfig[@"layouts"] isKindOfClass:NSArray.class] ? self.languageConfig[@"layouts"] : @[];
|
|
|
|
|
|
return layouts.count;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
|
|
|
|
|
|
return 56.0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
|
|
|
|
|
static NSString *cid = @"KBKeyboardLayoutCell";
|
|
|
|
|
|
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cid];
|
|
|
|
|
|
if (!cell) {
|
|
|
|
|
|
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cid];
|
|
|
|
|
|
cell.textLabel.font = [KBFont medium:16];
|
|
|
|
|
|
cell.detailTextLabel.font = [KBFont regular:12];
|
|
|
|
|
|
cell.detailTextLabel.textColor = [UIColor colorWithHex:0x999999];
|
|
|
|
|
|
}
|
|
|
|
|
|
NSArray *layouts = [self.languageConfig[@"layouts"] isKindOfClass:NSArray.class] ? self.languageConfig[@"layouts"] : @[];
|
|
|
|
|
|
NSDictionary *layout = (indexPath.row < layouts.count) ? layouts[indexPath.row] : @{};
|
|
|
|
|
|
NSString *title = [layout[@"title"] isKindOfClass:NSString.class] ? layout[@"title"] : @"";
|
|
|
|
|
|
NSString *variant = [layout[@"variant"] isKindOfClass:NSString.class] ? layout[@"variant"] : @"";
|
|
|
|
|
|
NSString *profileId = [layout[@"profileId"] isKindOfClass:NSString.class] ? layout[@"profileId"] : @"";
|
|
|
|
|
|
cell.textLabel.text = title;
|
|
|
|
|
|
cell.detailTextLabel.text = profileId;
|
|
|
|
|
|
BOOL selected = (self.pendingLayoutVariant.length > 0 && [self.pendingLayoutVariant isEqualToString:variant]);
|
|
|
|
|
|
cell.accessoryType = selected ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone;
|
|
|
|
|
|
return cell;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
|
|
|
|
|
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
|
|
|
|
|
NSArray *layouts = [self.languageConfig[@"layouts"] isKindOfClass:NSArray.class] ? self.languageConfig[@"layouts"] : @[];
|
|
|
|
|
|
NSDictionary *layout = (indexPath.row < layouts.count) ? layouts[indexPath.row] : nil;
|
|
|
|
|
|
if (![layout isKindOfClass:NSDictionary.class]) { return; }
|
|
|
|
|
|
self.pendingLayoutVariant = [layout[@"variant"] isKindOfClass:NSString.class] ? layout[@"variant"] : @"";
|
|
|
|
|
|
self.pendingProfileId = [layout[@"profileId"] isKindOfClass:NSString.class] ? layout[@"profileId"] : @"";
|
|
|
|
|
|
[self.tableView reloadData];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)onTapConfirm {
|
|
|
|
|
|
NSString *code = [self.languageConfig[@"code"] isKindOfClass:NSString.class] ? self.languageConfig[@"code"] : KBLanguageCodeEnglish;
|
|
|
|
|
|
if (self.onSelect && code.length > 0 && self.pendingLayoutVariant.length > 0 && self.pendingProfileId.length > 0) {
|
|
|
|
|
|
self.onSelect(code, self.pendingLayoutVariant, self.pendingProfileId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (BaseTableView *)tableView {
|
|
|
|
|
|
if (!_tableView) {
|
|
|
|
|
|
_tableView = [[BaseTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
|
|
|
|
|
|
_tableView.backgroundColor = UIColor.clearColor;
|
|
|
|
|
|
_tableView.dataSource = self;
|
|
|
|
|
|
_tableView.delegate = self;
|
|
|
|
|
|
_tableView.separatorInset = UIEdgeInsetsMake(0, 16, 0, 16);
|
|
|
|
|
|
}
|
|
|
|
|
|
return _tableView;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (UIButton *)confirmButton {
|
|
|
|
|
|
if (!_confirmButton) {
|
|
|
|
|
|
_confirmButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
|
|
|
|
|
[_confirmButton setTitle:KBLocalized(@"Confirm") forState:UIControlStateNormal];
|
|
|
|
|
|
[_confirmButton setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
|
|
|
|
|
|
_confirmButton.titleLabel.font = [KBFont medium:16];
|
|
|
|
|
|
_confirmButton.backgroundColor = [UIColor colorWithHex:0x111111];
|
|
|
|
|
|
_confirmButton.layer.cornerRadius = 12;
|
|
|
|
|
|
_confirmButton.layer.masksToBounds = YES;
|
|
|
|
|
|
[_confirmButton addTarget:self action:@selector(onTapConfirm) forControlEvents:UIControlEventTouchUpInside];
|
|
|
|
|
|
}
|
|
|
|
|
|
return _confirmButton;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
|
|
@interface KBKeyboardLanguageSelectVC : BaseViewController <UITableViewDelegate, UITableViewDataSource>
|
|
|
|
|
|
@property (nonatomic, strong) BaseTableView *tableView;
|
|
|
|
|
|
@property (nonatomic, strong) UIButton *confirmButton;
|
|
|
|
|
|
@property (nonatomic, copy) NSArray<NSDictionary *> *languageConfigs;
|
|
|
|
|
|
@property (nonatomic, copy) NSString *selectedLanguageCode;
|
|
|
|
|
|
@property (nonatomic, copy) NSString *selectedLayoutVariant;
|
|
|
|
|
|
@property (nonatomic, copy) NSString *pendingLanguageCode;
|
|
|
|
|
|
@property (nonatomic, copy) NSString *pendingLayoutVariant;
|
|
|
|
|
|
@property (nonatomic, copy) NSString *pendingProfileId;
|
|
|
|
|
|
@property (nonatomic, copy) KBInputProfileSelectHandler onSelect;
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
|
|
@implementation KBKeyboardLanguageSelectVC
|
|
|
|
|
|
|
|
|
|
|
|
- (void)viewDidLoad {
|
|
|
|
|
|
[super viewDidLoad];
|
|
|
|
|
|
/// 1:控件初始化
|
|
|
|
|
|
[self setupUI];
|
|
|
|
|
|
/// 2:初始化选中态
|
|
|
|
|
|
[self prepareDefaultSelection];
|
|
|
|
|
|
[self updateConfirmVisibility];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)setupUI {
|
|
|
|
|
|
self.kb_titleLabel.text = KBLocalized(@"Input Language");
|
|
|
|
|
|
self.view.backgroundColor = [UIColor colorWithHex:0xF8F8F8];
|
|
|
|
|
|
[self.view addSubview:self.tableView];
|
|
|
|
|
|
[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.left.right.equalTo(self.view);
|
|
|
|
|
|
make.top.equalTo(self.view).offset(KB_NAV_TOTAL_HEIGHT + 10);
|
|
|
|
|
|
make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom).offset(-72);
|
|
|
|
|
|
}];
|
|
|
|
|
|
[self.view addSubview:self.confirmButton];
|
|
|
|
|
|
[self.confirmButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.left.equalTo(self.view).offset(16);
|
|
|
|
|
|
make.right.equalTo(self.view).offset(-16);
|
|
|
|
|
|
make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom).offset(-12);
|
|
|
|
|
|
make.height.mas_equalTo(50);
|
|
|
|
|
|
}];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)prepareDefaultSelection {
|
|
|
|
|
|
NSDictionary *config = [self languageConfigForCode:self.selectedLanguageCode];
|
|
|
|
|
|
if (!config) {
|
|
|
|
|
|
config = self.languageConfigs.firstObject;
|
|
|
|
|
|
}
|
|
|
|
|
|
NSString *code = [config[@"code"] isKindOfClass:NSString.class] ? config[@"code"] : KBLanguageCodeEnglish;
|
|
|
|
|
|
NSArray *layouts = [config[@"layouts"] isKindOfClass:NSArray.class] ? config[@"layouts"] : @[];
|
|
|
|
|
|
NSDictionary *layoutMatched = nil;
|
|
|
|
|
|
for (NSDictionary *layout in layouts) {
|
|
|
|
|
|
NSString *variant = [layout[@"variant"] isKindOfClass:NSString.class] ? layout[@"variant"] : @"";
|
|
|
|
|
|
if (self.selectedLayoutVariant.length > 0 && [variant isEqualToString:self.selectedLayoutVariant]) {
|
|
|
|
|
|
layoutMatched = layout;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!layoutMatched) {
|
|
|
|
|
|
layoutMatched = layouts.firstObject;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.pendingLanguageCode = code;
|
|
|
|
|
|
self.pendingLayoutVariant = [layoutMatched[@"variant"] isKindOfClass:NSString.class] ? layoutMatched[@"variant"] : @"qwerty";
|
|
|
|
|
|
self.pendingProfileId = [layoutMatched[@"profileId"] isKindOfClass:NSString.class] ? layoutMatched[@"profileId"] : @"en_US_qwerty";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (nullable NSDictionary *)languageConfigForCode:(NSString *)code {
|
|
|
|
|
|
for (NSDictionary *item in self.languageConfigs) {
|
|
|
|
|
|
NSString *langCode = [item[@"code"] isKindOfClass:NSString.class] ? item[@"code"] : @"";
|
|
|
|
|
|
if ([langCode isEqualToString:code]) {
|
|
|
|
|
|
return item;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)updateConfirmVisibility {
|
|
|
|
|
|
NSDictionary *config = [self languageConfigForCode:self.pendingLanguageCode];
|
|
|
|
|
|
NSArray *layouts = [config[@"layouts"] isKindOfClass:NSArray.class] ? config[@"layouts"] : @[];
|
|
|
|
|
|
BOOL hasMultiLayout = (layouts.count > 1);
|
|
|
|
|
|
self.confirmButton.hidden = hasMultiLayout;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
|
|
|
|
|
return self.languageConfigs.count;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
|
|
|
|
|
|
return 56.0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
|
|
|
|
|
static NSString *cid = @"KBKeyboardLanguageCell";
|
|
|
|
|
|
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cid];
|
|
|
|
|
|
if (!cell) {
|
|
|
|
|
|
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cid];
|
|
|
|
|
|
cell.textLabel.font = [KBFont medium:16];
|
|
|
|
|
|
cell.detailTextLabel.font = [KBFont regular:12];
|
|
|
|
|
|
cell.detailTextLabel.textColor = [UIColor colorWithHex:0x999999];
|
|
|
|
|
|
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
|
|
|
|
|
}
|
|
|
|
|
|
NSDictionary *lang = (indexPath.row < self.languageConfigs.count) ? self.languageConfigs[indexPath.row] : @{};
|
|
|
|
|
|
NSString *name = [lang[@"name"] isKindOfClass:NSString.class] ? lang[@"name"] : @"";
|
|
|
|
|
|
NSString *code = [lang[@"code"] isKindOfClass:NSString.class] ? lang[@"code"] : @"";
|
|
|
|
|
|
NSArray *layouts = [lang[@"layouts"] isKindOfClass:NSArray.class] ? lang[@"layouts"] : @[];
|
|
|
|
|
|
BOOL multi = layouts.count > 1;
|
|
|
|
|
|
BOOL isSelected = (self.pendingLanguageCode.length > 0 && [self.pendingLanguageCode isEqualToString:code]);
|
|
|
|
|
|
cell.textLabel.text = name;
|
|
|
|
|
|
cell.detailTextLabel.text = multi ? KBLocalized(@"Multiple Keyboard Layouts") : @"QWERTY";
|
|
|
|
|
|
cell.accessoryType = multi ? UITableViewCellAccessoryDisclosureIndicator : (isSelected ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone);
|
|
|
|
|
|
return cell;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
|
|
|
|
|
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
|
|
|
|
|
NSDictionary *lang = (indexPath.row < self.languageConfigs.count) ? self.languageConfigs[indexPath.row] : nil;
|
|
|
|
|
|
if (![lang isKindOfClass:NSDictionary.class]) { return; }
|
|
|
|
|
|
NSString *code = [lang[@"code"] isKindOfClass:NSString.class] ? lang[@"code"] : KBLanguageCodeEnglish;
|
|
|
|
|
|
NSArray *layouts = [lang[@"layouts"] isKindOfClass:NSArray.class] ? lang[@"layouts"] : @[];
|
|
|
|
|
|
if (layouts.count > 1) {
|
|
|
|
|
|
self.pendingLanguageCode = code;
|
|
|
|
|
|
[self updateConfirmVisibility];
|
|
|
|
|
|
[self.tableView reloadData];
|
|
|
|
|
|
KBKeyboardLayoutSelectVC *layoutVC = [[KBKeyboardLayoutSelectVC alloc] init];
|
|
|
|
|
|
layoutVC.languageConfig = lang;
|
|
|
|
|
|
layoutVC.selectedLayoutVariant = self.selectedLanguageCode.length > 0 && [self.selectedLanguageCode isEqualToString:code] ? self.selectedLayoutVariant : @"";
|
|
|
|
|
|
__weak typeof(self) weakSelf = self;
|
|
|
|
|
|
layoutVC.onSelect = ^(NSString *languageCode, NSString *layoutVariant, NSString *profileId) {
|
|
|
|
|
|
weakSelf.pendingLanguageCode = languageCode;
|
|
|
|
|
|
weakSelf.pendingLayoutVariant = layoutVariant;
|
|
|
|
|
|
weakSelf.pendingProfileId = profileId;
|
|
|
|
|
|
if (weakSelf.onSelect) {
|
|
|
|
|
|
weakSelf.onSelect(languageCode, layoutVariant, profileId);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
[self.navigationController pushViewController:layoutVC animated:YES];
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
NSDictionary *layout = layouts.firstObject;
|
|
|
|
|
|
self.pendingLanguageCode = code;
|
|
|
|
|
|
self.pendingLayoutVariant = [layout[@"variant"] isKindOfClass:NSString.class] ? layout[@"variant"] : @"qwerty";
|
|
|
|
|
|
self.pendingProfileId = [layout[@"profileId"] isKindOfClass:NSString.class] ? layout[@"profileId"] : @"en_US_qwerty";
|
|
|
|
|
|
[self updateConfirmVisibility];
|
|
|
|
|
|
[self.tableView reloadData];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (BaseTableView *)tableView {
|
|
|
|
|
|
if (!_tableView) {
|
|
|
|
|
|
_tableView = [[BaseTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
|
|
|
|
|
|
_tableView.backgroundColor = UIColor.clearColor;
|
|
|
|
|
|
_tableView.dataSource = self;
|
|
|
|
|
|
_tableView.delegate = self;
|
|
|
|
|
|
_tableView.separatorInset = UIEdgeInsetsMake(0, 16, 0, 16);
|
|
|
|
|
|
}
|
|
|
|
|
|
return _tableView;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)onTapConfirm {
|
|
|
|
|
|
if (self.confirmButton.hidden) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (self.onSelect && self.pendingLanguageCode.length > 0 && self.pendingLayoutVariant.length > 0 && self.pendingProfileId.length > 0) {
|
|
|
|
|
|
self.onSelect(self.pendingLanguageCode, self.pendingLayoutVariant, self.pendingProfileId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (UIButton *)confirmButton {
|
|
|
|
|
|
if (!_confirmButton) {
|
|
|
|
|
|
_confirmButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
|
|
|
|
|
[_confirmButton setTitle:KBLocalized(@"Confirm") forState:UIControlStateNormal];
|
|
|
|
|
|
[_confirmButton setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
|
|
|
|
|
|
_confirmButton.titleLabel.font = [KBFont medium:16];
|
|
|
|
|
|
_confirmButton.backgroundColor = [UIColor colorWithHex:0x111111];
|
|
|
|
|
|
_confirmButton.layer.cornerRadius = 12;
|
|
|
|
|
|
_confirmButton.layer.masksToBounds = YES;
|
|
|
|
|
|
[_confirmButton addTarget:self action:@selector(onTapConfirm) forControlEvents:UIControlEventTouchUpInside];
|
|
|
|
|
|
}
|
|
|
|
|
|
return _confirmButton;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
2025-11-11 15:13:43 +08:00
|
|
|
|
@interface KBPersonInfoVC () <UITableViewDelegate, UITableViewDataSource, PHPickerViewControllerDelegate, UINavigationControllerDelegate, UIImagePickerControllerDelegate>
|
2025-11-11 14:56:57 +08:00
|
|
|
|
|
|
|
|
|
|
// 列表
|
|
|
|
|
|
@property (nonatomic, strong) BaseTableView *tableView; // 懒加载
|
|
|
|
|
|
|
|
|
|
|
|
// 头像区(表头)
|
|
|
|
|
|
@property (nonatomic, strong) UIView *headerView;
|
|
|
|
|
|
@property (nonatomic, strong) UIImageView *avatarView; // 头像
|
|
|
|
|
|
@property (nonatomic, strong) UIButton *editBadge; // 头像右下角的小铅笔
|
|
|
|
|
|
@property (nonatomic, strong) UILabel *modifyLabel; // “Modify” 文案
|
2026-03-04 21:16:21 +08:00
|
|
|
|
@property (nonatomic, strong) UILabel *userIdLabel; // 用户 ID
|
|
|
|
|
|
@property (nonatomic, strong) UIButton *userIdCopyButton; // 复制用户 ID
|
2025-11-11 14:56:57 +08:00
|
|
|
|
|
2026-02-27 14:49:46 +08:00
|
|
|
|
// 底部退出登录按钮
|
2025-11-11 14:56:57 +08:00
|
|
|
|
@property (nonatomic, strong) UIButton *logoutBtn;
|
|
|
|
|
|
|
|
|
|
|
|
// 数据
|
|
|
|
|
|
@property (nonatomic, copy) NSArray<NSDictionary *> *items; // {title,value,arrow,copy}
|
2025-11-11 15:13:43 +08:00
|
|
|
|
// 压缩后的头像 JPEG 数据(可用于上传)
|
|
|
|
|
|
@property (nonatomic, strong) NSData *avatarJPEGData;
|
2025-11-11 14:56:57 +08:00
|
|
|
|
|
2025-12-03 13:31:02 +08:00
|
|
|
|
@property (nonatomic, strong) KBMyVM *myVM;
|
2025-12-04 14:44:56 +08:00
|
|
|
|
@property (nonatomic, strong) KBMyVM *viewModel; // 我的页面 VM
|
|
|
|
|
|
@property (nonatomic, strong) KBUser *userModel;
|
2026-03-02 09:19:06 +08:00
|
|
|
|
@property (nonatomic, copy) NSString *selectedLanguageCode;
|
|
|
|
|
|
@property (nonatomic, copy) NSString *selectedLayoutVariant;
|
|
|
|
|
|
@property (nonatomic, copy) NSString *selectedProfileId;
|
2025-12-03 13:31:02 +08:00
|
|
|
|
|
2025-11-11 14:56:57 +08:00
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
|
|
@implementation KBPersonInfoVC
|
|
|
|
|
|
|
|
|
|
|
|
- (void)viewDidLoad {
|
|
|
|
|
|
[super viewDidLoad];
|
2026-03-02 09:19:06 +08:00
|
|
|
|
/// 1:控件初始化
|
|
|
|
|
|
[self setupUI];
|
|
|
|
|
|
/// 2:恢复输入配置
|
|
|
|
|
|
[self restoreInputProfile];
|
|
|
|
|
|
/// 3:加载用户资料
|
|
|
|
|
|
[self loadData];
|
|
|
|
|
|
/// 4:监听语言变化
|
|
|
|
|
|
[self bindNotifications];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)dealloc {
|
|
|
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)setupUI {
|
2025-11-25 18:54:53 +08:00
|
|
|
|
self.kb_titleLabel.text = KBLocalized(@"Settings"); // 导航标题
|
2025-11-11 14:56:57 +08:00
|
|
|
|
self.kb_navView.backgroundColor = [UIColor clearColor];
|
|
|
|
|
|
self.view.backgroundColor = [UIColor colorWithHex:0xF8F8F8];
|
|
|
|
|
|
|
|
|
|
|
|
[self.view addSubview:self.tableView];
|
|
|
|
|
|
[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.left.right.bottom.equalTo(self.view);
|
|
|
|
|
|
make.top.equalTo(self.view).offset(KB_NAV_TOTAL_HEIGHT + 10);
|
|
|
|
|
|
}];
|
|
|
|
|
|
|
2025-11-11 15:13:43 +08:00
|
|
|
|
// 表头
|
2025-11-11 14:56:57 +08:00
|
|
|
|
self.tableView.tableHeaderView = self.headerView;
|
2025-11-11 15:13:43 +08:00
|
|
|
|
|
2026-02-27 14:49:46 +08:00
|
|
|
|
// 底部退出登录按钮
|
2025-11-11 15:13:43 +08:00
|
|
|
|
[self.view addSubview:self.logoutBtn];
|
|
|
|
|
|
[self.logoutBtn mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.left.equalTo(self.view).offset(16);
|
|
|
|
|
|
make.right.equalTo(self.view).offset(-16);
|
|
|
|
|
|
make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom).offset(-12);
|
|
|
|
|
|
make.height.mas_equalTo(56);
|
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
|
|
// 列表底部腾出空间,避免被按钮挡住
|
|
|
|
|
|
UIEdgeInsets inset = self.tableView.contentInset;
|
2026-02-28 14:50:27 +08:00
|
|
|
|
inset.bottom = 56 + 24; // 按钮高度 + 额外间距
|
2025-11-11 15:13:43 +08:00
|
|
|
|
self.tableView.contentInset = inset;
|
2026-03-02 09:19:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)loadData {
|
2025-12-04 14:44:56 +08:00
|
|
|
|
self.viewModel = [[KBMyVM alloc] init];
|
|
|
|
|
|
__weak typeof(self) weakSelf = self;
|
|
|
|
|
|
[self.viewModel fetchUserDetailWithCompletion:^(KBUser * _Nullable user, NSError * _Nullable error) {
|
|
|
|
|
|
if (user) {
|
|
|
|
|
|
weakSelf.userModel = user;
|
2025-12-19 21:29:11 +08:00
|
|
|
|
[weakSelf.avatarView kb_setAvatarURL:weakSelf.userModel.avatarUrl placeholder:KBAvatarPlaceholderImage];
|
2025-12-04 14:44:56 +08:00
|
|
|
|
weakSelf.modifyLabel.text = weakSelf.userModel.nickName;
|
2026-03-04 21:16:21 +08:00
|
|
|
|
weakSelf.userIdLabel.text = weakSelf.userModel.userId ?: @"";
|
2025-12-04 14:44:56 +08:00
|
|
|
|
}
|
2026-03-02 09:19:06 +08:00
|
|
|
|
[weakSelf rebuildItems];
|
2025-12-04 14:44:56 +08:00
|
|
|
|
}];
|
2025-11-11 14:56:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-02 09:19:06 +08:00
|
|
|
|
- (void)bindNotifications {
|
|
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
|
|
|
|
selector:@selector(handleLanguageChanged)
|
|
|
|
|
|
name:KBLocalizationDidChangeNotification
|
|
|
|
|
|
object:nil];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)handleLanguageChanged {
|
|
|
|
|
|
self.kb_titleLabel.text = KBLocalized(@"Settings");
|
|
|
|
|
|
[self.logoutBtn setTitle:KBLocalized(@"Log Out") forState:UIControlStateNormal];
|
|
|
|
|
|
[self rebuildItems];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)rebuildItems {
|
|
|
|
|
|
NSString *nickname = self.userModel.nickName ?: @"";
|
|
|
|
|
|
NSString *genderText = [self kb_genderDisplayText];
|
|
|
|
|
|
NSString *languageText = [self currentInputProfileDisplayText];
|
|
|
|
|
|
self.items = @[
|
|
|
|
|
|
@{ @"title": KBLocalized(@"Nickname"), @"value": nickname, @"arrow": @YES, @"copy": @NO },
|
|
|
|
|
|
@{ @"title": KBLocalized(@"Gender"), @"value": genderText, @"arrow": @YES, @"copy": @NO },
|
|
|
|
|
|
@{ @"title": KBLocalized(@"Input Language"), @"value": languageText, @"arrow": @YES, @"copy": @NO },
|
|
|
|
|
|
];
|
|
|
|
|
|
[self.tableView reloadData];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-03 20:31:33 +08:00
|
|
|
|
|
2025-12-04 14:26:22 +08:00
|
|
|
|
/// 根据 userModel.gender 生成展示用的性别文案(带多语言)
|
|
|
|
|
|
- (NSString *)kb_genderDisplayText {
|
|
|
|
|
|
if (!self.userModel) {
|
|
|
|
|
|
return KBLocalized(@"Choose");
|
|
|
|
|
|
}
|
|
|
|
|
|
switch (self.userModel.gender) {
|
|
|
|
|
|
case UserSexMan:
|
|
|
|
|
|
return KBLocalized(@"Male");
|
|
|
|
|
|
case UserSexWeman:
|
|
|
|
|
|
return KBLocalized(@"Female");
|
|
|
|
|
|
case UserSexTwoSex:
|
|
|
|
|
|
return KBLocalized(@"The Third Gender");
|
|
|
|
|
|
default:
|
|
|
|
|
|
return KBLocalized(@"Choose");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-02 09:19:06 +08:00
|
|
|
|
- (void)restoreInputProfile {
|
|
|
|
|
|
NSUserDefaults *shared = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
|
|
|
|
|
|
NSString *savedCode = [shared stringForKey:AppGroup_SelectedKeyboardLanguageCode];
|
|
|
|
|
|
NSString *savedVariant = [shared stringForKey:AppGroup_SelectedKeyboardLayoutVariant];
|
|
|
|
|
|
NSString *savedProfile = [shared stringForKey:AppGroup_SelectedKeyboardProfileId];
|
|
|
|
|
|
|
|
|
|
|
|
NSString *currentCode = [KBLocalizationManager shared].currentLanguageCode ?: KBLanguageCodeEnglish;
|
|
|
|
|
|
NSDictionary *config = [self languageConfigForCode:(savedCode.length ? savedCode : currentCode)];
|
|
|
|
|
|
if (!config) {
|
|
|
|
|
|
config = [self languageConfigForCode:KBLanguageCodeEnglish];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
NSArray *layouts = [config[@"layouts"] isKindOfClass:NSArray.class] ? config[@"layouts"] : @[];
|
|
|
|
|
|
NSDictionary *layout = nil;
|
|
|
|
|
|
for (NSDictionary *item in layouts) {
|
|
|
|
|
|
NSString *variant = [item[@"variant"] isKindOfClass:NSString.class] ? item[@"variant"] : @"";
|
|
|
|
|
|
if (savedVariant.length > 0 && [variant isEqualToString:savedVariant]) {
|
|
|
|
|
|
layout = item;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!layout) { layout = layouts.firstObject; }
|
|
|
|
|
|
|
|
|
|
|
|
self.selectedLanguageCode = [config[@"code"] isKindOfClass:NSString.class] ? config[@"code"] : KBLanguageCodeEnglish;
|
|
|
|
|
|
self.selectedLayoutVariant = [layout[@"variant"] isKindOfClass:NSString.class] ? layout[@"variant"] : @"qwerty";
|
|
|
|
|
|
self.selectedProfileId = savedProfile.length > 0 ? savedProfile : ([layout[@"profileId"] isKindOfClass:NSString.class] ? layout[@"profileId"] : @"en_US_qwerty");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (NSArray<NSDictionary *> *)languageConfigs {
|
|
|
|
|
|
NSArray<KBInputProfile *> *profiles = [[KBInputProfileManager sharedManager] allProfiles];
|
|
|
|
|
|
NSMutableArray<NSDictionary *> *configs = [NSMutableArray array];
|
|
|
|
|
|
|
|
|
|
|
|
for (KBInputProfile *profile in profiles) {
|
|
|
|
|
|
NSMutableArray<NSDictionary *> *layouts = [NSMutableArray array];
|
|
|
|
|
|
for (KBInputProfileLayout *layout in profile.layouts) {
|
|
|
|
|
|
[layouts addObject:@{
|
|
|
|
|
|
@"variant": layout.variant,
|
|
|
|
|
|
@"title": layout.title,
|
|
|
|
|
|
@"profileId": layout.profileId
|
|
|
|
|
|
}];
|
|
|
|
|
|
}
|
2026-03-09 17:34:08 +08:00
|
|
|
|
// STTODO 繁体拼音下个版本更新
|
|
|
|
|
|
if ([profile.code isEqualToString:@"zh-Hant-Pinyin"]) {
|
|
|
|
|
|
// NSLog(@"===");
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-03-02 09:19:06 +08:00
|
|
|
|
[configs addObject:@{
|
|
|
|
|
|
@"code": profile.code,
|
|
|
|
|
|
@"name": profile.name,
|
|
|
|
|
|
@"layouts": [layouts copy]
|
|
|
|
|
|
}];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return [configs copy];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (nullable NSDictionary *)languageConfigForCode:(NSString *)languageCode {
|
|
|
|
|
|
for (NSDictionary *item in [self languageConfigs]) {
|
|
|
|
|
|
NSString *code = [item[@"code"] isKindOfClass:NSString.class] ? item[@"code"] : @"";
|
|
|
|
|
|
if ([code isEqualToString:languageCode]) {
|
|
|
|
|
|
return item;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (NSString *)layoutTitleForLanguageCode:(NSString *)languageCode variant:(NSString *)variant {
|
|
|
|
|
|
NSDictionary *config = [self languageConfigForCode:languageCode];
|
|
|
|
|
|
NSArray *layouts = [config[@"layouts"] isKindOfClass:NSArray.class] ? config[@"layouts"] : @[];
|
|
|
|
|
|
for (NSDictionary *layout in layouts) {
|
|
|
|
|
|
NSString *v = [layout[@"variant"] isKindOfClass:NSString.class] ? layout[@"variant"] : @"";
|
|
|
|
|
|
if ([v isEqualToString:variant]) {
|
|
|
|
|
|
return [layout[@"title"] isKindOfClass:NSString.class] ? layout[@"title"] : @"";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return @"";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (NSString *)currentInputProfileDisplayText {
|
|
|
|
|
|
NSDictionary *config = [self languageConfigForCode:self.selectedLanguageCode ?: KBLanguageCodeEnglish];
|
|
|
|
|
|
NSString *languageName = [config[@"name"] isKindOfClass:NSString.class] ? config[@"name"] : @"English";
|
2026-03-04 21:57:37 +08:00
|
|
|
|
if ([self.selectedLanguageCode isEqualToString:KBLanguageCodeSpanish]) {
|
|
|
|
|
|
languageName = @"Español";
|
|
|
|
|
|
}
|
2026-03-02 09:19:06 +08:00
|
|
|
|
NSString *layoutTitle = [self layoutTitleForLanguageCode:self.selectedLanguageCode variant:self.selectedLayoutVariant];
|
|
|
|
|
|
if (layoutTitle.length == 0) {
|
|
|
|
|
|
return languageName;
|
|
|
|
|
|
}
|
2026-03-04 21:57:37 +08:00
|
|
|
|
NSString *variant = self.selectedLayoutVariant ?: @"";
|
|
|
|
|
|
if ([variant.lowercaseString isEqualToString:@"qwerty"] ||
|
|
|
|
|
|
[layoutTitle.lowercaseString isEqualToString:@"qwerty"]) {
|
|
|
|
|
|
return languageName;
|
|
|
|
|
|
}
|
2026-03-02 09:19:06 +08:00
|
|
|
|
return [NSString stringWithFormat:@"%@ · %@", languageName, layoutTitle];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)openLanguageSelector {
|
|
|
|
|
|
KBKeyboardLanguageSelectVC *vc = [[KBKeyboardLanguageSelectVC alloc] init];
|
|
|
|
|
|
vc.languageConfigs = [self languageConfigs];
|
|
|
|
|
|
vc.selectedLanguageCode = self.selectedLanguageCode ?: KBLanguageCodeEnglish;
|
|
|
|
|
|
vc.selectedLayoutVariant = self.selectedLayoutVariant ?: @"qwerty";
|
|
|
|
|
|
__weak typeof(self) weakSelf = self;
|
|
|
|
|
|
vc.onSelect = ^(NSString *languageCode, NSString *layoutVariant, NSString *profileId) {
|
|
|
|
|
|
[weakSelf applyInputProfileWithLanguageCode:languageCode
|
|
|
|
|
|
layoutVariant:layoutVariant
|
|
|
|
|
|
profileId:profileId
|
|
|
|
|
|
completion:^(BOOL success) {
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
[weakSelf.navigationController popToViewController:weakSelf animated:YES];
|
|
|
|
|
|
}
|
|
|
|
|
|
}];
|
|
|
|
|
|
};
|
|
|
|
|
|
[self.navigationController pushViewController:vc animated:YES];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)applyInputProfileWithLanguageCode:(NSString *)languageCode
|
|
|
|
|
|
layoutVariant:(NSString *)layoutVariant
|
|
|
|
|
|
profileId:(NSString *)profileId
|
|
|
|
|
|
completion:(void(^ _Nullable)(BOOL success))completion {
|
|
|
|
|
|
if (languageCode.length == 0 || layoutVariant.length == 0 || profileId.length == 0) {
|
|
|
|
|
|
if (completion) { completion(NO); }
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
KBInputProfile *profile = [[KBInputProfileManager sharedManager] profileForLanguageCode:languageCode];
|
|
|
|
|
|
NSString *zipName = profile.defaultSkinZip;
|
2026-03-02 14:39:47 +08:00
|
|
|
|
|
2026-03-02 09:19:06 +08:00
|
|
|
|
if (zipName.length == 0) {
|
2026-03-02 14:39:47 +08:00
|
|
|
|
NSLog(@"[KBPersonInfoVC] No defaultSkinZip configured for %@, skipping skin installation", languageCode);
|
|
|
|
|
|
[self commitInputProfileSwitchWithLanguageCode:languageCode
|
|
|
|
|
|
layoutVariant:layoutVariant
|
|
|
|
|
|
profileId:profileId];
|
|
|
|
|
|
if (completion) { completion(YES); }
|
2026-03-02 09:19:06 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
__weak typeof(self) weakSelf = self;
|
|
|
|
|
|
[self installDefaultSkinForLanguageCode:languageCode completion:^(BOOL skinOK) {
|
|
|
|
|
|
if (!skinOK) {
|
|
|
|
|
|
[KBHUD showInfo:KBLocalized(@"Default skin install failed. Please check skin resource configuration.")];
|
|
|
|
|
|
if (completion) { completion(NO); }
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
[weakSelf commitInputProfileSwitchWithLanguageCode:languageCode
|
|
|
|
|
|
layoutVariant:layoutVariant
|
|
|
|
|
|
profileId:profileId];
|
|
|
|
|
|
if (completion) { completion(YES); }
|
|
|
|
|
|
}];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)commitInputProfileSwitchWithLanguageCode:(NSString *)languageCode
|
|
|
|
|
|
layoutVariant:(NSString *)layoutVariant
|
|
|
|
|
|
profileId:(NSString *)profileId {
|
|
|
|
|
|
self.selectedLanguageCode = languageCode;
|
|
|
|
|
|
self.selectedLayoutVariant = layoutVariant;
|
|
|
|
|
|
self.selectedProfileId = profileId;
|
|
|
|
|
|
[self rebuildItems];
|
|
|
|
|
|
|
|
|
|
|
|
NSUserDefaults *shared = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
|
|
|
|
|
|
[shared setObject:languageCode forKey:AppGroup_SelectedKeyboardLanguageCode];
|
|
|
|
|
|
[shared setObject:layoutVariant forKey:AppGroup_SelectedKeyboardLayoutVariant];
|
|
|
|
|
|
[shared setObject:profileId forKey:AppGroup_SelectedKeyboardProfileId];
|
2026-03-05 17:42:50 +08:00
|
|
|
|
[shared setBool:YES forKey:AppGroup_DidUserSelectKeyboardProfile];
|
2026-03-02 09:19:06 +08:00
|
|
|
|
[shared synchronize];
|
|
|
|
|
|
|
|
|
|
|
|
[[KBLocalizationManager shared] setCurrentLanguageCode:languageCode persist:YES];
|
|
|
|
|
|
|
|
|
|
|
|
NSString *message = KBLocalized(@"Changing language will reload the Home screen.");
|
|
|
|
|
|
[KBHUD showInfo:message];
|
|
|
|
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.35 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
|
|
|
|
id<UIApplicationDelegate> appDelegate = UIApplication.sharedApplication.delegate;
|
|
|
|
|
|
if ([appDelegate respondsToSelector:@selector(setupRootVC)]) {
|
|
|
|
|
|
#pragma clang diagnostic push
|
|
|
|
|
|
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|
|
|
|
|
[appDelegate performSelector:@selector(setupRootVC)];
|
|
|
|
|
|
#pragma clang diagnostic pop
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)installDefaultSkinForLanguageCode:(NSString *)languageCode completion:(void(^)(BOOL success))completion {
|
|
|
|
|
|
KBInputProfile *profile = [[KBInputProfileManager sharedManager] profileForLanguageCode:languageCode];
|
|
|
|
|
|
NSString *zipName = profile.defaultSkinZip;
|
|
|
|
|
|
if (zipName.length == 0) {
|
|
|
|
|
|
if (completion) { completion(NO); }
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
NSString *skinId = [NSString stringWithFormat:@"bundle_skin_default_%@", languageCode];
|
2026-03-02 20:20:28 +08:00
|
|
|
|
NSDictionary<NSString *, NSString *> *iconShortNames = [KBSkinInstallBridge iconShortNamesForLanguageCode:languageCode];
|
|
|
|
|
|
NSLog(@"[KBPersonInfoVC] Installing skin %@ with %lu icon mappings for language %@", skinId, (unsigned long)iconShortNames.count, languageCode);
|
|
|
|
|
|
|
2026-03-02 09:19:06 +08:00
|
|
|
|
[KBSkinInstallBridge publishBundleSkinRequestWithId:skinId
|
|
|
|
|
|
name:skinId
|
|
|
|
|
|
zipName:zipName
|
2026-03-02 20:20:28 +08:00
|
|
|
|
iconShortNames:iconShortNames];
|
2026-03-02 09:19:06 +08:00
|
|
|
|
[KBSkinInstallBridge consumePendingRequestFromBundle:NSBundle.mainBundle
|
|
|
|
|
|
completion:^(BOOL success, NSError * _Nullable error) {
|
|
|
|
|
|
if (!success && error) {
|
|
|
|
|
|
NSLog(@"[KBPersonInfoVC] default skin install failed: %@", error);
|
|
|
|
|
|
} else if (success) {
|
|
|
|
|
|
NSLog(@"[KBPersonInfoVC] default skin installed: %@", skinId);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (completion) { completion(success); }
|
|
|
|
|
|
}];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-11 14:56:57 +08:00
|
|
|
|
#pragma mark - UITableView
|
|
|
|
|
|
|
2026-02-28 14:50:27 +08:00
|
|
|
|
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 2; }
|
|
|
|
|
|
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
|
|
|
|
|
return section == 0 ? self.items.count : 1;
|
|
|
|
|
|
}
|
2025-11-11 14:56:57 +08:00
|
|
|
|
|
|
|
|
|
|
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
|
|
|
|
|
|
return 56.0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-28 14:50:27 +08:00
|
|
|
|
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
|
|
|
|
|
|
return section == 0 ? 12.0 : 15.0;
|
|
|
|
|
|
}
|
2025-11-11 14:56:57 +08:00
|
|
|
|
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { return [UIView new]; }
|
|
|
|
|
|
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section { return 0.01; }
|
|
|
|
|
|
|
|
|
|
|
|
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
|
|
|
|
|
static NSString *cid = @"KBPersonInfoItemCell";
|
|
|
|
|
|
KBPersonInfoItemCell *cell = [tableView dequeueReusableCellWithIdentifier:cid];
|
|
|
|
|
|
if (!cell) { cell = [[KBPersonInfoItemCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cid]; }
|
2026-02-28 14:50:27 +08:00
|
|
|
|
|
|
|
|
|
|
if (indexPath.section == 0) {
|
|
|
|
|
|
NSDictionary *it = self.items[indexPath.row];
|
|
|
|
|
|
BOOL isTop = (indexPath.row == 0);
|
|
|
|
|
|
BOOL isBottom = (indexPath.row == self.items.count - 1);
|
|
|
|
|
|
[cell configWithTitle:it[@"title"]
|
|
|
|
|
|
value:it[@"value"]
|
|
|
|
|
|
showArrow:[it[@"arrow"] boolValue]
|
|
|
|
|
|
showCopy:[it[@"copy"] boolValue]
|
|
|
|
|
|
isTop:isTop
|
|
|
|
|
|
isBottom:isBottom];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
[cell configWithTitle:KBLocalized(@"Cancel Account")
|
|
|
|
|
|
value:@""
|
|
|
|
|
|
showArrow:YES
|
|
|
|
|
|
showCopy:NO
|
|
|
|
|
|
isTop:YES
|
|
|
|
|
|
isBottom:YES];
|
|
|
|
|
|
}
|
2025-11-11 14:56:57 +08:00
|
|
|
|
return cell;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
2026-02-28 14:50:27 +08:00
|
|
|
|
if (indexPath.section == 1) {
|
|
|
|
|
|
KBCancelAccountVC *vc = [[KBCancelAccountVC alloc] init];
|
|
|
|
|
|
[self.navigationController pushViewController:vc animated:YES];
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-03-02 09:19:06 +08:00
|
|
|
|
if (indexPath.row == kKBPersonInfoRowNickname) {
|
2025-11-11 15:28:22 +08:00
|
|
|
|
// 昵称编辑 -> 弹窗
|
2025-11-11 15:55:52 +08:00
|
|
|
|
CGFloat width = KB_SCREEN_WIDTH;
|
2025-11-11 15:28:22 +08:00
|
|
|
|
KBChangeNicknamePopView *content = [[KBChangeNicknamePopView alloc] initWithFrame:CGRectMake(0, 0, width, 230)];
|
|
|
|
|
|
content.prefillNickname = self.items.firstObject[@"value"] ?: @"";
|
|
|
|
|
|
|
|
|
|
|
|
LSTPopView *pop = [LSTPopView initWithCustomView:content
|
|
|
|
|
|
parentView:self.view
|
|
|
|
|
|
popStyle:LSTPopStyleSmoothFromBottom
|
2025-11-11 17:36:12 +08:00
|
|
|
|
dismissStyle:LSTDismissStyleSmoothToBottom];
|
2025-11-11 15:28:22 +08:00
|
|
|
|
pop.bgColor = [[UIColor blackColor] colorWithAlphaComponent:0.4];
|
2025-11-11 15:55:52 +08:00
|
|
|
|
pop.hemStyle = LSTHemStyleBottom; // 居中
|
2025-11-11 15:28:22 +08:00
|
|
|
|
pop.isClickBgDismiss = YES; // 点击背景关闭
|
|
|
|
|
|
pop.isAvoidKeyboard = YES; // 规避键盘
|
|
|
|
|
|
pop.avoidKeyboardSpace = 10;
|
|
|
|
|
|
|
|
|
|
|
|
__weak typeof(self) weakSelf = self;
|
|
|
|
|
|
__weak typeof(pop) weakPop = pop;
|
|
|
|
|
|
content.closeHandler = ^{ [weakPop dismiss]; };
|
|
|
|
|
|
content.saveHandler = ^(NSString *nickname) {
|
|
|
|
|
|
if (nickname.length > 0) {
|
2025-12-04 14:44:56 +08:00
|
|
|
|
// 更新本地模型,避免返回再进入还是旧数据
|
|
|
|
|
|
weakSelf.userModel.nickName = nickname;
|
|
|
|
|
|
weakSelf.modifyLabel.text = nickname;
|
2026-03-02 09:19:06 +08:00
|
|
|
|
[weakSelf rebuildItems];
|
2025-12-04 14:53:25 +08:00
|
|
|
|
|
|
|
|
|
|
// 将修改后的用户信息同步到服务端
|
|
|
|
|
|
[weakSelf.myVM updateUserInfo:weakSelf.userModel completion:^(BOOL success, NSError * _Nullable error) {
|
|
|
|
|
|
if (error) {
|
|
|
|
|
|
[KBHUD showError:error.localizedDescription ?: KBLocalized(@"Network error")];
|
|
|
|
|
|
}
|
|
|
|
|
|
}];
|
2025-11-11 15:28:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
[weakPop dismiss];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
[pop pop];
|
2025-11-11 15:55:52 +08:00
|
|
|
|
// dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.15 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [content focusInput]; });
|
2026-03-02 09:19:06 +08:00
|
|
|
|
} else if (indexPath.row == kKBPersonInfoRowGender) {
|
2025-12-04 14:26:22 +08:00
|
|
|
|
// 性别选择 -> 弹窗(性别文案支持多语言)
|
|
|
|
|
|
NSArray *genders = @[
|
|
|
|
|
|
@{@"id":@"1",@"name":KBLocalized(@"Male")},
|
|
|
|
|
|
@{@"id":@"2",@"name":KBLocalized(@"Female")},
|
|
|
|
|
|
@{@"id":@"3",@"name":KBLocalized(@"The Third Gender")},
|
|
|
|
|
|
];
|
2025-11-11 15:55:52 +08:00
|
|
|
|
CGFloat width = KB_SCREEN_WIDTH;
|
|
|
|
|
|
KBGenderPickerPopView *content = [[KBGenderPickerPopView alloc] initWithFrame:CGRectMake(0, 0, width, 300)];
|
|
|
|
|
|
content.items = genders;
|
|
|
|
|
|
// 取当前展示值对应的 id(如果有的话)
|
2026-03-02 09:19:06 +08:00
|
|
|
|
NSString *curName = self.items[kKBPersonInfoRowGender][@"value"];
|
2025-11-11 15:55:52 +08:00
|
|
|
|
NSString *selId = nil;
|
|
|
|
|
|
for (NSDictionary *d in genders) { if ([d[@"name"] isEqualToString:curName]) { selId = d[@"id"]; break; } }
|
|
|
|
|
|
content.selectedId = selId;
|
|
|
|
|
|
|
|
|
|
|
|
LSTPopView *pop = [LSTPopView initWithCustomView:content
|
|
|
|
|
|
parentView:nil
|
|
|
|
|
|
popStyle:LSTPopStyleSmoothFromBottom
|
|
|
|
|
|
dismissStyle:LSTDismissStyleSmoothToBottom];
|
|
|
|
|
|
pop.bgColor = [[UIColor blackColor] colorWithAlphaComponent:0.4];
|
|
|
|
|
|
pop.hemStyle = LSTHemStyleBottom;
|
|
|
|
|
|
pop.isClickBgDismiss = YES;
|
|
|
|
|
|
|
|
|
|
|
|
__weak typeof(self) weakSelf = self; __weak typeof(pop) weakPop = pop;
|
|
|
|
|
|
content.closeHandler = ^{ [weakPop dismiss]; };
|
|
|
|
|
|
content.saveHandler = ^(NSDictionary *selected) {
|
|
|
|
|
|
NSString *name = selected[@"name"] ?: @"";
|
2025-12-04 14:17:47 +08:00
|
|
|
|
// 将选择结果同步到本地缓存,供后续登录接口使用
|
|
|
|
|
|
NSString *genderId = selected[@"id"];
|
|
|
|
|
|
NSInteger genderValue = 0;
|
|
|
|
|
|
if ([genderId isKindOfClass:NSString.class]) {
|
|
|
|
|
|
NSInteger v = [genderId integerValue];
|
|
|
|
|
|
// 后端/模型使用 0/1/2,对应弹窗里的 1/2/3
|
|
|
|
|
|
if (v >= 1 && v <= 3) {
|
|
|
|
|
|
genderValue = v - 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
[[NSUserDefaults standardUserDefaults] setInteger:genderValue forKey:KBSexSelectedGenderKey];
|
|
|
|
|
|
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
|
|
|
|
|
2025-12-04 14:44:56 +08:00
|
|
|
|
// 同步更新本地 userModel,避免再次进入页面还是旧的性别
|
|
|
|
|
|
weakSelf.userModel.gender = (UserSex)genderValue;
|
2026-03-02 09:19:06 +08:00
|
|
|
|
[weakSelf rebuildItems];
|
2025-12-04 14:53:25 +08:00
|
|
|
|
|
|
|
|
|
|
// 将修改后的用户信息同步到服务端
|
|
|
|
|
|
[weakSelf.myVM updateUserInfo:weakSelf.userModel completion:^(BOOL success, NSError * _Nullable error) {
|
|
|
|
|
|
if (error) {
|
|
|
|
|
|
[KBHUD showError:error.localizedDescription ?: KBLocalized(@"Network error")];
|
|
|
|
|
|
}
|
|
|
|
|
|
}];
|
|
|
|
|
|
|
2025-11-11 15:55:52 +08:00
|
|
|
|
[weakPop dismiss];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
[pop pop];
|
2026-03-02 09:19:06 +08:00
|
|
|
|
} else if (indexPath.row == kKBPersonInfoRowLanguage) {
|
|
|
|
|
|
[self openLanguageSelector];
|
|
|
|
|
|
} else if (indexPath.row == kKBPersonInfoRowUserID) {
|
|
|
|
|
|
NSString *userID = self.items[kKBPersonInfoRowUserID][@"value"];
|
2025-11-11 15:59:19 +08:00
|
|
|
|
if (userID.length == 0) return;
|
|
|
|
|
|
UIPasteboard.generalPasteboard.string = userID;
|
2025-11-17 20:07:39 +08:00
|
|
|
|
[KBHUD showInfo:KBLocalized(@"Copy Success")];
|
2025-11-11 14:56:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#pragma mark - Actions
|
|
|
|
|
|
|
2026-01-06 19:25:34 +08:00
|
|
|
|
- (void)onTapAvatarEdit {
|
|
|
|
|
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_person_avatar_edit"
|
|
|
|
|
|
pageId:@"person_info"
|
|
|
|
|
|
elementId:@"avatar_edit"
|
|
|
|
|
|
extra:nil
|
|
|
|
|
|
completion:nil];
|
|
|
|
|
|
[self presentImagePicker];
|
|
|
|
|
|
}
|
2025-11-11 14:56:57 +08:00
|
|
|
|
|
|
|
|
|
|
- (void)onTapLogout {
|
2026-01-06 19:25:34 +08:00
|
|
|
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_person_logout_btn"
|
|
|
|
|
|
pageId:@"person_info"
|
|
|
|
|
|
elementId:@"logout_btn"
|
|
|
|
|
|
extra:nil
|
|
|
|
|
|
completion:nil];
|
2025-12-03 13:31:02 +08:00
|
|
|
|
[self.myVM logout];
|
2025-11-11 14:56:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 21:16:21 +08:00
|
|
|
|
- (void)onTapUserIdCopy {
|
|
|
|
|
|
NSString *userId = self.userModel.userId ?: @"";
|
|
|
|
|
|
if (userId.length == 0) { return; }
|
|
|
|
|
|
UIPasteboard.generalPasteboard.string = userId;
|
|
|
|
|
|
[KBHUD showInfo:KBLocalized(@"Copied")];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-11 14:56:57 +08:00
|
|
|
|
#pragma mark - Lazy UI(懒加载)
|
|
|
|
|
|
|
|
|
|
|
|
- (UITableView *)tableView {
|
|
|
|
|
|
if (!_tableView) {
|
|
|
|
|
|
// 使用 Plain,自行实现卡片留白与圆角,更贴近设计图
|
|
|
|
|
|
_tableView = [[BaseTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
|
|
|
|
|
|
_tableView.backgroundColor = UIColor.clearColor;
|
|
|
|
|
|
_tableView.showsVerticalScrollIndicator = NO;
|
|
|
|
|
|
_tableView.separatorStyle = UITableViewCellSeparatorStyleNone; // 自定义分割
|
|
|
|
|
|
_tableView.dataSource = self; _tableView.delegate = self;
|
|
|
|
|
|
_tableView.contentInset = UIEdgeInsetsMake(8, 0, 16, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
return _tableView;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (UIView *)headerView {
|
|
|
|
|
|
if (!_headerView) {
|
|
|
|
|
|
CGFloat w = UIScreen.mainScreen.bounds.size.width;
|
2026-03-04 21:16:21 +08:00
|
|
|
|
UIView *hv = [[UIView alloc] initWithFrame:CGRectMake(0, 0, w, 200)];
|
2025-11-11 14:56:57 +08:00
|
|
|
|
hv.backgroundColor = UIColor.clearColor;
|
|
|
|
|
|
|
|
|
|
|
|
[hv addSubview:self.avatarView];
|
|
|
|
|
|
[hv addSubview:self.editBadge];
|
|
|
|
|
|
[hv addSubview:self.modifyLabel];
|
2026-03-04 21:16:21 +08:00
|
|
|
|
[hv addSubview:self.userIdLabel];
|
|
|
|
|
|
[hv addSubview:self.userIdCopyButton];
|
2025-11-11 14:56:57 +08:00
|
|
|
|
|
|
|
|
|
|
[self.avatarView mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.centerX.equalTo(hv);
|
|
|
|
|
|
make.top.equalTo(hv).offset(12);
|
|
|
|
|
|
make.width.height.mas_equalTo(96);
|
|
|
|
|
|
}];
|
|
|
|
|
|
[self.editBadge mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.width.height.mas_equalTo(24);
|
2025-11-11 15:13:43 +08:00
|
|
|
|
make.centerX.equalTo(self.avatarView.mas_right).offset(-15);
|
|
|
|
|
|
make.centerY.equalTo(self.avatarView.mas_bottom).offset(-15);
|
2025-11-11 14:56:57 +08:00
|
|
|
|
}];
|
|
|
|
|
|
[self.modifyLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.top.equalTo(self.avatarView.mas_bottom).offset(10);
|
|
|
|
|
|
make.centerX.equalTo(hv);
|
|
|
|
|
|
}];
|
2026-03-04 21:16:21 +08:00
|
|
|
|
[self.userIdLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.top.equalTo(self.modifyLabel.mas_bottom).offset(10);
|
|
|
|
|
|
make.centerX.equalTo(hv).offset(-KBFit(12.0));
|
|
|
|
|
|
}];
|
|
|
|
|
|
[self.userIdCopyButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.centerY.equalTo(self.userIdLabel);
|
|
|
|
|
|
make.left.equalTo(self.userIdLabel.mas_right).offset(6);
|
|
|
|
|
|
make.width.height.mas_equalTo(18);
|
|
|
|
|
|
}];
|
2025-11-11 14:56:57 +08:00
|
|
|
|
|
2025-11-11 15:13:43 +08:00
|
|
|
|
// 头像可点击:弹系统相册
|
|
|
|
|
|
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTapAvatarEdit)];
|
|
|
|
|
|
[self.avatarView addGestureRecognizer:tap];
|
|
|
|
|
|
|
2025-11-11 14:56:57 +08:00
|
|
|
|
_headerView = hv;
|
|
|
|
|
|
}
|
|
|
|
|
|
return _headerView;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (UIImageView *)avatarView {
|
|
|
|
|
|
if (!_avatarView) {
|
|
|
|
|
|
_avatarView = [[UIImageView alloc] init];
|
|
|
|
|
|
_avatarView.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0];
|
|
|
|
|
|
_avatarView.layer.cornerRadius = 48; _avatarView.layer.masksToBounds = YES;
|
|
|
|
|
|
// 外白描边
|
|
|
|
|
|
_avatarView.layer.borderWidth = 3.0; _avatarView.layer.borderColor = UIColor.whiteColor.CGColor;
|
|
|
|
|
|
// 占位图
|
|
|
|
|
|
UIGraphicsBeginImageContextWithOptions(CGSizeMake(96, 96), NO, 0);
|
|
|
|
|
|
CGContextRef ctx = UIGraphicsGetCurrentContext();
|
|
|
|
|
|
[[UIColor colorWithRed:0.86 green:0.95 blue:0.90 alpha:1] setFill];
|
|
|
|
|
|
CGContextFillEllipseInRect(ctx, CGRectMake(0, 0, 96, 96));
|
|
|
|
|
|
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
|
|
|
|
|
|
UIGraphicsEndImageContext();
|
|
|
|
|
|
_avatarView.image = img;
|
|
|
|
|
|
_avatarView.userInteractionEnabled = YES;
|
|
|
|
|
|
}
|
|
|
|
|
|
return _avatarView;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (UIButton *)editBadge {
|
|
|
|
|
|
if (!_editBadge) {
|
|
|
|
|
|
_editBadge = [UIButton buttonWithType:UIButtonTypeCustom];
|
|
|
|
|
|
_editBadge.layer.cornerRadius = 12; _editBadge.layer.masksToBounds = YES;
|
2025-11-11 15:13:43 +08:00
|
|
|
|
UIImage *img = [UIImage imageNamed:@"myperson_edit_icon"];
|
2025-11-11 14:56:57 +08:00
|
|
|
|
[_editBadge setImage:img forState:UIControlStateNormal];
|
|
|
|
|
|
[_editBadge addTarget:self action:@selector(onTapAvatarEdit) forControlEvents:UIControlEventTouchUpInside];
|
|
|
|
|
|
}
|
|
|
|
|
|
return _editBadge;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (UILabel *)modifyLabel {
|
|
|
|
|
|
if (!_modifyLabel) {
|
|
|
|
|
|
_modifyLabel = [UILabel new];
|
|
|
|
|
|
_modifyLabel.text = @"Modify";
|
2025-11-11 15:13:43 +08:00
|
|
|
|
_modifyLabel.textColor = [UIColor colorWithHex:KBBlackValue];
|
2025-11-25 15:36:16 +08:00
|
|
|
|
_modifyLabel.font = [KBFont bold:18];
|
2025-11-11 14:56:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
return _modifyLabel;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 21:16:21 +08:00
|
|
|
|
- (UILabel *)userIdLabel {
|
|
|
|
|
|
if (!_userIdLabel) {
|
|
|
|
|
|
_userIdLabel = [UILabel new];
|
|
|
|
|
|
_userIdLabel.textColor = [UIColor colorWithHex:0x9B9B9B];
|
|
|
|
|
|
_userIdLabel.font = [KBFont regular:12];
|
|
|
|
|
|
}
|
|
|
|
|
|
return _userIdLabel;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (UIButton *)userIdCopyButton {
|
|
|
|
|
|
if (!_userIdCopyButton) {
|
|
|
|
|
|
_userIdCopyButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
2026-03-04 21:57:37 +08:00
|
|
|
|
if (@available(iOS 13.0, *)) {
|
|
|
|
|
|
UIImage *image = [UIImage systemImageNamed:@"doc.on.doc"];
|
|
|
|
|
|
if (image) {
|
|
|
|
|
|
[_userIdCopyButton setImage:image forState:UIControlStateNormal];
|
|
|
|
|
|
_userIdCopyButton.tintColor = [UIColor colorWithHex:KBBlackValue];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!_userIdCopyButton.currentImage) {
|
2026-03-04 21:16:21 +08:00
|
|
|
|
[_userIdCopyButton setTitle:KBLocalized(@"Copy") forState:UIControlStateNormal];
|
|
|
|
|
|
[_userIdCopyButton setTitleColor:[UIColor colorWithHex:KBBlackValue] forState:UIControlStateNormal];
|
|
|
|
|
|
_userIdCopyButton.titleLabel.font = [KBFont regular:12];
|
|
|
|
|
|
}
|
|
|
|
|
|
[_userIdCopyButton addTarget:self action:@selector(onTapUserIdCopy) forControlEvents:UIControlEventTouchUpInside];
|
|
|
|
|
|
}
|
|
|
|
|
|
return _userIdCopyButton;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-11 15:13:43 +08:00
|
|
|
|
- (UIButton *)logoutBtn {
|
|
|
|
|
|
if (!_logoutBtn) {
|
|
|
|
|
|
_logoutBtn = [UIButton buttonWithType:UIButtonTypeSystem];
|
2025-11-25 15:36:16 +08:00
|
|
|
|
[_logoutBtn setTitle:KBLocalized(@"Log Out") forState:UIControlStateNormal];
|
2025-11-11 15:13:43 +08:00
|
|
|
|
[_logoutBtn setTitleColor:[UIColor colorWithHex:0xFF0000] forState:UIControlStateNormal];
|
2025-11-25 15:36:16 +08:00
|
|
|
|
_logoutBtn.titleLabel.font = [KBFont medium:16];
|
2025-11-11 15:13:43 +08:00
|
|
|
|
_logoutBtn.backgroundColor = UIColor.whiteColor;
|
|
|
|
|
|
_logoutBtn.layer.cornerRadius = 12; _logoutBtn.layer.masksToBounds = YES;
|
|
|
|
|
|
[_logoutBtn addTarget:self action:@selector(onTapLogout) forControlEvents:UIControlEventTouchUpInside];
|
|
|
|
|
|
}
|
|
|
|
|
|
return _logoutBtn;
|
|
|
|
|
|
}
|
2025-11-11 14:56:57 +08:00
|
|
|
|
|
2025-11-11 15:13:43 +08:00
|
|
|
|
#pragma mark - Image Picker
|
|
|
|
|
|
|
|
|
|
|
|
- (void)presentImagePicker {
|
|
|
|
|
|
if (@available(iOS 14.0, *)) {
|
|
|
|
|
|
PHPickerConfiguration *config = [[PHPickerConfiguration alloc] init];
|
|
|
|
|
|
config.selectionLimit = 1; // 只选一张
|
|
|
|
|
|
config.filter = [PHPickerFilter imagesFilter];
|
|
|
|
|
|
PHPickerViewController *picker = [[PHPickerViewController alloc] initWithConfiguration:config];
|
|
|
|
|
|
picker.delegate = self;
|
|
|
|
|
|
[self presentViewController:picker animated:YES completion:nil];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
UIImagePickerController *picker = [[UIImagePickerController alloc] init];
|
|
|
|
|
|
picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
|
|
|
|
|
|
picker.delegate = self;
|
|
|
|
|
|
[self presentViewController:picker animated:YES completion:nil];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-11 14:56:57 +08:00
|
|
|
|
|
2025-11-11 15:13:43 +08:00
|
|
|
|
#pragma mark - PHPickerViewControllerDelegate
|
|
|
|
|
|
|
|
|
|
|
|
- (void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray<PHPickerResult *> *)results API_AVAILABLE(ios(14.0)) {
|
|
|
|
|
|
[picker dismissViewControllerAnimated:YES completion:nil];
|
2025-12-04 13:37:11 +08:00
|
|
|
|
|
|
|
|
|
|
PHPickerResult *first = results.firstObject;
|
|
|
|
|
|
if (!first) return;
|
|
|
|
|
|
|
2025-11-11 15:13:43 +08:00
|
|
|
|
NSItemProvider *p = first.itemProvider;
|
|
|
|
|
|
if ([p canLoadObjectOfClass:UIImage.class]) {
|
|
|
|
|
|
__weak typeof(self) weakSelf = self;
|
|
|
|
|
|
[p loadObjectOfClass:UIImage.class completionHandler:^(__kindof id<NSItemProviderReading> _Nullable object, NSError * _Nullable error) {
|
|
|
|
|
|
UIImage *img = ([object isKindOfClass:UIImage.class] ? (UIImage *)object : nil);
|
|
|
|
|
|
if (!img) return;
|
2025-12-04 13:37:11 +08:00
|
|
|
|
NSUInteger targetKB = 50;
|
|
|
|
|
|
NSData *compressedData = [weakSelf kb_compressImage:img targetKB:targetKB];
|
|
|
|
|
|
if (!compressedData) return;
|
|
|
|
|
|
UIImage *compressedImage = [UIImage imageWithData:compressedData];
|
2025-11-11 15:13:43 +08:00
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
2025-12-04 14:07:12 +08:00
|
|
|
|
[weakSelf.myVM upLoadAvatarWithData:compressedData completion:^(BOOL success, NSError * _Nullable error) {
|
|
|
|
|
|
if (error) {
|
|
|
|
|
|
[KBHUD showError:error.localizedDescription];
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
|
|
weakSelf.avatarView.image = compressedImage;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
}];
|
2025-12-04 13:37:11 +08:00
|
|
|
|
|
2025-11-11 15:13:43 +08:00
|
|
|
|
});
|
|
|
|
|
|
}];
|
2025-11-11 14:56:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-11 15:13:43 +08:00
|
|
|
|
#pragma mark - UIImagePickerControllerDelegate
|
|
|
|
|
|
|
|
|
|
|
|
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<UIImagePickerControllerInfoKey,id> *)info {
|
|
|
|
|
|
UIImage *img = info[UIImagePickerControllerEditedImage] ?: info[UIImagePickerControllerOriginalImage];
|
|
|
|
|
|
if (img) {
|
|
|
|
|
|
UIImage *compressed = [self kb_compressImage:img maxPixel:512 quality:0.85];
|
|
|
|
|
|
self.avatarView.image = compressed;
|
|
|
|
|
|
self.avatarJPEGData = UIImageJPEGRepresentation(compressed, 0.85);
|
2025-11-11 14:56:57 +08:00
|
|
|
|
}
|
2025-11-11 15:13:43 +08:00
|
|
|
|
[picker dismissViewControllerAnimated:YES completion:nil];
|
2025-11-11 14:56:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-11 15:13:43 +08:00
|
|
|
|
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
|
|
|
|
|
|
[picker dismissViewControllerAnimated:YES completion:nil];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 压缩图片:最长边不超过 maxPixel,输出近似 quality 的 JPEG
|
|
|
|
|
|
- (UIImage *)kb_compressImage:(UIImage *)image maxPixel:(CGFloat)maxPixel quality:(CGFloat)quality {
|
|
|
|
|
|
if (!image) return nil;
|
|
|
|
|
|
maxPixel = MAX(64, maxPixel);
|
|
|
|
|
|
CGSize size = image.size;
|
|
|
|
|
|
CGFloat maxSide = MAX(size.width, size.height);
|
|
|
|
|
|
CGSize target = size;
|
|
|
|
|
|
if (maxSide > maxPixel) {
|
|
|
|
|
|
CGFloat scale = maxPixel / maxSide;
|
|
|
|
|
|
target = CGSizeMake(floor(size.width * scale), floor(size.height * scale));
|
2025-11-11 14:56:57 +08:00
|
|
|
|
}
|
2025-11-11 15:13:43 +08:00
|
|
|
|
UIGraphicsBeginImageContextWithOptions(target, YES, 1.0);
|
|
|
|
|
|
[image drawInRect:CGRectMake(0, 0, target.width, target.height)];
|
|
|
|
|
|
UIImage *scaled = UIGraphicsGetImageFromCurrentImageContext();
|
|
|
|
|
|
UIGraphicsEndImageContext();
|
|
|
|
|
|
|
|
|
|
|
|
NSData *jpeg = UIImageJPEGRepresentation(scaled ?: image, MIN(MAX(quality, 0.2), 0.95));
|
|
|
|
|
|
UIImage *result = [UIImage imageWithData:jpeg] ?: scaled ?: image;
|
|
|
|
|
|
return result;
|
2025-11-11 14:56:57 +08:00
|
|
|
|
}
|
2025-12-04 13:37:11 +08:00
|
|
|
|
/// 按目标大小(KB)压缩图片:
|
|
|
|
|
|
/// 1. 先逐步降低 JPEG 质量;
|
|
|
|
|
|
/// 2. 如果质量已经降到下限仍然太大,再按比例缩小尺寸,并重复尝试。
|
|
|
|
|
|
- (nullable NSData *)kb_compressImage:(UIImage *)image targetKB:(NSUInteger)targetKB {
|
|
|
|
|
|
if (!image || targetKB == 0) { return nil; }
|
|
|
|
|
|
|
|
|
|
|
|
NSUInteger maxBytes = targetKB * 1024;
|
|
|
|
|
|
|
|
|
|
|
|
// 初始质量参数
|
|
|
|
|
|
CGFloat compression = 0.9f;
|
|
|
|
|
|
CGFloat minCompression = 0.1f; // 不建议再低了,太低会糊
|
|
|
|
|
|
NSData *data = UIImageJPEGRepresentation(image, compression);
|
|
|
|
|
|
if (!data) return nil;
|
|
|
|
|
|
|
|
|
|
|
|
// 如果一开始就小于目标大小,直接返回
|
|
|
|
|
|
if (data.length <= maxBytes) {
|
|
|
|
|
|
return data;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 1) 先通过降低质量来压缩
|
|
|
|
|
|
while (data.length > maxBytes && compression > minCompression + 0.01f) {
|
|
|
|
|
|
compression -= 0.1f;
|
|
|
|
|
|
data = UIImageJPEGRepresentation(image, compression);
|
|
|
|
|
|
if (!data) return nil;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (data.length <= maxBytes) {
|
|
|
|
|
|
return data;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2) 质量降到下限还是太大 -> 等比缩放尺寸
|
|
|
|
|
|
UIImage *currentImage = image;
|
|
|
|
|
|
|
|
|
|
|
|
// 防止死循环,限定最多缩放几次
|
|
|
|
|
|
NSInteger maxResizeCount = 6;
|
|
|
|
|
|
NSInteger resizeCount = 0;
|
|
|
|
|
|
|
|
|
|
|
|
while (data.length > maxBytes && resizeCount < maxResizeCount) {
|
|
|
|
|
|
resizeCount++;
|
|
|
|
|
|
|
|
|
|
|
|
// 按面积比例来算缩放因子:新的面积约等于 (maxBytes / 当前字节数) * 原面积
|
|
|
|
|
|
CGFloat ratio = (CGFloat)maxBytes / (CGFloat)data.length;
|
|
|
|
|
|
// 为了更激进一点,可以乘个经验系数,比如 0.8
|
|
|
|
|
|
ratio *= 0.8f;
|
|
|
|
|
|
if (ratio <= 0.0f) {
|
|
|
|
|
|
ratio = 0.5f; // 兜底,至少缩小一半
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
CGFloat scale = sqrt(ratio);
|
|
|
|
|
|
if (scale >= 1.0f) {
|
|
|
|
|
|
scale = 0.5f; // 理论上不会走到这里,兜底
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
CGSize newSize = CGSizeMake(currentImage.size.width * scale,
|
|
|
|
|
|
currentImage.size.height * scale);
|
|
|
|
|
|
if (newSize.width < 10 || newSize.height < 10) {
|
|
|
|
|
|
// 太小就没意义了,直接 break,返回目前能做到的最小值
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 绘制缩小后的图片
|
|
|
|
|
|
UIGraphicsBeginImageContextWithOptions(newSize, NO, currentImage.scale);
|
|
|
|
|
|
[currentImage drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
|
|
|
|
|
|
UIImage *resizedImage = UIGraphicsGetImageFromCurrentImageContext();
|
|
|
|
|
|
UIGraphicsEndImageContext();
|
|
|
|
|
|
|
|
|
|
|
|
if (!resizedImage) {
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
currentImage = resizedImage;
|
|
|
|
|
|
|
|
|
|
|
|
// 每次尺寸缩小之后,再重新从稍高一点的质量开始压一轮
|
|
|
|
|
|
compression = 0.9f;
|
|
|
|
|
|
data = UIImageJPEGRepresentation(currentImage, compression);
|
|
|
|
|
|
if (!data) return nil;
|
|
|
|
|
|
|
|
|
|
|
|
// 再次在质量范围内往下压
|
|
|
|
|
|
while (data.length > maxBytes && compression > minCompression + 0.01f) {
|
|
|
|
|
|
compression -= 0.1f;
|
|
|
|
|
|
data = UIImageJPEGRepresentation(currentImage, compression);
|
|
|
|
|
|
if (!data) return nil;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 最终返回当前能做到的最小数据(可能略大于目标,但已经尽力)
|
|
|
|
|
|
return data;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-11 14:56:57 +08:00
|
|
|
|
|
2025-12-03 13:31:02 +08:00
|
|
|
|
- (KBMyVM *)myVM{
|
|
|
|
|
|
if (!_myVM) {
|
|
|
|
|
|
_myVM = [[KBMyVM alloc] init];
|
|
|
|
|
|
}
|
|
|
|
|
|
return _myVM;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-11 14:56:57 +08:00
|
|
|
|
@end
|