Compare commits

..

No commits in common. "4d67d8bffd934a4e9fd0b6443aef5cfcd6029eb4" and "9b99d9c1ff08a824c2c6417176f8b97b0557223c" have entirely different histories.

14 changed files with 173 additions and 395 deletions

View File

@ -7,68 +7,68 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
15550246A9E8DAF25E690478 /* BuggerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB7AD270E93EA343975AC6 /* BuggerApp.swift */; }; F6750BB1FBEE5BC90DD6BDBC /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 207CA4381D0CFEC95263A2DE; };
1E3C1F40E2FF64A041856909 /* BugListPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEABFC668F7B082847F65D38 /* BugListPopover.swift */; }; 15550246A9E8DAF25E690478 /* BuggerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB7AD270E93EA343975AC6; };
1FB7E762C0B6568C47B48EF0 /* URL+Feishu.swift in Sources */ = {isa = PBXBuildFile; fileRef = F888AA5F655343B19839B4C2 /* URL+Feishu.swift */; }; 58F16D20CC26D8248055CC2C /* AppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBEF4F77A6A2E2D041EF581; };
29AE37B117A1422EA21570CA /* DateFormatter+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87C935E6B4D1813AB01186A0 /* DateFormatter+Extensions.swift */; }; 4EA9BDBA3F98AF4A41712A18 /* Bug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846CA001267896C54A78441A; };
29EE2FE56882F97E14AAF58F /* FeishuService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21AC33F380B7DC34C39B61A6 /* FeishuService.swift */; }; DDB9EB345FEBB95DD9976364 /* BugChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B24E1E1D32F7B774D12DD85; };
3467A50268399ABE9804D4D4 /* BugRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1129D6F630904F03D06B5DC4 /* BugRow.swift */; }; A33561284CA44A0045902BCC /* BugPriority.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5359156BD70B765D2BF4E718; };
4309E8C70AE99466545A3E37 /* FeishuAuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A2C50E47966E8ADB0446A0C /* FeishuAuthService.swift */; }; E4CBAE27491AF1C52944FA91 /* BugStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 646C676356A4125B8E8D5FA5; };
47ED652FA0AC818086F85DF3 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED0A90DFCB8D4968E4E3CA1 /* Logger.swift */; }; D2855F82E68FADA8BEA43086 /* AppStateService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1DD178095EFCE7BC121C1C4; };
485A69E15EF1D805F50DF364 /* FloatingWidgetWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2FE32B404AB4388A066991 /* FloatingWidgetWindow.swift */; }; 4309E8C70AE99466545A3E37 /* FeishuAuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A2C50E47966E8ADB0446A0C; };
4EA9BDBA3F98AF4A41712A18 /* Bug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846CA001267896C54A78441A /* Bug.swift */; }; CB529549DB1DBB5F02884300 /* FeishuError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A83E76FBB83849E4B6704D3; };
56D44DCBAF4FE0E1D9E620FA /* PollerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F4628E7B599B69B499E09E9 /* PollerService.swift */; }; CAE624453A3F0375D872170A /* FeishuModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE9FBC39502FA635EF1A6A5C; };
58F16D20CC26D8248055CC2C /* AppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBEF4F77A6A2E2D041EF581 /* AppConfig.swift */; }; 29EE2FE56882F97E14AAF58F /* FeishuService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21AC33F380B7DC34C39B61A6; };
6148104B311B60C79B3B8572 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59CC175DE8E9FE3250757D33 /* NotificationService.swift */; }; 6148104B311B60C79B3B8572 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59CC175DE8E9FE3250757D33; };
64F1EE978536D1CB02B8F0D3 /* KeychainHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF06E1030AA21DA8190169A8 /* KeychainHelper.swift */; }; 56D44DCBAF4FE0E1D9E620FA /* PollerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F4628E7B599B69B499E09E9; };
7B0EAA6D83D40A593E36EE31 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D3693AA93F823859D15256D /* SettingsView.swift */; }; B882D310EB2B5D032375CDDF /* TokenManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7E3775F77DD36C27160A8C; };
A33561284CA44A0045902BCC /* BugPriority.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5359156BD70B765D2BF4E718 /* BugPriority.swift */; }; C5D9B1FACE813CD43236BB77 /* Color+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F99094B0F609809EBDB5F4A; };
AECED74595B57A400437B9FD /* BugStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 344F52FB34B285D91A3E4670 /* BugStore.swift */; }; 29AE37B117A1422EA21570CA /* DateFormatter+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87C935E6B4D1813AB01186A0; };
B066D78DA6F57A882A61C0B2 /* OAuthSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0A744D4F5DB88B1496176B /* OAuthSetupView.swift */; }; 64F1EE978536D1CB02B8F0D3 /* KeychainHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF06E1030AA21DA8190169A8; };
B882D310EB2B5D032375CDDF /* TokenManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7E3775F77DD36C27160A8C /* TokenManager.swift */; }; EEC5853D00ED565270DC867F /* LocalOAuthServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D11523ACB81DBB9D412995; };
C3F7FEB179B079BDDD2AA212 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 09B9A4D84F3F604C49171A89 /* Assets.xcassets */; }; 47ED652FA0AC818086F85DF3 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED0A90DFCB8D4968E4E3CA1; };
C5D9B1FACE813CD43236BB77 /* Color+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F99094B0F609809EBDB5F4A /* Color+Extensions.swift */; }; 1FB7E762C0B6568C47B48EF0 /* URL+Feishu.swift in Sources */ = {isa = PBXBuildFile; fileRef = F888AA5F655343B19839B4C2; };
CAE624453A3F0375D872170A /* FeishuModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE9FBC39502FA635EF1A6A5C /* FeishuModels.swift */; }; AECED74595B57A400437B9FD /* BugStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 344F52FB34B285D91A3E4670; };
CB529549DB1DBB5F02884300 /* FeishuError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A83E76FBB83849E4B6704D3 /* FeishuError.swift */; }; 485A69E15EF1D805F50DF364 /* FloatingWidgetWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2FE32B404AB4388A066991; };
D2855F82E68FADA8BEA43086 /* AppStateService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1DD178095EFCE7BC121C1C4 /* AppStateService.swift */; }; 1E3C1F40E2FF64A041856909 /* BugListPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEABFC668F7B082847F65D38; };
DDB9EB345FEBB95DD9976364 /* BugChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B24E1E1D32F7B774D12DD85 /* BugChange.swift */; }; 3467A50268399ABE9804D4D4 /* BugRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1129D6F630904F03D06B5DC4; };
E4CBAE27491AF1C52944FA91 /* BugStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 646C676356A4125B8E8D5FA5 /* BugStatus.swift */; }; B066D78DA6F57A882A61C0B2 /* OAuthSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0A744D4F5DB88B1496176B; };
EEC5853D00ED565270DC867F /* LocalOAuthServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D11523ACB81DBB9D412995 /* LocalOAuthServer.swift */; }; 7B0EAA6D83D40A593E36EE31 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D3693AA93F823859D15256D; };
F6750BB1FBEE5BC90DD6BDBC /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 207CA4381D0CFEC95263A2DE /* AppDelegate.swift */; }; C3F7FEB179B079BDDD2AA212 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 09B9A4D84F3F604C49171A89; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
09B9A4D84F3F604C49171A89 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
0B24E1E1D32F7B774D12DD85 /* BugChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugChange.swift; sourceTree = "<group>"; };
0E7E3775F77DD36C27160A8C /* TokenManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenManager.swift; sourceTree = "<group>"; };
0ED0A90DFCB8D4968E4E3CA1 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
0F99094B0F609809EBDB5F4A /* Color+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extensions.swift"; sourceTree = "<group>"; };
1129D6F630904F03D06B5DC4 /* BugRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugRow.swift; sourceTree = "<group>"; };
207CA4381D0CFEC95263A2DE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
21AC33F380B7DC34C39B61A6 /* FeishuService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeishuService.swift; sourceTree = "<group>"; };
2A83E76FBB83849E4B6704D3 /* FeishuError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeishuError.swift; sourceTree = "<group>"; };
2D3693AA93F823859D15256D /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
344F52FB34B285D91A3E4670 /* BugStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugStore.swift; sourceTree = "<group>"; };
34D11523ACB81DBB9D412995 /* LocalOAuthServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalOAuthServer.swift; sourceTree = "<group>"; };
3C0A744D4F5DB88B1496176B /* OAuthSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthSetupView.swift; sourceTree = "<group>"; };
4F4628E7B599B69B499E09E9 /* PollerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollerService.swift; sourceTree = "<group>"; };
5359156BD70B765D2BF4E718 /* BugPriority.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugPriority.swift; sourceTree = "<group>"; };
59CC175DE8E9FE3250757D33 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
646C676356A4125B8E8D5FA5 /* BugStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugStatus.swift; sourceTree = "<group>"; };
6A2C50E47966E8ADB0446A0C /* FeishuAuthService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeishuAuthService.swift; sourceTree = "<group>"; };
6E2FE32B404AB4388A066991 /* FloatingWidgetWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingWidgetWindow.swift; sourceTree = "<group>"; };
73AB234B54673C838E33AC7F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
846CA001267896C54A78441A /* Bug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bug.swift; sourceTree = "<group>"; };
87C935E6B4D1813AB01186A0 /* DateFormatter+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateFormatter+Extensions.swift"; sourceTree = "<group>"; };
9FBEF4F77A6A2E2D041EF581 /* AppConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfig.swift; sourceTree = "<group>"; };
CEABFC668F7B082847F65D38 /* BugListPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugListPopover.swift; sourceTree = "<group>"; };
DDDB7AD270E93EA343975AC6 /* BuggerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuggerApp.swift; sourceTree = "<group>"; };
E9B6FE6E06295095649B24A7 /* Bugger.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Bugger.entitlements; sourceTree = "<group>"; };
EF0182DF85A7CD0319333F24 /* Bugger.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Bugger.app; sourceTree = BUILT_PRODUCTS_DIR; }; EF0182DF85A7CD0319333F24 /* Bugger.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Bugger.app; sourceTree = BUILT_PRODUCTS_DIR; };
EF06E1030AA21DA8190169A8 /* KeychainHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainHelper.swift; sourceTree = "<group>"; }; 207CA4381D0CFEC95263A2DE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
DDDB7AD270E93EA343975AC6 /* BuggerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuggerApp.swift; sourceTree = "<group>"; };
9FBEF4F77A6A2E2D041EF581 /* AppConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfig.swift; sourceTree = "<group>"; };
846CA001267896C54A78441A /* Bug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bug.swift; sourceTree = "<group>"; };
0B24E1E1D32F7B774D12DD85 /* BugChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugChange.swift; sourceTree = "<group>"; };
5359156BD70B765D2BF4E718 /* BugPriority.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugPriority.swift; sourceTree = "<group>"; };
646C676356A4125B8E8D5FA5 /* BugStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugStatus.swift; sourceTree = "<group>"; };
F1DD178095EFCE7BC121C1C4 /* AppStateService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateService.swift; sourceTree = "<group>"; }; F1DD178095EFCE7BC121C1C4 /* AppStateService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateService.swift; sourceTree = "<group>"; };
F888AA5F655343B19839B4C2 /* URL+Feishu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Feishu.swift"; sourceTree = "<group>"; }; 6A2C50E47966E8ADB0446A0C /* FeishuAuthService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeishuAuthService.swift; sourceTree = "<group>"; };
2A83E76FBB83849E4B6704D3 /* FeishuError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeishuError.swift; sourceTree = "<group>"; };
FE9FBC39502FA635EF1A6A5C /* FeishuModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeishuModels.swift; sourceTree = "<group>"; }; FE9FBC39502FA635EF1A6A5C /* FeishuModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeishuModels.swift; sourceTree = "<group>"; };
21AC33F380B7DC34C39B61A6 /* FeishuService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeishuService.swift; sourceTree = "<group>"; };
59CC175DE8E9FE3250757D33 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
4F4628E7B599B69B499E09E9 /* PollerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollerService.swift; sourceTree = "<group>"; };
0E7E3775F77DD36C27160A8C /* TokenManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenManager.swift; sourceTree = "<group>"; };
0F99094B0F609809EBDB5F4A /* Color+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extensions.swift"; sourceTree = "<group>"; };
87C935E6B4D1813AB01186A0 /* DateFormatter+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateFormatter+Extensions.swift"; sourceTree = "<group>"; };
EF06E1030AA21DA8190169A8 /* KeychainHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainHelper.swift; sourceTree = "<group>"; };
34D11523ACB81DBB9D412995 /* LocalOAuthServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalOAuthServer.swift; sourceTree = "<group>"; };
0ED0A90DFCB8D4968E4E3CA1 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
F888AA5F655343B19839B4C2 /* URL+Feishu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Feishu.swift"; sourceTree = "<group>"; };
344F52FB34B285D91A3E4670 /* BugStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugStore.swift; sourceTree = "<group>"; };
6E2FE32B404AB4388A066991 /* FloatingWidgetWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingWidgetWindow.swift; sourceTree = "<group>"; };
CEABFC668F7B082847F65D38 /* BugListPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugListPopover.swift; sourceTree = "<group>"; };
1129D6F630904F03D06B5DC4 /* BugRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugRow.swift; sourceTree = "<group>"; };
3C0A744D4F5DB88B1496176B /* OAuthSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthSetupView.swift; sourceTree = "<group>"; };
2D3693AA93F823859D15256D /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
09B9A4D84F3F604C49171A89 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
73AB234B54673C838E33AC7F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
E9B6FE6E06295095649B24A7 /* Bugger.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Bugger.entitlements; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -82,7 +82,7 @@
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
14ED1DECD6D906F498B9ECB5 = { 14ED1DECD6D906F498B9ECB5 /* Root */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
287BE755981C092238309A9A /* Sources */, 287BE755981C092238309A9A /* Sources */,
@ -105,17 +105,6 @@
path = Sources; path = Sources;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
31DF436612EC1C3CD1194AA1 /* Views */ = {
isa = PBXGroup;
children = (
E7E4AD241EEE594C1EEB9724 /* FloatingWidget */,
EA9ED591BB8A18FEBD1B58A3 /* MenuBar */,
74AD95EF49A7D01C83865FAC /* OAuth */,
B70D2E54AD6B19DD455A8F82 /* Settings */,
);
path = Views;
sourceTree = "<group>";
};
65997CD9BD6FFEF8851BF59E /* Models */ = { 65997CD9BD6FFEF8851BF59E /* Models */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -128,30 +117,16 @@
path = Models; path = Models;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
6AB9730AA11690EFC8F21473 /* ViewModels */ = { A8C46168E4DFCE29B056A4E2 /* Services */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
344F52FB34B285D91A3E4670 /* BugStore.swift */, F1DD178095EFCE7BC121C1C4 /* AppStateService.swift */,
59CC175DE8E9FE3250757D33 /* NotificationService.swift */,
4F4628E7B599B69B499E09E9 /* PollerService.swift */,
0E7E3775F77DD36C27160A8C /* TokenManager.swift */,
E0E23D3899D89889546FB428 /* Feishu */,
); );
path = ViewModels; path = Services;
sourceTree = "<group>";
};
74AD95EF49A7D01C83865FAC /* OAuth */ = {
isa = PBXGroup;
children = (
3C0A744D4F5DB88B1496176B /* OAuthSetupView.swift */,
);
path = OAuth;
sourceTree = "<group>";
};
8AD6A16612A2DB7B2F899787 /* Resources */ = {
isa = PBXGroup;
children = (
09B9A4D84F3F604C49171A89 /* Assets.xcassets */,
73AB234B54673C838E33AC7F /* Info.plist */,
E9B6FE6E06295095649B24A7 /* Bugger.entitlements */,
);
path = Resources;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
A6646A2065F53ED50AD2CCA7 /* Utils */ = { A6646A2065F53ED50AD2CCA7 /* Utils */ = {
@ -167,32 +142,23 @@
path = Utils; path = Utils;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
A8C46168E4DFCE29B056A4E2 /* Services */ = { 6AB9730AA11690EFC8F21473 /* ViewModels */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
F1DD178095EFCE7BC121C1C4 /* AppStateService.swift */, 344F52FB34B285D91A3E4670 /* BugStore.swift */,
59CC175DE8E9FE3250757D33 /* NotificationService.swift */,
4F4628E7B599B69B499E09E9 /* PollerService.swift */,
0E7E3775F77DD36C27160A8C /* TokenManager.swift */,
E0E23D3899D89889546FB428 /* Feishu */,
); );
path = Services; path = ViewModels;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
B23D89B2D2DF3745A003AEB1 /* Products */ = { 31DF436612EC1C3CD1194AA1 /* Views */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
EF0182DF85A7CD0319333F24 /* Bugger.app */, E7E4AD241EEE594C1EEB9724 /* FloatingWidget */,
EA9ED591BB8A18FEBD1B58A3 /* MenuBar */,
74AD95EF49A7D01C83865FAC /* OAuth */,
B70D2E54AD6B19DD455A8F82 /* Settings */,
); );
name = Products; path = Views;
sourceTree = "<group>";
};
B70D2E54AD6B19DD455A8F82 /* Settings */ = {
isa = PBXGroup;
children = (
2D3693AA93F823859D15256D /* SettingsView.swift */,
);
path = Settings;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
E0E23D3899D89889546FB428 /* Feishu */ = { E0E23D3899D89889546FB428 /* Feishu */ = {
@ -223,6 +189,40 @@
path = MenuBar; path = MenuBar;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
74AD95EF49A7D01C83865FAC /* OAuth */ = {
isa = PBXGroup;
children = (
3C0A744D4F5DB88B1496176B /* OAuthSetupView.swift */,
);
path = OAuth;
sourceTree = "<group>";
};
B70D2E54AD6B19DD455A8F82 /* Settings */ = {
isa = PBXGroup;
children = (
2D3693AA93F823859D15256D /* SettingsView.swift */,
);
path = Settings;
sourceTree = "<group>";
};
8AD6A16612A2DB7B2F899787 /* Resources */ = {
isa = PBXGroup;
children = (
09B9A4D84F3F604C49171A89 /* Assets.xcassets */,
73AB234B54673C838E33AC7F /* Info.plist */,
E9B6FE6E06295095649B24A7 /* Bugger.entitlements */,
);
path = Resources;
sourceTree = "<group>";
};
B23D89B2D2DF3745A003AEB1 /* Products */ = {
isa = PBXGroup;
children = (
EF0182DF85A7CD0319333F24 /* Bugger.app */,
);
name = Products;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@ -353,18 +353,15 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 2;
FEISHU_APP_ID = cli_aacb82a1c0b8dcc0; FEISHU_APP_ID = "$(FEISHU_APP_ID)";
FEISHU_APP_SECRET = jUkU9ygRuQ0KaZFV2BSPfe33iXCahzNM; FEISHU_APP_SECRET = "$(FEISHU_APP_SECRET)";
FEISHU_BASE_DOMAIN = xmi5p6bgsnf8.feishu.cn; FEISHU_BASE_DOMAIN = "xorbitlab.feishu.cn";
GENERATE_INFOPLIST_FILE = NO; GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = Resources/Info.plist; INFOPLIST_FILE = Resources/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.2; MARKETING_VERSION = 1.1;
PRODUCT_BUNDLE_IDENTIFIER = com.xorbitlab.bugger; PRODUCT_BUNDLE_IDENTIFIER = com.xorbitlab.bugger;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@ -377,18 +374,15 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 2;
FEISHU_APP_ID = cli_aacb82a1c0b8dcc0; FEISHU_APP_ID = "$(FEISHU_APP_ID)";
FEISHU_APP_SECRET = jUkU9ygRuQ0KaZFV2BSPfe33iXCahzNM; FEISHU_APP_SECRET = "$(FEISHU_APP_SECRET)";
FEISHU_BASE_DOMAIN = xmi5p6bgsnf8.feishu.cn; FEISHU_BASE_DOMAIN = "xorbitlab.feishu.cn";
GENERATE_INFOPLIST_FILE = NO; GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = Resources/Info.plist; INFOPLIST_FILE = Resources/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.2; MARKETING_VERSION = 1.1;
PRODUCT_BUNDLE_IDENTIFIER = com.xorbitlab.bugger; PRODUCT_BUNDLE_IDENTIFIER = com.xorbitlab.bugger;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@ -418,6 +412,7 @@
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
/* End XCConfigurationList section */ /* End XCConfigurationList section */
}; };
rootObject = 46F86FAA6BBF9AC94A7E4595 /* Project object */; rootObject = 46F86FAA6BBF9AC94A7E4595 /* Project object */;
} }

