feat(concerts): show artwork-themed event details in app

This commit is contained in:
zarzet committed 2026-09-25 12:00:28 +07:00
1 parent ab58cdbb0c
commit abb8dc4a42
18 files changed
+1047 -23

No files matched your search

+4
View File
@@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
C0CE00000000000000000001 /* ConcertCalendarBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0CE00000000000000000002 /* ConcertCalendarBridge.swift */; };
A0D100000000000000000001 /* AudioOutputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0D100000000000000000002 /* AudioOutputView.swift */; };
F59500000000000000000011 /* PlayerWidgetState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F59500000000000000000001 /* PlayerWidgetState.swift */; };
F59500000000000000000012 /* PlayerControlIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = F59500000000000000000002 /* PlayerControlIntent.swift */; };
@@ -70,6 +71,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
C0CE00000000000000000002 /* ConcertCalendarBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcertCalendarBridge.swift; sourceTree = "<group>"; };
A0D100000000000000000002 /* AudioOutputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioOutputView.swift; sourceTree = "<group>"; };
F59500000000000000000009 /* PlayerWidget.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = PlayerWidget.xcconfig; sourceTree = "<group>"; };
F59500000000000000000001 /* PlayerWidgetState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerWidgetState.swift; sourceTree = "<group>"; };
@@ -213,6 +215,7 @@
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
F59500000000000000000004 /* PlayerWidgetBridge.swift */,
A0D100000000000000000002 /* AudioOutputView.swift */,
C0CE00000000000000000002 /* ConcertCalendarBridge.swift */,
F59500000000000000000007 /* Runner.entitlements */,
C03400000000000000000002 /* CoreBackend.swift */,
D08A594730CF000100000002 /* DownloadProgressSubscription.swift */,
@@ -475,6 +478,7 @@
F59500000000000000000012 /* PlayerControlIntent.swift in Sources */,
F59500000000000000000013 /* PlayerWidgetBridge.swift in Sources */,
A0D100000000000000000001 /* AudioOutputView.swift in Sources */,
C0CE00000000000000000001 /* ConcertCalendarBridge.swift in Sources */,
C03400000000000000000001 /* CoreBackend.swift in Sources */,
D08A594730CF000100000001 /* DownloadProgressSubscription.swift in Sources */,
A11CE0012F00000000000001 /* ExtensionCallbackParser.swift in Sources */,
+4
View File
@@ -23,6 +23,7 @@ import UniformTypeIdentifiers
private var lastLibraryScanProgressPayload: String?
private var libraryScanProgressGeneration: UInt64 = 0
private var backendChannel: FlutterMethodChannel?
private var concertCalendar: ConcertCalendarBridge?
private let coreBackend: CoreBackend = createCoreBackend()
private var pendingSessionGrantEvents: [[String: Any]] = []
@@ -55,6 +56,9 @@ import UniformTypeIdentifiers
}
private func configureFlutterEngine(_ messenger: FlutterBinaryMessenger, registry: FlutterPluginRegistry) {
concertCalendar = ConcertCalendarBridge(messenger: messenger) { [weak self] in
self?.activeWindow?.rootViewController
}
PlayerWidgetBridge.shared.attach(messenger)
let channel = FlutterMethodChannel(
name: CHANNEL,
+65
View File
@@ -0,0 +1,65 @@
import EventKit
import EventKitUI
import Flutter
import UIKit
final class ConcertCalendarBridge: NSObject, EKEventEditViewDelegate {
private let channel: FlutterMethodChannel
private let presenter: () -> UIViewController?
private let store = EKEventStore()
init(messenger: FlutterBinaryMessenger, presenter: @escaping () -> UIViewController?) {
channel = FlutterMethodChannel(name: "com.zarz.spotiflac/concert_calendar", binaryMessenger: messenger)
self.presenter = presenter
super.init()
channel.setMethodCallHandler { [weak self] call, result in
guard call.method == "add" else {
result(FlutterMethodNotImplemented)
return
}
guard let self, let data = call.arguments as? [String: Any],
let start = data["start"] as? NSNumber else {
result(false)
return
}
if #available(iOS 17.0, *) {
// The system editor saves only after the user confirms; it
// needs no permission to read the person's existing events.
self.present(data, start: start, result: result)
} else {
self.store.requestAccess(to: .event) { [weak self] allowed, _ in
DispatchQueue.main.async {
guard let self, allowed else { result(false); return }
self.present(data, start: start, result: result)
}
}
}
}
}
private func present(_ data: [String: Any], start: NSNumber, result: @escaping FlutterResult) {
guard let controller = presenter(), controller.viewIfLoaded?.window != nil,
controller.presentedViewController == nil else {
result(false)
return
}
let event = EKEvent(eventStore: store)
event.title = data["title"] as? String
event.location = data["location"] as? String
event.startDate = Date(timeIntervalSince1970: start.doubleValue / 1000)
let end = (data["end"] as? NSNumber)?.doubleValue ?? 0
event.endDate = end > start.doubleValue
? Date(timeIntervalSince1970: end / 1000)
: event.startDate.addingTimeInterval(3600)
if let url = data["url"] as? String { event.url = URL(string: url) }
let editor = EKEventEditViewController()
editor.eventStore = store
editor.event = event
editor.editViewDelegate = self
controller.present(editor, animated: true) { result(true) }
}
func eventEditViewController(_ controller: EKEventEditViewController, didCompleteWith action: EKEventEditViewAction) {
controller.dismiss(animated: true)
}
}
+2
View File
@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSCalendarsUsageDescription</key>
<string>Add a concert to your calendar after you review and save it.</string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleLocalizations</key>