Add embedded Feishu web view for record pages; bump to 1.6
Workflow (流程) fields like 流转状态 are entirely outside the OpenAPI — absent from record reads and silently ignored on writes (confirmed by Feishu engineers in larksuite/openclaw-lark#35). To advance them without leaving the app, the record page can now be rendered in an embedded WKWebView window: - FeishuWebView (persistent cookie store, feishu:// links routed to the system) in a reusable 1100x800 window via AppDelegate.openWebWindow - Entry points: "Open in App" toolbar button in the detail view and "Open in App (Web)" in the popover row context menu - Web content pins a min size — a bare WKWebView in a hosting controller collapses the window to ~1px Also: - Info.plist now uses $(MARKETING_VERSION)/$(CURRENT_PROJECT_VERSION) so the project generator is the single version source (it was hardcoded and overrode build settings) - Version 1.5 -> 1.6
This commit is contained in:
parent
72cff00f72
commit
a29010fd51
|
|
@ -42,6 +42,7 @@
|
||||||
3467A50268399ABE9804D4D4 /* BugRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1129D6F630904F03D06B5DC4; };
|
3467A50268399ABE9804D4D4 /* BugRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1129D6F630904F03D06B5DC4; };
|
||||||
B066D78DA6F57A882A61C0B2 /* OAuthSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0A744D4F5DB88B1496176B; };
|
B066D78DA6F57A882A61C0B2 /* OAuthSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0A744D4F5DB88B1496176B; };
|
||||||
7B0EAA6D83D40A593E36EE31 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D3693AA93F823859D15256D; };
|
7B0EAA6D83D40A593E36EE31 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D3693AA93F823859D15256D; };
|
||||||
|
34A7C60F7F533D8D589F1B0A /* FeishuWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1661D9F3D050C3CD75D910C; };
|
||||||
C3F7FEB179B079BDDD2AA212 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 09B9A4D84F3F604C49171A89; };
|
C3F7FEB179B079BDDD2AA212 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 09B9A4D84F3F604C49171A89; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
|
@ -82,6 +83,7 @@
|
||||||
1129D6F630904F03D06B5DC4 /* BugRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugRow.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>"; };
|
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>"; };
|
2D3693AA93F823859D15256D /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||||
|
C1661D9F3D050C3CD75D910C /* FeishuWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeishuWebView.swift; sourceTree = "<group>"; };
|
||||||
09B9A4D84F3F604C49171A89 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; 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>"; };
|
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>"; };
|
E9B6FE6E06295095649B24A7 /* Bugger.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Bugger.entitlements; sourceTree = "<group>"; };
|
||||||
|
|
@ -180,6 +182,7 @@
|
||||||
EA9ED591BB8A18FEBD1B58A3 /* MenuBar */,
|
EA9ED591BB8A18FEBD1B58A3 /* MenuBar */,
|
||||||
74AD95EF49A7D01C83865FAC /* OAuth */,
|
74AD95EF49A7D01C83865FAC /* OAuth */,
|
||||||
B70D2E54AD6B19DD455A8F82 /* Settings */,
|
B70D2E54AD6B19DD455A8F82 /* Settings */,
|
||||||
|
B8AD0CEC28B09402A27E531C /* Web */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -237,6 +240,14 @@
|
||||||
path = Settings;
|
path = Settings;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
B8AD0CEC28B09402A27E531C /* Web */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
C1661D9F3D050C3CD75D910C /* FeishuWebView.swift */,
|
||||||
|
);
|
||||||
|
path = Web;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
8AD6A16612A2DB7B2F899787 /* Resources */ = {
|
8AD6A16612A2DB7B2F899787 /* Resources */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
|
@ -359,6 +370,7 @@
|
||||||
3467A50268399ABE9804D4D4 /* BugRow.swift in Sources */,
|
3467A50268399ABE9804D4D4 /* BugRow.swift in Sources */,
|
||||||
B066D78DA6F57A882A61C0B2 /* OAuthSetupView.swift in Sources */,
|
B066D78DA6F57A882A61C0B2 /* OAuthSetupView.swift in Sources */,
|
||||||
7B0EAA6D83D40A593E36EE31 /* SettingsView.swift in Sources */,
|
7B0EAA6D83D40A593E36EE31 /* SettingsView.swift in Sources */,
|
||||||
|
34A7C60F7F533D8D589F1B0A /* FeishuWebView.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
@ -393,7 +405,7 @@
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 5;
|
CURRENT_PROJECT_VERSION = 6;
|
||||||
FEISHU_APP_ID = "$(FEISHU_APP_ID)";
|
FEISHU_APP_ID = "$(FEISHU_APP_ID)";
|
||||||
FEISHU_APP_SECRET = "$(FEISHU_APP_SECRET)";
|
FEISHU_APP_SECRET = "$(FEISHU_APP_SECRET)";
|
||||||
FEISHU_BASE_DOMAIN = "$(FEISHU_BASE_DOMAIN)";
|
FEISHU_BASE_DOMAIN = "$(FEISHU_BASE_DOMAIN)";
|
||||||
|
|
@ -401,7 +413,7 @@
|
||||||
INFOPLIST_FILE = Resources/Info.plist;
|
INFOPLIST_FILE = Resources/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
MARKETING_VERSION = 1.5;
|
MARKETING_VERSION = 1.6;
|
||||||
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;
|
||||||
|
|
@ -414,7 +426,7 @@
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 5;
|
CURRENT_PROJECT_VERSION = 6;
|
||||||
FEISHU_APP_ID = "$(FEISHU_APP_ID)";
|
FEISHU_APP_ID = "$(FEISHU_APP_ID)";
|
||||||
FEISHU_APP_SECRET = "$(FEISHU_APP_SECRET)";
|
FEISHU_APP_SECRET = "$(FEISHU_APP_SECRET)";
|
||||||
FEISHU_BASE_DOMAIN = "$(FEISHU_BASE_DOMAIN)";
|
FEISHU_BASE_DOMAIN = "$(FEISHU_BASE_DOMAIN)";
|
||||||
|
|
@ -422,7 +434,7 @@
|
||||||
INFOPLIST_FILE = Resources/Info.plist;
|
INFOPLIST_FILE = Resources/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
MARKETING_VERSION = 1.5;
|
MARKETING_VERSION = 1.6;
|
||||||
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;
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,9 @@
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.5</string>
|
<string>$(MARKETING_VERSION)</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>5</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>14.0</string>
|
<string>14.0</string>
|
||||||
<key>LSUIElement</key>
|
<key>LSUIElement</key>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
private var statusItem: NSStatusItem!
|
private var statusItem: NSStatusItem!
|
||||||
private var popover: NSPopover!
|
private var popover: NSPopover!
|
||||||
private var floatingWidget: FloatingWidgetWindow?
|
private var floatingWidget: FloatingWidgetWindow?
|
||||||
|
private var webWindow: NSWindow?
|
||||||
private let bugStore = BugStore.shared
|
private let bugStore = BugStore.shared
|
||||||
|
|
||||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||||
|
|
@ -94,4 +95,40 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
floatingWidget = nil
|
floatingWidget = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Opens (or reuses) a window rendering the Feishu record page in an
|
||||||
|
/// embedded web view — for actions the API can't do, like advancing the
|
||||||
|
/// 流转状态 workflow.
|
||||||
|
func openWebWindow(url: URL, title: String) {
|
||||||
|
if let webWindow {
|
||||||
|
webWindow.title = title
|
||||||
|
(webWindow.contentViewController as? NSHostingController<FeishuWebWindowContent>)?
|
||||||
|
.rootView = FeishuWebWindowContent(url: url)
|
||||||
|
// Recover from a collapsed window frame.
|
||||||
|
if webWindow.frame.width < 400 {
|
||||||
|
webWindow.setContentSize(NSSize(width: 1100, height: 800))
|
||||||
|
webWindow.center()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let window = NSWindow(
|
||||||
|
contentRect: NSRect(x: 0, y: 0, width: 1100, height: 800),
|
||||||
|
styleMask: [.titled, .closable, .resizable, .miniaturizable],
|
||||||
|
backing: .buffered,
|
||||||
|
defer: false
|
||||||
|
)
|
||||||
|
window.title = title
|
||||||
|
window.contentViewController = NSHostingController(
|
||||||
|
rootView: FeishuWebWindowContent(url: url)
|
||||||
|
)
|
||||||
|
window.contentMinSize = NSSize(width: 600, height: 400)
|
||||||
|
window.isReleasedWhenClosed = false
|
||||||
|
// Accessory app: ensure the window lands on the active Space.
|
||||||
|
window.collectionBehavior.insert(.moveToActiveSpace)
|
||||||
|
webWindow = window
|
||||||
|
}
|
||||||
|
webWindow?.orderFrontRegardless()
|
||||||
|
webWindow?.makeKeyAndOrderFront(nil)
|
||||||
|
// Accessory app: pull the window to the front explicitly.
|
||||||
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,16 @@ struct BugDetailView: View {
|
||||||
}
|
}
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItemGroup(placement: .primaryAction) {
|
ToolbarItemGroup(placement: .primaryAction) {
|
||||||
|
Button {
|
||||||
|
if let url = bug.feishuURL {
|
||||||
|
AppDelegate.shared?.openWebWindow(url: url, title: bug.title)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label("Open in App", systemImage: "rectangle.inset.filled")
|
||||||
|
}
|
||||||
|
.disabled(bug.feishuURL == nil)
|
||||||
|
.help("Render the Feishu record page inside the app (e.g. to advance 流转状态)")
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
copyLink()
|
copyLink()
|
||||||
} label: {
|
} label: {
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ struct BugListPopover: View {
|
||||||
}
|
}
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
Button("Open Details") { openDetails(bug) }
|
Button("Open Details") { openDetails(bug) }
|
||||||
|
Button("Open in App (Web)") { openInAppWeb(bug) }
|
||||||
Button("Open in Feishu") { openInFeishu(bug) }
|
Button("Open in Feishu") { openInFeishu(bug) }
|
||||||
}
|
}
|
||||||
Divider().padding(.leading, 44)
|
Divider().padding(.leading, 44)
|
||||||
|
|
@ -142,6 +143,14 @@ struct BugListPopover: View {
|
||||||
AppDelegate.shared?.closePopover()
|
AppDelegate.shared?.closePopover()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func openInAppWeb(_ bug: Bug) {
|
||||||
|
guard let url = bug.feishuURL else { return }
|
||||||
|
AppDelegate.shared?.openWebWindow(url: url, title: bug.title)
|
||||||
|
bugStore.markSeen(bug.id)
|
||||||
|
AppDelegate.shared?.updateBadge(count: bugStore.activeBugs.count)
|
||||||
|
AppDelegate.shared?.closePopover()
|
||||||
|
}
|
||||||
|
|
||||||
private func openInFeishu(_ bug: Bug) {
|
private func openInFeishu(_ bug: Bug) {
|
||||||
guard let url = bug.feishuURL else { return }
|
guard let url = bug.feishuURL else { return }
|
||||||
NSWorkspace.shared.open(url)
|
NSWorkspace.shared.open(url)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
import AppKit
|
||||||
|
import SwiftUI
|
||||||
|
import WebKit
|
||||||
|
|
||||||
|
/// Embeds the Feishu record page inside the app. Uses the default persistent
|
||||||
|
/// data store, so the Feishu login survives app restarts. Workflow (流程)
|
||||||
|
/// fields can't be reached via the API — this is the in-app way to advance them.
|
||||||
|
struct FeishuWebView: NSViewRepresentable {
|
||||||
|
let url: URL
|
||||||
|
|
||||||
|
func makeNSView(context: Context) -> WKWebView {
|
||||||
|
let configuration = WKWebViewConfiguration()
|
||||||
|
configuration.websiteDataStore = .default()
|
||||||
|
let webView = WKWebView(frame: .zero, configuration: configuration)
|
||||||
|
webView.navigationDelegate = context.coordinator
|
||||||
|
webView.allowsBackForwardNavigationGestures = true
|
||||||
|
webView.load(URLRequest(url: url))
|
||||||
|
return webView
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateNSView(_ webView: WKWebView, context: Context) {
|
||||||
|
if webView.url?.absoluteString != url.absoluteString {
|
||||||
|
webView.load(URLRequest(url: url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator {
|
||||||
|
Coordinator()
|
||||||
|
}
|
||||||
|
|
||||||
|
final class Coordinator: NSObject, WKNavigationDelegate {
|
||||||
|
// Only http(s) navigates inside the webview. Custom schemes the page
|
||||||
|
// may try (e.g. feishu:// to hand off to the desktop client) are
|
||||||
|
// routed to the system instead of failing silently.
|
||||||
|
func webView(
|
||||||
|
_ webView: WKWebView,
|
||||||
|
decidePolicyFor navigationAction: WKNavigationAction
|
||||||
|
) async -> WKNavigationActionPolicy {
|
||||||
|
guard let url = navigationAction.request.url,
|
||||||
|
let scheme = url.scheme else { return .allow }
|
||||||
|
if scheme == "http" || scheme == "https" {
|
||||||
|
return .allow
|
||||||
|
}
|
||||||
|
NSWorkspace.shared.open(url)
|
||||||
|
return .cancel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Window content wrapper: WKWebView has no intrinsic size, so a bare hosting
|
||||||
|
/// controller collapses the window to ~1px — pin a minimum size here.
|
||||||
|
struct FeishuWebWindowContent: View {
|
||||||
|
let url: URL
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
FeishuWebView(url: url)
|
||||||
|
.frame(minWidth: 900, minHeight: 600)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -337,7 +337,7 @@ def build_pbxproj() -> str:
|
||||||
target_settings = [
|
target_settings = [
|
||||||
("ASSETCATALOG_COMPILER_APPICON_NAME", "AppIcon"),
|
("ASSETCATALOG_COMPILER_APPICON_NAME", "AppIcon"),
|
||||||
("COMBINE_HIDPI_IMAGES", "YES"),
|
("COMBINE_HIDPI_IMAGES", "YES"),
|
||||||
("CURRENT_PROJECT_VERSION", "5"),
|
("CURRENT_PROJECT_VERSION", "6"),
|
||||||
("FEISHU_APP_ID", '"$(FEISHU_APP_ID)"'),
|
("FEISHU_APP_ID", '"$(FEISHU_APP_ID)"'),
|
||||||
("FEISHU_APP_SECRET", '"$(FEISHU_APP_SECRET)"'),
|
("FEISHU_APP_SECRET", '"$(FEISHU_APP_SECRET)"'),
|
||||||
("FEISHU_BASE_DOMAIN", '"$(FEISHU_BASE_DOMAIN)"'),
|
("FEISHU_BASE_DOMAIN", '"$(FEISHU_BASE_DOMAIN)"'),
|
||||||
|
|
@ -346,7 +346,7 @@ def build_pbxproj() -> str:
|
||||||
("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.5"),
|
("MARKETING_VERSION", "1.6"),
|
||||||
("PRODUCT_BUNDLE_IDENTIFIER", BUNDLE_ID),
|
("PRODUCT_BUNDLE_IDENTIFIER", BUNDLE_ID),
|
||||||
("PRODUCT_NAME", '"$(TARGET_NAME)"'),
|
("PRODUCT_NAME", '"$(TARGET_NAME)"'),
|
||||||
("SWIFT_EMIT_LOC_STRINGS", "YES"),
|
("SWIFT_EMIT_LOC_STRINGS", "YES"),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue