Giter Site home page Giter Site logo

blug's People

Contributors

s1ntoneli avatar

Stargazers

 avatar

Watchers

 avatar  avatar

blug's Issues

如何拦截全局键盘事件防止其它 App 使用?

通常情况下大家都会用 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()

image

如何创建一个 Swift Package + Example App 结构的项目?

想把自己用到的一些通用方法模块化,结果发现用 Xcode 创建下面这种结构良好的 Swift Package 不是一件易事:

CleanShot 2024-04-18 at 15 22 10@2x

其中有一些障碍:

  1. xcodeproj 文件不能与 Package.swift 处于同一级别
  2. Example 项目不能直接在 Swift Package Manager 中直接添加处于上级目录中的 Swift Package

这两个障碍要求两者既不能在同一目录,Example 也不能在 Swift Package 上级目录???而要发布软件包,Package.swift 又必须在顶层目录。
难道两个人真的不能在一起吗?

研究了一番,发现解决办法其实也很简单,只是很多地方没有提到这个做法。

解决步骤:

  1. 新建 Swift Package 软件包
  2. 新建子目录 Example
  3. 在 Example 新建 Example App
  4. 关键步骤:打开 Example App,直接把软件包目录拖入 Example App 工作区
  5. 在 Example App target 添加依赖,这时候就能找到刚才新创建的依赖包了

Cheers!

如何防止服务提供 App 在服务被调用时被激活?

“借用”自定义系统服务获取选中文字时,App 会被激活导致原来的窗口失去焦点。这在某些业务逻辑中是不允许的。

解决方法:

NSApp.setActivationPolicy(.prohibited)

它用于设置应用程序的激活策略。具体来说,.prohibited表示禁止应用程序被激活,即应用程序将无法成为活动应用程序,用户无法切换到该应用程序并与其交互。

这个方法通常用于特定情况下,例如当应用程序需要在后台运行或作为服务运行时,可以将其激活策略设置为.prohibited,以确保用户无法意外地与应用程序进行交互。

CleanShot 2024-04-28 at 23 30 05@2x

一个系统自带的方便调试 macOS 应用的小办法

启动应用时带上 -_NS_4445425547 YES 参数,比如:

/Applications/CleanClip.app/Contents/MacOS/CleanClip -_NS_4445425547 YES

这时菜单栏里会多出一个调试的菜单选项🐞,可以查看很多有用的调试信息。

CleanShot 2024-04-09 at 17 01 40@2x

Cloudflare Worker 一个文件快速为你的出海产品实现购买力平价(Purchasing Power Parity)

由于各个国家、地区购买力不一,出海产品需要为不同购买力的地区设定不同的价格。

实现购买力平价需要两个数据:

  1. 用户的位置数据
  2. 一份各国家的购买力水平清单

逻辑就很简单了:
位置数据 -> 购买力水平 -> 匹配相应价格信息并应用

那么这两个数据怎么获取呢?

位置数据

Cloudflare Worker 的 request 参数带了非常详细的位置数据,这让我们不需要再使用第三方服务就能方便地获取位置:
Cloudflare Worker request 字段

它包括了经纬度、地区代码等,我们的颗粒度精确到国家,这里用国家代码 country 字段

购买力水平数据

购买力水平等级可以在这个 gist 下载:各国家购买力水平清单

逻辑和实现代码

大致逻辑是:

  1. 从 Worker 的 request 参数获取国家代码
  2. 使用国家代码从购买能力列表获取购买力水平
  3. 根据水平匹配相应的折扣信息并应用

我在 CleanClip(Mac 上的剪贴板工具) 中简单起见,直接为不同国家应用不同的折扣。
LemonSqueezy 可以这样直接应用折扣码:PRODUCT_URL + "?checkout%5Bdiscount_code%5D=" + discountCode

