Files
keyboard/CustomKeyboard/View/KBFunctionView.m

912 lines
38 KiB
Mathematica
Raw Normal View History

2025-10-28 14:30:03 +08:00
//
// KBFunctionView.m
// CustomKeyboard
//
// Created by Mac on 2025/10/28.
//
#import "KBFunctionView.h"
2025-11-04 16:37:24 +08:00
#import "KBResponderUtils.h" // UIInputViewController
2025-10-28 14:30:03 +08:00
#import "KBFunctionBarView.h"
#import "KBFunctionPasteView.h"
#import "KBFunctionTagCell.h"
#import "Masonry.h"
2025-10-30 20:23:34 +08:00
#import <MBProgressHUD.h>
2025-10-30 20:46:54 +08:00
#import "KBFullAccessGuideView.h"
2025-11-03 13:25:41 +08:00
#import "KBFullAccessManager.h"
2025-11-04 21:01:46 +08:00
#import "KBSkinManager.h"
2025-11-12 21:23:31 +08:00
#import "KBAuthManager.h" //
2025-11-21 18:26:02 +08:00
#import "KBULBridgeNotification.h" // Darwin UL
#import "KBHostAppLauncher.h"
2025-11-11 20:24:13 +08:00
#import "KBStreamTextView.h" //
2025-11-12 16:03:30 +08:00
#import "KBStreamOverlayView.h" //
#import "KBFunctionTagListView.h"
2025-12-09 14:32:21 +08:00
#import "WJXEventSource.h"
2025-12-08 16:39:47 +08:00
#import "KBTagItemModel.h"
#import <MJExtension/MJExtension.h>
2025-12-17 17:01:49 +08:00
#import "KBBizCode.h"
2025-12-19 18:45:14 +08:00
#import "KBBackspaceLongPressHandler.h"
2025-12-19 19:21:08 +08:00
#import "KBBackspaceUndoManager.h"
2025-10-28 14:30:03 +08:00
2025-11-12 16:03:30 +08:00
@interface KBFunctionView () <KBFunctionBarViewDelegate, KBStreamOverlayViewDelegate, KBFunctionTagListViewDelegate>
2025-10-28 14:30:03 +08:00
// UI
@property (nonatomic, strong) KBFunctionBarView *barViewInternal;
@property (nonatomic, strong) KBFunctionPasteView *pasteViewInternal;
2025-11-12 16:03:30 +08:00
@property (nonatomic, strong) KBFunctionTagListView *tagListView;
2025-10-28 14:30:03 +08:00
@property (nonatomic, strong) UIView *rightButtonContainer; //
@property (nonatomic, strong) UIButton *pasteButtonInternal;
@property (nonatomic, strong) UIButton *deleteButtonInternal;
@property (nonatomic, strong) UIButton *clearButtonInternal;
@property (nonatomic, strong) UIButton *sendButtonInternal;
2025-11-12 16:03:30 +08:00
// +
@property (nonatomic, strong, nullable) KBStreamOverlayView *streamOverlay;
2025-11-11 20:24:13 +08:00
2025-11-12 14:18:56 +08:00
//
2025-12-09 14:32:21 +08:00
@property (nonatomic, strong, nullable) WJXEventSource *eventSource;
2025-11-12 14:18:56 +08:00
@property (nonatomic, assign) BOOL streamHasOutput; // \t
2025-11-12 16:49:19 +08:00
@property (nonatomic, strong, nullable) NSNumber *loadingTagIndex; // loadingindex
@property (nonatomic, copy, nullable) NSString *loadingTagTitle;
2025-12-09 14:32:21 +08:00
@property (nonatomic, assign) BOOL eventSourceDidReceiveDone;
@property (nonatomic, copy, nullable) NSString *eventSourceSplitPrefix;
2025-11-12 13:43:48 +08:00
2025-10-28 14:30:03 +08:00
// Data
2025-12-08 16:39:47 +08:00
//@property (nonatomic, strong) NSArray<NSString *> *itemsInternal;
@property (nonatomic, strong) NSMutableArray<KBTagItemModel *> *modelArray;
2025-10-29 15:49:43 +08:00
//
@property (nonatomic, strong) NSTimer *pasteboardTimer; // 线
@property (nonatomic, assign) NSInteger lastHandledPBCount; // changeCount
2025-11-12 21:23:31 +08:00
// UL
@property (nonatomic, assign) NSUInteger kb_ulSeq; // UL
@property (nonatomic, assign) BOOL kb_ulHandledFlag; // App UL
2025-12-19 18:45:14 +08:00
@property (nonatomic, strong) KBBackspaceLongPressHandler *backspaceHandler;
2025-10-28 14:30:03 +08:00
@end
@implementation KBFunctionView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
2025-11-04 21:01:46 +08:00
// 使
[self kb_applyTheme];
2025-12-19 18:45:14 +08:00
self.backspaceHandler = [[KBBackspaceLongPressHandler alloc] initWithContainerView:self];
2025-10-28 14:30:03 +08:00
[self setupUI];
2025-12-08 16:39:47 +08:00
// [self reloadDemoData];
[self kb_reloadTagsFromSharedDefaults];
2025-10-29 15:49:43 +08:00
//
_lastHandledPBCount = [UIPasteboard generalPasteboard].changeCount;
// 访访 TCC/XPC
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(kb_fullAccessChanged) name:KBFullAccessChangedNotification object:nil];
2025-11-12 21:23:31 +08:00
// App Darwin UL
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
(__bridge const void *)(self),
KBULDarwinCallback,
(__bridge CFStringRef)KBDarwinULHandled,
NULL,
CFNotificationSuspensionBehaviorDeliverImmediately);
2025-10-28 14:30:03 +08:00
}
return self;
}
2025-12-08 16:39:47 +08:00
#pragma mark - Data
/// App Group NSUserDefaults JSON model +
- (void)kb_reloadTagsFromSharedDefaults {
NSUserDefaults *sharedDefaults = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
NSDictionary *jsonDict = [sharedDefaults objectForKey:AppGroup_MyKbJson];
if (jsonDict != nil) {
id dataObj = jsonDict[@"data"];
NSArray<KBTagItemModel *> *modelList = [KBTagItemModel mj_objectArrayWithKeyValuesArray:(NSArray *)dataObj];
if (modelList.count > 0) {
self.modelArray = [NSMutableArray array];
[self.modelArray addObjectsFromArray:modelList];
// [self.collectionView reloadData];
[self.tagListView setItems:self.modelArray];
}
}else{
NSLog(@"json❎");
}
}
2025-11-04 21:01:46 +08:00
#pragma mark - Theme
- (void)kb_applyTheme {
2025-11-26 19:46:23 +08:00
// KBSkinManager *mgr = [KBSkinManager shared];
// UIColor *accent = mgr.current.accentColor ?: [UIColor colorWithRed:0.77 green:0.93 blue:0.82 alpha:1.0];
// BOOL hasImg = ([mgr currentBackgroundImage] != nil);
self.backgroundColor = [UIColor colorWithHex:0xD0D3DA];
2025-11-04 21:01:46 +08:00
}
2025-10-29 15:49:43 +08:00
- (void)dealloc {
[self stopPasteboardMonitor];
2025-11-12 13:43:48 +08:00
[self kb_stopNetworkStreaming];
[[NSNotificationCenter defaultCenter] removeObserver:self];
2025-11-12 21:23:31 +08:00
CFNotificationCenterRemoveObserver(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge const void *)(self), (__bridge CFStringRef)KBDarwinULHandled, NULL);
2025-10-29 15:49:43 +08:00
}
2025-10-28 14:30:03 +08:00
#pragma mark - UI
- (void)setupUI {
// 1. Bar
[self addSubview:self.barViewInternal];
[self.barViewInternal mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(self);
make.top.equalTo(self.mas_top).offset(6);
2025-11-26 21:16:56 +08:00
make.height.mas_equalTo(52);
2025-10-28 14:30:03 +08:00
}];
//
[self addSubview:self.rightButtonContainer];
[self.rightButtonContainer mas_makeConstraints:^(MASConstraintMaker *make) {
2025-11-26 21:16:56 +08:00
make.right.equalTo(self.mas_right).offset(-4);
make.top.equalTo(self.barViewInternal.mas_bottom).offset(0);
2025-10-28 14:30:03 +08:00
make.bottom.equalTo(self.mas_bottom).offset(-10);
2025-11-26 21:16:56 +08:00
make.width.mas_equalTo(60);
2025-10-28 14:30:03 +08:00
}];
//
[self.rightButtonContainer addSubview:self.pasteButtonInternal];
[self.rightButtonContainer addSubview:self.deleteButtonInternal];
[self.rightButtonContainer addSubview:self.clearButtonInternal];
[self.rightButtonContainer addSubview:self.sendButtonInternal];
//
2025-11-26 21:16:56 +08:00
CGFloat smallH = 41;
2025-10-28 14:30:03 +08:00
CGFloat bigH = 56;
// 10 276 8 AutoLayout
2025-11-26 21:16:56 +08:00
CGFloat vSpace = 4;
2025-10-28 14:30:03 +08:00
[self.pasteButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.rightButtonContainer.mas_top);
make.left.right.equalTo(self.rightButtonContainer);
make.height.mas_equalTo(smallH);
}];
[self.deleteButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.pasteButtonInternal.mas_bottom).offset(vSpace);
make.left.right.equalTo(self.rightButtonContainer);
make.height.equalTo(self.pasteButtonInternal);
}];
[self.clearButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.deleteButtonInternal.mas_bottom).offset(vSpace);
make.left.right.equalTo(self.rightButtonContainer);
make.height.equalTo(self.pasteButtonInternal);
}];
[self.sendButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.clearButtonInternal.mas_bottom).offset(vSpace);
make.left.right.equalTo(self.rightButtonContainer);
// smallH
make.height.greaterThanOrEqualTo(@(smallH));
make.height.lessThanOrEqualTo(@(bigH));
make.bottom.lessThanOrEqualTo(self.rightButtonContainer.mas_bottom);
2025-10-28 14:30:03 +08:00
}];
// 2.
[self addSubview:self.pasteViewInternal];
[self.pasteViewInternal mas_makeConstraints:^(MASConstraintMaker *make) {
2025-11-26 21:16:56 +08:00
make.left.equalTo(self.mas_left).offset(vSpace);
make.right.equalTo(self.rightButtonContainer.mas_left).offset(-vSpace);
make.top.equalTo(self.barViewInternal.mas_bottom).offset(0);
make.height.mas_equalTo(smallH);
2025-10-28 14:30:03 +08:00
}];
2025-11-28 16:55:26 +08:00
// Paste
[self.pasteViewInternal.pasBtn addTarget:self action:@selector(onTapPaste) forControlEvents:UIControlEventTouchUpInside];
2025-10-28 14:30:03 +08:00
2025-11-12 16:03:30 +08:00
// 3. Tag List View
[self addSubview:self.tagListView];
[self.tagListView mas_makeConstraints:^(MASConstraintMaker *make) {
2025-11-26 21:16:56 +08:00
make.left.equalTo(self.pasteViewInternal);
make.right.equalTo(self.rightButtonContainer.mas_left).offset(-vSpace);
make.top.equalTo(self.pasteViewInternal.mas_bottom).offset(vSpace);
2025-10-28 14:30:03 +08:00
make.bottom.equalTo(self.mas_bottom).offset(-10);
}];
}
#pragma mark - Data
2025-12-08 16:39:47 +08:00
//- (void)reloadDemoData {
// //
// self.itemsInternal = @[KBLocalized(@"Warm hearted man"),
// KBLocalized(@"Warm2 hearted man"),
// KBLocalized(@"Warm3 hearted man"),
// KBLocalized(@"撩女生啊u发顺丰大师傅"),
// KBLocalized(@"Warm = man"),
// KBLocalized(@"Warm hearted man"),
// KBLocalized(@"一枚暖男发放"),
// KBLocalized(@"聊天搭子"),
// KBLocalized(@"表达爱意"),
// KBLocalized(@"更多话术")];
// [self.tagListView setItems:self.itemsInternal];
//}
2025-10-28 14:30:03 +08:00
2025-11-12 19:46:07 +08:00
// UICollectionView KBFunctionTagListView
2025-10-28 14:30:03 +08:00
2025-11-11 20:24:13 +08:00
- (void)kb_showStreamTextViewIfNeededWithTitle:(NSString *)title {
//
2025-11-12 16:03:30 +08:00
if (self.streamOverlay.superview) { return; }
2025-11-11 20:24:13 +08:00
// 使
2025-11-12 16:03:30 +08:00
self.tagListView.hidden = YES;
KBStreamOverlayView *overlay = [[KBStreamOverlayView alloc] init];
overlay.delegate = (id)self;
[self addSubview:overlay];
[overlay mas_makeConstraints:^(MASConstraintMaker *make) {
2025-12-05 21:23:54 +08:00
//
CGFloat vSpace = 4.0;
make.left.equalTo(self.pasteViewInternal);
make.right.equalTo(self).offset(-vSpace);
2025-11-11 20:24:13 +08:00
make.top.equalTo(self.pasteViewInternal.mas_bottom).offset(10);
make.bottom.equalTo(self.mas_bottom).offset(-10);
}];
2025-12-05 21:23:54 +08:00
// //Paste
self.pasteButtonInternal.hidden = NO;
self.deleteButtonInternal.hidden = YES;
self.clearButtonInternal.hidden = YES;
self.sendButtonInternal.hidden = YES;
2025-11-12 17:55:59 +08:00
//
overlay.textView.contentHorizontalPadding = 8.0;
2025-11-12 16:03:30 +08:00
self.streamOverlay = overlay;
2025-11-11 21:48:26 +08:00
2025-11-12 16:49:19 +08:00
// UI cell start
2025-11-11 20:24:13 +08:00
}
- (void)kb_onTapStreamDelete {
//
2025-11-12 13:43:48 +08:00
[self kb_stopNetworkStreaming];
2025-11-12 16:03:30 +08:00
[self.streamOverlay removeFromSuperview];
self.streamOverlay = nil;
self.tagListView.hidden = NO;
2025-12-05 21:23:54 +08:00
//
self.pasteButtonInternal.hidden = NO;
self.deleteButtonInternal.hidden = NO;
self.clearButtonInternal.hidden = NO;
self.sendButtonInternal.hidden = NO;
2025-11-12 16:03:30 +08:00
}
//
- (void)streamOverlayDidTapClose:(KBStreamOverlayView *)overlay {
[self kb_onTapStreamDelete];
2025-11-11 20:24:13 +08:00
}
2025-11-11 21:48:26 +08:00
2025-12-09 14:32:21 +08:00
#pragma mark - Network Streaming (WJXEventSource)
2025-11-12 13:43:48 +08:00
2025-11-12 15:40:30 +08:00
- (void)kb_startNetworkStreamingWithSeed:(NSString *)seedTitle {
2025-11-12 13:43:48 +08:00
[self kb_stopNetworkStreaming];
2025-11-12 15:40:30 +08:00
if (![[KBFullAccessManager shared] hasFullAccess]) { return; }
2025-11-12 13:43:48 +08:00
2025-12-05 21:15:48 +08:00
NSString *apiUrl = [NSString stringWithFormat:@"%@%@", KB_BASE_URL, API_AI_TALK];
NSURL *url = [NSURL URLWithString:apiUrl];
2025-11-12 15:40:30 +08:00
if (!url) { return; }
2025-11-12 13:43:48 +08:00
2025-12-08 19:48:13 +08:00
NSInteger characterId = 0;
if (self.loadingTagIndex != nil) {
NSInteger idx = self.loadingTagIndex.integerValue;
if (idx >= 0 && idx < self.modelArray.count) {
KBTagItemModel *model = self.modelArray[idx];
characterId = model.characterId;
}
}
NSInteger resolvedCharacterId = (characterId > 0) ? characterId : 75;
2025-12-09 14:32:21 +08:00
NSString *message = seedTitle.length > 0 ? seedTitle : @"aliqua non cupidatat";
2025-12-09 16:12:54 +08:00
// message = [NSString stringWithFormat:@"%@%d",message,arc4random() % 10000];
2025-12-08 19:48:13 +08:00
NSDictionary *payload = @{
@"characterId": @(resolvedCharacterId),
2025-12-09 16:12:54 +08:00
@"message": message
2025-12-08 19:48:13 +08:00
};
2025-12-09 14:32:21 +08:00
NSLog(@"[KBFunction] request payload: %@", payload);
2025-12-08 19:48:13 +08:00
NSError *bodyError = nil;
NSData *bodyData = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&bodyError];
if (bodyError || bodyData.length == 0) {
NSLog(@"[KBFunction] build body failed: %@", bodyError);
return;
}
2025-12-09 14:32:21 +08:00
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:60];
request.HTTPMethod = @"POST";
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
2025-12-08 19:48:13 +08:00
NSString *token = KBAuthManager.shared.current.accessToken ?: @"";
if (token.length > 0) {
2025-12-09 14:32:21 +08:00
[request setValue:token forHTTPHeaderField:@"auth-token"];
2025-12-08 19:48:13 +08:00
}
2025-12-09 14:32:21 +08:00
request.HTTPBody = bodyData;
self.streamHasOutput = NO;
self.eventSourceSplitPrefix = nil;
self.eventSourceDidReceiveDone = NO;
__weak typeof(self) weakSelf = self;
WJXEventSource *source = [[WJXEventSource alloc] initWithRquest:request];
source.ignoreRetryAction = YES;
[source addListener:^(WJXEvent * _Nonnull event) {
2025-11-12 14:18:56 +08:00
__strong typeof(weakSelf) self = weakSelf; if (!self) return;
2025-12-09 14:32:21 +08:00
[self kb_handleEventSourceMessage:event];
} forEvent:WJXEventNameMessage queue:NSOperationQueue.mainQueue];
[source addListener:^(WJXEvent * _Nonnull event) {
2025-11-12 14:18:56 +08:00
__strong typeof(weakSelf) self = weakSelf; if (!self) return;
2025-12-09 14:32:21 +08:00
[self kb_handleEventSourceError:event.error];
} forEvent:WJXEventNameError queue:NSOperationQueue.mainQueue];
self.eventSource = source;
[self.eventSource open];
2025-11-12 13:43:48 +08:00
}
2025-11-12 14:18:56 +08:00
- (void)kb_stopNetworkStreaming {
2025-12-09 14:32:21 +08:00
[self.eventSource close];
self.eventSource = nil;
self.eventSourceSplitPrefix = nil;
self.eventSourceDidReceiveDone = NO;
2025-11-12 14:18:56 +08:00
self.streamHasOutput = NO;
2025-12-09 14:32:21 +08:00
}
- (void)kb_handleEventSourceMessage:(WJXEvent *)event {
if (event.data.length == 0) { return; }
NSLog(@"[KBStream] SSE raw payload: %@", event.data);
NSData *jsonData = [event.data dataUsingEncoding:NSUTF8StringEncoding];
if (!jsonData) { return; }
NSError *error = nil;
NSDictionary *payload = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
if (error || ![payload isKindOfClass:[NSDictionary class]]) { return; }
2025-12-17 17:01:49 +08:00
if ([self kb_handleBizErrorIfNeeded:payload]) { return; }
2025-12-09 14:32:21 +08:00
NSString *type = payload[@"type"];
if (![type isKindOfClass:[NSString class]]) { return; }
if ([type isEqualToString:@"llm_chunk"]) {
NSString *chunk = [self kb_normalizedLLMChunkString:payload[@"data"]];
if (chunk.length > 0) {
[self kb_appendChunkToStreamView:chunk];
}
return;
}
if ([type isEqualToString:@"search_result"]) {
NSString *text = [self kb_formattedSearchResultString:payload[@"data"]];
if (text.length > 0) {
[self kb_appendChunkToStreamView:text];
}
return;
}
if ([type isEqualToString:@"done"]) {
self.eventSourceDidReceiveDone = YES;
[self kb_finishEventSourceWithError:nil];
return;
}
}
- (void)kb_handleEventSourceError:(NSError * _Nullable)error {
if (self.eventSourceDidReceiveDone) { return; }
[self kb_finishEventSourceWithError:error];
}
- (void)kb_finishEventSourceWithError:(NSError * _Nullable)error {
[self.eventSource close];
self.eventSource = nil;
if (!self.streamHasOutput && self.loadingTagIndex) {
[self.tagListView setLoading:NO atIndex:self.loadingTagIndex.integerValue];
self.loadingTagIndex = nil;
self.loadingTagTitle = nil;
}
BOOL shouldShowError = (error != nil);
if (shouldShowError) {
[KBHUD showInfo:error.localizedDescription ?: KBLocalized(@"拉取失败")];
}
if (self.streamOverlay) {
[self.streamOverlay finish];
}
self.eventSourceSplitPrefix = nil;
self.eventSourceDidReceiveDone = NO;
}
2025-12-17 17:01:49 +08:00
- (BOOL)kb_handleBizErrorIfNeeded:(NSDictionary *)payload {
NSInteger code = KBBizCodeFromJSONObject(payload);
if (code == NSNotFound || code == KBBizCodeSuccess) {
return NO;
}
BOOL needSubscriptionGuide = (code == KBBizCodeQuotaExhausted);
NSString *msg = KBBizMessageFromJSONObject(payload);
if (msg.length == 0) {
msg = KBLocalized(@"拉取失败");
}
NSError *bizError = [NSError errorWithDomain:@"KBStreamBizError"
code:code
userInfo:@{NSLocalizedDescriptionKey: msg}];
[self kb_finishEventSourceWithError:bizError];
if (needSubscriptionGuide) {
[self kb_requestSubscriptionGuide];
}
return YES;
}
- (void)kb_requestSubscriptionGuide {
if ([self.delegate respondsToSelector:@selector(functionViewDidRequestSubscription:)]) {
[self.delegate functionViewDidRequestSubscription:self];
}
}
2025-12-09 14:32:21 +08:00
#pragma mark - Event Parsing
- (NSString *)kb_normalizedLLMChunkString:(id)dataValue {
if (![dataValue isKindOfClass:[NSString class]]) { return @""; }
NSString *text = (NSString *)dataValue;
2025-12-09 16:12:54 +08:00
// 1. <SPLIT> "<SP" + "LIT>"
2025-12-09 14:32:21 +08:00
if (self.eventSourceSplitPrefix.length > 0) {
text = [self.eventSourceSplitPrefix stringByAppendingString:text ?: @""];
self.eventSourceSplitPrefix = nil;
}
2025-12-09 16:12:54 +08:00
if (text.length == 0) { return @""; }
// 2.
2025-12-09 14:32:21 +08:00
while (text.length > 0) {
unichar c0 = [text characterAtIndex:0];
if (c0 == '\n' || c0 == '\r') {
text = [text substringFromIndex:1];
continue;
}
break;
}
2025-12-09 16:12:54 +08:00
if (text.length == 0) { return @""; }
// 3. "<SPLIT"
2025-12-09 14:32:21 +08:00
NSString *suffix = [self kb_pendingSplitSuffixForString:text];
if (suffix.length > 0) {
self.eventSourceSplitPrefix = suffix;
text = [text substringToIndex:text.length - suffix.length];
}
2025-12-09 16:12:54 +08:00
if (text.length == 0) { return @""; }
// 4. <SPLIT> \t
text = [text stringByReplacingOccurrencesOfString:@"<SPLIT>" withString:@"\t"];
// /t UI
2025-12-09 14:32:21 +08:00
return text;
}
2025-12-09 16:12:54 +08:00
2025-12-09 14:32:21 +08:00
- (NSString *)kb_formattedSearchResultString:(id)dataValue {
2025-12-09 15:19:10 +08:00
// data
2025-12-09 14:32:21 +08:00
if (![dataValue isKindOfClass:[NSArray class]]) { return @""; }
NSArray *list = (NSArray *)dataValue;
2025-12-09 15:19:10 +08:00
2025-12-09 14:32:21 +08:00
NSMutableArray<NSString *> *segments = [NSMutableArray array];
2025-12-09 15:19:10 +08:00
2025-12-09 14:32:21 +08:00
[list enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSString *payload = nil;
2025-12-09 15:19:10 +08:00
2025-12-09 14:32:21 +08:00
if ([obj isKindOfClass:[NSDictionary class]]) {
id val = obj[@"payload"];
if ([val isKindOfClass:[NSString class]]) {
payload = (NSString *)val;
}
} else if ([obj isKindOfClass:[NSString class]]) {
2025-12-09 15:19:10 +08:00
//
2025-12-09 14:32:21 +08:00
payload = (NSString *)obj;
}
2025-12-09 15:19:10 +08:00
2025-12-09 14:32:21 +08:00
payload = [payload stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if (payload.length > 0) {
2025-12-09 15:19:10 +08:00
// payload
[segments addObject:payload];
2025-12-09 14:32:21 +08:00
}
}];
2025-12-09 15:19:10 +08:00
2025-12-09 14:32:21 +08:00
if (segments.count == 0) { return @""; }
2025-12-09 15:19:10 +08:00
// \t KBStreamTextView \t label
NSMutableString *result = [NSMutableString string];
[segments enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
// \t
[result appendFormat:@"\t%@", obj];
}];
2025-12-09 14:32:21 +08:00
return result;
}
2025-12-09 15:19:10 +08:00
2025-12-09 14:32:21 +08:00
- (NSString *)kb_pendingSplitSuffixForString:(NSString *)text {
static NSString * const token = @"<SPLIT>";
if (text.length == 0) { return @""; }
NSUInteger tokenLen = token.length;
if (tokenLen <= 1) { return @""; }
NSUInteger maxLen = MIN(tokenLen - 1, text.length);
for (NSUInteger len = maxLen; len > 0; len--) {
NSString *suffix = [text substringFromIndex:text.length - len];
NSString *prefix = [token substringToIndex:len];
if ([suffix isEqualToString:prefix]) {
return suffix;
}
}
return @"";
2025-11-12 13:43:48 +08:00
}
2025-11-12 14:18:56 +08:00
#pragma mark - Helpers
/// KBStreamTextView
2025-12-09 14:32:21 +08:00
/// - `<SPLIT>` `\t`
2025-11-12 15:31:22 +08:00
/// - UI
2025-11-12 14:18:56 +08:00
- (void)kb_appendChunkToStreamView:(NSString *)chunk {
2025-11-12 16:49:19 +08:00
if (chunk.length == 0) return;
// overlay cell
if (!self.streamOverlay) {
[self kb_showStreamTextViewIfNeededWithTitle:self.loadingTagTitle ?: @""];
if (self.loadingTagIndex) {
[self.tagListView setLoading:NO atIndex:self.loadingTagIndex.integerValue];
self.loadingTagIndex = nil; self.loadingTagTitle = nil;
}
}
if (!self.streamOverlay) return;
2025-11-12 16:03:30 +08:00
[self.streamOverlay appendChunk:chunk];
2025-11-12 14:18:56 +08:00
self.streamHasOutput = YES;
2025-11-12 13:43:48 +08:00
}
2025-11-28 16:55:26 +08:00
///
/// -
/// - +
- (void)kb_updatePasteButtonWithDisplayText:(NSString * _Nullable)text {
if (text.length > 0) {
NSString *displayText = text;
if (displayText.length > 30) {
displayText = [[displayText substringToIndex:30] stringByAppendingString:@"…"];
}
[self.pasteView.pasBtn setImage:nil forState:UIControlStateNormal];
[self.pasteView.pasBtn setTitle:displayText forState:UIControlStateNormal];
} else {
UIImage *img = [UIImage imageNamed:@"kb_zt_icon"];
[self.pasteView.pasBtn setImage:img forState:UIControlStateNormal];
[self.pasteView.pasBtn setTitle:KBLocalized(@" Paste Ta's Words") forState:UIControlStateNormal];
}
}
2025-11-12 16:03:30 +08:00
#pragma mark - KBFunctionTagListViewDelegate
- (void)tagListView:(KBFunctionTagListView *)view didSelectIndex:(NSInteger)index title:(NSString *)title {
2025-11-12 21:23:31 +08:00
// 1) 访
if (![[KBFullAccessManager shared] hasFullAccess]) {
// 访
2025-11-17 20:07:39 +08:00
[KBHUD showInfo:KBLocalized(@"处理中…")];
2025-11-12 21:23:31 +08:00
[[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self];
2025-11-12 16:03:30 +08:00
return;
}
2025-11-12 21:23:31 +08:00
// 2) -> App App
if (!KBAuthManager.shared.isLoggedIn) {
2025-12-05 21:15:48 +08:00
2025-11-12 21:23:31 +08:00
UIInputViewController *ivc = KBFindInputViewController(self);
2025-12-05 21:15:48 +08:00
NSString *schemeStr = [NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME];
NSURL *scheme = [NSURL URLWithString:schemeStr];
// UIApplication App
BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:ivc.view];
2025-11-12 21:23:31 +08:00
return;
2025-12-05 21:15:48 +08:00
// if (!ivc) return;
// NSString *encodedTitle = [title stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]] ?: @"";
// NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=functionView&index=%ld&title=%@", KB_UL_LOGIN, (long)index, encodedTitle]];
// if (!ul) return;
// // UL ok
// dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// [ivc.extensionContext openURL:ul completionHandler:^(__unused BOOL ok) {}];
// });
// // 500ms App 退 Scheme宿 UIApplication
// self.kb_ulHandledFlag = NO;
// NSUInteger token = ++self.kb_ulSeq;
// dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// if (token != self.kb_ulSeq) return; //
// if (self.kb_ulHandledFlag) return; // App
// NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@://login?src=functionView&index=%ld&title=%@", KB_APP_SCHEME, (long)index, encodedTitle]];
// if (!scheme) return;
// UIResponder *start = ivc.view ?: (UIResponder *)self;
// //
// [ivc dismissKeyboard];
// BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:start];
// if (!ok) {
// [KBHUD showInfo:KBLocalized(@"请切换到主App完成登录")];
// }else{
//
// }
// });
// return;
2025-11-12 21:23:31 +08:00
}
2025-12-09 16:12:54 +08:00
BOOL hasPasteText = ![self.pasteView.pasBtn.currentTitle isEqualToString:KBLocalized(@" Paste Ta's Words")];
// BOOL hasPasteText = (self.pasteView.pasBtn.imageView.image == nil);
if (!hasPasteText) {
[KBHUD showInfo:KBLocalized(@"Please copy the text first")];
return;
}
NSString *copyTitle = self.pasteView.pasBtn.currentTitle;
2025-11-12 21:23:31 +08:00
// 3)
[self.tagListView setLoading:YES atIndex:index];
self.loadingTagIndex = @(index);
self.loadingTagTitle = title ?: @"";
2025-12-09 16:12:54 +08:00
[self kb_startNetworkStreamingWithSeed:copyTitle];
2025-11-12 21:23:31 +08:00
return;
}
// Darwin App UL
static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo) {
KBFunctionView *self_ = (__bridge KBFunctionView *)observer;
if (!self_) return;
dispatch_async(dispatch_get_main_queue(), ^{ self_.kb_ulHandledFlag = YES; });
2025-11-12 16:03:30 +08:00
}
2025-11-11 20:24:13 +08:00
// UL App Scheme访
// 访 KBStreamTextView
2025-10-30 20:23:34 +08:00
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
2025-11-11 20:24:13 +08:00
// + 访访
if ([[KBFullAccessManager shared] hasFullAccess]) {
2025-12-08 16:39:47 +08:00
KBTagItemModel *selModel = self.modelArray[indexPath.item];
[self kb_showStreamTextViewIfNeededWithTitle:selModel.characterName];
2025-11-11 20:24:13 +08:00
return;
}
2025-11-17 20:07:39 +08:00
[KBHUD showInfo:KBLocalized(@"处理中…")];
2025-11-11 20:24:13 +08:00
2025-11-04 16:37:24 +08:00
UIInputViewController *ivc = KBFindInputViewController(self);
2025-10-30 20:23:34 +08:00
if (!ivc) return;
2025-12-08 16:39:47 +08:00
NSString *title = self.modelArray[indexPath.item].characterName;
2025-10-30 20:23:34 +08:00
NSString *encodedTitle = [title stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]] ?: @"";
2025-10-30 20:46:54 +08:00
2025-10-30 20:53:44 +08:00
NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=functionView&index=%ld&title=%@", KB_UL_LOGIN, (long)indexPath.item, encodedTitle]];
2025-10-30 20:46:54 +08:00
if (!ul) return;
2025-10-30 20:23:34 +08:00
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// extensionContext UL
[ivc.extensionContext openURL:ul completionHandler:^(BOOL ok) {
if (ok) {
return;
}
// UL 宿 UIApplication + Scheme
NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//login?src=functionView&index=%ld&title=%@", KB_APP_SCHEME, (long)indexPath.item, encodedTitle]];
UIResponder *start = ivc.view ?: (UIResponder *)self;
BOOL ok2 = [KBHostAppLauncher openHostAppURL:scheme fromResponder:start];
if (!ok2) {
2025-11-21 18:26:02 +08:00
// 访宿 Manager
dispatch_async(dispatch_get_main_queue(), ^{
[[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self];
});
}
2025-10-30 20:23:34 +08:00
}];
});
}
2025-10-28 14:30:03 +08:00
#pragma mark - Button Actions
2025-10-29 15:49:43 +08:00
- (void)onTapPaste {
//
// - iOS16+ App
// - iOS15
// viewDidLoad
// + 访访
if (![[KBFullAccessManager shared] hasFullAccess]) {
// 访
[[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self];
return;
}
2025-10-29 15:49:43 +08:00
UIPasteboard *pb = [UIPasteboard generalPasteboard];
NSString *text = pb.string; //
2025-11-28 16:55:26 +08:00
if (text.length <= 0) {
2025-10-29 15:49:43 +08:00
//
NSLog(@"粘贴板无可用文本或未授权粘贴");
2025-11-28 16:55:26 +08:00
[KBHUD showInfo:KBLocalized(@"Clipboard is empty")];
return;
2025-10-29 15:49:43 +08:00
}
2025-11-28 16:55:26 +08:00
// 1
2025-12-09 16:12:54 +08:00
// UIInputViewController *ivc = KBFindInputViewController(self);
// if (ivc) {
// id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
// [proxy insertText:text];
// }
2025-11-28 16:55:26 +08:00
// 2便便
[self kb_updatePasteButtonWithDisplayText:text];
2025-10-29 15:49:43 +08:00
}
#pragma mark -
//
// -
// - changeCount pasteboard.string
// * iOS16+
// * iOS15
// - / changeCount
- (void)startPasteboardMonitor {
// 访宿/
if (![[KBFullAccessManager shared] hasFullAccess]) return;
2025-10-29 15:49:43 +08:00
if (self.pasteboardTimer) return;
2025-11-10 15:38:30 +08:00
KBWeakSelf
2025-10-29 15:49:43 +08:00
self.pasteboardTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 repeats:YES block:^(NSTimer * _Nonnull timer) {
__strong typeof(weakSelf) self = weakSelf; if (!self) return;
UIPasteboard *pb = [UIPasteboard generalPasteboard];
NSInteger cc = pb.changeCount;
if (cc <= self.lastHandledPBCount) return; //
self.lastHandledPBCount = cc; //
// iOS16+
NSString *text = pb.string;
2025-11-28 16:55:26 +08:00
// -> / -> +
[self kb_updatePasteButtonWithDisplayText:text];
2025-10-29 15:49:43 +08:00
}];
}
- (void)stopPasteboardMonitor {
[self.pasteboardTimer invalidate];
self.pasteboardTimer = nil;
}
- (void)didMoveToWindow {
[super didMoveToWindow];
[self kb_refreshPasteboardMonitor];
2025-10-29 15:49:43 +08:00
}
- (void)setHidden:(BOOL)hidden {
BOOL wasHidden = self.isHidden;
[super setHidden:hidden];
if (wasHidden != hidden) {
[self kb_refreshPasteboardMonitor];
}
}
// 访
- (void)kb_refreshPasteboardMonitor {
BOOL visible = (self.window && !self.isHidden);
if (visible && [[KBFullAccessManager shared] hasFullAccess]) {
[self startPasteboardMonitor];
} else {
[self stopPasteboardMonitor];
2025-10-29 15:49:43 +08:00
}
}
- (void)kb_fullAccessChanged {
dispatch_async(dispatch_get_main_queue(), ^{ [self kb_refreshPasteboardMonitor]; });
}
2025-11-17 20:07:39 +08:00
- (void)onTapDelete {
2025-10-30 13:27:09 +08:00
NSLog(@"点击:删除");
2025-12-19 19:21:08 +08:00
[[KBBackspaceUndoManager shared] registerNonClearAction];
2025-11-04 16:37:24 +08:00
UIInputViewController *ivc = KBFindInputViewController(self);
2025-10-30 13:27:09 +08:00
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
[proxy deleteBackward];
}
2025-12-19 18:45:14 +08:00
- (void)onTapClear {
2025-10-30 13:27:09 +08:00
NSLog(@"点击:清空");
2025-12-19 18:45:14 +08:00
[self.backspaceHandler performClearAction];
2025-10-30 13:27:09 +08:00
}
2025-11-17 20:07:39 +08:00
- (void)onTapSend {
2025-10-30 13:27:09 +08:00
NSLog(@"点击:发送");
2025-12-19 19:21:08 +08:00
[[KBBackspaceUndoManager shared] registerNonClearAction];
2025-10-30 13:27:09 +08:00
// App
2025-11-04 16:37:24 +08:00
UIInputViewController *ivc = KBFindInputViewController(self);
2025-10-30 13:27:09 +08:00
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
[proxy insertText:@"\n"];
}
2025-10-28 14:30:03 +08:00
#pragma mark - Lazy
- (KBFunctionBarView *)barViewInternal {
if (!_barViewInternal) {
_barViewInternal = [[KBFunctionBarView alloc] init];
2025-10-28 15:18:12 +08:00
_barViewInternal.delegate = self; // BarView
2025-10-28 14:30:03 +08:00
}
return _barViewInternal;
}
2025-10-28 15:18:12 +08:00
#pragma mark - KBFunctionBarViewDelegate
- (void)functionBarView:(KBFunctionBarView *)bar didTapLeftAtIndex:(NSInteger)index {
//
if ([self.delegate respondsToSelector:@selector(functionView:didTapToolActionAtIndex:)]) {
[self.delegate functionView:self didTapToolActionAtIndex:index];
}
}
- (void)functionBarView:(KBFunctionBarView *)bar didTapRightAtIndex:(NSInteger)index {
// /
2025-11-26 21:16:56 +08:00
if ([self.delegate respondsToSelector:@selector(functionView:didRightTapToolActionAtIndex:)]) {
[self.delegate functionView:self didRightTapToolActionAtIndex:index];
}
2025-10-28 15:18:12 +08:00
}
2025-10-28 14:30:03 +08:00
- (KBFunctionPasteView *)pasteViewInternal {
if (!_pasteViewInternal) {
_pasteViewInternal = [[KBFunctionPasteView alloc] init];
}
return _pasteViewInternal;
}
2025-11-12 16:03:30 +08:00
- (KBFunctionTagListView *)tagListView {
if (!_tagListView) {
_tagListView = [[KBFunctionTagListView alloc] init];
_tagListView.delegate = (id)self;
2025-10-28 14:30:03 +08:00
}
2025-11-12 16:03:30 +08:00
return _tagListView;
2025-10-28 14:30:03 +08:00
}
- (UIView *)rightButtonContainer {
if (!_rightButtonContainer) {
_rightButtonContainer = [[UIView alloc] init];
_rightButtonContainer.backgroundColor = [UIColor clearColor];
}
return _rightButtonContainer;
}
- (UIButton *)buildRightButtonWithTitle:(NSString *)title color:(UIColor *)color {
2025-11-26 21:16:56 +08:00
UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
2025-10-28 14:30:03 +08:00
btn.backgroundColor = color;
2025-11-26 21:16:56 +08:00
btn.layer.cornerRadius = 8.0;
2025-10-28 14:30:03 +08:00
btn.layer.masksToBounds = YES;
2025-11-26 21:16:56 +08:00
btn.titleLabel.font = [KBFont medium:13];
2025-10-28 14:30:03 +08:00
[btn setTitle:title forState:UIControlStateNormal];
[btn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
return btn;
}
- (UIButton *)pasteButtonInternal {
if (!_pasteButtonInternal) {
2025-11-17 20:07:39 +08:00
_pasteButtonInternal = [self buildRightButtonWithTitle:KBLocalized(@"Paste") color:[UIColor colorWithRed:0.13 green:0.73 blue:0.60 alpha:1.0]];
2025-10-28 14:30:03 +08:00
[_pasteButtonInternal addTarget:self action:@selector(onTapPaste) forControlEvents:UIControlEventTouchUpInside];
}
return _pasteButtonInternal;
}
- (UIButton *)deleteButtonInternal {
if (!_deleteButtonInternal) {
2025-11-26 21:16:56 +08:00
_deleteButtonInternal = [UIButton buttonWithType:UIButtonTypeCustom];
_deleteButtonInternal.backgroundColor = [UIColor colorWithHex:0xB9BDC8];
_deleteButtonInternal.layer.cornerRadius = 8.0;
2025-10-28 14:30:03 +08:00
_deleteButtonInternal.layer.masksToBounds = YES;
2025-11-26 21:16:56 +08:00
[_deleteButtonInternal setImage:[UIImage imageNamed:@"kb_del_icon"] forState:UIControlStateNormal];
2025-10-28 14:30:03 +08:00
[_deleteButtonInternal addTarget:self action:@selector(onTapDelete) forControlEvents:UIControlEventTouchUpInside];
2025-12-19 18:45:14 +08:00
[self.backspaceHandler bindDeleteButton:_deleteButtonInternal];
2025-10-28 14:30:03 +08:00
}
return _deleteButtonInternal;
}
- (UIButton *)clearButtonInternal {
if (!_clearButtonInternal) {
2025-11-26 21:16:56 +08:00
_clearButtonInternal = [UIButton buttonWithType:UIButtonTypeCustom];
_clearButtonInternal.backgroundColor = [UIColor colorWithHex:0xB9BDC8];
_clearButtonInternal.layer.cornerRadius = 8.0;
2025-10-28 14:30:03 +08:00
_clearButtonInternal.layer.masksToBounds = YES;
2025-11-26 21:16:56 +08:00
_clearButtonInternal.titleLabel.font = [KBFont medium:13];
2025-11-17 20:07:39 +08:00
[_clearButtonInternal setTitle:KBLocalized(@"Clear") forState:UIControlStateNormal];
2025-10-28 14:30:03 +08:00
[_clearButtonInternal setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[_clearButtonInternal addTarget:self action:@selector(onTapClear) forControlEvents:UIControlEventTouchUpInside];
}
return _clearButtonInternal;
}
2025-11-26 21:16:56 +08:00
- (UIButton *)sendButtonInternal {
2025-10-28 14:30:03 +08:00
if (!_sendButtonInternal) {
2025-11-26 21:16:56 +08:00
_sendButtonInternal = [self buildRightButtonWithTitle:KBLocalized(@"Send") color:[UIColor colorWithHex:0x02BEAC]];
2025-10-28 14:30:03 +08:00
[_sendButtonInternal addTarget:self action:@selector(onTapSend) forControlEvents:UIControlEventTouchUpInside];
}
return _sendButtonInternal;
}
2025-11-12 21:23:31 +08:00
2025-10-28 14:30:03 +08:00
#pragma mark - Expose
2025-11-12 16:03:30 +08:00
- (UICollectionView *)collectionView { return self.tagListView.collectionView; }
2025-12-08 16:39:47 +08:00
//- (NSArray<NSString *> *)items { return self.itemsInternal; }
2025-10-28 14:30:03 +08:00
- (KBFunctionBarView *)barView { return self.barViewInternal; }
- (KBFunctionPasteView *)pasteView { return self.pasteViewInternal; }
- (UIButton *)pasteButton { return self.pasteButtonInternal; }
- (UIButton *)deleteButton { return self.deleteButtonInternal; }
- (UIButton *)clearButton { return self.clearButtonInternal; }
- (UIButton *)sendButton { return self.sendButtonInternal; }
2025-10-30 13:27:09 +08:00
#pragma mark - Find Owner Controller
2025-11-04 16:37:24 +08:00
// KBResponderUtils.h
2025-10-30 13:27:09 +08:00
2025-10-28 14:30:03 +08:00
@end