Skip to content

Commit 4148cd4

Browse files
committed
feat: add macOS WidgetKit widget extension
Add a native macOS widget that displays Claude usage data with Apple-style circular progress rings. Features: - Small and Medium widget sizes - Usage rings for 5h, 7d, Opus, and Sonnet limits - Color-coded progress (green → orange → red based on utilization) - Automatic data sync via App Groups - 15-minute refresh interval New files: - Usage4ClaudeWidget/ - Widget extension target - Usage4Claude/Shared/SharedUsageData.swift - Shared data model Modified files: - DataRefreshManager.swift - Sync data to widget on refresh - project.pbxproj - Widget extension target configuration - Usage4Claude.entitlements - App Groups capability Requirements: - App Group: group.xyz.fi5h.Usage4Claude - macOS 14.0+ for widget extension
1 parent 356c15c commit 4148cd4

File tree

8 files changed

+983
-6
lines changed

8 files changed

+983
-6
lines changed

Usage4Claude.xcodeproj/project.pbxproj

Lines changed: 197 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,41 @@
66
objectVersion = 77;
77
objects = {
88

9+
/* Begin PBXBuildFile section */
10+
WIDGET001 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = WIDGET000 /* WidgetKit.framework */; };
11+
WIDGET002 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = WIDGET003 /* SwiftUI.framework */; };
12+
WIDGET004 /* Usage4ClaudeWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = WIDGETPROD /* Usage4ClaudeWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
13+
/* End PBXBuildFile section */
14+
15+
/* Begin PBXContainerItemProxy section */
16+
WIDGETPROXY /* PBXContainerItemProxy */ = {
17+
isa = PBXContainerItemProxy;
18+
containerPortal = 8873E4C72E9F61C700ACFF5C /* Project object */;
19+
proxyType = 1;
20+
remoteGlobalIDString = WIDGETTARGET;
21+
remoteInfo = Usage4ClaudeWidgetExtension;
22+
};
23+
/* End PBXContainerItemProxy section */
24+
25+
/* Begin PBXCopyFilesBuildPhase section */
26+
WIDGETEMBED /* Embed Foundation Extensions */ = {
27+
isa = PBXCopyFilesBuildPhase;
28+
buildActionMask = 2147483647;
29+
dstPath = "";
30+
dstSubfolderSpec = 13;
31+
files = (
32+
WIDGET004 /* Usage4ClaudeWidgetExtension.appex in Embed Foundation Extensions */,
33+
);
34+
name = "Embed Foundation Extensions";
35+
runOnlyForDeploymentPostprocessing = 0;
36+
};
37+
/* End PBXCopyFilesBuildPhase section */
38+
939
/* Begin PBXFileReference section */
1040
8873E4CF2E9F61C700ACFF5C /* Usage4Claude.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Usage4Claude.app; sourceTree = BUILT_PRODUCTS_DIR; };
41+
WIDGETPROD /* Usage4ClaudeWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = Usage4ClaudeWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
42+
WIDGET000 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
43+
WIDGET003 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
1144
/* End PBXFileReference section */
1245

1346
/* Begin PBXFileSystemSynchronizedRootGroup section */
@@ -16,6 +49,11 @@
1649
path = Usage4Claude;
1750
sourceTree = "<group>";
1851
};
52+
WIDGETGRP /* Usage4ClaudeWidget */ = {
53+
isa = PBXFileSystemSynchronizedRootGroup;
54+
path = Usage4ClaudeWidget;
55+
sourceTree = "<group>";
56+
};
1957
/* End PBXFileSystemSynchronizedRootGroup section */
2058

2159
/* Begin PBXFrameworksBuildPhase section */
@@ -26,13 +64,24 @@
2664
);
2765
runOnlyForDeploymentPostprocessing = 0;
2866
};
67+
WIDGETFWK /* Frameworks */ = {
68+
isa = PBXFrameworksBuildPhase;
69+
buildActionMask = 2147483647;
70+
files = (
71+
WIDGET002 /* SwiftUI.framework in Frameworks */,
72+
WIDGET001 /* WidgetKit.framework in Frameworks */,
73+
);
74+
runOnlyForDeploymentPostprocessing = 0;
75+
};
2976
/* End PBXFrameworksBuildPhase section */
3077

3178
/* Begin PBXGroup section */
3279
8873E4C62E9F61C700ACFF5C = {
3380
isa = PBXGroup;
3481
children = (
3582
8873E4D12E9F61C700ACFF5C /* Usage4Claude */,
83+
WIDGETGRP /* Usage4ClaudeWidget */,
84+
FWKGROUP /* Frameworks */,
3685
8873E4D02E9F61C700ACFF5C /* Products */,
3786
);
3887
sourceTree = "<group>";
@@ -41,10 +90,20 @@
4190
isa = PBXGroup;
4291
children = (
4392
8873E4CF2E9F61C700ACFF5C /* Usage4Claude.app */,
93+
WIDGETPROD /* Usage4ClaudeWidgetExtension.appex */,
4494
);
4595
name = Products;
4696
sourceTree = "<group>";
4797
};
98+
FWKGROUP /* Frameworks */ = {
99+
isa = PBXGroup;
100+
children = (
101+
WIDGET000 /* WidgetKit.framework */,
102+
WIDGET003 /* SwiftUI.framework */,
103+
);
104+
name = Frameworks;
105+
sourceTree = "<group>";
106+
};
48107
/* End PBXGroup section */
49108

50109
/* Begin PBXNativeTarget section */
@@ -55,10 +114,12 @@
55114
8873E4CB2E9F61C700ACFF5C /* Sources */,
56115
8873E4CC2E9F61C700ACFF5C /* Frameworks */,
57116
8873E4CD2E9F61C700ACFF5C /* Resources */,
117+
WIDGETEMBED /* Embed Foundation Extensions */,
58118
);
59119
buildRules = (
60120
);
61121
dependencies = (
122+
WIDGETDEP /* PBXTargetDependency */,
62123
);
63124
fileSystemSynchronizedGroups = (
64125
8873E4D12E9F61C700ACFF5C /* Usage4Claude */,
@@ -70,6 +131,28 @@
70131
productReference = 8873E4CF2E9F61C700ACFF5C /* Usage4Claude.app */;
71132
productType = "com.apple.product-type.application";
72133
};
134+
WIDGETTARGET /* Usage4ClaudeWidgetExtension */ = {
135+
isa = PBXNativeTarget;
136+
buildConfigurationList = WIDGETCFGLIST /* Build configuration list for PBXNativeTarget "Usage4ClaudeWidgetExtension" */;
137+
buildPhases = (
138+
WIDGETSRC /* Sources */,
139+
WIDGETFWK /* Frameworks */,
140+
WIDGETRES /* Resources */,
141+
);
142+
buildRules = (
143+
);
144+
dependencies = (
145+
);
146+
fileSystemSynchronizedGroups = (
147+
WIDGETGRP /* Usage4ClaudeWidget */,
148+
);
149+
name = Usage4ClaudeWidgetExtension;
150+
packageProductDependencies = (
151+
);
152+
productName = Usage4ClaudeWidgetExtension;
153+
productReference = WIDGETPROD /* Usage4ClaudeWidgetExtension.appex */;
154+
productType = "com.apple.product-type.app-extension";
155+
};
73156
/* End PBXNativeTarget section */
74157

75158
/* Begin PBXProject section */
@@ -84,6 +167,9 @@
84167
CreatedOnToolsVersion = 26.0.1;
85168
LastSwiftMigration = 2600;
86169
};
170+
WIDGETTARGET = {
171+
CreatedOnToolsVersion = 26.0.1;
172+
};
87173
};
88174
};
89175
buildConfigurationList = 8873E4CA2E9F61C700ACFF5C /* Build configuration list for PBXProject "Usage4Claude" */;
@@ -105,6 +191,7 @@
105191
projectRoot = "";
106192
targets = (
107193
8873E4CE2E9F61C700ACFF5C /* Usage4Claude */,
194+
WIDGETTARGET /* Usage4ClaudeWidgetExtension */,
108195
);
109196
};
110197
/* End PBXProject section */
@@ -117,6 +204,13 @@
117204
);
118205
runOnlyForDeploymentPostprocessing = 0;
119206
};
207+
WIDGETRES /* Resources */ = {
208+
isa = PBXResourcesBuildPhase;
209+
buildActionMask = 2147483647;
210+
files = (
211+
);
212+
runOnlyForDeploymentPostprocessing = 0;
213+
};
120214
/* End PBXResourcesBuildPhase section */
121215

122216
/* Begin PBXSourcesBuildPhase section */
@@ -127,8 +221,23 @@
127221
);
128222
runOnlyForDeploymentPostprocessing = 0;
129223
};
224+
WIDGETSRC /* Sources */ = {
225+
isa = PBXSourcesBuildPhase;
226+
buildActionMask = 2147483647;
227+
files = (
228+
);
229+
runOnlyForDeploymentPostprocessing = 0;
230+
};
130231
/* End PBXSourcesBuildPhase section */
131232

233+
/* Begin PBXTargetDependency section */
234+
WIDGETDEP /* PBXTargetDependency */ = {
235+
isa = PBXTargetDependency;
236+
target = WIDGETTARGET /* Usage4ClaudeWidgetExtension */;
237+
targetProxy = WIDGETPROXY /* PBXContainerItemProxy */;
238+
};
239+
/* End PBXTargetDependency section */
240+
132241
/* Begin XCBuildConfiguration section */
133242
8873E4D82E9F61C800ACFF5C /* Debug */ = {
134243
isa = XCBuildConfiguration;
@@ -253,12 +362,12 @@
253362
8873E4DB2E9F61C800ACFF5C /* Debug */ = {
254363
isa = XCBuildConfiguration;
255364
buildSettings = {
365+
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
256366
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
257367
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
258368
CLANG_ENABLE_MODULES = YES;
259-
CODE_SIGN_IDENTITY = "Usage4Claude-CodeSigning";
260-
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Usage4Claude-CodeSigning";
261-
CODE_SIGN_STYLE = Manual;
369+
CODE_SIGN_ENTITLEMENTS = Usage4Claude/Usage4Claude.entitlements;
370+
CODE_SIGN_STYLE = Automatic;
262371
COMBINE_HIDPI_IMAGES = YES;
263372
CURRENT_PROJECT_VERSION = 1;
264373
DEVELOPMENT_TEAM = "";
@@ -303,12 +412,12 @@
303412
8873E4DC2E9F61C800ACFF5C /* Release */ = {
304413
isa = XCBuildConfiguration;
305414
buildSettings = {
415+
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
306416
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
307417
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
308418
CLANG_ENABLE_MODULES = YES;
309-
CODE_SIGN_IDENTITY = "Usage4Claude-CodeSigning";
310-
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Usage4Claude-CodeSigning";
311-
CODE_SIGN_STYLE = Manual;
419+
CODE_SIGN_ENTITLEMENTS = Usage4Claude/Usage4Claude.entitlements;
420+
CODE_SIGN_STYLE = Automatic;
312421
COMBINE_HIDPI_IMAGES = YES;
313422
CURRENT_PROJECT_VERSION = 1;
314423
DEVELOPMENT_TEAM = "";
@@ -349,6 +458,79 @@
349458
};
350459
name = Release;
351460
};
461+
WIDGETCFGDEBUG /* Debug */ = {
462+
isa = XCBuildConfiguration;
463+
buildSettings = {
464+
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
465+
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
466+
CLANG_ENABLE_MODULES = YES;
467+
CODE_SIGN_ENTITLEMENTS = Usage4ClaudeWidget/Usage4ClaudeWidget.entitlements;
468+
CODE_SIGN_STYLE = Automatic;
469+
CURRENT_PROJECT_VERSION = 1;
470+
DEVELOPMENT_TEAM = "";
471+
ENABLE_APP_SANDBOX = YES;
472+
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
473+
GENERATE_INFOPLIST_FILE = YES;
474+
INFOPLIST_FILE = "";
475+
INFOPLIST_KEY_CFBundleDisplayName = "Usage4Claude Widget";
476+
INFOPLIST_KEY_NSHumanReadableCopyright = "";
477+
LD_RUNPATH_SEARCH_PATHS = (
478+
"$(inherited)",
479+
"@executable_path/../Frameworks",
480+
"@executable_path/../../../../Frameworks",
481+
);
482+
MACOSX_DEPLOYMENT_TARGET = 14.0;
483+
MARKETING_VERSION = 2.0.0;
484+
PRODUCT_BUNDLE_IDENTIFIER = xyz.fi5h.Usage4Claude.Widget;
485+
PRODUCT_NAME = "$(TARGET_NAME)";
486+
PROVISIONING_PROFILE_SPECIFIER = "";
487+
REGISTER_APP_GROUPS = YES;
488+
SKIP_INSTALL = YES;
489+
SWIFT_APPROACHABLE_CONCURRENCY = YES;
490+
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
491+
SWIFT_EMIT_LOC_STRINGS = YES;
492+
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
493+
SWIFT_STRICT_CONCURRENCY = minimal;
494+
SWIFT_VERSION = 5.0;
495+
};
496+
name = Debug;
497+
};
498+
WIDGETCFGRELEASE /* Release */ = {
499+
isa = XCBuildConfiguration;
500+
buildSettings = {
501+
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
502+
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
503+
CLANG_ENABLE_MODULES = YES;
504+
CODE_SIGN_ENTITLEMENTS = Usage4ClaudeWidget/Usage4ClaudeWidget.entitlements;
505+
CODE_SIGN_STYLE = Automatic;
506+
CURRENT_PROJECT_VERSION = 1;
507+
DEVELOPMENT_TEAM = "";
508+
ENABLE_APP_SANDBOX = YES;
509+
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
510+
GENERATE_INFOPLIST_FILE = YES;
511+
INFOPLIST_FILE = "";
512+
INFOPLIST_KEY_CFBundleDisplayName = "Usage4Claude Widget";
513+
INFOPLIST_KEY_NSHumanReadableCopyright = "";
514+
LD_RUNPATH_SEARCH_PATHS = (
515+
"$(inherited)",
516+
"@executable_path/../Frameworks",
517+
"@executable_path/../../../../Frameworks",
518+
);
519+
MACOSX_DEPLOYMENT_TARGET = 14.0;
520+
MARKETING_VERSION = 2.0.0;
521+
PRODUCT_BUNDLE_IDENTIFIER = xyz.fi5h.Usage4Claude.Widget;
522+
PRODUCT_NAME = "$(TARGET_NAME)";
523+
PROVISIONING_PROFILE_SPECIFIER = "";
524+
REGISTER_APP_GROUPS = YES;
525+
SKIP_INSTALL = YES;
526+
SWIFT_APPROACHABLE_CONCURRENCY = YES;
527+
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
528+
SWIFT_EMIT_LOC_STRINGS = YES;
529+
SWIFT_STRICT_CONCURRENCY = minimal;
530+
SWIFT_VERSION = 5.0;
531+
};
532+
name = Release;
533+
};
352534
/* End XCBuildConfiguration section */
353535

354536
/* Begin XCConfigurationList section */
@@ -370,6 +552,15 @@
370552
defaultConfigurationIsVisible = 0;
371553
defaultConfigurationName = Release;
372554
};
555+
WIDGETCFGLIST /* Build configuration list for PBXNativeTarget "Usage4ClaudeWidgetExtension" */ = {
556+
isa = XCConfigurationList;
557+
buildConfigurations = (
558+
WIDGETCFGDEBUG /* Debug */,
559+
WIDGETCFGRELEASE /* Release */,
560+
);
561+
defaultConfigurationIsVisible = 0;
562+
defaultConfigurationName = Release;
563+
};
373564
/* End XCConfigurationList section */
374565
};
375566
rootObject = 8873E4C72E9F61C700ACFF5C /* Project object */;

Usage4Claude/Helpers/DataRefreshManager.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import Foundation
1010
import Combine
1111
import OSLog
12+
import WidgetKit
1213

1314
/// 数据刷新管理器
1415
/// 负责管理所有数据刷新、定时器、更新检查和重置验证逻辑
@@ -98,6 +99,9 @@ class DataRefreshManager: ObservableObject {
9899
self.usageData = data
99100
self.errorMessage = nil
100101

102+
// Save to App Group for widget
103+
self.syncDataToWidget(data)
104+
101105
// 智能模式:根据百分比变化调整刷新频率
102106
self.settings.updateSmartMonitoringMode(currentUtilization: data.percentage)
103107

@@ -396,6 +400,19 @@ class DataRefreshManager: ObservableObject {
396400
updateChecker.checkForUpdates(manually: true)
397401
}
398402

403+
// MARK: - Widget Sync
404+
405+
/// Sync usage data to App Group for widget access
406+
/// - Parameter data: The usage data to sync
407+
private func syncDataToWidget(_ data: UsageData) {
408+
let sharedData = SharedUsageData(from: data)
409+
sharedData.save()
410+
411+
// Reload widget timeline
412+
WidgetCenter.shared.reloadTimelines(ofKind: "Usage4ClaudeWidget")
413+
Logger.menuBar.debug("Widget data synced and timeline reloaded")
414+
}
415+
399416
// MARK: - Cleanup
400417

401418
/// 清理所有资源

0 commit comments

Comments
 (0)