一些细节:

  • 折扣信息保存在环境变量里,方便随时修改
  • Access-Control-Max-Age 缓存设为 0,可方便随时改动,即时生效。(不设置会导致上次结果保留过久,实践大概是 3、4 天左右才生效,设为 0 即时生效)
  • 可以将这个 worker 连接到其它 worker 下,价格信息在这里统一维护,方便多页面、业务使用
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);
  },
};

答用户问:为什么不是所有的剪贴板 App 都支持记录 Final Cut Pro 的复制项?

用户问:

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快速粘贴剪辑和剪辑效果,我知道并非所有剪贴板管理器都记录这些非文本非图像的剪贴板项目。
出于好奇,需要额外的步骤来实现这一点吗(作为非开发人员提问)?我想知道为什么并非所有剪贴板管理器都支持这个功能。

https://www.reddit.com/r/macapps/comments/193ds1j/comment/khe5xjx/?utm_source=share&utm_medium=web2x&context=3

回答:

我简单解释一下。

剪贴板工作流程很简单,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 的界面上标记出这段数据的大致内容:

  • 如果是文字,就解析出前几个字符显示。
  • 如果是图片,就解析出然后显示个预览图、图片尺寸。
  • 如果是 fcp,我们解析不了。因此界面上除了“fcp”字符外,我们没法让这段数据更友好。
    image

为什么有些剪贴板支持有些不支持?

理由可能是多种多样的。根据上面的说明,我们可以知道一个可能的原因:

1. 这些私有数据没有数据特征,只知道类型名,对用户体验不友好

比如我在 FCP 中复制了十个内容,但因为不知道数据内容。所以就算支持了,用户也区分不出来这十段哪个是哪个。对使用没有意义。

2. 产品定位限制

有的 app 为了某些用户需求,设置了类型白名单功能。比如 maccy。它允许用户选择允许监听的类型白名单。

因为私有类型是无限多的,我们不可能把它穷举出来。无法穷举,就无法让所有的私有类型都被允许。因此注定会屏蔽大量的私有数据。

3. 小众需求,团队选择不支持

相较于那些公有类型,这些私有类型的需求都属于小众需求。影响的用户群体小,团队有可能主动选择不支持。

不过我认为在剪贴板在剪辑领域里有非常大的可挖掘空间,我打算支持,有机会的话希望能和你们多沟通,挖掘挖掘使用需求。

4. 开发者失误

这是很有可能的,如果没有足够的用户群体反映问题,开发者对剪贴版的开发经验又不够的话。有概率不会注意到这个问题。

我能想到大概就这些吧:要么是开发团队选择不支持,要么是因为经验没有意识到有这类问题。

我本来想简单地解释一下,但是不小心说了太多哈哈。希望这些话满足了你的好奇心。

PopClip 是如何获取选中的文字的?

目前的测试发现是两步:

  1. 使用辅助模式检测是否有选中的文字,方法见:#7

  2. 如果第一步未检测文字,则模拟系统的复制能力 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)
    }
}

如何获取任意 app 中选中的文字?

利用辅助模式,无需使用剪贴板。
但是在 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
    }
}

SwiftUI 点击未激活的 NSWindow 上的按钮时,默认让按钮处理鼠标事件,而不是窗口

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")
  }

如何解决 vitepress 站点部署到 Cloudflare 时页面出错的问题

今天把新的 vitepress 站点部署到了 Cloudflare pages,结果页面总是有很多内容不显示。一遍遍排查后发现是 cf 的 Auto Minify 的问题。

终端显示错误:

Hydration completed but contains mismatches.

进入网站 → YOUR_DOMAIN → 速度 → 优化,在 Auto Minify 选项中关闭 JavaScript 和 HTML 。

警告
CloudFlare 中的 Auto Minify 错误地处理 HTML 空格和换行符,这可能导致 Vue 在初始化期间触发 SSR 不匹配。

关闭 Auto Minify

简单解决 Web axios 网络请求跨域错误

不想折腾的办法是:

服务端加上 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 请求也返回正确的应答。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.