Merge remote-tracking branch 'origin/main'

This commit is contained in:
zarzet
2026-09-25 00:21:03 +07:00
4 changed files with 106 additions and 20 deletions
+4 -4
View File
@@ -7,12 +7,12 @@
"name": "SpotiFLAC Mobile",
"bundleIdentifier": "com.zarz.spotiflac",
"developerName": "zarzet",
"version": "4.9.6",
"versionDate": "2026-09-07",
"downloadURL": "https://github.com/spotiflacapp/SpotiFLAC-Mobile/releases/download/v4.9.6/SpotiFLAC-v4.9.6-ios-unsigned.ipa",
"version": "5.0.0",
"versionDate": "2026-09-23",
"downloadURL": "https://github.com/spotiflacapp/SpotiFLAC-Mobile/releases/download/v5.0.0/SpotiFLAC-v5.0.0-ios-unsigned.ipa",
"localizedDescription": "SpotiFLAC Mobile is written in Flutter and uses installable extensions for metadata and audio downloads.",
"iconURL": "https://raw.githubusercontent.com/spotiflacapp/SpotiFLAC-Mobile/main/assets/images/logo.png",
"size": 34171700
"size": 34325236
}
]
}
+1 -1
View File
@@ -19,7 +19,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
Flutter: 71a624a5bc0c04062bf19101d501e466baf2fb47
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1
SpotiFLACBackend: e0c81656e8e4c31e86b9f14bbb8e245fb3a14011
PODFILE CHECKSUM: 6ca58d78915f3e4fcb64938b9167b26e33d44a8a
+80 -15
View File
@@ -4,7 +4,7 @@ import UIKit
import UniformTypeIdentifiers
@main
@objc class AppDelegate: FlutterAppDelegate {
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
private let CHANNEL = "com.zarz.spotiflac/backend"
private let DOWNLOAD_PROGRESS_STREAM_CHANNEL = "com.zarz.spotiflac/download_progress_stream"
private let LIBRARY_SCAN_PROGRESS_STREAM_CHANNEL = "com.zarz.spotiflac/library_scan_progress_stream"
@@ -43,11 +43,21 @@ import UniformTypeIdentifiers
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// With the UIScene lifecycle (required from iOS 27) there is no root
// view controller yet: channels and plugins are registered once the
// storyboard's implicit engine exists, in
// didInitializeImplicitFlutterEngine below.
if let url = launchOptions?[.url] as? URL {
_ = handleExtensionOAuthRedirect(url: url)
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
let controller = window?.rootViewController as! FlutterViewController
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
let messenger = engineBridge.applicationRegistrar.messenger()
let channel = FlutterMethodChannel(
name: CHANNEL,
binaryMessenger: controller.binaryMessenger
binaryMessenger: messenger
)
backendChannel = channel
if !pendingSessionGrantEvents.isEmpty {
@@ -59,11 +69,11 @@ import UniformTypeIdentifiers
}
let downloadProgressEvents = FlutterEventChannel(
name: DOWNLOAD_PROGRESS_STREAM_CHANNEL,
binaryMessenger: controller.binaryMessenger
binaryMessenger: messenger
)
let libraryScanProgressEvents = FlutterEventChannel(
name: LIBRARY_SCAN_PROGRESS_STREAM_CHANNEL,
binaryMessenger: controller.binaryMessenger
binaryMessenger: messenger
)
channel.setMethodCallHandler { [weak self] call, result in
@@ -93,19 +103,23 @@ import UniformTypeIdentifiers
}
)
)
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
GeneratedPluginRegistrant.register(with: self)
if let url = launchOptions?[.url] as? URL {
_ = handleExtensionOAuthRedirect(url: url)
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
/// The window of the foreground scene. Under the UIScene lifecycle the
/// app delegate's own `window` is never set.
var activeWindow: UIWindow? {
let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
let active = scenes.first { $0.activationState == .foregroundActive } ?? scenes.first
return active?.windows.first { $0.isKeyWindow } ?? active?.windows.first ?? window
}
/// Extension return URLs:
/// - OAuth: spotiflac://callback?code=...&state=<one_time_nonce>
/// - Signed session: spotiflac://session-grant?grant=...&state=<one_time_nonce>
@discardableResult
private func handleExtensionOAuthRedirect(url: URL) -> Bool {
func handleExtensionOAuthRedirect(url: URL) -> Bool {
guard let route = ExtensionCallbackParser.parse(url) else { return false }
streamQueue.async {
var extensionId = ""
@@ -336,13 +350,23 @@ import UniformTypeIdentifiers
override func applicationDidEnterBackground(_ application: UIApplication) {
super.applicationDidEnterBackground(application)
didEnterBackground()
}
override func applicationWillEnterForeground(_ application: UIApplication) {
super.applicationWillEnterForeground(application)
willEnterForeground()
}
/// Called by the application callbacks above and, under the UIScene
/// lifecycle (where UIKit no longer sends those), by SceneDelegate.
func didEnterBackground() {
if downloadsActive {
beginBackgroundDownloadTask()
}
}
override func applicationWillEnterForeground(_ application: UIApplication) {
super.applicationWillEnterForeground(application)
func willEnterForeground() {
endBackgroundDownloadTask()
}
@@ -588,7 +612,7 @@ import UniformTypeIdentifiers
))
return
}
guard var topController = window?.rootViewController else {
guard var topController = activeWindow?.rootViewController else {
result(FlutterError(
code: "NO_VIEW_CONTROLLER",
message: "No view controller available to present the folder picker",
@@ -745,7 +769,7 @@ import UniformTypeIdentifiers
@available(iOS 13.0, *)
extension AppDelegate: ASWebAuthenticationPresentationContextProviding {
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return window ?? ASPresentationAnchor()
return activeWindow ?? ASPresentationAnchor()
}
}
@@ -816,3 +840,44 @@ private final class ClosureStreamHandler: NSObject, FlutterStreamHandler {
onCancelHandler(arguments)
}
}
/// UIScene lifecycle entry point (required from iOS 27). FlutterSceneDelegate
/// hosts the storyboard's FlutterViewController; this subclass forwards what
/// UIKit now delivers to the scene instead of the app delegate: OAuth
/// redirect URLs and the background/foreground transitions that keep
/// downloads alive.
class SceneDelegate: FlutterSceneDelegate {
private var appDelegate: AppDelegate? {
UIApplication.shared.delegate as? AppDelegate
}
override func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
super.scene(scene, willConnectTo: session, options: connectionOptions)
for context in connectionOptions.urlContexts {
_ = appDelegate?.handleExtensionOAuthRedirect(url: context.url)
}
}
override func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
let unhandled = URLContexts.filter {
appDelegate?.handleExtensionOAuthRedirect(url: $0.url) != true
}
if !unhandled.isEmpty {
super.scene(scene, openURLContexts: unhandled)
}
}
override func sceneDidEnterBackground(_ scene: UIScene) {
super.sceneDidEnterBackground(scene)
appDelegate?.didEnterBackground()
}
override func sceneWillEnterForeground(_ scene: UIScene) {
super.sceneWillEnterForeground(scene)
appDelegate?.willEnterForeground()
}
}
+21
View File
@@ -45,6 +45,27 @@
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>