feat(ios): add iOS 26+ continued processing and Live Activities (#1514)

This commit is contained in:
UltraGeek0102 authored and GitHub committed 2026-09-15 22:49:11 +08:00
1 parent cee5f3336e
commit e47a20a1bd
18 files changed
+2983 -3

No files matched your search

+12
View File
@@ -7,6 +7,7 @@ import (
nativebridge "github.com/GopeedLab/gopeed/bind/native"
"github.com/GopeedLab/gopeed/internal/webview/rpcprovider"
goapi "github.com/GopeedLab/gopeed/pkg/api"
"github.com/GopeedLab/gopeed/pkg/rest"
"github.com/GopeedLab/gopeed/pkg/rest/model"
)
@@ -90,3 +91,14 @@ func applyWebViewProvider(config *model.StartConfig) {
}
config.WebViewProvider = rpcprovider.New(*config.WebViewRPCConfig)
}
func LiveActivityTaskEventMask() int64 {
return int64(
goapi.TaskEventDone |
goapi.TaskEventError |
goapi.TaskEventStart |
goapi.TaskEventProgress |
goapi.TaskEventPause |
goapi.TaskEventDelete,
)
}
+22
View File
@@ -10,8 +10,15 @@ import (
type TaskEventMask uint64
const (
// Keep these first two unchanged because Flutter already uses
// bit 0 for done and bit 1 for error.
TaskEventDone TaskEventMask = 1 << iota
TaskEventError
TaskEventStart
TaskEventProgress
TaskEventPause
TaskEventDelete
)
type TaskEvent struct {
@@ -42,11 +49,26 @@ func (s *Service) emitTaskEvent(event *download.Event) {
}
var mask TaskEventMask
switch event.Key {
case download.EventKeyDone:
mask = TaskEventDone
case download.EventKeyError:
mask = TaskEventError
case download.EventKeyStart:
mask = TaskEventStart
case download.EventKeyProgress:
mask = TaskEventProgress
case download.EventKeyPause:
mask = TaskEventPause
case download.EventKeyDelete:
mask = TaskEventDelete
default:
return
}
@@ -0,0 +1,236 @@
import ActivityKit
import SwiftUI
import WidgetKit
@main
struct GopeedLiveActivityBundle: WidgetBundle {
var body: some Widget {
GopeedLiveActivityWidget()
}
}
struct GopeedLiveActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(
for: GopeedDownloadAttributes.self
) { context in
// MARK: Lock Screen / notification banner
VStack(spacing: 10) {
HStack {
Image(systemName: statusIcon(context.state.status))
.font(.title3)
VStack(alignment: .leading, spacing: 2) {
Text(context.attributes.fileName)
.font(.headline)
.lineLimit(1)
Text(statusText(context.state.status))
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Text(
"\(Int(context.state.progress * 100))%"
)
.font(.headline)
.monospacedDigit()
}
progressView(context)
HStack {
Text(
"\(formatBytes(context.state.downloaded)) / \(formatBytes(context.state.total))"
)
Spacer()
if context.state.speed > 0 {
Text(
"\(formatBytes(context.state.speed))/s"
)
}
}
.font(.caption)
.foregroundStyle(.secondary)
}
.padding()
.activityBackgroundTint(.black.opacity(0.85))
.activitySystemActionForegroundColor(.white)
} dynamicIsland: { context in
DynamicIsland {
// MARK: Expanded Dynamic Island
DynamicIslandExpandedRegion(.leading) {
Image(
systemName: statusIcon(
context.state.status
)
)
.font(.title3)
}
DynamicIslandExpandedRegion(.center) {
Text(context.attributes.fileName)
.lineLimit(1)
.font(.headline)
}
DynamicIslandExpandedRegion(.trailing) {
Text(
"\(Int(context.state.progress * 100))%"
)
.monospacedDigit()
.font(.headline)
}
DynamicIslandExpandedRegion(.bottom) {
VStack(spacing: 6) {
progressView(context)
HStack {
Text(
"\(formatBytes(context.state.downloaded)) / \(formatBytes(context.state.total))"
)
Spacer()
if context.state.speed > 0 {
Text(
"\(formatBytes(context.state.speed))/s"
)
}
}
.font(.caption2)
.foregroundStyle(.secondary)
}
}
} compactLeading: {
Image(
systemName: statusIcon(
context.state.status
)
)
} compactTrailing: {
Text(
"\(Int(context.state.progress * 100))%"
)
.monospacedDigit()
} minimal: {
Image(systemName: "arrow.down")
}
}
}
// MARK: - Progress bar
@ViewBuilder
private func progressView(
_ context:
ActivityViewContext<GopeedDownloadAttributes>
) -> some View {
if context.state.usesEstimatedProgress,
context.state.estimatedEnd >
context.state.estimatedStart {
ProgressView(
timerInterval:
context.state.estimatedStart
...
context.state.estimatedEnd,
countsDown: false
)
} else {
ProgressView(
value: context.state.progress,
total: 1.0
)
}
}
// MARK: - Formatting
private func formatBytes(
_ bytes: Int64
) -> String {
guard bytes > 0 else {
return "0 B"
}
return ByteCountFormatter.string(
fromByteCount: bytes,
countStyle: .file
)
}
private func statusIcon(
_ status: String
) -> String {
switch status {
case "done":
return "checkmark.circle.fill"
case "error":
return "exclamationmark.triangle.fill"
case "pause", "paused":
return "pause.circle.fill"
default:
return "arrow.down.circle.fill"
}
}
private func statusText(
_ status: String
) -> String {
switch status {
case "done":
return "Download complete"
case "error":
return "Download failed"
case "pause", "paused":
return "Paused"
default:
return "Downloading"
}
}
}
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-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>CFBundleDisplayName</key>
<string>Gopeed</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string>
</dict>
</dict>
</plist>
@@ -7,13 +7,19 @@
objects = {
/* Begin PBXBuildFile section */
032447D610CFAF8A75642212 /* GopeedLiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB8C142A455C80367DDD68AE /* GopeedLiveActivityManager.swift */; };
0C585CBA2D41E28900FF2EC0 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C585CB92D41E28900FF2EC0 /* ShareViewController.swift */; };
0C585CBD2D41E28900FF2EC0 /* Base in Resources */ = {isa = PBXBuildFile; fileRef = 0C585CBC2D41E28900FF2EC0 /* Base */; };
0C585CC12D41E28900FF2EC0 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 0C585CB72D41E28900FF2EC0 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
1045ACE898D35C0D8D8A993E /* GopeedDownloadAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09AC32C4BF90EAC54D048584 /* GopeedDownloadAttributes.swift */; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
1E29080079AAFD1CF2C71262 /* GopeedDownloadAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09AC32C4BF90EAC54D048584 /* GopeedDownloadAttributes.swift */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
3EF29839EA13551EB8144269 /* GopeedLiveActivityExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 76ED41E0163DFA97F018D1AC /* GopeedLiveActivityExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
5BF69191A426106A38B62F9F /* GopeedContinuedProcessingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3879B6DB5398A19281BA5F8F /* GopeedContinuedProcessingManager.swift */; };
6085D504396791024CEC496B /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8823E31DA135C410E362683E /* Pods_RunnerTests.framework */; };
6243BE8F995F5284DBDD7DE3 /* GopeedLiveActivityWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C9F8DE4B991051758F2BE1 /* GopeedLiveActivityWidget.swift */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
@@ -30,6 +36,13 @@
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
03B416C78831516F6591BD24 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = FFA6D07C1B1CB56543DE4B67;
remoteInfo = GopeedLiveActivityExtension;
};
0C585CBF2D41E28900FF2EC0 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
@@ -54,6 +67,7 @@
dstSubfolderSpec = 13;
files = (
0C585CC12D41E28900FF2EC0 /* ShareExtension.appex in Embed Foundation Extensions */,
3EF29839EA13551EB8144269 /* GopeedLiveActivityExtension.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
@@ -72,6 +86,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
09AC32C4BF90EAC54D048584 /* GopeedDownloadAttributes.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GopeedDownloadAttributes.swift; sourceTree = "<group>"; };
0C585CB72D41E28900FF2EC0 /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
0C585CB92D41E28900FF2EC0 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
0C585CBC2D41E28900FF2EC0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
@@ -85,11 +100,15 @@
3217CFC346202B3E2A8C52DD /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
35C9F8DE4B991051758F2BE1 /* GopeedLiveActivityWidget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GopeedLiveActivityWidget.swift; sourceTree = "<group>"; };
3879B6DB5398A19281BA5F8F /* GopeedContinuedProcessingManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GopeedContinuedProcessingManager.swift; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
52A13633B52EDC9D3F479DFD /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
55356FCC7E573C3FEEC2DF4D /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
5B918B8D5281BAE2DEA6BE3C /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
76ED41E0163DFA97F018D1AC /* GopeedLiveActivityExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = GopeedLiveActivityExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
78EE8538B1DF04DAF73E75D3 /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.profile.xcconfig"; sourceTree = "<group>"; };
78F0D48FED63C04B9A3FFDE4 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
@@ -112,6 +131,7 @@
D554135997A5C7A2C62E1B64 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
D081C25B294826C0006EB10B /* libc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; };
D0E623A729482D030001185E /* Libgopeed.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Libgopeed.xcframework; path = Frameworks/Libgopeed.xcframework; sourceTree = "<group>"; };
EB8C142A455C80367DDD68AE /* GopeedLiveActivityManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GopeedLiveActivityManager.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -123,6 +143,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
2134C1710299C602C39EE9D6 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -194,6 +221,16 @@
name = Frameworks;
sourceTree = "<group>";
};
5FB22DCA9F2EA11B7C1F1A22 /* GopeedLiveActivity */ = {
isa = PBXGroup;
children = (
35C9F8DE4B991051758F2BE1 /* GopeedLiveActivityWidget.swift */,
52A13633B52EDC9D3F479DFD /* Info.plist */,
);
name = GopeedLiveActivity;
path = GopeedLiveActivity;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
@@ -215,6 +252,7 @@
331C8082294A63A400263BE5 /* RunnerTests */,
2B6C6F082D376286EF3D5FA3 /* Pods */,
44BA5D7C75A3E4957F6560C7 /* Frameworks */,
5FB22DCA9F2EA11B7C1F1A22 /* GopeedLiveActivity */,
);
sourceTree = "<group>";
};
@@ -224,6 +262,7 @@
97C146EE1CF9000F007C117D /* Runner.app */,
0C585CB72D41E28900FF2EC0 /* ShareExtension.appex */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
76ED41E0163DFA97F018D1AC /* GopeedLiveActivityExtension.appex */,
);
name = Products;
sourceTree = "<group>";
@@ -244,6 +283,9 @@
A20000000000000000000003 /* GopeedTaskEventForwarder.h */,
A20000000000000000000002 /* GopeedTaskEventForwarder.m */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
09AC32C4BF90EAC54D048584 /* GopeedDownloadAttributes.swift */,
EB8C142A455C80367DDD68AE /* GopeedLiveActivityManager.swift */,
3879B6DB5398A19281BA5F8F /* GopeedContinuedProcessingManager.swift */,
);
path = Runner;
sourceTree = "<group>";
@@ -306,12 +348,30 @@
);
dependencies = (
0C585CC02D41E28900FF2EC0 /* PBXTargetDependency */,
FBEBBD3469D2B152FDD0B424 /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
FFA6D07C1B1CB56543DE4B67 /* GopeedLiveActivityExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = DAF208923F589C87CBB49D56 /* Build configuration list for PBXNativeTarget "GopeedLiveActivityExtension" */;
buildPhases = (
1A1326FD8C4C86ED5A529666 /* Sources */,
2134C1710299C602C39EE9D6 /* Frameworks */,
9AB02E797D34C8878FDBED22 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = GopeedLiveActivityExtension;
productName = GopeedLiveActivityExtension;
productReference = 76ED41E0163DFA97F018D1AC /* GopeedLiveActivityExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -351,6 +411,7 @@
97C146ED1CF9000F007C117D /* Runner */,
0C585CB62D41E28900FF2EC0 /* ShareExtension */,
331C8080294A63A400263BE5 /* RunnerTests */,
FFA6D07C1B1CB56543DE4B67 /* GopeedLiveActivityExtension */,
);
};
/* End PBXProject section */
@@ -382,6 +443,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
9AB02E797D34C8878FDBED22 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
@@ -510,6 +578,15 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
1A1326FD8C4C86ED5A529666 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
6243BE8F995F5284DBDD7DE3 /* GopeedLiveActivityWidget.swift in Sources */,
1045ACE898D35C0D8D8A993E /* GopeedDownloadAttributes.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -527,6 +604,9 @@
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
A10000000000000000000001 /* LocationKeepAliveManager.swift in Sources */,
A20000000000000000000001 /* GopeedTaskEventForwarder.m in Sources */,
1E29080079AAFD1CF2C71262 /* GopeedDownloadAttributes.swift in Sources */,
032447D610CFAF8A75642212 /* GopeedLiveActivityManager.swift in Sources */,
5BF69191A426106A38B62F9F /* GopeedContinuedProcessingManager.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -543,6 +623,12 @@
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
FBEBBD3469D2B152FDD0B424 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
name = GopeedLiveActivityExtension;
target = FFA6D07C1B1CB56543DE4B67 /* GopeedLiveActivityExtension */;
targetProxy = 03B416C78831516F6591BD24 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
@@ -774,6 +860,61 @@
};
name = Profile;
};
6CADF5E48DB3AF5F00998F2A /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB31CF90195004384FC /* Generated.xcconfig */;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_OBJC_WEAK = NO;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = GopeedLiveActivity/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Gopeed;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
PRODUCT_BUNDLE_IDENTIFIER = com.gopeed.gopeed.GopeedLiveActivityExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
91F84B2C36416D1F9C500144 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB31CF90195004384FC /* Generated.xcconfig */;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_OBJC_WEAK = NO;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = GopeedLiveActivity/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Gopeed;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
PRODUCT_BUNDLE_IDENTIFIER = com.gopeed.gopeed.GopeedLiveActivityExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -930,6 +1071,34 @@
};
name = Release;
};
ABCC87B2280A98A3C9E482CF /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB31CF90195004384FC /* Generated.xcconfig */;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_OBJC_WEAK = NO;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = GopeedLiveActivity/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Gopeed;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
PRODUCT_BUNDLE_IDENTIFIER = com.gopeed.gopeed.GopeedLiveActivityExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -973,6 +1142,16 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
DAF208923F589C87CBB49D56 /* Build configuration list for PBXNativeTarget "GopeedLiveActivityExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
6CADF5E48DB3AF5F00998F2A /* Release */,
91F84B2C36416D1F9C500144 /* Debug */,
ABCC87B2280A98A3C9E482CF /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
+63 -2
View File
@@ -63,8 +63,19 @@ import UIKit
GopeedInvokeAsyncWithResult(method, path, query, body, requestID, result)
case "subscribeTaskEvents":
let arguments = call.arguments as? [String: Any]
let mask = (arguments?["mask"] as? NSNumber)?.int64Value ?? 0
GopeedSubscribeTaskEventsWithForwarder(mask, mask == 0 ? nil : taskEventForwarder)
let flutterMask =
(arguments?["mask"] as? NSNumber)?.int64Value ?? 0
let liveActivityMask =
LibgopeedLiveActivityTaskEventMask()
let combinedMask =
flutterMask | liveActivityMask
GopeedSubscribeTaskEventsWithForwarder(
combinedMask,
taskEventForwarder
)
result(nil)
default:
result(FlutterMethodNotImplemented)
@@ -91,5 +102,55 @@ import UIKit
result(FlutterMethodNotImplemented)
}
}
let continuedProcessingChannel =
FlutterMethodChannel(
name: "gopeed/continued_processing",
binaryMessenger: messenger
)
continuedProcessingChannel
.setMethodCallHandler { call, result in
switch call.method {
case "isSupported":
if #available(iOS 26.0, *) {
result(true)
} else {
result(false)
}
case "setEnabled":
let arguments =
call.arguments
as? [String: Any]
let enabled =
arguments?["enabled"]
as? Bool
?? false
if #available(iOS 26.0, *) {
GopeedContinuedProcessingManager
.shared
.setEnabled(enabled) {
success in
result(success)
}
} else {
result(!enabled)
}
default:
result(
FlutterMethodNotImplemented
)
}
}
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,28 @@
import ActivityKit
import Foundation
@available(iOS 16.2, *)
struct GopeedDownloadAttributes: ActivityAttributes {
struct ContentState: Codable, Hashable {
// Real confirmed progress.
var progress: Double
var downloaded: Int64
var total: Int64
var speed: Int64
var status: String
// Used by the self-moving estimated progress bar.
var estimatedStart: Date
var estimatedEnd: Date
var usesEstimatedProgress: Bool
}
// Static properties.
var taskId: String
var fileName: String
var generation: String
}
File diff suppressed because it is too large. Load diff
@@ -13,6 +13,18 @@ FOUNDATION_EXPORT void GopeedSubscribeTaskEventsWithForwarder(
int64_t mask,
GopeedTaskEventForwarder * _Nullable listener);
typedef void (^GopeedNativeInvokeCompletion)(
BOOL success,
NSString *payload);
FOUNDATION_EXPORT void GopeedInvokeAsyncNative(
NSString *method,
NSString *path,
NSString *query,
NSString *body,
GopeedNativeInvokeCompletion completion);
FOUNDATION_EXPORT void GopeedInvokeAsyncWithResult(
NSString *method,
NSString *path,
@@ -2,6 +2,8 @@
#import <Libgopeed/Libgopeed.h>
#import "Runner-Swift.h"
@interface GopeedTaskEventForwarder () <LibgopeedTaskEventListener>
@property(nonatomic, strong) FlutterMethodChannel *channel;
@@ -47,6 +49,45 @@
@end
@interface GopeedNativeInvokeResultForwarder
: NSObject <LibgopeedInvokeResultListener>
@property(nonatomic, copy, nullable)
GopeedNativeInvokeCompletion completion;
- (instancetype)initWithCompletion:
(GopeedNativeInvokeCompletion)completion;
@end
@implementation GopeedNativeInvokeResultForwarder
- (instancetype)initWithCompletion:
(GopeedNativeInvokeCompletion)completion {
self = [super init];
if (self) {
_completion = [completion copy];
}
return self;
}
- (void)onResult:(int64_t)requestID
success:(BOOL)success
payload:(NSString * _Nullable)payload {
GopeedNativeInvokeCompletion completion =
self.completion;
self.completion = nil;
if (completion == nil) {
return;
}
completion(success, payload ?: @"");
}
@end
@implementation GopeedTaskEventForwarder
- (instancetype)initWithChannel:(FlutterMethodChannel *)channel {
@@ -59,9 +100,59 @@
- (void)onTaskEvent:(NSString * _Nullable)payload {
NSString *arguments = payload ?: @"";
NSData *data =
[arguments dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *json = nil;
if (data) {
json =
[NSJSONSerialization JSONObjectWithData:data
options:0
error:nil];
}
NSString *type = json[@"type"];
NSString *taskID = json[@"taskId"];
BOOL continuedProcessingHandlesTask = NO;
// iOS 26 system continued-processing Live Activity.
if (@available(iOS 26.0, *)) {
GopeedContinuedProcessingManager *manager =
[GopeedContinuedProcessingManager shared];
[manager handleTaskEventPayload:arguments];
if (taskID.length > 0) {
continuedProcessingHandlesTask =
[manager isHandlingTaskId:taskID];
}
}
// When BGCPT is active, let Apple's system Live Activity
// represent progress. Otherwise keep using our existing
// custom Gopeed ActivityKit implementation.
if (!continuedProcessingHandlesTask) {
[[GopeedLiveActivityManager shared]
handleTaskEventPayload:arguments];
}
// Flutter currently understands only done/error.
BOOL flutterEvent =
[type isEqualToString:@"task.done"] ||
[type isEqualToString:@"task.error"];
if (!flutterEvent) {
return;
}
FlutterMethodChannel *channel = self.channel;
dispatch_async(dispatch_get_main_queue(), ^{
[channel invokeMethod:@"taskEvent" arguments:arguments];
[channel invokeMethod:@"taskEvent"
arguments:arguments];
});
}
@@ -84,3 +175,26 @@ void GopeedInvokeAsyncWithResult(
[[GopeedInvokeResultForwarder alloc] initWithResult:result];
LibgopeedInvokeAsync(method, path, query, body, requestID, forwarder);
}
void GopeedInvokeAsyncNative(
NSString *method,
NSString *path,
NSString *query,
NSString *body,
GopeedNativeInvokeCompletion completion) {
if (completion == nil) {
return;
}
GopeedNativeInvokeResultForwarder *forwarder =
[[GopeedNativeInvokeResultForwarder alloc]
initWithCompletion:completion];
LibgopeedInvokeAsync(
method,
path,
query,
body,
0,
forwarder);
}
+10
View File
@@ -115,6 +115,7 @@
<key>UIBackgroundModes</key>
<array>
<string>location</string>
<string>processing</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>
@@ -156,5 +157,14 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSSupportsLiveActivities</key>
<true/>
<key>NSSupportsLiveActivitiesFrequentUpdates</key>
<true/>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).continuedDownload.*</string>
</array>
</dict>
</plist>
@@ -125,6 +125,7 @@ class ExtraConfig {
bool notifyWhenNewVersion;
bool desktopNotification;
bool backgroundLocationKeepAlive;
bool backgroundContinuedProcessing;
WindowStateConfig windowState;
Map<String, String> bookmarks;
List<String> createHistory;
@@ -146,6 +147,7 @@ class ExtraConfig {
this.notifyWhenNewVersion = true,
this.desktopNotification = true,
this.backgroundLocationKeepAlive = false,
this.backgroundContinuedProcessing = false,
WindowStateConfig? windowState,
this.bookmarks = const {},
this.createHistory = const [],
@@ -119,6 +119,10 @@ ExtraConfig _$ExtraConfigFromJson(Map<String, dynamic> json) =>
notifyWhenNewVersion: json['notifyWhenNewVersion'] as bool? ?? true,
desktopNotification: json['desktopNotification'] as bool? ?? true,
backgroundLocationKeepAlive: json['backgroundLocationKeepAlive'] as bool? ?? false,
backgroundContinuedProcessing:
json['backgroundContinuedProcessing']
as bool? ??
false,
windowState: json['windowState'] == null
? null
: WindowStateConfig.fromJson(json['windowState'] as Map<String, dynamic>?),
@@ -146,6 +150,8 @@ Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) => <String, dynam
'notifyWhenNewVersion': instance.notifyWhenNewVersion,
'desktopNotification': instance.desktopNotification,
'backgroundLocationKeepAlive': instance.backgroundLocationKeepAlive,
'backgroundContinuedProcessing':
instance.backgroundContinuedProcessing,
'windowState': instance.windowState.toJson(),
'bookmarks': instance.bookmarks,
'createHistory': instance.createHistory,
@@ -23,6 +23,7 @@ import '../../util/util.dart';
import '../rpc/webview_rpc_service.dart';
import 'android_foreground_service.dart';
import 'location_keep_alive.dart';
import 'continued_processing.dart';
const unixSocketPath = 'gopeed.sock';
@@ -105,8 +106,14 @@ class AppRuntimeController extends AsyncNotifier<AppRuntimeState> {
),
);
unawaited(LocationKeepAliveCoordinator.instance.reconcile(enabled: config.extra.backgroundLocationKeepAlive));
unawaited(
ContinuedProcessing.setEnabled(
config.extra.backgroundContinuedProcessing,
),
);
}
Future<AppRuntimeState> _init() async {
await AppInitializer.ensureStorageInitialized();
try {
@@ -180,6 +187,13 @@ class AppRuntimeController extends AsyncNotifier<AppRuntimeState> {
startupError: startupError,
);
if (Util.isIOS()) {
// Make sure the native Continued Processing manager has the
// persisted setting before app initialization completes. This
// avoids the first download racing ahead of setEnabled(true).
await ContinuedProcessing.setEnabled(
config.extra.backgroundContinuedProcessing,
);
LocationKeepAliveCoordinator.instance.start(
() =>
state.value?.downloaderConfig.extra.backgroundLocationKeepAlive ?? config.extra.backgroundLocationKeepAlive,
@@ -0,0 +1,39 @@
import 'dart:io';
import 'package:flutter/services.dart';
class ContinuedProcessing {
const ContinuedProcessing._();
static const _channel =
MethodChannel('gopeed/continued_processing');
static Future<bool> isSupported() async {
if (!Platform.isIOS) return false;
try {
return await _channel.invokeMethod<bool>(
'isSupported',
) ??
false;
} catch (_) {
return false;
}
}
static Future<bool> setEnabled(
bool enabled,
) async {
if (!Platform.isIOS) return false;
try {
return await _channel.invokeMethod<bool>(
'setEnabled',
{'enabled': enabled},
) ??
false;
} catch (_) {
return false;
}
}
}
@@ -13,6 +13,7 @@ import '../../../../app/application/app_appearance_controller.dart';
import '../../../../app/application/app_platform_controller.dart';
import '../../../../app/application/app_runtime_controller.dart';
import '../../../../app/application/location_keep_alive.dart';
import '../../../../app/application/continued_processing.dart';
import '../../../../core/common/start_config.dart';
import '../../../../core/network/gopeed/gopeed_transport.dart';
import '../../../../core/utils/breakpoints.dart';
@@ -377,6 +378,19 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
onChanged: (value) => unawaited(_setBackgroundLocationKeepAlive(value)),
),
),
if (Util.isIOS())
SettingsItem(
title: context.l10n.backgroundContinuedProcessing,
subtitle:
context.l10n.backgroundContinuedProcessingDescription,
child: shad.Switch(
value:
config.extra.backgroundContinuedProcessing,
onChanged: (value) => unawaited(
_setBackgroundContinuedProcessing(value),
),
),
),
],
),
),
@@ -1237,6 +1251,43 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
_mutateConfig((config) => config.extra.backgroundLocationKeepAlive = enabled);
await LocationKeepAliveCoordinator.instance.reconcile(enabled: enabled);
}
Future<void>
_setBackgroundContinuedProcessing(
bool enabled,
) async {
if (enabled &&
!await ContinuedProcessing.isSupported()) {
if (mounted) {
_toast(
context.l10n
.backgroundContinuedProcessingUnsupported,
);
}
return;
}
final applied =
await ContinuedProcessing.setEnabled(
enabled,
);
if (enabled && !applied) {
if (mounted) {
_toast(
context.l10n
.backgroundContinuedProcessingUnsupported,
);
}
return;
}
_mutateConfig(
(config) =>
config.extra
.backgroundContinuedProcessing =
enabled,
);
}
List<String> _lines(String text) {
return text.split('\n').map((line) => line.trim()).where((line) => line.isNotEmpty).toList();
+3
View File
@@ -248,6 +248,7 @@
"ed2kNodesDatHint": "nodes.dat path or URL, one per line",
"desktopNotification": "Desktop Notifications",
"backgroundLocationKeepAlive": "Background Downloads",
"backgroundContinuedProcessing": "Continued Processing (iOS 26+)",
"notificationTaskDone": "Task completed",
"notificationTaskError": "Task error",
"appTitle": "Gopeed Download Manager",
@@ -290,6 +291,8 @@
"browserExtensionDescription": "Send browser downloads directly to Gopeed after installation",
"desktopNotificationDescription": "Show a system notification when a task completes or fails",
"backgroundLocationKeepAliveDescription": "Use location access to keep downloads active in the background",
"backgroundContinuedProcessingDescription": "Use iOS continued processing and its system Live Activity to keep downloads running without location access",
"backgroundContinuedProcessingUnsupported": "Continued Processing requires iOS 26 or later",
"currentVersion": "Current version {version}",
"contributors": "Contributors",
"viewContributors": "View contributors",