s1ntoneli / blug Goto Github PK
View Code? Open in Web Editor NEW👀 Issues 更新独家珍藏技术内容 👀 忽略项目本身内容。
👀 Issues 更新独家珍藏技术内容 👀 忽略项目本身内容。
page
目前的测试发现是两步:
使用辅助模式检测是否有选中的文字,方法见:#7
如果第一步未检测文字,则模拟系统的复制能力 CMD+C,看能否复制到新文字。检测完,把之前的复制内容存回去。
把原来的内容恢复是为了保证一些读取剪贴板内容的 App 不受影响。
判断依据:
在无法使用辅助模式获取选中文字的 App 中,每次做出拖拽选择、双击鼠标左键后,Pasteboard 的 changeCount 都会改变。而在辅助模式可以获取文字的 App 中,changeCount 则不会改变。
func performGlobalCopyShortcut() {
func keyEvents(forPressAndReleaseVirtualKey virtualKey: Int) -> [CGEvent] {
let eventSource = CGEventSource(stateID: .hidSystemState)
return [
CGEvent(keyboardEventSource: eventSource, virtualKey: CGKeyCode(virtualKey), keyDown: true)!,
CGEvent(keyboardEventSource: eventSource, virtualKey: CGKeyCode(virtualKey), keyDown: false)!,
]
}
let tapLocation = CGEventTapLocation.cghidEventTap
let events = keyEvents(forPressAndReleaseVirtualKey: 8)
events.forEach {
$0.flags = .maskCommand
$0.post(tap: tapLocation)
}
}
不想折腾的办法是:
服务端加上 response header:
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS, POST, PUT, DELETE",
"Access-Control-Max-Age": "86400"
}
注意 axios 请求时会先发送 OPTIONS 预请求,以确认是否支持该请求。所以也要对 OPTIONS 请求也返回正确的应答。
利用辅助模式,无需使用剪贴板。
但是在 obsidian 类型的非 native app 中不一定好使。
func getSelectedText() -> String? {
let systemWideElement = AXUIElementCreateSystemWide()
var selectedTextValue: AnyObject?
let errorCode = AXUIElementCopyAttributeValue(systemWideElement, kAXFocusedUIElementAttribute as CFString, &selectedTextValue)
if errorCode == .success {
let selectedTextElement = selectedTextValue as! AXUIElement
var selectedText: AnyObject?
let textErrorCode = AXUIElementCopyAttributeValue(selectedTextElement, kAXSelectedTextAttribute as CFString, &selectedText)
if textErrorCode == .success, let selectedTextString = selectedText as? String {
return selectedTextString
} else {
return nil
}
} else {
return nil
}
}
由于各个国家、地区购买力不一,出海产品需要为不同购买力的地区设定不同的价格。
实现购买力平价需要两个数据:
逻辑就很简单了:
位置数据 -> 购买力水平 -> 匹配相应价格信息并应用
那么这两个数据怎么获取呢?
Cloudflare Worker 的 request 参数带了非常详细的位置数据,这让我们不需要再使用第三方服务就能方便地获取位置:
它包括了经纬度、地区代码等,我们的颗粒度精确到国家,这里用国家代码 country 字段。
购买力水平等级可以在这个 gist 下载:各国家购买力水平清单
大致逻辑是:
我在 CleanClip(Mac 上的剪贴板工具) 中简单起见,直接为不同国家应用不同的折扣。
LemonSqueezy 可以这样直接应用折扣码:PRODUCT_URL + "?checkout%5Bdiscount_code%5D=" + discountCode
一些细节:
import ppp from "./pppdata.js";
const flatppp = ppp.flatMap(category => category.countries.map( countryInfo => {
return {
range: category.range,
countryCode: countryInfo.country,
countryName: countryInfo.countryName
}
}))
// 在购买力水平列表中找到对应国家
function findCountry(countryCode) {
return flatppp.find(deal => deal.countryCode == countryCode)
}
// 根据购买力水平,在环境变量里找到配置的折扣信息
function getDiscount(env, range) {
switch(range) {
case "0.0-0.1": return { code: env.level0_1 ?? "", discount: parseInt(env.level0_1_discount ?? "0") ?? 0 }
case "0.1-0.2": return { code: env.level1_2 ?? "", discount: parseInt(env.level1_2_discount ?? "0") ?? 0 }
case "0.2-0.3": return { code: env.level2_3 ?? "", discount: parseInt(env.level2_3_discount ?? "0") ?? 0 }
case "0.3-0.4": return { code: env.level3_4 ?? "", discount: parseInt(env.level3_4_discount ?? "0") ?? 0 }
case "0.4-0.5": return { code: env.level4_5 ?? "", discount: parseInt(env.level4_5_discount ?? "0") ?? 0 }
case "0.5-0.6": return { code: env.level5_6 ?? "", discount: parseInt(env.level5_6_discount ?? "0") ?? 0 }
case "0.6-0.7": return { code: env.level6_7 ?? "", discount: parseInt(env.level6_7_discount ?? "0") ?? 0 }
case "0.7-0.8": return { code: env.level7_8 ?? "", discount: parseInt(env.level7_8_discount ?? "0") ?? 0 }
case "0.8-0.9": return { code: env.level8_9 ?? "", discount: parseInt(env.level8_9_discount ?? "0") ?? 0 }
case "0.9-1.0": return { code: env.level9_10 ?? "", discount: parseInt(env.level9_10_discount ?? "0") ?? 0 }
case "1.0-1.1": return { code: env.level10_11 ?? "", discount: parseInt(env.level10_11_discount ?? "0") ?? 0 }
case "1.1-1.2": return { code: env.level11_12 ?? "", discount: parseInt(env.level11_12_discount ?? "0") ?? 0 }
case "1.2-1.3": return { code: env.level12_13 ?? "", discount: parseInt(env.level12_13_discount ?? "0") ?? 0 }
case "1.3-1.4": return { code: env.level13_14 ?? "", discount: parseInt(env.level13_14_discount ?? "0") ?? 0 }
default: return {code: "", discount: 0}
}
}
export default {
async fetch(request, env, ctx) {
// 获取国家编码
const countryCode = request.cf.country
console.log(countryCode)
// 在购买力列表中找到该国家
let countryPPP = findCountry(countryCode)
console.log("find", countryPPP)
// 通过该国家购买力获取对应优惠信息
let discount = getDiscount(env, countryPPP.range)
console.log(discount)
// 返回结果
if (countryPPP && discount) {
let result = JSON.stringify({
range: countryPPP.range,
countryCode: countryPPP.countryCode,
countryName: countryPPP.countryName,
discountCode: discount.code,
discount: discount.discount
});
console.log("result", result);
return new Response(result, {
status: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS, POST, PUT, DELETE",
"Access-Control-Max-Age": "0"
}
});
} else {
return new Response("Error", { status: 500, headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS, POST, PUT, DELETE",
"Access-Control-Max-Age": "86400"
}});
}
// 或者直接 301 重定向到指定优惠链接
// let url = env.TARGET_DOMAIN
// if (discountCode !== undefined && discountCode.length > 0) {
// url = env.TARGET_DOMAIN + "?checkout%5Bdiscount_code%5D=" + discountCode
// }
// var response = Response.redirect(url, 301);
},
};
想把自己用到的一些通用方法模块化,结果发现用 Xcode 创建下面这种结构良好的 Swift Package 不是一件易事:
其中有一些障碍:
这两个障碍要求两者既不能在同一目录,Example 也不能在 Swift Package 上级目录???而要发布软件包,Package.swift 又必须在顶层目录。
难道两个人真的不能在一起吗?
研究了一番,发现解决办法其实也很简单,只是很多地方没有提到这个做法。
解决步骤:
Cheers!
NSView 重载 acceptsFirstMouse 方法,始终返回 true
。
SwiftUI 的视图里 overlay 添加这个重载的 View。
import SwiftUI
import Cocoa
// Just mouse accepter
class MyViewView : NSView {
override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
return true
}
}
// Representable wrapper (bridge to SwiftUI)
struct AcceptingFirstMouse : NSViewRepresentable {
func makeNSView(context: NSViewRepresentableContext<AcceptingFirstMouse>) -> MyViewView {
return MyViewView()
}
func updateNSView(_ nsView: MyViewView, context: NSViewRepresentableContext<AcceptingFirstMouse>) {
nsView.setNeedsDisplay(nsView.bounds)
}
typealias NSViewType = MyViewView
}
// Usage (somewhere in your SwiftUI view stack)
Text("Click me")
.padding(20)
.background(Color.yellow)
.overlay(AcceptingFirstMouse()) // must be on top (no confuse, it is transparent)
.onTapGesture {
print("Label tapped")
}
当窗口是关键窗口时,该值设置为 .key ;当窗口失去焦点并且我们激活另一个窗口时,该值设置为 .inactive 。
@Environment(\.controlActiveState) var controlActiveState
page
Cocoa 框架的文档。
感觉越是古老的文档,信息量越大。新官网的文档简直了,很多资料相当于没写。
通常情况下大家都会用 NSEvent.addGlobalMonitorForEvents
来监听全局的键盘事件。但是少量使用场景会需要监听并且拦截,以限制其它 App 使用。
使用 NSEvent
的方法是无法做到拦截的,而 CGEvent
的监听方法则可以做到。
下面方法,只需要在 callback
中返回 nil,则可拦截指定事件。
let eventMask = (1 << CGEventType.keyDown.rawValue)
// 创建一个事件监听器,并指定位置为 cghidEventTap
guard let eventTap =
CGEvent.tapCreate(tap: .cghidEventTap,
place: .headInsertEventTap,
options: .defaultTap,
eventsOfInterest: CGEventMask(eventMask),
callback: { (proxy, type, event, refcon) in
// 处理事件的回调函数
return Unmanaged.passRetained(event)
}, userInfo: nil) else { return }
let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0)
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes)
CGEvent.tapEnable(tap: eventTap, enable: true)
CFRunLoopRun()
Does it (CleanClip) log copied items from pro apps like Final Cut Pro? I use Paste to quickly paste clips and clip effects, and I know not all clipboard managers log these kind of non-text non-image clipboard items.
Out of curiosity, what extra steps were required to do this (asking as a non-developer)? I wonder why not all clipboard managers support this.
它 (CleanClip) 是否记录来自专业应用程序(如Final Cut Pro)的复制项目?我使用Paste快速粘贴剪辑和剪辑效果,我知道并非所有剪贴板管理器都记录这些非文本非图像的剪贴板项目。
出于好奇,需要额外的步骤来实现这一点吗(作为非开发人员提问)?我想知道为什么并非所有剪贴板管理器都支持这个功能。
我简单解释一下。
剪贴板工作流程很简单,A App 把自己的东西放到剪贴板里,B 从剪贴板里取出来放到自己里面。
这里面就涉及一个问题,B 怎么知道 A 放的是啥,该怎么用这个数据呢?
Mac 约定,每个放到剪贴板里的内容都要有至少两个东西:1. 类型名,用来区分这是复制的啥类型。2. 复制的内容本身。
这样 B 取数据时就会先问一下剪贴板:这是啥数据呀?剪贴板说是图片(类型名)。B 会再说,那你把数据给我吧。然后 B 拿到数据后就会用解析图片的方式解析这段数据,然后再去显示。
其它类型的数据都是这个道理。
这就要再讲讲公有剪贴板类型和私有剪贴板类型。
公有类型:
最基本的文字、图片、文件,它们是公有类型。因为这是 Mac 系统定义的,所有 Mac 上的 app 都知道它们的类型名是啥,数据结构啥样。
私有类型:
另外有一些,就像 FCP 这样子的app,它们想要复制的内容很复杂,每按一次 CMD+C,可能都要在剪贴板里存储这些数据:3 段视频,每一段的时长,复制时它们在时间轴上的位置,层级关系等等。
这时候,Mac支持的那些公有类型就不够用了,那怎么办呢?FCP 说,我新建一个类型就叫 “fcp”,数据结构我自己定。
这样,在 FCP App 的内部,他就能自由地复制、粘贴 “fcp” 类型的数据了。因为 FCP 自己知道这个类型名叫“fcp”的数据,它应该怎么解析。
某个 app 自己定义,其他 app 不知道的,这是私有类型。
事实上,每个 app 都能看到有这么一个类型是 ”fcp“ 的数据,只是不知道怎么解析而已。
能不能解析私有数据有什么区别?
一个很重要的点是,剪贴板 App 只需要知道有啥类型的数据,而不一定要解析数据。
知道有啥类型的数据,能帮助我们在 剪贴板 App 的 界面上标记出这个复制项的类型:文字类型、图片类型、fcp 类型。
如果能解析数据,能帮助我们把界面变得更友好,我们就能在剪贴板 App 的界面上标记出这段数据的大致内容:
为什么有些剪贴板支持有些不支持?
理由可能是多种多样的。根据上面的说明,我们可以知道一个可能的原因:
1. 这些私有数据没有数据特征,只知道类型名,对用户体验不友好
比如我在 FCP 中复制了十个内容,但因为不知道数据内容。所以就算支持了,用户也区分不出来这十段哪个是哪个。对使用没有意义。
2. 产品定位限制
有的 app 为了某些用户需求,设置了类型白名单功能。比如 maccy。它允许用户选择允许监听的类型白名单。
因为私有类型是无限多的,我们不可能把它穷举出来。无法穷举,就无法让所有的私有类型都被允许。因此注定会屏蔽大量的私有数据。
3. 小众需求,团队选择不支持
相较于那些公有类型,这些私有类型的需求都属于小众需求。影响的用户群体小,团队有可能主动选择不支持。
不过我认为在剪贴板在剪辑领域里有非常大的可挖掘空间,我打算支持,有机会的话希望能和你们多沟通,挖掘挖掘使用需求。
4. 开发者失误
这是很有可能的,如果没有足够的用户群体反映问题,开发者对剪贴版的开发经验又不够的话。有概率不会注意到这个问题。
我能想到大概就这些吧:要么是开发团队选择不支持,要么是因为经验没有意识到有这类问题。
我本来想简单地解释一下,但是不小心说了太多哈哈。希望这些话满足了你的好奇心。
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.