Compare commits
4 Commits
6fb9e56720
...
efdcf60ed1
| Author | SHA1 | Date | |
|---|---|---|---|
| efdcf60ed1 | |||
| 7a1b17d060 | |||
| f43f94b94d | |||
| 3e2dc4bcb6 |
@@ -2,6 +2,10 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>kbkeyboardAppExtension</string>
|
||||
</array>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#import "Masonry.h"
|
||||
#import "KBAuthManager.h"
|
||||
#import "KBFullAccessManager.h"
|
||||
#import "KBSkinManager.h"
|
||||
|
||||
static CGFloat KEYBOARDHEIGHT = 256 + 20;
|
||||
|
||||
@@ -22,6 +23,7 @@ static CGFloat KEYBOARDHEIGHT = 256 + 20;
|
||||
@property (nonatomic, strong) KBKeyBoardMainView *keyBoardMainView; // 功能面板视图(点击工具栏第0个时显示)
|
||||
@property (nonatomic, strong) KBFunctionView *functionView; // 功能面板视图(点击工具栏第0个时显示)
|
||||
@property (nonatomic, strong) KBSettingView *settingView; // 设置页
|
||||
@property (nonatomic, strong) UIImageView *bgImageView; // 背景图(在底层)
|
||||
@end
|
||||
|
||||
@implementation KeyboardViewController
|
||||
@@ -40,12 +42,32 @@ static CGFloat KEYBOARDHEIGHT = 256 + 20;
|
||||
__unused id token = [[NSNotificationCenter defaultCenter] addObserverForName:KBFullAccessChangedNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(__unused NSNotification * _Nonnull note) {
|
||||
// 如需,可在此刷新与完全访问相关的 UI
|
||||
}];
|
||||
|
||||
// 皮肤变化时,立即应用
|
||||
__unused id token2 = [[NSNotificationCenter defaultCenter] addObserverForName:KBSkinDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(__unused NSNotification * _Nonnull note) {
|
||||
[self kb_applyTheme];
|
||||
}];
|
||||
[self kb_applyTheme];
|
||||
}
|
||||
|
||||
|
||||
- (void)setupUI {
|
||||
// 固定键盘整体高度
|
||||
[self.view.heightAnchor constraintEqualToConstant:KEYBOARDHEIGHT].active = YES;
|
||||
// 固定键盘整体高度;为减少与系统自调整高度产生的告警,将优先级设置为 High
|
||||
NSLayoutConstraint *h = [self.view.heightAnchor constraintEqualToConstant:KEYBOARDHEIGHT];
|
||||
h.priority = UILayoutPriorityDefaultHigh; // 750
|
||||
h.active = YES;
|
||||
// 关闭 UIInputView 自适应(某些系统版本会尝试放大为全屏高度导致冲突)
|
||||
if ([self.view isKindOfClass:[UIInputView class]]) {
|
||||
UIInputView *iv = (UIInputView *)self.view;
|
||||
if ([iv respondsToSelector:@selector(setAllowsSelfSizing:)]) {
|
||||
iv.allowsSelfSizing = NO;
|
||||
}
|
||||
}
|
||||
// 背景图铺底
|
||||
[self.view addSubview:self.bgImageView];
|
||||
[self.bgImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(self.view);
|
||||
}];
|
||||
// 预置功能面板(默认隐藏),与键盘区域共享相同布局
|
||||
self.functionView.hidden = YES;
|
||||
[self.view addSubview:self.functionView];
|
||||
@@ -209,7 +231,8 @@ static CGFloat KEYBOARDHEIGHT = 256 + 20;
|
||||
}
|
||||
|
||||
- (void)kb_tryOpenContainerForLoginIfNeeded {
|
||||
NSURL *url = [NSURL URLWithString:@"kbkeyboard://login?src=keyboard"];
|
||||
// 使用与主 App 一致的自定义 Scheme
|
||||
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//login?src=keyboard", KB_APP_SCHEME]];
|
||||
if (!url) return;
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[self.extensionContext openURL:url completionHandler:^(__unused BOOL success) {
|
||||
@@ -217,4 +240,40 @@ static CGFloat KEYBOARDHEIGHT = 256 + 20;
|
||||
__unused typeof(weakSelf) selfStrong = weakSelf;
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Theme
|
||||
|
||||
- (void)kb_applyTheme {
|
||||
KBSkinTheme *t = [KBSkinManager shared].current;
|
||||
UIImage *img = [[KBSkinManager shared] currentBackgroundImage];
|
||||
self.bgImageView.image = img;
|
||||
BOOL hasImg = (img != nil);
|
||||
self.view.backgroundColor = hasImg ? [UIColor clearColor] : t.keyboardBackground;
|
||||
self.keyBoardMainView.backgroundColor = hasImg ? [UIColor clearColor] : t.keyboardBackground;
|
||||
// 触发键区按主题重绘
|
||||
if ([self.keyBoardMainView respondsToSelector:@selector(kb_applyTheme)]) {
|
||||
// method declared in KBKeyBoardMainView.h
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
[self.keyBoardMainView performSelector:@selector(kb_applyTheme)];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
if ([self.functionView respondsToSelector:@selector(kb_applyTheme)]) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
[self.functionView performSelector:@selector(kb_applyTheme)];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Lazy
|
||||
|
||||
- (UIImageView *)bgImageView {
|
||||
if (!_bgImageView) {
|
||||
_bgImageView = [[UIImageView alloc] init];
|
||||
_bgImageView.contentMode = UIViewContentModeScaleAspectFill;
|
||||
_bgImageView.clipsToBounds = YES;
|
||||
}
|
||||
return _bgImageView;
|
||||
}
|
||||
@end
|
||||
|
||||
@@ -23,9 +23,15 @@
|
||||
|
||||
// 通用链接(Universal Links)统一配置
|
||||
// 配置好 AASA 与 Associated Domains 后,只需修改这里即可切换域名/path。
|
||||
#define KB_UL_BASE @"https://your.domain/ul" // 替换为你的真实域名与前缀路径
|
||||
#define KB_UL_BASE @"https://app.tknb.net/ul" // 与 Associated Domains 一致
|
||||
#define KB_UL_LOGIN KB_UL_BASE @"/login"
|
||||
#define KB_UL_SETTINGS KB_UL_BASE @"/settings"
|
||||
|
||||
// 在扩展内,启用 URL Bridge(仅在显式的用户点击动作中使用)
|
||||
// 这样即便宿主 App(如备忘录)拒绝 extensionContext 的 openURL,仍可通过响应链兜底拉起容器 App。
|
||||
#ifndef KB_URL_BRIDGE_ENABLE
|
||||
#define KB_URL_BRIDGE_ENABLE 1
|
||||
#endif
|
||||
|
||||
|
||||
#endif /* PrefixHeader_pch */
|
||||
|
||||
30
CustomKeyboard/Utils/KBURLOpenBridge.h
Normal file
30
CustomKeyboard/Utils/KBURLOpenBridge.h
Normal file
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// KBURLOpenBridge.h
|
||||
// 非公开:通过响应链查找 `openURL:` 选择器,尝试在扩展环境中打开自定义 scheme。
|
||||
// 警告:存在审核风险。默认仅 Debug 启用(见 KB_URL_BRIDGE_ENABLE)。
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
#ifndef KB_URL_BRIDGE_ENABLE
|
||||
#if DEBUG
|
||||
#define KB_URL_BRIDGE_ENABLE 1
|
||||
#else
|
||||
#define KB_URL_BRIDGE_ENABLE 0
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@interface KBURLOpenBridge : NSObject
|
||||
|
||||
/// 尝试通过响应链调用 openURL:(仅在 KB_URL_BRIDGE_ENABLE 为 1 时执行)。
|
||||
/// @param url 自定义 scheme,如 kbkeyboard://settings
|
||||
/// @param start 起始 responder(传 self 或任意视图)
|
||||
/// @return 是否看起来已发起打开动作(不保证一定成功)
|
||||
+ (BOOL)openURLViaResponder:(NSURL *)url from:(UIResponder *)start;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
46
CustomKeyboard/Utils/KBURLOpenBridge.m
Normal file
46
CustomKeyboard/Utils/KBURLOpenBridge.m
Normal file
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// KBURLOpenBridge.m
|
||||
//
|
||||
|
||||
#import "KBURLOpenBridge.h"
|
||||
#import <objc/message.h>
|
||||
|
||||
@implementation KBURLOpenBridge
|
||||
|
||||
+ (BOOL)openURLViaResponder:(NSURL *)url from:(UIResponder *)start {
|
||||
#if KB_URL_BRIDGE_ENABLE
|
||||
if (!url || !start) return NO;
|
||||
SEL sel = NSSelectorFromString(@"openURL:");
|
||||
UIResponder *responder = start;
|
||||
while (responder) {
|
||||
@try {
|
||||
if ([responder respondsToSelector:sel]) {
|
||||
// 尽量按签名调用;若失败则回退 performSelector
|
||||
BOOL handled = NO;
|
||||
// 尝试 (BOOL)openURL:(NSURL *)
|
||||
BOOL (*funcBool)(id, SEL, NSURL *) = (BOOL (*)(id, SEL, NSURL *))objc_msgSend;
|
||||
if (funcBool) {
|
||||
handled = funcBool(responder, sel, url);
|
||||
} else {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
[responder performSelector:sel withObject:url];
|
||||
handled = YES;
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
} @catch (__unused NSException *e) {
|
||||
// ignore and continue
|
||||
}
|
||||
responder = responder.nextResponder;
|
||||
}
|
||||
return NO;
|
||||
#else
|
||||
(void)url; (void)start;
|
||||
return NO;
|
||||
#endif
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -5,10 +5,15 @@
|
||||
|
||||
#import "KBFullAccessGuideView.h"
|
||||
#import "Masonry.h"
|
||||
#import "KBResponderUtils.h" // 统一查找 UIInputViewController 的工具
|
||||
#import "KBHUD.h"
|
||||
#import "KBURLOpenBridge.h"
|
||||
|
||||
@interface KBFullAccessGuideView ()
|
||||
@property (nonatomic, strong) UIControl *backdrop;
|
||||
@property (nonatomic, strong) UIView *card;
|
||||
// 预先保存当前键盘控制器,避免运行时通过响应链找不到
|
||||
@property (nonatomic, weak) UIInputViewController *ivc;
|
||||
@end
|
||||
|
||||
@implementation KBFullAccessGuideView
|
||||
@@ -109,7 +114,8 @@
|
||||
}
|
||||
|
||||
- (void)presentIn:(UIView *)parent {
|
||||
UIView *container = parent.window ?: parent;
|
||||
if (!parent) return;
|
||||
UIView *container = parent; // 关键:加到键盘视图树中,而不是 window
|
||||
self.frame = container.bounds;
|
||||
self.alpha = 0;
|
||||
[container addSubview:self];
|
||||
@@ -125,15 +131,19 @@
|
||||
|
||||
+ (void)showInView:(UIView *)parent {
|
||||
if (!parent) return;
|
||||
// 避免重复
|
||||
for (UIView *v in (parent.window ?: parent).subviews) {
|
||||
// 避免重复(仅在 parent 层级检查)
|
||||
for (UIView *v in parent.subviews) {
|
||||
if ([v isKindOfClass:[KBFullAccessGuideView class]]) return;
|
||||
}
|
||||
[[KBFullAccessGuideView build] presentIn:parent];
|
||||
KBFullAccessGuideView *view = [KBFullAccessGuideView build];
|
||||
// 预取 ivc
|
||||
view.ivc = KBFindInputViewController(parent);
|
||||
[view presentIn:parent];
|
||||
}
|
||||
|
||||
+ (void)dismissFromView:(UIView *)parent {
|
||||
UIView *container = parent.window ?: parent;
|
||||
UIView *container = parent;
|
||||
if (!container) return;
|
||||
for (UIView *v in container.subviews) {
|
||||
if ([v isKindOfClass:[KBFullAccessGuideView class]]) {
|
||||
[(KBFullAccessGuideView *)v dismiss];
|
||||
@@ -146,40 +156,74 @@
|
||||
|
||||
#pragma mark - Actions
|
||||
|
||||
- (UIInputViewController *)kb_findInputController {
|
||||
UIResponder *res = self;
|
||||
while (res) {
|
||||
if ([res isKindOfClass:[UIInputViewController class]]) {
|
||||
return (UIInputViewController *)res;
|
||||
}
|
||||
res = res.nextResponder;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 工具方法已提取到 KBResponderUtils.h
|
||||
// 打开主 App,引导用户去系统设置开启完全访问:优先 Scheme,失败再试 UL;仍失败则提示手动路径。
|
||||
- (void)onTapGoEnable {
|
||||
// 在扩展中无法使用 UIApplication。改为委托宿主打开链接:
|
||||
// 方案:优先拉起主 App 并由主 App 打开设置页,避免宿主拦截。
|
||||
UIInputViewController *ivc = [self kb_findInputController];
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
if (!ivc) { [self dismiss]; return; }
|
||||
|
||||
// 先尝试 Universal Link(如未配置可改为你的域名),失败再用自定义 scheme。
|
||||
|
||||
// 自定义 Scheme(App 里在 openURL 中转到设置页)
|
||||
// 统一使用主 App 的自定义 Scheme
|
||||
NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//settings?src=kb_extension", KB_APP_SCHEME]];
|
||||
// Universal Link(需 AASA/Associated Domains 配置且 KB_UL_BASE 与域名一致)
|
||||
NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=kb_extension", KB_UL_SETTINGS]];
|
||||
void (^fallback)(void) = ^{
|
||||
NSURL *scheme = [NSURL URLWithString:@"kbkeyboard://settings?src=kb_extension"]; // 主App在 openURL 中处理
|
||||
[ivc.extensionContext openURL:scheme completionHandler:^(__unused BOOL ok2) {
|
||||
// 无论成功与否,都收起当前提示层,避免遮挡
|
||||
[self dismiss];
|
||||
}];
|
||||
|
||||
void (^finish)(BOOL) = ^(BOOL ok){
|
||||
if (ok) { [self dismiss]; }
|
||||
else {
|
||||
[KBHUD showInfo:@"无法自动打开,请按路径:设置→通用→键盘→键盘→恋爱键盘→允许完全访问"]; // 失败兜底提示
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 先试 Scheme(更可能被宿主允许直接拉起 App)
|
||||
if (scheme) {
|
||||
[ivc.extensionContext openURL:scheme completionHandler:^(BOOL ok) {
|
||||
if (ok) { finish(YES); return; }
|
||||
if (ul) {
|
||||
[ivc.extensionContext openURL:ul completionHandler:^(BOOL ok2) {
|
||||
if (ok2) { finish(YES); return; }
|
||||
// 兜底:在用户点击触发的场景下,尝试通过响应链调用 openURL:
|
||||
BOOL bridged = NO;
|
||||
@try {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wunguarded-availability"
|
||||
bridged = [KBURLOpenBridge openURLViaResponder:scheme from:self];
|
||||
if (!bridged && ul) {
|
||||
bridged = [KBURLOpenBridge openURLViaResponder:ul from:self];
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
} @catch (__unused NSException *e) { bridged = NO; }
|
||||
finish(bridged);
|
||||
}];
|
||||
} else {
|
||||
// 没有 UL,则直接尝试桥接 Scheme
|
||||
BOOL bridged = NO;
|
||||
@try {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wunguarded-availability"
|
||||
bridged = [KBURLOpenBridge openURLViaResponder:scheme from:self];
|
||||
#pragma clang diagnostic pop
|
||||
} @catch (__unused NSException *e) { bridged = NO; }
|
||||
finish(bridged);
|
||||
}
|
||||
}];
|
||||
return;
|
||||
}
|
||||
// 无 scheme 时,直接尝试 UL
|
||||
if (ul) {
|
||||
[ivc.extensionContext openURL:ul completionHandler:^(BOOL ok) {
|
||||
if (ok) { [self dismiss]; }
|
||||
else { fallback(); }
|
||||
if (ok) { finish(YES); return; }
|
||||
BOOL bridged = NO;
|
||||
@try {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wunguarded-availability"
|
||||
bridged = [KBURLOpenBridge openURLViaResponder:ul from:self];
|
||||
#pragma clang diagnostic pop
|
||||
} @catch (__unused NSException *e) { bridged = NO; }
|
||||
finish(bridged);
|
||||
}];
|
||||
} else {
|
||||
fallback();
|
||||
finish(NO);
|
||||
}
|
||||
}
|
||||
@end
|
||||
|
||||
@@ -7,12 +7,14 @@
|
||||
|
||||
#import "KBFunctionBarView.h"
|
||||
#import "Masonry.h"
|
||||
#import "KBResponderUtils.h" // 查找 UIInputViewController,用于系统切换输入法
|
||||
|
||||
@interface KBFunctionBarView ()
|
||||
@property (nonatomic, strong) UIView *leftContainer; // 左侧按钮容器
|
||||
@property (nonatomic, strong) UIView *rightContainer; // 右侧按钮容器
|
||||
@property (nonatomic, strong) NSArray<UIButton *> *leftButtonsInternal;
|
||||
@property (nonatomic, strong) NSArray<UIButton *> *rightButtonsInternal;
|
||||
@property (nonatomic, strong) UIButton *globeButtonInternal; // 可选:系统“切换输入法”键
|
||||
@end
|
||||
|
||||
@implementation KBFunctionBarView
|
||||
@@ -38,6 +40,7 @@
|
||||
- (void)buildUI {
|
||||
// 左右两个容器,方便分别布局
|
||||
[self addSubview:self.leftContainer];
|
||||
[self addSubview:self.globeButtonInternal];
|
||||
[self addSubview:self.rightContainer];
|
||||
|
||||
[self.rightContainer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
@@ -46,8 +49,15 @@
|
||||
make.height.mas_equalTo(36);
|
||||
}];
|
||||
|
||||
[self.leftContainer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
// 左侧地球键(按需显示)
|
||||
[self.globeButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.mas_left).offset(12);
|
||||
make.centerY.equalTo(self.mas_centerY);
|
||||
make.width.height.mas_equalTo(32);
|
||||
}];
|
||||
|
||||
[self.leftContainer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.globeButtonInternal.mas_right).offset(8);
|
||||
make.right.equalTo(self.rightContainer.mas_left).offset(-12);
|
||||
make.centerY.equalTo(self.mas_centerY);
|
||||
make.height.mas_equalTo(36);
|
||||
@@ -113,6 +123,9 @@
|
||||
}
|
||||
|
||||
self.rightButtonsInternal = rightBtns.copy;
|
||||
|
||||
// 初始刷新地球键可见性与事件绑定
|
||||
[self kb_refreshGlobeVisibility];
|
||||
}
|
||||
|
||||
#pragma mark - Actions
|
||||
@@ -158,4 +171,56 @@
|
||||
return _rightContainer;
|
||||
}
|
||||
|
||||
- (UIButton *)globeButtonInternal {
|
||||
if (!_globeButtonInternal) {
|
||||
_globeButtonInternal = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
_globeButtonInternal.layer.cornerRadius = 16;
|
||||
_globeButtonInternal.layer.masksToBounds = YES;
|
||||
_globeButtonInternal.backgroundColor = [UIColor colorWithWhite:1 alpha:0.9];
|
||||
[_globeButtonInternal setTitle:@"🌐" forState:UIControlStateNormal];
|
||||
[_globeButtonInternal setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
|
||||
}
|
||||
return _globeButtonInternal;
|
||||
}
|
||||
|
||||
#pragma mark - Globe (Input Mode Switch)
|
||||
|
||||
- (void)kb_refreshGlobeVisibility {
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
BOOL needSwitchKey = YES;
|
||||
if (ivc && [ivc respondsToSelector:@selector(needsInputModeSwitchKey)]) {
|
||||
needSwitchKey = ivc.needsInputModeSwitchKey;
|
||||
}
|
||||
|
||||
self.globeButtonInternal.hidden = !needSwitchKey;
|
||||
|
||||
// 左容器左约束:根据是否显示地球键动态调整
|
||||
[self.leftContainer mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
if (needSwitchKey) {
|
||||
make.left.equalTo(self.globeButtonInternal.mas_right).offset(8);
|
||||
} else {
|
||||
make.left.equalTo(self.mas_left).offset(12);
|
||||
}
|
||||
make.right.equalTo(self.rightContainer.mas_left).offset(-12);
|
||||
make.centerY.equalTo(self.mas_centerY);
|
||||
make.height.mas_equalTo(36);
|
||||
}];
|
||||
|
||||
// 绑定系统输入法切换事件
|
||||
[self.globeButtonInternal removeTarget:nil action:NULL forControlEvents:UIControlEventAllEvents];
|
||||
if (needSwitchKey && ivc) {
|
||||
SEL sel = NSSelectorFromString(@"handleInputModeListFromView:withEvent:");
|
||||
if ([ivc respondsToSelector:sel]) {
|
||||
[self.globeButtonInternal addTarget:ivc action:sel forControlEvents:UIControlEventAllTouchEvents];
|
||||
} else {
|
||||
[self.globeButtonInternal addTarget:ivc action:@selector(advanceToNextInputMode) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)didMoveToWindow {
|
||||
[super didMoveToWindow];
|
||||
[self kb_refreshGlobeVisibility];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -33,6 +33,9 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@property (nonatomic, strong, readonly) UIButton *clearButton; // 右侧-清空
|
||||
@property (nonatomic, strong, readonly) UIButton *sendButton; // 右侧-发送
|
||||
|
||||
/// 应用当前皮肤(更新背景/强调色)
|
||||
- (void)kb_applyTheme;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
#import "KBFunctionView.h"
|
||||
#import "KBResponderUtils.h" // 统一查找 UIInputViewController 的工具
|
||||
#import "KBFunctionBarView.h"
|
||||
#import "KBFunctionPasteView.h"
|
||||
#import "KBFunctionTagCell.h"
|
||||
@@ -13,6 +14,8 @@
|
||||
#import <MBProgressHUD.h>
|
||||
#import "KBFullAccessGuideView.h"
|
||||
#import "KBFullAccessManager.h"
|
||||
#import "KBSkinManager.h"
|
||||
#import "KBURLOpenBridge.h" // 兜底从扩展侧直接尝试 openURL:
|
||||
|
||||
static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
|
||||
|
||||
@@ -39,20 +42,33 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
// 整体绿色背景(接近截图效果,项目可自行替换素材)
|
||||
self.backgroundColor = [UIColor colorWithRed:0.77 green:0.93 blue:0.82 alpha:1.0];
|
||||
// 背景使用当前主题强调色
|
||||
[self kb_applyTheme];
|
||||
|
||||
[self setupUI];
|
||||
[self reloadDemoData];
|
||||
|
||||
// 初始化剪贴板监控状态
|
||||
_lastHandledPBCount = [UIPasteboard generalPasteboard].changeCount;
|
||||
|
||||
// 监听“完全访问”状态变化,动态启停剪贴板监控,避免在未开完全访问时触发 TCC/XPC 错误日志
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(kb_fullAccessChanged) name:KBFullAccessChangedNotification object:nil];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Theme
|
||||
|
||||
- (void)kb_applyTheme {
|
||||
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 = hasImg ? [accent colorWithAlphaComponent:0.65] : accent;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self stopPasteboardMonitor];
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
#pragma mark - UI
|
||||
@@ -81,10 +97,11 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
|
||||
[self.rightButtonContainer addSubview:self.clearButtonInternal];
|
||||
[self.rightButtonContainer addSubview:self.sendButtonInternal];
|
||||
|
||||
// 竖向排布:粘贴、删除、清空为等高,发送略高
|
||||
// 竖向排布:粘贴、删除、清空为等高;发送优先更高,但允许在空间不足时压缩
|
||||
CGFloat smallH = 44;
|
||||
CGFloat bigH = 56;
|
||||
CGFloat vSpace = 10;
|
||||
// 原 10 在键盘总高度 276 下容易超出容器,改为 8 以避免 AutoLayout 冲突
|
||||
CGFloat vSpace = 8;
|
||||
[self.pasteButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.rightButtonContainer.mas_top);
|
||||
make.left.right.equalTo(self.rightButtonContainer);
|
||||
@@ -103,8 +120,10 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
|
||||
[self.sendButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.clearButtonInternal.mas_bottom).offset(vSpace);
|
||||
make.left.right.equalTo(self.rightButtonContainer);
|
||||
make.height.mas_equalTo(bigH);
|
||||
make.bottom.lessThanOrEqualTo(self.rightButtonContainer.mas_bottom); // 底部可伸缩
|
||||
// 允许在空间不足时缩短到 smallH,避免产生约束冲突
|
||||
make.height.greaterThanOrEqualTo(@(smallH));
|
||||
make.height.lessThanOrEqualTo(@(bigH));
|
||||
make.bottom.lessThanOrEqualTo(self.rightButtonContainer.mas_bottom);
|
||||
}];
|
||||
|
||||
// 2. 粘贴区(位于右侧按钮左侧)
|
||||
@@ -169,7 +188,7 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
|
||||
[KBHUD showInfo:@"处理中…"];
|
||||
// return;
|
||||
|
||||
UIInputViewController *ivc = [self findInputViewController];
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
if (!ivc) return;
|
||||
|
||||
NSString *title = (indexPath.item < self.itemsInternal.count) ? self.itemsInternal[indexPath.item] : @"";
|
||||
@@ -182,9 +201,22 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
|
||||
[ivc.extensionContext openURL:ul completionHandler:^(BOOL ok) {
|
||||
if (ok) return; // Universal Link 成功
|
||||
|
||||
NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"kbkeyboard://login?src=functionView&index=%ld&title=%@", (long)indexPath.item, encodedTitle]];
|
||||
// 统一使用主 App 注册的自定义 Scheme
|
||||
NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//login?src=functionView&index=%ld&title=%@", KB_APP_SCHEME, (long)indexPath.item, encodedTitle]];
|
||||
[ivc.extensionContext openURL:scheme completionHandler:^(BOOL ok2) {
|
||||
if (!ok2) {
|
||||
if (ok2) return;
|
||||
|
||||
// 兜底:在用户点击触发的场景下,尝试通过响应链调用 openURL:
|
||||
// 以提升在“备忘录”等宿主中的成功率。
|
||||
BOOL bridged = NO;
|
||||
@try {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wunguarded-availability"
|
||||
bridged = [KBURLOpenBridge openURLViaResponder:scheme from:self];
|
||||
#pragma clang diagnostic pop
|
||||
} @catch (__unused NSException *e) { bridged = NO; }
|
||||
|
||||
if (!bridged) {
|
||||
// 两条路都失败:大概率未开完全访问或宿主拦截。统一交由 Manager 引导。
|
||||
dispatch_async(dispatch_get_main_queue(), ^{ [[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self]; });
|
||||
}
|
||||
@@ -224,6 +256,8 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
|
||||
// - 无论允许/拒绝,都把本次 changeCount 记为已处理,避免一直重复询问。
|
||||
|
||||
- (void)startPasteboardMonitor {
|
||||
// 未开启“完全访问”时不做自动读取,避免宿主/系统拒绝并刷错误日志
|
||||
if (![[KBFullAccessManager shared] hasFullAccess]) return;
|
||||
if (self.pasteboardTimer) return;
|
||||
__weak typeof(self) weakSelf = self;
|
||||
self.pasteboardTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 repeats:YES block:^(NSTimer * _Nonnull timer) {
|
||||
@@ -248,34 +282,40 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
|
||||
|
||||
- (void)didMoveToWindow {
|
||||
[super didMoveToWindow];
|
||||
if (self.window && !self.isHidden) {
|
||||
[self startPasteboardMonitor];
|
||||
} else {
|
||||
[self stopPasteboardMonitor];
|
||||
}
|
||||
[self kb_refreshPasteboardMonitor];
|
||||
}
|
||||
|
||||
- (void)setHidden:(BOOL)hidden {
|
||||
BOOL wasHidden = self.isHidden;
|
||||
[super setHidden:hidden];
|
||||
if (wasHidden != hidden) {
|
||||
if (!hidden && self.window) {
|
||||
[self startPasteboardMonitor];
|
||||
} else {
|
||||
[self stopPasteboardMonitor];
|
||||
}
|
||||
[self kb_refreshPasteboardMonitor];
|
||||
}
|
||||
}
|
||||
|
||||
// 根据窗口可见性与完全访问状态,统一启停粘贴板监控
|
||||
- (void)kb_refreshPasteboardMonitor {
|
||||
BOOL visible = (self.window && !self.isHidden);
|
||||
if (visible && [[KBFullAccessManager shared] hasFullAccess]) {
|
||||
[self startPasteboardMonitor];
|
||||
} else {
|
||||
[self stopPasteboardMonitor];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)kb_fullAccessChanged {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{ [self kb_refreshPasteboardMonitor]; });
|
||||
}
|
||||
- (void)onTapDelete {
|
||||
NSLog(@"点击:删除");
|
||||
UIInputViewController *ivc = [self findInputViewController];
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
[proxy deleteBackward];
|
||||
}
|
||||
- (void)onTapClear {
|
||||
NSLog(@"点击:清空");
|
||||
// 连续删除:仅清空光标之前的输入(不改动 pasteView 的内容)
|
||||
UIInputViewController *ivc = [self findInputViewController];
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
// 逐批读取 documentContextBeforeInput 并删除,避免 50 字符窗口限制带来的残留
|
||||
NSInteger guard = 0; // 上限保护,避免极端情况下长时间阻塞
|
||||
@@ -292,7 +332,7 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
|
||||
- (void)onTapSend {
|
||||
NSLog(@"点击:发送");
|
||||
// 发送:插入换行。大多数聊天类 App 会把回车视为“发送”
|
||||
UIInputViewController *ivc = [self findInputViewController];
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
[proxy insertText:@"\n"];
|
||||
}
|
||||
@@ -417,16 +457,6 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
|
||||
|
||||
#pragma mark - Find Owner Controller
|
||||
|
||||
// 在视图的响应链中查找宿主 UIInputViewController(KeyboardViewController)
|
||||
- (UIInputViewController *)findInputViewController {
|
||||
UIResponder *responder = self;
|
||||
while (responder) {
|
||||
if ([responder isKindOfClass:[UIInputViewController class]]) {
|
||||
return (UIInputViewController *)responder;
|
||||
}
|
||||
responder = responder.nextResponder;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
// 工具方法已提取到 KBResponderUtils.h
|
||||
|
||||
@end
|
||||
|
||||
@@ -28,6 +28,9 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@interface KBKeyBoardMainView : UIView
|
||||
@property (nonatomic, weak) id<KBKeyBoardMainViewDelegate> delegate;
|
||||
|
||||
/// 应用当前皮肤(会触发键区重载以应用按键颜色)
|
||||
- (void)kb_applyTheme;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#import "KBFunctionView.h"
|
||||
#import "KBKey.h"
|
||||
#import "Masonry.h"
|
||||
#import "KBSkinManager.h"
|
||||
|
||||
@interface KBKeyBoardMainView ()<KBToolBarDelegate, KBKeyboardViewDelegate>
|
||||
@property (nonatomic, strong) KBToolBar *topBar;
|
||||
@@ -21,7 +22,7 @@
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
self.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0];
|
||||
self.backgroundColor = [KBSkinManager shared].current.keyboardBackground;
|
||||
// 顶部栏
|
||||
self.topBar = [[KBToolBar alloc] init];
|
||||
self.topBar.delegate = self;
|
||||
@@ -31,7 +32,7 @@
|
||||
make.top.equalTo(self.mas_top).offset(6);
|
||||
make.height.mas_equalTo(40);
|
||||
}];
|
||||
|
||||
|
||||
// 键盘区域
|
||||
self.keyboardView = [[KBKeyboardView alloc] init];
|
||||
self.keyboardView.delegate = self;
|
||||
@@ -41,9 +42,9 @@
|
||||
make.top.equalTo(self.topBar.mas_bottom).offset(4);
|
||||
make.bottom.equalTo(self.mas_bottom).offset(-4);
|
||||
}];
|
||||
|
||||
|
||||
// 功能面板切换交由外部控制器处理;此处不直接创建/管理
|
||||
|
||||
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -114,5 +115,15 @@
|
||||
// 切换功能面板交由外部控制器处理(此处不再实现)
|
||||
|
||||
// 设置页展示改由 KeyboardViewController 统一处理
|
||||
#pragma mark - Theme
|
||||
|
||||
- (void)kb_applyTheme {
|
||||
KBSkinManager *mgr = [KBSkinManager shared];
|
||||
BOOL hasImg = ([mgr currentBackgroundImage] != nil);
|
||||
UIColor *bg = mgr.current.keyboardBackground;
|
||||
self.backgroundColor = hasImg ? [UIColor clearColor] : bg;
|
||||
self.keyboardView.backgroundColor = hasImg ? [UIColor clearColor] : bg;
|
||||
[self.keyboardView reloadKeys];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#import "KBKeyButton.h"
|
||||
#import "KBKey.h"
|
||||
#import "KBSkinManager.h"
|
||||
|
||||
@implementation KBKeyButton
|
||||
|
||||
@@ -16,10 +17,11 @@
|
||||
}
|
||||
|
||||
- (void)applyDefaultStyle {
|
||||
self.titleLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightSemibold]; // 字体样式
|
||||
[self setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
|
||||
[self setTitleColor:[UIColor blackColor] forState:UIControlStateHighlighted];
|
||||
self.backgroundColor = [UIColor whiteColor];
|
||||
self.titleLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightSemibold];
|
||||
KBSkinTheme *t = [KBSkinManager shared].current;
|
||||
[self setTitleColor:t.keyTextColor forState:UIControlStateNormal];
|
||||
[self setTitleColor:t.keyTextColor forState:UIControlStateHighlighted];
|
||||
self.backgroundColor = t.keyBackground;
|
||||
self.layer.cornerRadius = 6.0; // 圆角
|
||||
self.layer.masksToBounds = NO;
|
||||
self.layer.shadowColor = [UIColor colorWithWhite:0 alpha:0.1].CGColor; // 阴影效果
|
||||
@@ -46,10 +48,11 @@
|
||||
|
||||
- (void)refreshStateAppearance {
|
||||
// 选中态用于 Shift/CapsLock 等特殊按键的高亮显示
|
||||
KBSkinTheme *t = [KBSkinManager shared].current;
|
||||
if (self.isSelected) {
|
||||
self.backgroundColor = [UIColor colorWithWhite:0.85 alpha:1.0];
|
||||
self.backgroundColor = t.keyHighlightBackground ?: t.keyBackground;
|
||||
} else {
|
||||
self.backgroundColor = [UIColor whiteColor];
|
||||
self.backgroundColor = t.keyBackground;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#import "KBKeyboardView.h"
|
||||
#import "KBKeyButton.h"
|
||||
#import "KBKey.h"
|
||||
#import "KBResponderUtils.h" // 封装的响应链工具
|
||||
#import "KBSkinManager.h"
|
||||
|
||||
@interface KBKeyboardView ()
|
||||
@property (nonatomic, strong) UIView *row1;
|
||||
@@ -13,13 +15,15 @@
|
||||
@property (nonatomic, strong) UIView *row3;
|
||||
@property (nonatomic, strong) UIView *row4;
|
||||
@property (nonatomic, strong) NSArray<NSArray<KBKey *> *> *keysForRows;
|
||||
// 长按退格的一次次删除控制标记(不使用 NSTimer,仅用 GCD 递归调度)
|
||||
@property (nonatomic, assign) BOOL backspaceHoldActive;
|
||||
@end
|
||||
|
||||
@implementation KBKeyboardView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
self.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0];
|
||||
self.backgroundColor = [KBSkinManager shared].current.keyboardBackground;
|
||||
_layoutStyle = KBKeyboardLayoutStyleLetters;
|
||||
// 默认小写:与需求一致,初始不开启 Shift
|
||||
_shiftOn = NO;
|
||||
@@ -196,6 +200,16 @@
|
||||
[btn addTarget:self action:@selector(onKeyTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[row addSubview:btn];
|
||||
|
||||
// ⌫ 长按:开始连续逐个删除(无需 NSTimer)。使用 UILongPressGestureRecognizer 识别长按,
|
||||
// 在开始态触发递归的轻量调度,每次删除 1 个字符,直到松手或无内容。
|
||||
if (key.type == KBKeyTypeBackspace) {
|
||||
UILongPressGestureRecognizer *lp = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(onBackspaceLongPress:)];
|
||||
// 稍短的判定时间,提升响应(默认约 0.5s)。
|
||||
lp.minimumPressDuration = 0.35;
|
||||
lp.cancelsTouchesInView = YES; // 被识别为长按时,取消普通点击
|
||||
[btn addGestureRecognizer:lp];
|
||||
}
|
||||
|
||||
// Shift 按钮选中态随大小写状态变化
|
||||
if (key.type == KBKeyTypeShift) {
|
||||
btn.selected = self.shiftOn;
|
||||
@@ -250,6 +264,9 @@
|
||||
if (firstChar) {
|
||||
for (KBKeyButton *b in row.subviews) {
|
||||
if (![b isKindOfClass:[KBKeyButton class]]) continue;
|
||||
// 当本行没有字符键时,firstChar 可能是一个“特殊键”,
|
||||
// 避免对基准按钮自身添加 self == self * k 的无效约束
|
||||
if (b == firstChar) continue;
|
||||
if (b.key.type == KBKeyTypeCharacter) continue;
|
||||
CGFloat multiplier = 1.5;
|
||||
if (b.key.type == KBKeyTypeSpace) multiplier = 4.0;
|
||||
@@ -293,6 +310,42 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 长按退格:按住时以小间隔逐个删除;松手停止。(不使用 NSTimer/DisplayLink)
|
||||
- (void)onBackspaceLongPress:(UILongPressGestureRecognizer *)gr {
|
||||
switch (gr.state) {
|
||||
case UIGestureRecognizerStateBegan: {
|
||||
self.backspaceHoldActive = YES;
|
||||
[self kb_backspaceStep];
|
||||
} break;
|
||||
case UIGestureRecognizerStateEnded:
|
||||
case UIGestureRecognizerStateCancelled:
|
||||
case UIGestureRecognizerStateFailed: {
|
||||
self.backspaceHoldActive = NO;
|
||||
} break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Helpers
|
||||
|
||||
// 单步删除并在需要时安排下一次,直到松手或无内容
|
||||
- (void)kb_backspaceStep {
|
||||
if (!self.backspaceHoldActive) { return; }
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
if (!ivc) { self.backspaceHoldActive = NO; return; }
|
||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
||||
if (before.length <= 0) { self.backspaceHoldActive = NO; return; }
|
||||
[proxy deleteBackward]; // 每次仅删 1 个
|
||||
|
||||
// 轻量递归调度下一次;不使用 NSTimer,避免复杂状态管理
|
||||
__weak typeof(self) weakSelf = self;
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.06 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
__strong typeof(weakSelf) selfStrong = weakSelf;
|
||||
[selfStrong kb_backspaceStep];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Lazy
|
||||
|
||||
- (UIView *)row1 { if (!_row1) _row1 = [UIView new]; return _row1; }
|
||||
|
||||
28
CustomKeyboard/View/KBResponderUtils.h
Normal file
28
CustomKeyboard/View/KBResponderUtils.h
Normal file
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// KBResponderUtils.h
|
||||
// CustomKeyboard
|
||||
//
|
||||
// 统一封装:从任意 UIView/UIResponder 起,向响应链上查找 UIInputViewController。
|
||||
// 作为 header‑only 的工具,便于多处直接引入使用。
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#ifndef KBResponderUtils_h
|
||||
#define KBResponderUtils_h
|
||||
|
||||
/// 从给定 responder 开始,沿响应链查找宿主 UIInputViewController。
|
||||
/// 用法:UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
static inline UIInputViewController *KBFindInputViewController(UIResponder *start) {
|
||||
UIResponder *responder = start;
|
||||
while (responder) {
|
||||
if ([responder isKindOfClass:[UIInputViewController class]]) {
|
||||
return (UIInputViewController *)responder;
|
||||
}
|
||||
responder = responder.nextResponder;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
#endif /* KBResponderUtils_h */
|
||||
|
||||
@@ -6,11 +6,13 @@
|
||||
//
|
||||
|
||||
#import "KBToolBar.h"
|
||||
#import "KBResponderUtils.h" // 查找 UIInputViewController,用于系统切换输入法
|
||||
|
||||
@interface KBToolBar ()
|
||||
@property (nonatomic, strong) UIView *leftContainer;
|
||||
@property (nonatomic, strong) NSArray<UIButton *> *leftButtonsInternal;
|
||||
@property (nonatomic, strong) UIButton *settingsButtonInternal;
|
||||
@property (nonatomic, strong) UIButton *globeButtonInternal; // 可选:系统“切换输入法”键
|
||||
@end
|
||||
|
||||
@implementation KBToolBar
|
||||
@@ -50,6 +52,7 @@
|
||||
- (void)setupUI {
|
||||
[self addSubview:self.leftContainer];
|
||||
[self addSubview:self.settingsButtonInternal];
|
||||
[self addSubview:self.globeButtonInternal];
|
||||
|
||||
// 右侧设置按钮
|
||||
[self.settingsButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
@@ -58,9 +61,16 @@
|
||||
make.width.height.mas_equalTo(32);
|
||||
}];
|
||||
|
||||
// 左侧地球键(按需显示)
|
||||
[self.globeButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.mas_left).offset(12);
|
||||
make.centerY.equalTo(self.mas_centerY);
|
||||
make.width.height.mas_equalTo(32);
|
||||
}];
|
||||
|
||||
// 左侧容器占用剩余空间
|
||||
[self.leftContainer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.mas_left).offset(12);
|
||||
make.left.equalTo(self.globeButtonInternal.mas_right).offset(8);
|
||||
make.right.equalTo(self.settingsButtonInternal.mas_left).offset(-12);
|
||||
make.centerY.equalTo(self.mas_centerY);
|
||||
make.height.mas_equalTo(32);
|
||||
@@ -89,6 +99,9 @@
|
||||
make.right.equalTo(self.leftContainer.mas_right);
|
||||
}];
|
||||
self.leftButtonsInternal = buttons.copy;
|
||||
|
||||
// 初始刷新地球键的可见性与事件绑定
|
||||
[self kb_refreshGlobeVisibility];
|
||||
}
|
||||
|
||||
- (UIButton *)buildActionButtonAtIndex:(NSInteger)idx {
|
||||
@@ -142,4 +155,59 @@
|
||||
return _settingsButtonInternal;
|
||||
}
|
||||
|
||||
- (UIButton *)globeButtonInternal {
|
||||
if (!_globeButtonInternal) {
|
||||
_globeButtonInternal = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
_globeButtonInternal.layer.cornerRadius = 16;
|
||||
_globeButtonInternal.layer.masksToBounds = YES;
|
||||
_globeButtonInternal.backgroundColor = [UIColor colorWithWhite:1 alpha:0.9];
|
||||
[_globeButtonInternal setTitle:@"🌐" forState:UIControlStateNormal];
|
||||
[_globeButtonInternal setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
|
||||
}
|
||||
return _globeButtonInternal;
|
||||
}
|
||||
|
||||
#pragma mark - Globe (Input Mode Switch)
|
||||
|
||||
// 根据宿主是否已提供系统切换键,决定是否显示地球按钮;并绑定系统事件。
|
||||
- (void)kb_refreshGlobeVisibility {
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
BOOL needSwitchKey = YES;
|
||||
if (ivc && [ivc respondsToSelector:@selector(needsInputModeSwitchKey)]) {
|
||||
needSwitchKey = ivc.needsInputModeSwitchKey; // YES 表示自定义键盘需要提供切换键
|
||||
}
|
||||
|
||||
self.globeButtonInternal.hidden = !needSwitchKey;
|
||||
|
||||
// 重新调整 leftContainer 的左约束:若不需要地球键,则贴左边距 12
|
||||
[self.leftContainer mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
if (needSwitchKey) {
|
||||
make.left.equalTo(self.globeButtonInternal.mas_right).offset(8);
|
||||
} else {
|
||||
make.left.equalTo(self.mas_left).offset(12);
|
||||
}
|
||||
make.right.equalTo(self.settingsButtonInternal.mas_left).offset(-12);
|
||||
make.centerY.equalTo(self.mas_centerY);
|
||||
make.height.mas_equalTo(32);
|
||||
}];
|
||||
|
||||
// 绑定系统提供的输入法切换处理(点按切换、长按弹出列表)
|
||||
// 仅在需要时绑定,避免多余的事件转发
|
||||
[self.globeButtonInternal removeTarget:nil action:NULL forControlEvents:UIControlEventAllEvents];
|
||||
if (needSwitchKey && ivc) {
|
||||
SEL sel = NSSelectorFromString(@"handleInputModeListFromView:withEvent:");
|
||||
if ([ivc respondsToSelector:sel]) {
|
||||
[self.globeButtonInternal addTarget:ivc action:sel forControlEvents:UIControlEventAllTouchEvents];
|
||||
} else {
|
||||
// 回退:至少在点按时切换
|
||||
[self.globeButtonInternal addTarget:ivc action:@selector(advanceToNextInputMode) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)didMoveToWindow {
|
||||
[super didMoveToWindow];
|
||||
[self kb_refreshGlobeVisibility];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
// Universal Links 通用链接
|
||||
#ifndef KB_UL_BASE
|
||||
#define KB_UL_BASE @"https://your.domain/ul"
|
||||
#define KB_UL_BASE @"https://app.tknb.net/ul"
|
||||
#endif
|
||||
|
||||
#define KB_UL_LOGIN KB_UL_BASE @"/login"
|
||||
@@ -37,6 +37,14 @@
|
||||
#define KB_KEYBOARD_EXTENSION_BUNDLE_ID @"com.loveKey.nyx.CustomKeyboard"
|
||||
#endif
|
||||
|
||||
// --- 应用自定义 Scheme ---
|
||||
// 主 App 在 Info.plist 中注册的 URL Scheme,用于从键盘扩展唤起容器 App。
|
||||
// 注意:AppDelegate 中对 scheme 做了小写化比较(kbkeyboardappextension),iOS 对大小写不敏感;
|
||||
// 这里统一通过宏引用,避免出现与 App 端不一致的字符串。
|
||||
#ifndef KB_APP_SCHEME
|
||||
#define KB_APP_SCHEME @"kbkeyboardAppExtension"
|
||||
#endif
|
||||
|
||||
// --- 常用宏 ---
|
||||
// 弱引用 self(在 block 中避免循环引用):使用处直接写 KBWeakSelf;
|
||||
#ifndef KBWeakSelf
|
||||
|
||||
58
Shared/KBSkinManager.h
Normal file
58
Shared/KBSkinManager.h
Normal file
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// KBSkinManager.h
|
||||
// App & Keyboard Extension shared skin/theme manager.
|
||||
//
|
||||
// Stores a lightweight theme (colors, identifiers) to shared keychain so
|
||||
// both targets see the same current skin. Cross-process updates are delivered
|
||||
// via Darwin notification. Intended for immediate reflection in extension.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern NSString * const KBSkinDidChangeNotification; // in-process
|
||||
extern NSString * const KBDarwinSkinChanged; // cross-process
|
||||
|
||||
/// Simple theme model (colors only; assets can be added later via App Group)
|
||||
@interface KBSkinTheme : NSObject <NSSecureCoding>
|
||||
@property (nonatomic, copy) NSString *skinId; // e.g. "mint"
|
||||
@property (nonatomic, copy) NSString *name; // display name
|
||||
@property (nonatomic, strong) UIColor *keyboardBackground;
|
||||
@property (nonatomic, strong) UIColor *keyBackground;
|
||||
@property (nonatomic, strong) UIColor *keyTextColor;
|
||||
@property (nonatomic, strong) UIColor *keyHighlightBackground; // selected/highlighted
|
||||
@property (nonatomic, strong) UIColor *accentColor; // function view accents
|
||||
/// 可选:键盘背景图片的 PNG/JPEG 数据(若存在,优先显示图片)
|
||||
@property (nonatomic, strong, nullable) NSData *backgroundImageData;
|
||||
@end
|
||||
|
||||
/// Shared skin manager (Keychain Sharing based)
|
||||
@interface KBSkinManager : NSObject
|
||||
|
||||
+ (instancetype)shared;
|
||||
|
||||
@property (atomic, strong, readonly) KBSkinTheme *current; // never nil (fallback to default)
|
||||
|
||||
/// Save theme from JSON dictionary (keys: id, name, background, key_bg, key_text, key_highlight, accent)
|
||||
- (BOOL)applyThemeFromJSON:(NSDictionary *)json;
|
||||
|
||||
/// Save explicit theme
|
||||
- (BOOL)applyTheme:(KBSkinTheme *)theme;
|
||||
|
||||
/// Reset to default theme
|
||||
- (void)resetToDefault;
|
||||
|
||||
/// 直接应用图片皮肤(使用 JPEG/PNG 数据)。建议大小 < 512KB。
|
||||
- (BOOL)applyImageSkinWithData:(NSData *)imageData skinId:(NSString *)skinId name:(NSString *)name;
|
||||
|
||||
/// 当前背景图片(若存在)
|
||||
- (nullable UIImage *)currentBackgroundImage;
|
||||
|
||||
/// Parse a hex color string like "#RRGGBB"/"#RRGGBBAA"
|
||||
+ (UIColor *)colorFromHexString:(NSString *)hex defaultColor:(UIColor *)fallback;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
214
Shared/KBSkinManager.m
Normal file
214
Shared/KBSkinManager.m
Normal file
@@ -0,0 +1,214 @@
|
||||
//
|
||||
// KBSkinManager.m
|
||||
//
|
||||
|
||||
#import "KBSkinManager.h"
|
||||
#import <Security/Security.h>
|
||||
#import "KBConfig.h"
|
||||
|
||||
NSString * const KBSkinDidChangeNotification = @"KBSkinDidChangeNotification";
|
||||
NSString * const KBDarwinSkinChanged = @"com.loveKey.nyx.skin.changed";
|
||||
|
||||
static NSString * const kKBSkinService = @"com.loveKey.nyx.skin"; // Keychain service
|
||||
static NSString * const kKBSkinAccount = @"current"; // Keychain account
|
||||
|
||||
@implementation KBSkinTheme
|
||||
|
||||
+ (BOOL)supportsSecureCoding { return YES; }
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)coder {
|
||||
[coder encodeObject:self.skinId forKey:@"skinId"];
|
||||
[coder encodeObject:self.name forKey:@"name"];
|
||||
[coder encodeObject:self.keyboardBackground forKey:@"keyboardBackground"];
|
||||
[coder encodeObject:self.keyBackground forKey:@"keyBackground"];
|
||||
[coder encodeObject:self.keyTextColor forKey:@"keyTextColor"];
|
||||
[coder encodeObject:self.keyHighlightBackground forKey:@"keyHighlightBackground"];
|
||||
[coder encodeObject:self.accentColor forKey:@"accentColor"];
|
||||
if (self.backgroundImageData) {
|
||||
[coder encodeObject:self.backgroundImageData forKey:@"backgroundImageData"];
|
||||
}
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)coder {
|
||||
if (self = [super init]) {
|
||||
_skinId = [coder decodeObjectOfClass:NSString.class forKey:@"skinId"] ?: @"default";
|
||||
_name = [coder decodeObjectOfClass:NSString.class forKey:@"name"] ?: @"Default";
|
||||
_keyboardBackground = [coder decodeObjectOfClass:UIColor.class forKey:@"keyboardBackground"] ?: [UIColor colorWithWhite:0.95 alpha:1.0];
|
||||
_keyBackground = [coder decodeObjectOfClass:UIColor.class forKey:@"keyBackground"] ?: UIColor.whiteColor;
|
||||
_keyTextColor = [coder decodeObjectOfClass:UIColor.class forKey:@"keyTextColor"] ?: UIColor.blackColor;
|
||||
_keyHighlightBackground = [coder decodeObjectOfClass:UIColor.class forKey:@"keyHighlightBackground"] ?: [UIColor colorWithWhite:0.85 alpha:1.0];
|
||||
_accentColor = [coder decodeObjectOfClass:UIColor.class forKey:@"accentColor"] ?: [UIColor colorWithRed:0.77 green:0.93 blue:0.82 alpha:1.0];
|
||||
_backgroundImageData = [coder decodeObjectOfClass:NSData.class forKey:@"backgroundImageData"];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface KBSkinManager ()
|
||||
@property (atomic, strong, readwrite) KBSkinTheme *current;
|
||||
@end
|
||||
|
||||
@implementation KBSkinManager
|
||||
|
||||
+ (instancetype)shared {
|
||||
static KBSkinManager *m; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ m = [KBSkinManager new]; });
|
||||
return m;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
_current = [self p_loadFromKeychain] ?: [self.class defaultTheme];
|
||||
// Observe Darwin notification for cross-process updates
|
||||
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
|
||||
(__bridge const void *)(self),
|
||||
KBSkinDarwinCallback,
|
||||
(__bridge CFStringRef)KBDarwinSkinChanged,
|
||||
NULL,
|
||||
CFNotificationSuspensionBehaviorDeliverImmediately);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo) {
|
||||
KBSkinManager *self = (__bridge KBSkinManager *)observer;
|
||||
[self p_reloadFromKeychainAndBroadcast:YES];
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
CFNotificationCenterRemoveObserver(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge const void *)(self), (__bridge CFStringRef)KBDarwinSkinChanged, NULL);
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (BOOL)applyThemeFromJSON:(NSDictionary *)json {
|
||||
if (json.count == 0) return NO;
|
||||
KBSkinTheme *t = [KBSkinTheme new];
|
||||
t.skinId = [json[@"id"] isKindOfClass:NSString.class] ? json[@"id"] : @"custom";
|
||||
t.name = [json[@"name"] isKindOfClass:NSString.class] ? json[@"name"] : t.skinId;
|
||||
t.keyboardBackground = [self.class colorFromHexString:json[@"background"] defaultColor:[self.class defaultTheme].keyboardBackground];
|
||||
t.keyBackground = [self.class colorFromHexString:json[@"key_bg"] defaultColor:[self.class defaultTheme].keyBackground];
|
||||
t.keyTextColor = [self.class colorFromHexString:json[@"key_text"] defaultColor:[self.class defaultTheme].keyTextColor];
|
||||
t.keyHighlightBackground = [self.class colorFromHexString:json[@"key_highlight"] defaultColor:[self.class defaultTheme].keyHighlightBackground];
|
||||
t.accentColor = [self.class colorFromHexString:json[@"accent"] defaultColor:[self.class defaultTheme].accentColor];
|
||||
return [self applyTheme:t];
|
||||
}
|
||||
|
||||
- (BOOL)applyTheme:(KBSkinTheme *)theme {
|
||||
if (!theme) return NO;
|
||||
if ([self p_saveToKeychain:theme]) {
|
||||
self.current = theme;
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:KBSkinDidChangeNotification object:nil];
|
||||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge CFStringRef)KBDarwinSkinChanged, NULL, NULL, true);
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)resetToDefault {
|
||||
[self applyTheme:[self.class defaultTheme]];
|
||||
}
|
||||
|
||||
- (BOOL)applyImageSkinWithData:(NSData *)imageData skinId:(NSString *)skinId name:(NSString *)name {
|
||||
if (imageData.length == 0) return NO;
|
||||
// 构造新主题,继承当前配色作为按键/强调色的默认值
|
||||
KBSkinTheme *base = self.current ?: [self.class defaultTheme];
|
||||
KBSkinTheme *t = [KBSkinTheme new];
|
||||
t.skinId = skinId ?: @"image";
|
||||
t.name = name ?: t.skinId;
|
||||
t.keyboardBackground = base.keyboardBackground ?: [self.class defaultTheme].keyboardBackground;
|
||||
t.keyBackground = base.keyBackground ?: [self.class defaultTheme].keyBackground;
|
||||
t.keyTextColor = base.keyTextColor ?: [self.class defaultTheme].keyTextColor;
|
||||
t.keyHighlightBackground = base.keyHighlightBackground ?: [self.class defaultTheme].keyHighlightBackground;
|
||||
t.accentColor = base.accentColor ?: [self.class defaultTheme].accentColor;
|
||||
t.backgroundImageData = imageData;
|
||||
return [self applyTheme:t];
|
||||
}
|
||||
|
||||
- (UIImage *)currentBackgroundImage {
|
||||
NSData *d = self.current.backgroundImageData;
|
||||
if (d.length == 0) return nil;
|
||||
return [UIImage imageWithData:d scale:[UIScreen mainScreen].scale] ?: nil;
|
||||
}
|
||||
|
||||
+ (UIColor *)colorFromHexString:(NSString *)hex defaultColor:(UIColor *)fallback {
|
||||
if (![hex isKindOfClass:NSString.class] || hex.length == 0) return fallback;
|
||||
NSString *s = [[hex stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] lowercaseString];
|
||||
if ([s hasPrefix:@"#"]) s = [s substringFromIndex:1];
|
||||
unsigned long long v = 0; NSScanner *scanner = [NSScanner scannerWithString:s];
|
||||
if (![scanner scanHexLongLong:&v]) return fallback;
|
||||
if (s.length == 6) { // RRGGBB
|
||||
CGFloat r = ((v >> 16) & 0xFF) / 255.0;
|
||||
CGFloat g = ((v >> 8) & 0xFF) / 255.0;
|
||||
CGFloat b = (v & 0xFF) / 255.0;
|
||||
return [UIColor colorWithRed:r green:g blue:b alpha:1.0];
|
||||
} else if (s.length == 8) { // RRGGBBAA
|
||||
CGFloat r = ((v >> 24) & 0xFF) / 255.0;
|
||||
CGFloat g = ((v >> 16) & 0xFF) / 255.0;
|
||||
CGFloat b = ((v >> 8) & 0xFF) / 255.0;
|
||||
CGFloat a = (v & 0xFF) / 255.0;
|
||||
return [UIColor colorWithRed:r green:g blue:b alpha:a];
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
#pragma mark - Defaults
|
||||
|
||||
+ (KBSkinTheme *)defaultTheme {
|
||||
KBSkinTheme *t = [KBSkinTheme new];
|
||||
t.skinId = @"default";
|
||||
t.name = @"Default";
|
||||
t.keyboardBackground = [UIColor colorWithWhite:0.95 alpha:1.0];
|
||||
t.keyBackground = UIColor.whiteColor;
|
||||
t.keyTextColor = UIColor.blackColor;
|
||||
t.keyHighlightBackground = [UIColor colorWithWhite:0.85 alpha:1.0];
|
||||
t.accentColor = [UIColor colorWithRed:0.77 green:0.93 blue:0.82 alpha:1.0];
|
||||
t.backgroundImageData = nil;
|
||||
return t;
|
||||
}
|
||||
|
||||
#pragma mark - Keychain
|
||||
|
||||
- (NSMutableDictionary *)baseKCQuery {
|
||||
NSMutableDictionary *q = [@{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
|
||||
(__bridge id)kSecAttrService: kKBSkinService,
|
||||
(__bridge id)kSecAttrAccount: kKBSkinAccount } mutableCopy];
|
||||
q[(__bridge id)kSecAttrAccessGroup] = KB_KEYCHAIN_ACCESS_GROUP;
|
||||
return q;
|
||||
}
|
||||
|
||||
- (BOOL)p_saveToKeychain:(KBSkinTheme *)theme {
|
||||
NSError *err = nil;
|
||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:theme requiringSecureCoding:YES error:&err];
|
||||
if (err || data.length == 0) return NO;
|
||||
NSMutableDictionary *q = [self baseKCQuery];
|
||||
SecItemDelete((__bridge CFDictionaryRef)q);
|
||||
q[(__bridge id)kSecValueData] = data;
|
||||
q[(__bridge id)kSecAttrAccessible] = (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
|
||||
OSStatus st = SecItemAdd((__bridge CFDictionaryRef)q, NULL);
|
||||
return (st == errSecSuccess);
|
||||
}
|
||||
|
||||
- (KBSkinTheme *)p_loadFromKeychain {
|
||||
NSMutableDictionary *q = [self baseKCQuery];
|
||||
q[(__bridge id)kSecReturnData] = @YES;
|
||||
q[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
|
||||
CFTypeRef dataRef = NULL; OSStatus st = SecItemCopyMatching((__bridge CFDictionaryRef)q, &dataRef);
|
||||
if (st != errSecSuccess || !dataRef) return nil;
|
||||
NSData *data = (__bridge_transfer NSData *)dataRef;
|
||||
if (data.length == 0) return nil;
|
||||
@try {
|
||||
KBSkinTheme *t = [NSKeyedUnarchiver unarchivedObjectOfClass:KBSkinTheme.class fromData:data error:NULL];
|
||||
return t;
|
||||
} @catch (__unused NSException *e) { return nil; }
|
||||
}
|
||||
|
||||
- (void)p_reloadFromKeychainAndBroadcast:(BOOL)broadcast {
|
||||
KBSkinTheme *t = [self p_loadFromKeychain] ?: [self.class defaultTheme];
|
||||
self.current = t;
|
||||
if (broadcast) {
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:KBSkinDidChangeNotification object:nil];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -8,6 +8,10 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
043FBCD22EAF97630036AFE1 /* KBPermissionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 04C6EAE12EAF940F0089C901 /* KBPermissionViewController.m */; };
|
||||
0459D1B42EBA284C00F2D189 /* KBSkinCenterVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B32EBA284C00F2D189 /* KBSkinCenterVC.m */; };
|
||||
0459D1B72EBA287900F2D189 /* KBSkinManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B62EBA287900F2D189 /* KBSkinManager.m */; };
|
||||
0459D1B82EBA287900F2D189 /* KBSkinManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B62EBA287900F2D189 /* KBSkinManager.m */; };
|
||||
0477BD952EBAFF4E0055D639 /* KBURLOpenBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 0477BD932EBAFF4E0055D639 /* KBURLOpenBridge.m */; };
|
||||
04A9FE0F2EB481100020DB6D /* KBHUD.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC97082EB31B14007BD342 /* KBHUD.m */; };
|
||||
04A9FE132EB4D0D20020DB6D /* KBFullAccessManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 04A9FE112EB4D0D20020DB6D /* KBFullAccessManager.m */; };
|
||||
04A9FE162EB873C80020DB6D /* UIViewController+Extension.m in Sources */ = {isa = PBXBuildFile; fileRef = 04A9FE152EB873C80020DB6D /* UIViewController+Extension.m */; };
|
||||
@@ -87,6 +91,13 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
0459D1B22EBA284C00F2D189 /* KBSkinCenterVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSkinCenterVC.h; sourceTree = "<group>"; };
|
||||
0459D1B32EBA284C00F2D189 /* KBSkinCenterVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSkinCenterVC.m; sourceTree = "<group>"; };
|
||||
0459D1B52EBA287900F2D189 /* KBSkinManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSkinManager.h; sourceTree = "<group>"; };
|
||||
0459D1B62EBA287900F2D189 /* KBSkinManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSkinManager.m; sourceTree = "<group>"; };
|
||||
0477BD922EBAFF4E0055D639 /* KBURLOpenBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBURLOpenBridge.h; sourceTree = "<group>"; };
|
||||
0477BD932EBAFF4E0055D639 /* KBURLOpenBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBURLOpenBridge.m; sourceTree = "<group>"; };
|
||||
04A9A67D2EB9E1690023B8F4 /* KBResponderUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBResponderUtils.h; sourceTree = "<group>"; };
|
||||
04A9FE102EB4D0D20020DB6D /* KBFullAccessManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFullAccessManager.h; sourceTree = "<group>"; };
|
||||
04A9FE112EB4D0D20020DB6D /* KBFullAccessManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFullAccessManager.m; sourceTree = "<group>"; };
|
||||
04A9FE142EB873C80020DB6D /* UIViewController+Extension.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIViewController+Extension.h"; sourceTree = "<group>"; };
|
||||
@@ -209,6 +220,15 @@
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
0477BD942EBAFF4E0055D639 /* Utils */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0477BD922EBAFF4E0055D639 /* KBURLOpenBridge.h */,
|
||||
0477BD932EBAFF4E0055D639 /* KBURLOpenBridge.m */,
|
||||
);
|
||||
path = Utils;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
04A9FE122EB4D0D20020DB6D /* Manager */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -249,6 +269,7 @@
|
||||
04C6EAD72EAF870B0089C901 /* CustomKeyboard */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0477BD942EBAFF4E0055D639 /* Utils */,
|
||||
04A9FE122EB4D0D20020DB6D /* Manager */,
|
||||
04FC95662EB0546C007BD342 /* Model */,
|
||||
04C6EADA2EAF8C7B0089C901 /* View */,
|
||||
@@ -266,6 +287,7 @@
|
||||
children = (
|
||||
04C6EADB2EAF8CEB0089C901 /* KBToolBar.h */,
|
||||
04C6EADC2EAF8CEB0089C901 /* KBToolBar.m */,
|
||||
04A9A67D2EB9E1690023B8F4 /* KBResponderUtils.h */,
|
||||
04FC95682EB05497007BD342 /* KBKeyButton.h */,
|
||||
04FC95692EB05497007BD342 /* KBKeyButton.m */,
|
||||
04FC956B2EB054B7007BD342 /* KBKeyboardView.h */,
|
||||
@@ -327,6 +349,8 @@
|
||||
04FC95CE2EB1E7A1007BD342 /* HomeVC.m */,
|
||||
A1B2D7002EB8C00100000001 /* KBLangTestVC.h */,
|
||||
A1B2D7012EB8C00100000001 /* KBLangTestVC.m */,
|
||||
0459D1B22EBA284C00F2D189 /* KBSkinCenterVC.h */,
|
||||
0459D1B32EBA284C00F2D189 /* KBSkinCenterVC.m */,
|
||||
);
|
||||
path = VC;
|
||||
sourceTree = "<group>";
|
||||
@@ -580,6 +604,8 @@
|
||||
A1B2C4222EB4B7A100000001 /* KBKeyboardPermissionManager.m */,
|
||||
04A9FE182EB892460020DB6D /* KBLocalizationManager.h */,
|
||||
04A9FE192EB892460020DB6D /* KBLocalizationManager.m */,
|
||||
0459D1B52EBA287900F2D189 /* KBSkinManager.h */,
|
||||
0459D1B62EBA287900F2D189 /* KBSkinManager.m */,
|
||||
);
|
||||
path = Shared;
|
||||
sourceTree = "<group>";
|
||||
@@ -815,6 +841,7 @@
|
||||
04FC95792EB09BC8007BD342 /* KBKeyBoardMainView.m in Sources */,
|
||||
04FC95732EB09570007BD342 /* KBFunctionBarView.m in Sources */,
|
||||
04C6EAD82EAF870B0089C901 /* KeyboardViewController.m in Sources */,
|
||||
0459D1B82EBA287900F2D189 /* KBSkinManager.m in Sources */,
|
||||
04FC95762EB095DE007BD342 /* KBFunctionPasteView.m in Sources */,
|
||||
A1B2C3D42EB0A0A100000001 /* KBFunctionTagCell.m in Sources */,
|
||||
04A9FE1A2EB892460020DB6D /* KBLocalizationManager.m in Sources */,
|
||||
@@ -828,6 +855,7 @@
|
||||
A1B2C4002EB4A0A100000003 /* KBAuthManager.m in Sources */,
|
||||
04A9FE132EB4D0D20020DB6D /* KBFullAccessManager.m in Sources */,
|
||||
A1B2C4202EB4B7A100000001 /* KBKeyboardPermissionManager.m in Sources */,
|
||||
0477BD952EBAFF4E0055D639 /* KBURLOpenBridge.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -858,10 +886,12 @@
|
||||
A1B2D7022EB8C00100000001 /* KBLangTestVC.m in Sources */,
|
||||
04C6EABF2EAF86530089C901 /* main.m in Sources */,
|
||||
04FC95CC2EB1E780007BD342 /* BaseTabBarController.m in Sources */,
|
||||
0459D1B72EBA287900F2D189 /* KBSkinManager.m in Sources */,
|
||||
04FC95F42EB339C1007BD342 /* AppleSignInManager.m in Sources */,
|
||||
04C6EAC12EAF86530089C901 /* ViewController.m in Sources */,
|
||||
A1B2C4002EB4A0A100000004 /* KBAuthManager.m in Sources */,
|
||||
A1B2C4212EB4B7A100000001 /* KBKeyboardPermissionManager.m in Sources */,
|
||||
0459D1B42EBA284C00F2D189 /* KBSkinCenterVC.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -989,7 +1019,7 @@
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = keyBoard/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "YOLO输入法";
|
||||
INFOPLIST_KEY_CFBundleURLTypes = "{\n CFBundleURLName = \"com.loveKey.nyx.keyboard\";\n CFBundleURLSchemes = (\n kbkeyboard\n );\n}";
|
||||
INFOPLIST_KEY_CFBundleURLTypes = "{\n CFBundleURLName = \"com.loveKey.nyx.keyboard\";\n CFBundleURLSchemes = (\n kbkeyboardAppExtension\n );\n}";
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
||||
@@ -1028,7 +1058,7 @@
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = keyBoard/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "YOLO输入法";
|
||||
INFOPLIST_KEY_CFBundleURLTypes = "{\n CFBundleURLName = \"com.loveKey.nyx.keyboard\";\n CFBundleURLSchemes = (\n kbkeyboard\n );\n}";
|
||||
INFOPLIST_KEY_CFBundleURLTypes = "{\n CFBundleURLName = \"com.loveKey.nyx.keyboard\";\n CFBundleURLSchemes = (\n kbkeyboardAppExtension\n );\n}";
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "2600"
|
||||
version = "1.7">
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
@@ -27,8 +27,6 @@
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
@@ -38,18 +36,20 @@
|
||||
ReferencedContainer = "container:keyBoard.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
enableGPUFrameCaptureMode = "3"
|
||||
enableGPUValidationMode = "1"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUFrameCaptureMode = "3"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
@@ -87,4 +87,3 @@
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
||||
|
||||
@@ -61,10 +61,25 @@ static NSString * const kKBKeyboardExtensionBundleId = @"com.loveKey.nyx.CustomK
|
||||
|
||||
#pragma mark - Deep Link
|
||||
|
||||
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler {
|
||||
if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
|
||||
NSURL *url = userActivity.webpageURL;
|
||||
if (!url) return NO;
|
||||
NSString *host = url.host.lowercaseString ?: @"";
|
||||
if ([host hasSuffix:@"app.tknb.net"]) {
|
||||
NSString *path = url.path.lowercaseString ?: @"";
|
||||
if ([path hasPrefix:@"/ul/settings"]) { [self kb_openAppSettings]; return YES; }
|
||||
if ([path hasPrefix:@"/ul/login"]) { [self kb_presentLoginSheetIfNeeded]; return YES; }
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
// iOS 9+
|
||||
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
|
||||
if (!url) return NO;
|
||||
if ([[url.scheme lowercaseString] isEqualToString:@"kbkeyboard"]) {
|
||||
// 注意:已对 scheme 做了小写化,比较值也应为小写
|
||||
if ([[url.scheme lowercaseString] isEqualToString:@"kbkeyboardappextension"]) {
|
||||
NSString *urlHost = url.host ?: @"";
|
||||
NSString *host = [urlHost lowercaseString];
|
||||
if ([host isEqualToString:@"login"]) { // kbkeyboard://login
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
self.tableView.tableHeaderView = header;
|
||||
[self.view addSubview:self.tableView];
|
||||
|
||||
self.items = @[ KBLocalized(@"home_item_lang_test"), KBLocalized(@"home_item_keyboard_permission") ];
|
||||
self.items = @[ KBLocalized(@"home_item_lang_test"), KBLocalized(@"home_item_keyboard_permission"), @"皮肤中心" ];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{ [self.textView becomeFirstResponder]; });
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
- (void)viewWillAppear:(BOOL)animated{
|
||||
[super viewWillAppear:animated];
|
||||
self.title = KBLocalized(@"home_title");
|
||||
self.items = @[ KBLocalized(@"home_item_lang_test"), KBLocalized(@"home_item_keyboard_permission") ];
|
||||
self.items = @[ KBLocalized(@"home_item_lang_test"), KBLocalized(@"home_item_keyboard_permission"), @"皮肤中心" ];
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
@@ -85,6 +85,10 @@
|
||||
// 键盘权限引导页
|
||||
KBGuideVC *vc = [KBGuideVC new];
|
||||
[self.navigationController pushViewController:vc animated:YES];
|
||||
} else if (indexPath.row == 2) {
|
||||
// 皮肤中心
|
||||
UIViewController *vc = [NSClassFromString(@"KBSkinCenterVC") new];
|
||||
if (vc) { [self.navigationController pushViewController:vc animated:YES]; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
11
keyBoard/Class/Home/VC/KBSkinCenterVC.h
Normal file
11
keyBoard/Class/Home/VC/KBSkinCenterVC.h
Normal file
@@ -0,0 +1,11 @@
|
||||
//
|
||||
// KBSkinCenterVC.h
|
||||
// 简单的皮肤中心:展示两款示例皮肤,点击“下载并应用”后立即同步到键盘扩展。
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface KBSkinCenterVC : UIViewController
|
||||
|
||||
@end
|
||||
|
||||
132
keyBoard/Class/Home/VC/KBSkinCenterVC.m
Normal file
132
keyBoard/Class/Home/VC/KBSkinCenterVC.m
Normal file
@@ -0,0 +1,132 @@
|
||||
//
|
||||
// KBSkinCenterVC.m
|
||||
//
|
||||
|
||||
#import "KBSkinCenterVC.h"
|
||||
#import "Masonry.h"
|
||||
#import "KBNetworkManager.h"
|
||||
#import "KBSkinManager.h"
|
||||
#import "KBHUD.h"
|
||||
|
||||
@interface KBSkinCell : UITableViewCell
|
||||
@property (nonatomic, strong) UIButton *applyBtn;
|
||||
@end
|
||||
|
||||
@implementation KBSkinCell
|
||||
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
|
||||
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
|
||||
self.selectionStyle = UITableViewCellSelectionStyleNone;
|
||||
_applyBtn = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
[_applyBtn setTitle:@"下载并应用" forState:UIControlStateNormal];
|
||||
_applyBtn.layer.cornerRadius = 6; _applyBtn.layer.borderWidth = 1;
|
||||
_applyBtn.layer.borderColor = [UIColor colorWithWhite:0.85 alpha:1].CGColor;
|
||||
[self.contentView addSubview:_applyBtn];
|
||||
[_applyBtn mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.right.equalTo(self.contentView).offset(-16);
|
||||
make.centerY.equalTo(self.contentView);
|
||||
make.width.mas_equalTo(110);
|
||||
make.height.mas_equalTo(34);
|
||||
}];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@end
|
||||
|
||||
@interface KBSkinCenterVC () <UITableViewDelegate, UITableViewDataSource>
|
||||
@property (nonatomic, strong) UITableView *tableView;
|
||||
@property (nonatomic, copy) NSArray<NSDictionary *> *skins; // id, name, img(url relative to KB_BASE_URL)
|
||||
@end
|
||||
|
||||
@implementation KBSkinCenterVC
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.title = @"皮肤中心";
|
||||
self.view.backgroundColor = [UIColor whiteColor];
|
||||
// 绝对 URL 的测试皮肤图片(无需 KB_BASE_URL)。
|
||||
// 说明:使用 picsum.photos 的固定 id,稳定可直接访问。
|
||||
self.skins = @[
|
||||
@{ @"id": @"aurora", @"name": @"极光", @"img": @"https://picsum.photos/id/1018/1600/900.jpg" },
|
||||
@{ @"id": @"alps", @"name": @"雪山", @"img": @"https://picsum.photos/id/1016/1600/900.jpg" },
|
||||
@{ @"id": @"lake", @"name": @"湖面", @"img": @"https://picsum.photos/id/1039/1600/900.jpg" },
|
||||
];
|
||||
|
||||
self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStyleInsetGrouped];
|
||||
self.tableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
self.tableView.delegate = self; self.tableView.dataSource = self;
|
||||
[self.view addSubview:self.tableView];
|
||||
}
|
||||
|
||||
#pragma mark - UITableView
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; }
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.skins.count; }
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
static NSString *cid = @"skin.cell";
|
||||
KBSkinCell *cell = [tableView dequeueReusableCellWithIdentifier:cid];
|
||||
if (!cell) { cell = [[KBSkinCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cid]; }
|
||||
NSDictionary *skin = self.skins[indexPath.row];
|
||||
cell.textLabel.text = skin[@"name"]; cell.detailTextLabel.text = skin[@"id"];
|
||||
[cell.applyBtn removeTarget:nil action:NULL forControlEvents:UIControlEventTouchUpInside];
|
||||
[cell.applyBtn addTarget:self action:@selector(onApplyBtn:) forControlEvents:UIControlEventTouchUpInside];
|
||||
cell.applyBtn.tag = indexPath.row;
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (void)onApplyBtn:(UIButton *)sender {
|
||||
NSInteger idx = sender.tag;
|
||||
if (idx < 0 || idx >= self.skins.count) return;
|
||||
NSDictionary *skin = self.skins[idx];
|
||||
NSString *path = skin[@"img"] ?: @""; // 相对 KB_BASE_URL
|
||||
|
||||
// 下载图片数据(非 JSON 将以 NSData 返回)
|
||||
[[KBNetworkManager shared] GET:path parameters:nil headers:nil completion:^(id jsonOrData, NSURLResponse *response, NSError *error) {
|
||||
NSData *data = ([jsonOrData isKindOfClass:NSData.class] ? (NSData *)jsonOrData : nil);
|
||||
// 尝试压缩尺寸,避免 Keychain 过大:将宽度限制到 1500px
|
||||
if (data && data.length > 0) {
|
||||
UIImage *img = [UIImage imageWithData:data];
|
||||
if (img) {
|
||||
CGFloat maxW = 1500.0;
|
||||
if (img.size.width > maxW) {
|
||||
CGFloat scale = maxW / img.size.width;
|
||||
CGSize newSize = CGSizeMake(maxW, floor(img.size.height * scale));
|
||||
UIGraphicsBeginImageContextWithOptions(newSize, YES, 1.0);
|
||||
[img drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
|
||||
UIImage *resized = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
img = resized ?: img;
|
||||
}
|
||||
data = UIImageJPEGRepresentation(img, 0.85) ?: data; // 压成 JPEG
|
||||
}
|
||||
}
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSData *payload = data;
|
||||
if (payload.length == 0) {
|
||||
// 兜底:生成一张简单渐变图片
|
||||
CGSize size = CGSizeMake(1200, 600);
|
||||
UIGraphicsBeginImageContextWithOptions(size, YES, 1.0);
|
||||
CGContextRef ctx = UIGraphicsGetCurrentContext();
|
||||
UIColor *c1 = [UIColor colorWithRed:0.76 green:0.91 blue:0.86 alpha:1];
|
||||
UIColor *c2 = [UIColor colorWithRed:0.93 green:0.97 blue:0.91 alpha:1];
|
||||
if ([skin[@"id"] hasPrefix:@"dark"]) {
|
||||
c1 = [UIColor colorWithRed:0.1 green:0.12 blue:0.16 alpha:1];
|
||||
c2 = [UIColor colorWithRed:0.22 green:0.24 blue:0.28 alpha:1];
|
||||
}
|
||||
CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB();
|
||||
NSArray *colors = @[(__bridge id)c1.CGColor, (__bridge id)c2.CGColor];
|
||||
CGFloat locs[] = {0,1};
|
||||
CGGradientRef grad = CGGradientCreateWithColors(space, (__bridge CFArrayRef)colors, locs);
|
||||
CGContextDrawLinearGradient(ctx, grad, CGPointZero, CGPointMake(size.width, size.height), 0);
|
||||
CGGradientRelease(grad); CGColorSpaceRelease(space);
|
||||
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
payload = UIImageJPEGRepresentation(img, 0.9);
|
||||
}
|
||||
BOOL ok = (payload.length > 0) ? [[KBSkinManager shared] applyImageSkinWithData:payload skinId:skin[@"id"] name:skin[@"name"]] : NO;
|
||||
[KBHUD showInfo:(ok ? @"已应用,切到键盘查看" : @"应用失败")];
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -5,11 +5,13 @@
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.loveKey.nyx.keyboard</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>kbkeyboard</string>
|
||||
<string>kbkeyboardAppExtension</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
//-----------------------------------------------宏定义全局----------------------------------------------------------/
|
||||
// 通用链接(Universal Links)统一配置
|
||||
// 仅需修改这里的域名/前缀,工程内所有使用 UL 的地方都会同步。
|
||||
#define KB_UL_BASE @"https://your.domain/ul" // 替换为你的真实域名与前缀路径
|
||||
#define KB_UL_BASE @"https://app.tknb.net/ul" // 与 Associated Domains 一致
|
||||
#define KB_UL_LOGIN KB_UL_BASE @"/login"
|
||||
#define KB_UL_SETTINGS KB_UL_BASE @"/settings"
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
<array>
|
||||
<string>Default</string>
|
||||
</array>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>applinks:app.tknb.net</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)com.loveKey.nyx.shared</string>
|
||||
|
||||
Reference in New Issue
Block a user