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" */;
 | 
								buildConfigurationList = 45F070D32E249A0700B1170B /* Build configuration list for PBXProject "ChromiumCertificate" */;
 | 
				
			||||||
			developmentRegion = en;
 | 
								developmentRegion = "zh-Hans";
 | 
				
			||||||
			hasScannedForEncodings = 0;
 | 
								hasScannedForEncodings = 0;
 | 
				
			||||||
			knownRegions = (
 | 
								knownRegions = (
 | 
				
			||||||
				en,
 | 
					 | 
				
			||||||
				Base,
 | 
									Base,
 | 
				
			||||||
 | 
									"zh-Hans",
 | 
				
			||||||
			);
 | 
								);
 | 
				
			||||||
			mainGroup = 45F070CF2E249A0700B1170B;
 | 
								mainGroup = 45F070CF2E249A0700B1170B;
 | 
				
			||||||
			minimizedProjectReferenceProxies = 1;
 | 
								minimizedProjectReferenceProxies = 1;
 | 
				
			||||||
| 
						 | 
					@ -252,8 +252,11 @@
 | 
				
			||||||
				CODE_SIGN_STYLE = Automatic;
 | 
									CODE_SIGN_STYLE = Automatic;
 | 
				
			||||||
				COMBINE_HIDPI_IMAGES = YES;
 | 
									COMBINE_HIDPI_IMAGES = YES;
 | 
				
			||||||
				CURRENT_PROJECT_VERSION = 1;
 | 
									CURRENT_PROJECT_VERSION = 1;
 | 
				
			||||||
 | 
									DEVELOPMENT_TEAM = U496NXS4BR;
 | 
				
			||||||
 | 
									ENABLE_HARDENED_RUNTIME = YES;
 | 
				
			||||||
				ENABLE_PREVIEWS = YES;
 | 
									ENABLE_PREVIEWS = YES;
 | 
				
			||||||
				GENERATE_INFOPLIST_FILE = YES;
 | 
									GENERATE_INFOPLIST_FILE = YES;
 | 
				
			||||||
 | 
									INFOPLIST_KEY_CFBundleDisplayName = "Chromium 喜报";
 | 
				
			||||||
				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 | 
									INFOPLIST_KEY_NSHumanReadableCopyright = "";
 | 
				
			||||||
				LD_RUNPATH_SEARCH_PATHS = (
 | 
									LD_RUNPATH_SEARCH_PATHS = (
 | 
				
			||||||
					"$(inherited)",
 | 
										"$(inherited)",
 | 
				
			||||||
| 
						 | 
					@ -277,8 +280,11 @@
 | 
				
			||||||
				CODE_SIGN_STYLE = Automatic;
 | 
									CODE_SIGN_STYLE = Automatic;
 | 
				
			||||||
				COMBINE_HIDPI_IMAGES = YES;
 | 
									COMBINE_HIDPI_IMAGES = YES;
 | 
				
			||||||
				CURRENT_PROJECT_VERSION = 1;
 | 
									CURRENT_PROJECT_VERSION = 1;
 | 
				
			||||||
 | 
									DEVELOPMENT_TEAM = U496NXS4BR;
 | 
				
			||||||
 | 
									ENABLE_HARDENED_RUNTIME = YES;
 | 
				
			||||||
				ENABLE_PREVIEWS = YES;
 | 
									ENABLE_PREVIEWS = YES;
 | 
				
			||||||
				GENERATE_INFOPLIST_FILE = YES;
 | 
									GENERATE_INFOPLIST_FILE = YES;
 | 
				
			||||||
 | 
									INFOPLIST_KEY_CFBundleDisplayName = "Chromium 喜报";
 | 
				
			||||||
				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 | 
									INFOPLIST_KEY_NSHumanReadableCopyright = "";
 | 
				
			||||||
				LD_RUNPATH_SEARCH_PATHS = (
 | 
									LD_RUNPATH_SEARCH_PATHS = (
 | 
				
			||||||
					"$(inherited)",
 | 
										"$(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" : [
 | 
					  "images" : [
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					      "filename" : "icon_16x16.png",
 | 
				
			||||||
      "idiom" : "mac",
 | 
					      "idiom" : "mac",
 | 
				
			||||||
      "scale" : "1x",
 | 
					      "scale" : "1x",
 | 
				
			||||||
      "size" : "16x16"
 | 
					      "size" : "16x16"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					      "filename" : "icon_16x16@2x.png",
 | 
				
			||||||
      "idiom" : "mac",
 | 
					      "idiom" : "mac",
 | 
				
			||||||
      "scale" : "2x",
 | 
					      "scale" : "2x",
 | 
				
			||||||
      "size" : "16x16"
 | 
					      "size" : "16x16"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					      "filename" : "icon_32x32.png",
 | 
				
			||||||
      "idiom" : "mac",
 | 
					      "idiom" : "mac",
 | 
				
			||||||
      "scale" : "1x",
 | 
					      "scale" : "1x",
 | 
				
			||||||
      "size" : "32x32"
 | 
					      "size" : "32x32"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					      "filename" : "icon_32x32@2x.png",
 | 
				
			||||||
      "idiom" : "mac",
 | 
					      "idiom" : "mac",
 | 
				
			||||||
      "scale" : "2x",
 | 
					      "scale" : "2x",
 | 
				
			||||||
      "size" : "32x32"
 | 
					      "size" : "32x32"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					      "filename" : "icon_128x128.png",
 | 
				
			||||||
      "idiom" : "mac",
 | 
					      "idiom" : "mac",
 | 
				
			||||||
      "scale" : "1x",
 | 
					      "scale" : "1x",
 | 
				
			||||||
      "size" : "128x128"
 | 
					      "size" : "128x128"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					      "filename" : "icon_128x128@2x.png",
 | 
				
			||||||
      "idiom" : "mac",
 | 
					      "idiom" : "mac",
 | 
				
			||||||
      "scale" : "2x",
 | 
					      "scale" : "2x",
 | 
				
			||||||
      "size" : "128x128"
 | 
					      "size" : "128x128"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					      "filename" : "icon_256x256.png",
 | 
				
			||||||
      "idiom" : "mac",
 | 
					      "idiom" : "mac",
 | 
				
			||||||
      "scale" : "1x",
 | 
					      "scale" : "1x",
 | 
				
			||||||
      "size" : "256x256"
 | 
					      "size" : "256x256"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					      "filename" : "icon_256x256@2x.png",
 | 
				
			||||||
      "idiom" : "mac",
 | 
					      "idiom" : "mac",
 | 
				
			||||||
      "scale" : "2x",
 | 
					      "scale" : "2x",
 | 
				
			||||||
      "size" : "256x256"
 | 
					      "size" : "256x256"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					      "filename" : "icon_512x512.png",
 | 
				
			||||||
      "idiom" : "mac",
 | 
					      "idiom" : "mac",
 | 
				
			||||||
      "scale" : "1x",
 | 
					      "scale" : "1x",
 | 
				
			||||||
      "size" : "512x512"
 | 
					      "size" : "512x512"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					      "filename" : "icon_512x512@2x.png",
 | 
				
			||||||
      "idiom" : "mac",
 | 
					      "idiom" : "mac",
 | 
				
			||||||
      "scale" : "2x",
 | 
					      "scale" : "2x",
 | 
				
			||||||
      "size" : "512x512"
 | 
					      "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
 | 
					@main
 | 
				
			||||||
struct ChromiumCertificateApp: App {
 | 
					struct ChromiumCertificateApp: App {
 | 
				
			||||||
    var body: some Scene {
 | 
						var body: some Scene {
 | 
				
			||||||
        WindowGroup {
 | 
							WindowGroup {
 | 
				
			||||||
            ContentView()
 | 
								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
 | 
					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 {
 | 
					struct ContentView: View {
 | 
				
			||||||
    var body: some View {
 | 
						let count = ChromiumDetector.getChromiumAppCount()
 | 
				
			||||||
        VStack {
 | 
						
 | 
				
			||||||
            Image(systemName: "globe")
 | 
						@State private var presentSheet: Bool = false
 | 
				
			||||||
                .imageScale(.large)
 | 
						
 | 
				
			||||||
                .foregroundStyle(.tint)
 | 
						var body: some View {
 | 
				
			||||||
            Text("Hello, world!")
 | 
							ZStack {
 | 
				
			||||||
        }
 | 
								Image("AnnouncementBg").resizable().frame(width: 640, height: 480)
 | 
				
			||||||
        .padding()
 | 
								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 {
 | 
					#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 内存的考验!*
 | 
				
			||||||