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; };
|
||||
B066D78DA6F57A882A61C0B2 /* OAuthSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0A744D4F5DB88B1496176B; };
|
||||
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; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
|
|
@ -82,6 +83,7 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
|
@ -180,6 +182,7 @@
|
|||
EA9ED591BB8A18FEBD1B58A3 /* MenuBar */,
|
||||
74AD95EF49A7D01C83865FAC /* OAuth */,
|
||||
B70D2E54AD6B19DD455A8F82 /* Settings */,
|
||||
B8AD0CEC28B09402A27E531C /* Web */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -237,6 +240,14 @@
|
|||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B8AD0CEC28B09402A27E531C /* Web */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C1661D9F3D050C3CD75D910C /* FeishuWebView.swift */,
|
||||
);
|
||||
path = Web;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
8AD6A16612A2DB7B2F899787 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -359,6 +370,7 @@
|
|||
3467A50268399ABE9804D4D4 /* BugRow.swift in Sources */,
|
||||
B066D78DA6F57A882A61C0B2 /* OAuthSetupView.swift in Sources */,
|
||||
7B0EAA6D83D40A593E36EE31 /* SettingsView.swift in Sources */,
|
||||
34A7C60F7F533D8D589F1B0A /* FeishuWebView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
@ -393,7 +405,7 @@
|
|||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 5;
|
||||
CURRENT_PROJECT_VERSION = 6;
|
||||
FEISHU_APP_ID = "$(FEISHU_APP_ID)";
|
||||
FEISHU_APP_SECRET = "$(FEISHU_APP_SECRET)";
|
||||
FEISHU_BASE_DOMAIN = "$(FEISHU_BASE_DOMAIN)";
|
||||
|
|
@ -401,7 +413,7 @@
|
|||
INFOPLIST_FILE = Resources/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 1.5;
|
||||
MARKETING_VERSION = 1.6;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.xorbitlab.bugger;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
|
|
@ -414,7 +426,7 @@
|
|||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 5;
|
||||
CURRENT_PROJECT_VERSION = 6;
|
||||
FEISHU_APP_ID = "$(FEISHU_APP_ID)";
|
||||
FEISHU_APP_SECRET = "$(FEISHU_APP_SECRET)";
|
||||
FEISHU_BASE_DOMAIN = "$(FEISHU_BASE_DOMAIN)";
|
||||
|
|
@ -422,7 +434,7 @@
|
|||
INFOPLIST_FILE = Resources/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 1.5;
|
||||
MARKETING_VERSION = 1.6;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.xorbitlab.bugger;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.5</string>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>5</string>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>14.0</string>
|
||||
<key>LSUIElement</key>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
private var statusItem: NSStatusItem!
|
||||
private var popover: NSPopover!
|
||||
private var floatingWidget: FloatingWidgetWindow?
|
||||
private var webWindow: NSWindow?
|
||||
private let bugStore = BugStore.shared
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
|
|
@ -94,4 +95,40 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
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 {
|
||||
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 {
|
||||
copyLink()
|
||||
} label: {
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ struct BugListPopover: View {
|
|||
}
|
||||
.contextMenu {
|
||||
Button("Open Details") { openDetails(bug) }
|
||||
Button("Open in App (Web)") { openInAppWeb(bug) }
|
||||
Button("Open in Feishu") { openInFeishu(bug) }
|
||||
}
|
||||
Divider().padding(.leading, 44)
|
||||
|
|
@ -142,6 +143,14 @@ struct BugListPopover: View {
|
|||
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) {
|
||||
guard let url = bug.feishuURL else { return }
|
||||
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 = [
|
||||
("ASSETCATALOG_COMPILER_APPICON_NAME", "AppIcon"),
|
||||
("COMBINE_HIDPI_IMAGES", "YES"),
|
||||
("CURRENT_PROJECT_VERSION", "5"),
|
||||
("CURRENT_PROJECT_VERSION", "6"),
|
||||
("FEISHU_APP_ID", '"$(FEISHU_APP_ID)"'),
|
||||
("FEISHU_APP_SECRET", '"$(FEISHU_APP_SECRET)"'),
|
||||
("FEISHU_BASE_DOMAIN", '"$(FEISHU_BASE_DOMAIN)"'),
|
||||
|
|
@ -346,7 +346,7 @@ def build_pbxproj() -> str:
|
|||
("LD_RUNPATH_SEARCH_PATHS",
|
||||
'"$(inherited) @executable_path/../Frameworks"'),
|
||||
("MACOSX_DEPLOYMENT_TARGET", "14.0"),
|
||||
("MARKETING_VERSION", "1.5"),
|
||||
("MARKETING_VERSION", "1.6"),
|
||||
("PRODUCT_BUNDLE_IDENTIFIER", BUNDLE_ID),
|
||||
("PRODUCT_NAME", '"$(TARGET_NAME)"'),
|
||||
("SWIFT_EMIT_LOC_STRINGS", "YES"),
|
||||
|
|
|
|||
Loading…
Reference in New Issue