View File

@ -4,8 +4,3 @@
FEISHU_APP_ID = cli_your_app_id FEISHU_APP_ID = cli_your_app_id
FEISHU_APP_SECRET = your_app_secret FEISHU_APP_SECRET = your_app_secret
FEISHU_BASE_DOMAIN = xorbitlab.feishu.cn FEISHU_BASE_DOMAIN = xorbitlab.feishu.cn
// Bitable app token & table ID (from the Bitable URL). Baked into the app at
// build time as defaults so internal users don't have to enter them in Settings.
FEISHU_APP_TOKEN = your_bitable_app_token
FEISHU_TABLE_ID = your_bitable_table_id

View File

@ -15,9 +15,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.2</string> <string>1.1</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>3</string> <string>2</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>14.0</string> <string>14.0</string>
<key>LSUIElement</key> <key>LSUIElement</key>
@ -28,9 +28,5 @@
<string>$(FEISHU_APP_SECRET)</string> <string>$(FEISHU_APP_SECRET)</string>
<key>FEISHU_BASE_DOMAIN</key> <key>FEISHU_BASE_DOMAIN</key>
<string>$(FEISHU_BASE_DOMAIN)</string> <string>$(FEISHU_BASE_DOMAIN)</string>
<key>FEISHU_APP_TOKEN</key>
<string>$(FEISHU_APP_TOKEN)</string>
<key>FEISHU_TABLE_ID</key>
<string>$(FEISHU_TABLE_ID)</string>
</dict> </dict>
</plist> </plist>

View File

@ -1,8 +1,8 @@
import Foundation import Foundation
struct AppConfig: Codable, Equatable { struct AppConfig: Codable, Equatable {
var appToken: String = BundledDefault.appToken var appToken: String = ""
var tableId: String = BundledDefault.tableId var tableId: String = ""
var assigneeName: String = "" var assigneeName: String = ""
var fieldMappings: FieldMappings = FieldMappings() var fieldMappings: FieldMappings = FieldMappings()
var pollIntervalSeconds: Int = 300 var pollIntervalSeconds: Int = 300
@ -11,11 +11,7 @@ struct AppConfig: Codable, Equatable {
var dailyRefreshTimes: [String] = [] var dailyRefreshTimes: [String] = []
var showFloatingWidget: Bool = false var showFloatingWidget: Bool = false
var launchAtLogin: Bool = true var launchAtLogin: Bool = true
var feishuBaseDomain: String = BundledDefault.feishuBaseDomain var feishuBaseDomain: String = "xorbitlab.feishu.cn"
/// When false (the default), Bugger waits for the next scheduled poll
/// instead of fetching immediately on launch. Avoids a crash-on-start
/// loop if the table fetch is failing.
var refreshOnStart: Bool = false
struct FieldMappings: Codable, Equatable { struct FieldMappings: Codable, Equatable {
var titleField: String = "Title" var titleField: String = "Title"
@ -53,44 +49,4 @@ struct AppConfig: Codable, Equatable {
var isConfigured: Bool { var isConfigured: Bool {
!appToken.isEmpty && !tableId.isEmpty !appToken.isEmpty && !tableId.isEmpty
} }
// Explicit keys + tolerant decoding so a config persisted before a new
// field was added (missing key) doesn't fail to decode and wipe settings.
enum CodingKeys: String, CodingKey {
case appToken, tableId, assigneeName, fieldMappings
case pollIntervalSeconds, dailyRefreshTimes
case showFloatingWidget, launchAtLogin, feishuBaseDomain, refreshOnStart
}
init() {}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
appToken = try c.decodeIfPresent(String.self, forKey: .appToken) ?? BundledDefault.appToken
tableId = try c.decodeIfPresent(String.self, forKey: .tableId) ?? BundledDefault.tableId
assigneeName = try c.decodeIfPresent(String.self, forKey: .assigneeName) ?? ""
fieldMappings = try c.decodeIfPresent(FieldMappings.self, forKey: .fieldMappings) ?? FieldMappings()
pollIntervalSeconds = try c.decodeIfPresent(Int.self, forKey: .pollIntervalSeconds) ?? 300
dailyRefreshTimes = try c.decodeIfPresent([String].self, forKey: .dailyRefreshTimes) ?? []
showFloatingWidget = try c.decodeIfPresent(Bool.self, forKey: .showFloatingWidget) ?? false
launchAtLogin = try c.decodeIfPresent(Bool.self, forKey: .launchAtLogin) ?? true
feishuBaseDomain = try c.decodeIfPresent(String.self, forKey: .feishuBaseDomain) ?? BundledDefault.feishuBaseDomain
refreshOnStart = try c.decodeIfPresent(Bool.self, forKey: .refreshOnStart) ?? false
}
}
/// Values baked into the app at build time via Config.xcconfig Info.plist.
/// Used as defaults so internal users don't have to enter them in Settings.
enum BundledDefault {
static var appToken: String { string(for: "FEISHU_APP_TOKEN") }
static var tableId: String { string(for: "FEISHU_TABLE_ID") }
static var feishuBaseDomain: String { string(for: "FEISHU_BASE_DOMAIN") }
private static func string(for key: String) -> String {
let raw = (Bundle.main.object(forInfoDictionaryKey: key) as? String) ?? ""
// Xcode leaves an undefined $(...) substitution as the literal string;
// treat that as "not set" rather than showing "$(FEISHU_APP_TOKEN)".
if raw.hasPrefix("$(") { return "" }
return raw.trimmingCharacters(in: .whitespacesAndNewlines)
}
} }

View File

@ -10,7 +10,7 @@ struct Bug: Identifiable, Equatable, Hashable {
let customer: String? let customer: String?
let createdAt: Date let createdAt: Date
let updatedAt: Date let updatedAt: Date
let feishuURL: URL? let feishuURL: URL
var age: TimeInterval { var age: TimeInterval {
Date().timeIntervalSince(createdAt) Date().timeIntervalSince(createdAt)

View File

@ -7,8 +7,6 @@ enum FeishuError: Error, LocalizedError {
case decodingError(Error, rawBody: String? = nil) case decodingError(Error, rawBody: String? = nil)
case notConfigured case notConfigured
case missingCredentials case missingCredentials
case invalidConfiguration(String)
case paginationLimitExceeded
var errorDescription: String? { var errorDescription: String? {
switch self { switch self {
@ -27,10 +25,6 @@ enum FeishuError: Error, LocalizedError {
return "Feishu table not configured. Open Settings." return "Feishu table not configured. Open Settings."
case .missingCredentials: case .missingCredentials:
return "Feishu app credentials are missing. Set FEISHU_APP_ID and FEISHU_APP_SECRET." return "Feishu app credentials are missing. Set FEISHU_APP_ID and FEISHU_APP_SECRET."
case .invalidConfiguration(let detail):
return "Invalid Feishu configuration: \(detail)"
case .paginationLimitExceeded:
return "Feishu table has too many records to load in one pass."
} }
} }
} }

View File

@ -19,16 +19,8 @@ final class FeishuService {
) async throws -> [Bug] { ) async throws -> [Bug] {
var allRecords: [RecordItem] = [] var allRecords: [RecordItem] = []
var pageToken: String? var pageToken: String?
var pages = 0
// 25,000 records at 500/page guard against a runaway cursor that
// would otherwise loop forever accumulating memory.
let maxPages = 50
repeat { repeat {
pages += 1
guard pages <= maxPages else {
throw FeishuError.paginationLimitExceeded
}
let page = try await fetchPage( let page = try await fetchPage(
appToken: config.appToken, appToken: config.appToken,
tableId: config.tableId, tableId: config.tableId,
@ -68,25 +60,16 @@ final class FeishuService {
pageSize: Int = 500, pageSize: Int = 500,
accessToken: String accessToken: String
) async throws -> RecordListData { ) async throws -> RecordListData {
guard !appToken.isEmpty, !tableId.isEmpty else { var components = URLComponents(
throw FeishuError.invalidConfiguration("App token and table ID must not be empty.")
}
guard var components = URLComponents(
string: "\(baseURL)/bitable/v1/apps/\(appToken)/tables/\(tableId)/records" string: "\(baseURL)/bitable/v1/apps/\(appToken)/tables/\(tableId)/records"
) else { )!
throw FeishuError.invalidConfiguration("App token or table ID contains invalid characters.")
}
var queryItems = [URLQueryItem(name: "page_size", value: String(pageSize))] var queryItems = [URLQueryItem(name: "page_size", value: String(pageSize))]
if let pageToken { if let pageToken {
queryItems.append(URLQueryItem(name: "page_token", value: pageToken)) queryItems.append(URLQueryItem(name: "page_token", value: pageToken))
} }
components.queryItems = queryItems components.queryItems = queryItems
guard let url = components.url else { var request = URLRequest(url: components.url!)
throw FeishuError.invalidConfiguration("Could not build records request URL.")
}
var request = URLRequest(url: url)
request.httpMethod = "GET" request.httpMethod = "GET"
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
request.timeoutInterval = 30 request.timeoutInterval = 30

View File

@ -24,11 +24,7 @@ final class NotificationService: NSObject, UNUserNotificationCenterDelegate {
guard isAuthorized else { return } guard isAuthorized else { return }
let significant = changes let significant = changes
.filter { change in .filter { $0.type == .newBug || $0.type == .priorityChanged }
guard change.type == .newBug || change.type == .priorityChanged else { return false }
// Skip activity toasts for bugs that are already done.
return change.bug.status != .resolved && change.bug.status != .closed
}
.prefix(3) .prefix(3)
for change in significant { for change in significant {
@ -60,11 +56,10 @@ final class NotificationService: NSObject, UNUserNotificationCenterDelegate {
return return
} }
var userInfo: [String: Any] = ["bugId": change.bug.id] content.userInfo = [
if let url = change.bug.feishuURL { "bugId": change.bug.id,
userInfo["feishuURL"] = url.absoluteString "feishuURL": change.bug.feishuURL.absoluteString
} ]
content.userInfo = userInfo
let request = UNNotificationRequest( let request = UNNotificationRequest(
identifier: "bugger-\(change.bug.id)-\(Date().timeIntervalSince1970)", identifier: "bugger-\(change.bug.id)-\(Date().timeIntervalSince1970)",

View File

@ -24,16 +24,14 @@ final class PollerService {
tokenManager.isAuthenticated else { tokenManager.isAuthenticated else {
return return
} }
start(interval: TimeInterval(config.pollIntervalSeconds), fetchImmediately: config.refreshOnStart) start(interval: TimeInterval(config.pollIntervalSeconds))
} }
func start(interval: TimeInterval, fetchImmediately: Bool = true) { func start(interval: TimeInterval) {
guard !isRunning else { return } guard !isRunning else { return }
isRunning = true isRunning = true
if fetchImmediately { Task { await performFetch() }
Task { await performFetch() }
}
if interval < 0 { if interval < 0 {
// Daily schedule mode: check every 60s, fire at configured times // Daily schedule mode: check every 60s, fire at configured times

View File

@ -6,26 +6,12 @@ enum FeishuURLBuilder {
appToken: String, appToken: String,
tableId: String, tableId: String,
recordId: String recordId: String
) -> URL? { ) -> URL {
// Feishu Bitable record deep link: uses query parameters. // Feishu Bitable record deep link: uses query parameters
// Returns nil for malformed inputs (e.g. config containing spaces or URL(string: "https://\(baseDomain)/base/\(appToken)?table=\(tableId)&record=\(recordId)")!
// slashes) instead of force-unwrapping and crashing.
var components = URLComponents()
components.scheme = "https"
components.host = baseDomain
components.path = "/base/\(appToken)"
components.queryItems = [
URLQueryItem(name: "table", value: tableId),
URLQueryItem(name: "record", value: recordId)
]
return components.url
} }
static func tableURL(baseDomain: String, appToken: String) -> URL? { static func tableURL(baseDomain: String, appToken: String) -> URL {
var components = URLComponents() URL(string: "https://\(baseDomain)/base/\(appToken)")!
components.scheme = "https"
components.host = baseDomain
components.path = "/base/\(appToken)"
return components.url
} }
} }

View File

@ -2,12 +2,9 @@ import AppKit
import SwiftUI import SwiftUI
final class FloatingWidgetWindow: NSPanel { final class FloatingWidgetWindow: NSPanel {
private let margin: CGFloat = 16
private let bottomOffset: CGFloat = 200
init() { init() {
super.init( super.init(
contentRect: NSRect(x: 0, y: 0, width: 1, height: 1), contentRect: NSRect(x: 0, y: 0, width: 140, height: 70),
styleMask: [.borderless, .nonactivatingPanel], styleMask: [.borderless, .nonactivatingPanel],
backing: .buffered, backing: .buffered,
defer: false defer: false
@ -23,63 +20,20 @@ final class FloatingWidgetWindow: NSPanel {
hidesOnDeactivate = false hidesOnDeactivate = false
animationBehavior = .none animationBehavior = .none
// Place the 1x1 placeholder in the bottom-right corner so the first
// content measurement anchors to the screen's right edge.
if let screen = NSScreen.main { if let screen = NSScreen.main {
let screenFrame = screen.visibleFrame let screenFrame = screen.visibleFrame
setFrameOrigin(NSPoint( setFrameOrigin(NSPoint(x: screenFrame.maxX - 100, y: screenFrame.minY + 200))
x: screenFrame.maxX - 1 - margin,
y: screenFrame.minY + bottomOffset
))
} }
let rootView = FloatingWidgetView { [weak self] size in contentView = NSHostingView(rootView: FloatingWidgetView())
self?.applyContentSize(size)
}
let hostingView = NSHostingView(rootView: rootView)
contentView = hostingView
// Size to the content's intrinsic size up front to avoid a 1x1 flash
// before the first SwiftUI layout pass reports a measurement.
applyContentSize(hostingView.fittingSize)
} }
override var canBecomeKey: Bool { false } override var canBecomeKey: Bool { false }
override var canBecomeMain: Bool { false } override var canBecomeMain: Bool { false }
/// Resizes the window to fit its content while keeping the bottom-right
/// corner anchored, so a growing bug count extends leftward instead of
/// spilling off-screen. Clamped to the visible screen frame as a safety
/// net (e.g. when the user has dragged the widget toward an edge).
private func applyContentSize(_ size: CGSize) {
guard size.width > 0, size.height > 0 else { return }
// Skip no-op resizes to avoid a layout feedback loop with SwiftUI.
guard abs(size.width - frame.width) > 0.5
|| abs(size.height - frame.height) > 0.5 else { return }
var newFrame = frame
// Keep the right edge: grow leftward as the count widens.
newFrame.origin.x = newFrame.maxX - size.width
// Keep the bottom edge: grow upward when the priority row appears.
newFrame.origin.y = newFrame.minY
newFrame.size = size
if let screen = NSScreen.main {
let visible = screen.visibleFrame
newFrame.origin.x = max(newFrame.origin.x, visible.minX)
newFrame.origin.y = min(
max(newFrame.origin.y, visible.minY),
visible.maxY - size.height
)
}
setFrame(newFrame, display: true)
}
} }
struct FloatingWidgetView: View { struct FloatingWidgetView: View {
@State private var bugStore = BugStore.shared @State private var bugStore = BugStore.shared
let onSizeChange: (CGSize) -> Void
private var priorityCounts: [(BugPriority, Int)] { private var priorityCounts: [(BugPriority, Int)] {
let active = bugStore.activeBugs let active = bugStore.activeBugs
@ -102,7 +56,6 @@ struct FloatingWidgetView: View {
Text("\(bugStore.activeBugs.count)") Text("\(bugStore.activeBugs.count)")
.font(.title3) .font(.title3)
.fontWeight(.bold) .fontWeight(.bold)
.monospacedDigit()
} }
if !priorityCounts.isEmpty { if !priorityCounts.isEmpty {
HStack(spacing: 6) { HStack(spacing: 6) {
@ -114,7 +67,6 @@ struct FloatingWidgetView: View {
Text("\(count)") Text("\(count)")
.font(.caption2) .font(.caption2)
.fontWeight(.medium) .fontWeight(.medium)
.monospacedDigit()
} }
} }
} }
@ -129,17 +81,5 @@ struct FloatingWidgetView: View {
) )
} }
.buttonStyle(.plain) .buttonStyle(.plain)
// Let the button take its intrinsic size so the window can fit it
// exactly, regardless of the (initially 1x1) hosting container.
.fixedSize()
.background(
GeometryReader { proxy in
Color.clear
.onAppear { onSizeChange(proxy.size) }
.onChange(of: proxy.size) { _, newSize in
onSizeChange(newSize)
}
}
)
} }
} }

View File

@ -4,7 +4,6 @@ import SwiftUI
struct BugListPopover: View { struct BugListPopover: View {
@State private var bugStore = BugStore.shared @State private var bugStore = BugStore.shared
@State private var tokenManager = TokenManager.shared @State private var tokenManager = TokenManager.shared
@Environment(\.openSettings) private var openSettings
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
@ -91,16 +90,7 @@ struct BugListPopover: View {
Button("Refresh") { Button("Refresh") {
Task { await PollerService.shared.fetchNow() } Task { await PollerService.shared.fetchNow() }
} }
Button { SettingsLink {
openSettings()
// Bugger runs as an accessory (no Dock icon), so the Settings
// window would otherwise open behind whatever is frontmost.
// Activate on the next runloop pass, once the window exists,
// to bring it on top.
DispatchQueue.main.async {
NSRunningApplication.current.activate(options: [.activateAllWindows])
}
} label: {
Label("Settings", systemImage: "gear") Label("Settings", systemImage: "gear")
} }
Button(action: { NSApp.terminate(nil) }) { Button(action: { NSApp.terminate(nil) }) {
@ -114,17 +104,16 @@ struct BugListPopover: View {
} }
private func openInFeishu(_ bug: Bug) { private func openInFeishu(_ bug: Bug) {
guard let url = bug.feishuURL else { return } NSWorkspace.shared.open(bug.feishuURL)
NSWorkspace.shared.open(url)
AppDelegate.shared?.closePopover() AppDelegate.shared?.closePopover()
} }
private func openFeishuTable() { private func openFeishuTable() {
guard let config = AppStateService.shared.config, guard let config = AppStateService.shared.config else { return }
let url = FeishuURLBuilder.tableURL( let url = FeishuURLBuilder.tableURL(
baseDomain: config.feishuBaseDomain, baseDomain: config.feishuBaseDomain,
appToken: config.appToken appToken: config.appToken
) else { return } )
NSWorkspace.shared.open(url) NSWorkspace.shared.open(url)
} }
@ -176,11 +165,6 @@ struct ErrorStateView: View {
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
Button("Retry") {
Task { await PollerService.shared.fetchNow() }
}
.buttonStyle(.bordered)
.padding(.top, 4)
} }
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
.padding() .padding()

View File

@ -13,29 +13,11 @@ struct SettingsView: View {
var body: some View { var body: some View {
ScrollView { ScrollView {
VStack(spacing: 12) { Form {
// Highlighted "Start here" card lives outside the Form so its
// Spacer + multi-element HStack don't disturb the Form's
// two-column label/control layout (which was pushing the
// Feishu Bitable fields off the right edge of the window).
HStack(spacing: 12) {
Image(systemName: "person.fill")
.font(.title2)
.foregroundStyle(.tint)
Text("Your Name")
.font(.headline)
TextField("e.g. Alice", text: $config.assigneeName)
.textFieldStyle(.roundedBorder)
Spacer()
}
.padding(10)
.background(RoundedRectangle(cornerRadius: 8).fill(.tint.opacity(0.08)))
.overlay(RoundedRectangle(cornerRadius: 8).strokeBorder(.tint.opacity(0.35), lineWidth: 1))
Form {
Section("Feishu Bitable") { Section("Feishu Bitable") {
TextField("App Token (from Bitable URL)", text: $config.appToken) TextField("App Token (from Bitable URL)", text: $config.appToken)
TextField("Table ID", text: $config.tableId) TextField("Table ID", text: $config.tableId)
TextField("Your Name (as in Assignee column, optional)", text: $config.assigneeName)
TextField("Feishu domain", text: $config.feishuBaseDomain) TextField("Feishu domain", text: $config.feishuBaseDomain)
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
@ -104,13 +86,6 @@ struct SettingsView: View {
.buttonStyle(.plain) .buttonStyle(.plain)
} }
} }
VStack(alignment: .leading, spacing: 4) {
Toggle("Refresh on launch", isOn: $config.refreshOnStart)
Text("Fetch bugs immediately when Bugger starts. Off = wait for the next scheduled poll.")
.font(.caption)
.foregroundStyle(.secondary)
}
} }
Section("Display") { Section("Display") {
@ -125,35 +100,24 @@ struct SettingsView: View {
Toggle("Launch at login", isOn: $config.launchAtLogin) Toggle("Launch at login", isOn: $config.launchAtLogin)
} }
Section { Section("Field Mappings") {
DisclosureGroup(isExpanded: $showAdvanced) { TextField("Title field", text: $config.fieldMappings.titleField)
Text("Field Mappings") TextField("Priority field", text: $config.fieldMappings.priorityField)
.font(.caption) TextField("Status field", text: $config.fieldMappings.statusField)
.fontWeight(.semibold) TextField("Assignee field", text: $config.fieldMappings.assigneeField)
.foregroundStyle(.secondary) TextField("Reporter field", text: $config.fieldMappings.reporterField)
TextField("Customer field", text: $config.fieldMappings.customerField)
TextField("Created At field", text: $config.fieldMappings.createdAtField)
TextField("Updated At field", text: $config.fieldMappings.updatedAtField)
}
TextField("Title field", text: $config.fieldMappings.titleField) Section("Status Mappings") {
TextField("Priority field", text: $config.fieldMappings.priorityField) Text("Map your Feishu status values to standard Bugger statuses.")
TextField("Status field", text: $config.fieldMappings.statusField) .font(.caption)
TextField("Assignee field", text: $config.fieldMappings.assigneeField) .foregroundStyle(.secondary)
TextField("Reporter field", text: $config.fieldMappings.reporterField)
TextField("Customer field", text: $config.fieldMappings.customerField)
TextField("Created At field", text: $config.fieldMappings.createdAtField)
TextField("Updated At field", text: $config.fieldMappings.updatedAtField)
Text("Status Mappings") ForEach(BugStatus.allCases.filter { $0 != .unknown }, id: \.self) { status in
.font(.caption) statusMappingRow(for: status)
.fontWeight(.semibold)
.foregroundStyle(.secondary)
Text("Map your Feishu status values to standard Bugger statuses.")
.font(.caption)
.foregroundStyle(.secondary)
ForEach(BugStatus.allCases.filter { $0 != .unknown }, id: \.self) { status in
statusMappingRow(for: status)
}
} label: {
Label("Advanced: field & status mappings", systemImage: "slider.horizontal.3")
} }
} }
@ -169,8 +133,7 @@ struct SettingsView: View {
} }
.foregroundStyle(.red) .foregroundStyle(.red)
} }
} // Form }
} // VStack
.padding() .padding()
} }
.frame(minWidth: 500, idealWidth: 520, minHeight: 400, maxHeight: 800) .frame(minWidth: 500, idealWidth: 520, minHeight: 400, maxHeight: 800)

View File

@ -6,6 +6,7 @@ cd "$(dirname "$0")/.."
PROJECT="Bugger.xcodeproj" PROJECT="Bugger.xcodeproj"
SCHEME="Bugger" SCHEME="Bugger"
VERSION="1.1"
BUILD_DIR="build" BUILD_DIR="build"
APP_NAME="Bugger.app" APP_NAME="Bugger.app"
@ -35,16 +36,8 @@ if [ -z "$APP_PATH" ]; then
exit 1 exit 1
fi fi
# Read the marketing version from the built app so the DMG name stays in
# sync with MARKETING_VERSION / CFBundleShortVersionString automatically.
VERSION=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$APP_PATH/Contents/Info.plist")
if [ -z "$VERSION" ]; then
echo "ERROR: Could not read version from $APP_PATH/Contents/Info.plist"
exit 1
fi
echo "" echo ""
echo "=== Built: $APP_PATH (v$VERSION) ===" echo "=== Built: $APP_PATH ==="
echo "" echo ""
# Copy to build root # Copy to build root