feat: 实现 Chromium 喜报应用
- 创建 Chromium/Electron 应用检测器 - 设计喜报风格的 UI 界面 - 添加应用列表查看功能 - 配置应用图标和颜色资源 - 添加 README 文档 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
|
@ -86,11 +86,11 @@
|
|||
};
|
||||
};
|
||||
buildConfigurationList = 45F070D32E249A0700B1170B /* Build configuration list for PBXProject "ChromiumCertificate" */;
|
||||
developmentRegion = en;
|
||||
developmentRegion = "zh-Hans";
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
"zh-Hans",
|
||||
);
|
||||
mainGroup = 45F070CF2E249A0700B1170B;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
|
@ -252,8 +252,11 @@
|
|||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = U496NXS4BR;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Chromium 喜报";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
@ -277,8 +280,11 @@
|
|||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = U496NXS4BR;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Chromium 喜报";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Bucket
|
||||
uuid = "3F09E1D6-128F-4462-8367-9B46EA2D3AB6"
|
||||
type = "1"
|
||||
version = "2.0">
|
||||
</Bucket>
|
12
ChromiumCertificate/Assets.xcassets/AnnouncementBg.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "bg.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
ChromiumCertificate/Assets.xcassets/AnnouncementBg.imageset/bg.png
vendored
Normal file
After Width: | Height: | Size: 1.2 MiB |
|
@ -1,51 +1,61 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon_16x16.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_16x16@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_32x32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_32x32@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_128x128.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_128x128@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_256x256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_256x256@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_512x512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_512x512@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
|
|
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 57 KiB |
After Width: | Height: | Size: 759 B |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 56 KiB |
After Width: | Height: | Size: 238 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 238 KiB |
After Width: | Height: | Size: 1.0 MiB |
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x00",
|
||||
"green" : "0xFF",
|
||||
"red" : "0xFE"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x00",
|
||||
"green" : "0x00",
|
||||
"red" : "0xD0"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
56
ChromiumCertificate/ChromiumBasedAppListView.swift
Normal file
|
@ -0,0 +1,56 @@
|
|||
//
|
||||
// ChromiumBasedAppListView.swift
|
||||
// ChromiumCertificate
|
||||
//
|
||||
// Created by Astrian Zheng on 14/7/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ChromiumBasedAppListView: View {
|
||||
@Binding var isPresented: Bool
|
||||
|
||||
let chromiumAppsList: [ChromiumApp] = ChromiumDetector.detectChromiumApps()
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
Text("所有带有 Chromium 的应用程序").font(.headline)
|
||||
Spacer()
|
||||
Button {
|
||||
self.isPresented.toggle()
|
||||
} label: {
|
||||
Text("关闭")
|
||||
}
|
||||
}.padding()
|
||||
|
||||
Divider()
|
||||
|
||||
ScrollView {
|
||||
if chromiumAppsList.isEmpty {
|
||||
Text("你的电脑没有遭受 Chromium 的荼毒!望君继续努力。").multilineTextAlignment(.center).padding()
|
||||
}
|
||||
VStack(spacing: 8) {
|
||||
ForEach(Array(chromiumAppsList.enumerated()), id: \.element.id) { index, chromiumApp in
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text(chromiumApp.name).bold()
|
||||
Text(chromiumApp.path)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
|
||||
if index < chromiumAppsList.count - 1 {
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}.padding()
|
||||
}
|
||||
}.frame(width: 300).frame(minHeight: 0, maxHeight: 300)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ChromiumBasedAppListView(isPresented: .constant(true))
|
||||
}
|
|
@ -9,9 +9,12 @@ import SwiftUI
|
|||
|
||||
@main
|
||||
struct ChromiumCertificateApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
}
|
||||
.windowResizability(.contentSize)
|
||||
.defaultSize(width: 640, height: 480)
|
||||
.windowStyle(.hiddenTitleBar)
|
||||
}
|
||||
}
|
||||
|
|
158
ChromiumCertificate/ChromiumDetector.swift
Normal file
|
@ -0,0 +1,158 @@
|
|||
//
|
||||
// ChromiumDetector.swift
|
||||
// ChromiumCertificate
|
||||
//
|
||||
// Created by Astrian Zheng on 14/7/2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct ChromiumApp: Identifiable {
|
||||
let id = UUID()
|
||||
let name: String
|
||||
let type: ChromiumType
|
||||
let path: String
|
||||
}
|
||||
|
||||
enum ChromiumType: String, CaseIterable {
|
||||
case electron = "Electron"
|
||||
case chromium = "Chromium"
|
||||
case chromiumLibrary = "Chromium库"
|
||||
case electronIdentifier = "Electron标识"
|
||||
}
|
||||
|
||||
class ChromiumDetector {
|
||||
|
||||
static func detectChromiumApps() -> [ChromiumApp] {
|
||||
var chromiumApps: [ChromiumApp] = []
|
||||
let fileManager = FileManager.default
|
||||
let applicationsURL = URL(fileURLWithPath: "/Applications")
|
||||
|
||||
do {
|
||||
let appURLs = try fileManager.contentsOfDirectory(
|
||||
at: applicationsURL,
|
||||
includingPropertiesForKeys: [.isDirectoryKey],
|
||||
options: [.skipsHiddenFiles]
|
||||
)
|
||||
|
||||
for appURL in appURLs {
|
||||
if appURL.pathExtension == "app" {
|
||||
if let chromiumApp = analyzeApp(at: appURL) {
|
||||
chromiumApps.append(chromiumApp)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Error reading applications directory: \(error)")
|
||||
}
|
||||
|
||||
return chromiumApps.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
|
||||
}
|
||||
|
||||
private static func analyzeApp(at appURL: URL) -> ChromiumApp? {
|
||||
let appName = appURL.deletingPathExtension().lastPathComponent
|
||||
let contentsURL = appURL.appendingPathComponent("Contents")
|
||||
let frameworksURL = contentsURL.appendingPathComponent("Frameworks")
|
||||
let infoPlistURL = contentsURL.appendingPathComponent("Info.plist")
|
||||
|
||||
// 检查 Electron Framework
|
||||
let electronFrameworkURL = frameworksURL.appendingPathComponent("Electron Framework.framework")
|
||||
if FileManager.default.fileExists(atPath: electronFrameworkURL.path) {
|
||||
return ChromiumApp(name: appName, type: .electron, path: appURL.path)
|
||||
}
|
||||
|
||||
// 检查 Chromium 相关框架
|
||||
if hasChromiumFrameworks(at: frameworksURL) {
|
||||
return ChromiumApp(name: appName, type: .chromium, path: appURL.path)
|
||||
}
|
||||
|
||||
// 检查可执行文件是否链接到 Chromium 库
|
||||
let executableURL = contentsURL.appendingPathComponent("MacOS").appendingPathComponent(appName)
|
||||
if hasChromiumLibraries(executablePath: executableURL.path) {
|
||||
return ChromiumApp(name: appName, type: .chromiumLibrary, path: appURL.path)
|
||||
}
|
||||
|
||||
// 检查 Info.plist 中的 Electron 标识
|
||||
if hasElectronIdentifier(infoPlistPath: infoPlistURL.path) {
|
||||
return ChromiumApp(name: appName, type: .electronIdentifier, path: appURL.path)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func hasChromiumFrameworks(at frameworksURL: URL) -> Bool {
|
||||
do {
|
||||
let frameworks = try FileManager.default.contentsOfDirectory(atPath: frameworksURL.path)
|
||||
return frameworks.contains { $0.localizedCaseInsensitiveContains("chromium") }
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private static func hasChromiumLibraries(executablePath: String) -> Bool {
|
||||
guard FileManager.default.fileExists(atPath: executablePath) else { return false }
|
||||
|
||||
let task = Process()
|
||||
task.launchPath = "/usr/bin/otool"
|
||||
task.arguments = ["-L", executablePath]
|
||||
|
||||
let pipe = Pipe()
|
||||
task.standardOutput = pipe
|
||||
task.standardError = Pipe()
|
||||
|
||||
do {
|
||||
try task.run()
|
||||
task.waitUntilExit()
|
||||
|
||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let output = String(data: data, encoding: .utf8) ?? ""
|
||||
|
||||
return output.localizedCaseInsensitiveContains("chromium")
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private static func hasElectronIdentifier(infoPlistPath: String) -> Bool {
|
||||
guard FileManager.default.fileExists(atPath: infoPlistPath) else { return false }
|
||||
|
||||
let task = Process()
|
||||
task.launchPath = "/usr/libexec/PlistBuddy"
|
||||
task.arguments = ["-c", "Print CFBundleIdentifier", infoPlistPath]
|
||||
|
||||
let pipe = Pipe()
|
||||
task.standardOutput = pipe
|
||||
task.standardError = Pipe()
|
||||
|
||||
do {
|
||||
try task.run()
|
||||
task.waitUntilExit()
|
||||
|
||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let output = String(data: data, encoding: .utf8) ?? ""
|
||||
|
||||
return output.localizedCaseInsensitiveContains("electron")
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
static func getChromiumAppCount() -> Int {
|
||||
return detectChromiumApps().count
|
||||
}
|
||||
|
||||
static func getChromiumAppNames() -> [String] {
|
||||
return detectChromiumApps().map { $0.name }
|
||||
}
|
||||
|
||||
static func getChromiumAppsByType() -> [ChromiumType: [ChromiumApp]] {
|
||||
let apps = detectChromiumApps()
|
||||
var groupedApps: [ChromiumType: [ChromiumApp]] = [:]
|
||||
|
||||
for type in ChromiumType.allCases {
|
||||
groupedApps[type] = apps.filter { $0.type == type }
|
||||
}
|
||||
|
||||
return groupedApps
|
||||
}
|
||||
}
|
|
@ -7,16 +7,55 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
struct StrokeText: ViewModifier {
|
||||
var strokeSize: CGFloat = 1
|
||||
var strokeColor: Color = .black
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.background(
|
||||
Rectangle()
|
||||
.foregroundColor(strokeColor)
|
||||
.mask(content)
|
||||
.blur(radius: strokeSize)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func stroke(color: Color = .black, width: CGFloat = 1) -> some View {
|
||||
modifier(StrokeText(strokeSize: width, strokeColor: color))
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
Image(systemName: "globe")
|
||||
.imageScale(.large)
|
||||
.foregroundStyle(.tint)
|
||||
Text("Hello, world!")
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
let count = ChromiumDetector.getChromiumAppCount()
|
||||
|
||||
@State private var presentSheet: Bool = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Image("AnnouncementBg").resizable().frame(width: 640, height: 480)
|
||||
VStack {
|
||||
Text(count != 0 ? "这台 Mac 上一共有 \(count) 个 Chromium" : "这台 Mac 一个 Chromium 都没有!")
|
||||
.font(.system(size: 35, weight: .semibold))
|
||||
.foregroundColor(Color("TextColor"))
|
||||
.stroke(color: Color("TextBorderColor"), width: 5)
|
||||
|
||||
Button {
|
||||
self.presentSheet.toggle()
|
||||
} label: {
|
||||
Text("查看列表").font(.system(size: 20)).padding(.horizontal).padding(.vertical, 8)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.red)
|
||||
.sheet(isPresented: self.$presentSheet) {
|
||||
ChromiumBasedAppListView(isPresented: self.$presentSheet)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(width: 640, height: 420)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
|
|
67
README.md
Normal file
|
@ -0,0 +1,67 @@
|
|||
# 🎉 Chromium 喜报 - 您的 Mac 健康检测工具
|
||||
|
||||
> 热烈祝贺您的 Mac 电脑成功安装了 N 个基于 Chromium 的应用程序!
|
||||
|
||||
## 🏆 这是什么?
|
||||
|
||||
一个能够帮您统计 Mac 上到底有多少个 Chromium/Electron 应用的"健康检测"工具。运行后会以喜报的形式展示检测结果,让您充分认识到自己的电脑已经被 Chromium 占领到何种程度。
|
||||
|
||||
## 🎯 功能特色
|
||||
|
||||
- **精准检测**:扫描您的 /Applications 目录,识别所有基于 Chromium/Electron 的应用
|
||||
- **喜报展示**:采用传统喜报设计,红底金字,喜气洋洋
|
||||
- **详细列表**:查看具体是哪些应用在"助力"您的内存消耗
|
||||
- **一键查看**:简洁明了的界面,让您快速了解"战况"
|
||||
|
||||
## 📸 效果预览
|
||||
|
||||
想象一下,打开应用后看到:
|
||||
|
||||
```
|
||||
🎊 喜报 🎊
|
||||
这台 Mac 上一共有 23 个 Chromium
|
||||
[查看列表]
|
||||
```
|
||||
|
||||
## 🤔 为什么要做这个?
|
||||
|
||||
在这个美好的时代,我们的电脑里充斥着各种 Electron 应用:
|
||||
- VS Code(写代码)
|
||||
- Slack(聊天)
|
||||
- Discord(游戏聊天)
|
||||
- Notion(笔记)
|
||||
- Figma(设计)
|
||||
- ...
|
||||
|
||||
每个都是独立的 Chromium 实例,每个都在努力地消耗您的内存。是时候让我们正视这个"喜人"的现实了!
|
||||
|
||||
## 🚀 使用方法
|
||||
|
||||
1. 下载 DMG 文件
|
||||
2. 拖拽到 Applications 文件夹
|
||||
3. 运行应用
|
||||
4. 欣赏您的"成就"
|
||||
5. 分享到朋友圈,让大家一起"喜悦"
|
||||
|
||||
## 💭 检测原理
|
||||
|
||||
应用会检查以下特征来识别 Chromium/Electron 应用:
|
||||
- Electron Framework 存在
|
||||
- Chromium 相关框架
|
||||
- 动态链接库中的 Chromium 依赖
|
||||
- Info.plist 中的 Electron 标识
|
||||
|
||||
## 🎈 温馨提示
|
||||
|
||||
- 检测结果仅供娱乐参考
|
||||
- Chromium 应用并非都是坏事(真的吗?)
|
||||
- 如果数字太大,请保持冷静
|
||||
- 建议定期检测,见证数字的增长
|
||||
|
||||
## 📝 开源协议
|
||||
|
||||
本项目采用 MIT 协议开源,欢迎 fork、star、提 issue。
|
||||
|
||||
---
|
||||
|
||||
*记住:每一个 Chromium 应用都是对您 16GB 内存的考验!*
|