Circular accessory on iOS

This commit is contained in:
2026-01-04 23:03:18 +08:00
parent 1a74f2b3e9
commit 479a79c7f6
2 changed files with 164 additions and 95 deletions

View File

@@ -3,7 +3,7 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 77; objectVersion = 54;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
@@ -216,6 +216,8 @@
}; };
7310A7D52EB10962002C0FD3 /* Solian Watch App */ = { 7310A7D52EB10962002C0FD3 /* Solian Watch App */ = {
isa = PBXFileSystemSynchronizedRootGroup; isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = "Solian Watch App"; path = "Solian Watch App";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@@ -757,14 +759,10 @@
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
); );
inputPaths = (
);
name = "[CP] Copy Pods Resources"; name = "[CP] Copy Pods Resources";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
); );
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@@ -822,14 +820,10 @@
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
); );
inputPaths = (
);
name = "[CP] Embed Pods Frameworks"; name = "[CP] Embed Pods Frameworks";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
); );
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
@@ -880,14 +874,10 @@
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks-${CONFIGURATION}-input-files.xcfilelist",
); );
inputPaths = (
);
name = "[CP] Embed Pods Frameworks"; name = "[CP] Embed Pods Frameworks";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks-${CONFIGURATION}-output-files.xcfilelist",
); );
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks.sh\"\n";

View File

@@ -276,6 +276,18 @@ struct NotificationWidgetEntryView: View {
if case .accessoryRectangular = family { if case .accessoryRectangular = family {
return true return true
} }
if case .accessoryCircular = family {
return true
}
}
return false
}
private var isCircular: Bool {
if #available(iOS 16.0, *) {
if case .accessoryCircular = family {
return true
}
} }
return false return false
} }
@@ -285,40 +297,57 @@ struct NotificationWidgetEntryView: View {
Link(destination: URL(string: "solian://notifications")!) { Link(destination: URL(string: "solian://notifications")!) {
if isCompact { if isCompact {
if isAccessory { if isAccessory {
VStack(alignment: .leading, spacing: 2) { if isCircular {
HStack(spacing: 4) { ZStack {
Image(systemName: "bell.fill") Image(systemName: "bell.fill")
.font(.caption2) .font(.system(size: 20))
.foregroundColor(.orange) .foregroundColor(unreadCount > 0 ? .orange : .gray)
.padding(.leading, 1.5)
Text(NSLocalizedString("notifications", comment: "Notifications")) if unreadCount > 0 {
.font(.caption2) Text("\(min(unreadCount, 99))")
.fontWeight(.bold) .font(.system(size: 10, weight: .bold))
.foregroundColor(.white)
Spacer() .padding(4)
} .background(Circle().fill(Color.blue))
.offset(x: 12, y: -12)
if unreadCount > 0 {
HStack(spacing: 4) {
Text("\(unreadCount)")
.font(.caption2)
.fontWeight(.bold)
.padding(.horizontal, 6)
.background(
Capsule()
.fill(Color.blue.opacity(0.5))
)
Text(NSLocalizedString("unread", comment: "unread"))
.font(.caption2)
} }
} }
} else {
Text("on the Solar Network") VStack(alignment: .leading, spacing: 2) {
.font(.caption2) HStack(spacing: 4) {
.foregroundColor(.secondary) Image(systemName: "bell.fill")
.padding(.horizontal, 1.5) .font(.caption2)
.foregroundColor(.orange)
.padding(.leading, 1.5)
Text(NSLocalizedString("notifications", comment: "Notifications"))
.font(.caption2)
.fontWeight(.bold)
Spacer()
}
if unreadCount > 0 {
HStack(spacing: 4) {
Text("\(unreadCount)")
.font(.caption2)
.fontWeight(.bold)
.padding(.horizontal, 6)
.background(
Capsule()
.fill(Color.blue.opacity(0.5))
)
Text(NSLocalizedString("unread", comment: "unread"))
.font(.caption2)
}
}
Text("on the Solar Network")
.font(.caption2)
.foregroundColor(.secondary)
.padding(.horizontal, 1.5)
}
} }
} else { } else {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
@@ -490,79 +519,100 @@ struct NotificationWidgetEntryView: View {
@ViewBuilder @ViewBuilder
private func EmptyView() -> some View { private func EmptyView() -> some View {
Link(destination: URL(string: "solian://notifications")!) { Link(destination: URL(string: "solian://notifications")!) {
VStack(alignment: .leading, spacing: isAccessory ? 4 : 8) { if isCircular {
HStack(spacing: 6) { ZStack {
Image(systemName: "bell") Image(systemName: "bell")
.font(isAccessory ? .caption : .title3) .font(.system(size: 20))
.foregroundColor(.secondary) .foregroundColor(.gray)
Text(NSLocalizedString("notifications", comment: "Notifications"))
.font(isAccessory ? .caption2 : .headline)
.fontWeight(.bold)
Spacer()
} }
} else {
if !isAccessory { VStack(alignment: .leading, spacing: isAccessory ? 4 : 8) {
Text(NSLocalizedString("noNotifications", comment: "No notifications yet")) HStack(spacing: 6) {
.font(.caption) Image(systemName: "bell")
.foregroundColor(.secondary) .font(isAccessory ? .caption : .title3)
.foregroundColor(.secondary)
Text(NSLocalizedString("notifications", comment: "Notifications"))
.font(isAccessory ? .caption2 : .headline)
.fontWeight(.bold)
Spacer()
}
Spacer() if !isAccessory {
Text(NSLocalizedString("noNotifications", comment: "No notifications yet"))
.font(.caption)
.foregroundColor(.secondary)
Spacer()
}
} }
.padding(isAccessory ? 4 : 12)
} }
.padding(isAccessory ? 4 : 12)
} }
} }
@ViewBuilder @ViewBuilder
private func LoadingView() -> some View { private func LoadingView() -> some View {
VStack(alignment: .leading, spacing: isAccessory ? 4 : 8) { if isCircular {
HStack(spacing: 6) { ProgressView()
ProgressView() .scaleEffect(0.8)
.scaleEffect(isAccessory ? 0.6 : 0.8) } else {
Text(NSLocalizedString("loading", comment: "Loading...")) VStack(alignment: .leading, spacing: isAccessory ? 4 : 8) {
.font(isAccessory ? .caption2 : .caption) HStack(spacing: 6) {
.foregroundColor(.secondary) ProgressView()
Spacer() .scaleEffect(isAccessory ? 0.6 : 0.8)
} Text(NSLocalizedString("loading", comment: "Loading..."))
.font(isAccessory ? .caption2 : .caption)
if !isAccessory { .foregroundColor(.secondary)
Spacer() Spacer()
}
if !isAccessory {
Spacer()
}
} }
.padding(isAccessory ? 4 : 12)
} }
.padding(isAccessory ? 4 : 12)
} }
@ViewBuilder @ViewBuilder
private func ErrorView(error: String) -> some View { private func ErrorView(error: String) -> some View {
Link(destination: URL(string: "solian://notifications")!) { Link(destination: URL(string: "solian://notifications")!) {
VStack(alignment: .leading, spacing: isAccessory ? 4 : 8) { if isCircular {
HStack(spacing: 6) { ZStack {
Image(systemName: "exclamationmark.triangle") Image(systemName: "exclamationmark.triangle")
.foregroundColor(.secondary) .font(.system(size: 20))
.font(isAccessory ? .caption : .title3) .foregroundColor(.red)
Text(NSLocalizedString("error", comment: "Error"))
.font(isAccessory ? .caption2 : .headline)
Spacer()
} }
} else {
if !isAccessory { VStack(alignment: .leading, spacing: isAccessory ? 4 : 8) {
Text(NSLocalizedString("openAppToRefresh", comment: "Open app to refresh")) HStack(spacing: 6) {
.font(.caption) Image(systemName: "exclamationmark.triangle")
.foregroundColor(.secondary) .foregroundColor(.secondary)
.font(isAccessory ? .caption : .title3)
Text(NSLocalizedString("error", comment: "Error"))
.font(isAccessory ? .caption2 : .headline)
Spacer()
}
Text(error) if !isAccessory {
.font(.footnote) Text(NSLocalizedString("openAppToRefresh", comment: "Open app to refresh"))
.foregroundStyle(.secondary) .font(.caption)
.lineLimit(nil) .foregroundColor(.secondary)
.multilineTextAlignment(.leading)
Text(error)
Spacer() .font(.footnote)
.foregroundStyle(.secondary)
.lineLimit(nil)
.multilineTextAlignment(.leading)
Spacer()
}
} }
.padding(isAccessory ? 4 : 12)
} }
.padding(isAccessory ? 4 : 12)
} }
} }
@@ -647,13 +697,14 @@ struct SolianNotificationWidget: Widget {
private var supportedFamilies: [WidgetFamily] { private var supportedFamilies: [WidgetFamily] {
#if os(iOS) #if os(iOS)
return [.systemSmall, .systemMedium, .systemLarge, .accessoryRectangular] return [.systemSmall, .systemMedium, .systemLarge, .accessoryRectangular, .accessoryCircular]
#else #else
return [.systemSmall, .systemMedium, .systemLarge] return [.systemSmall, .systemMedium, .systemLarge]
#endif #endif
} }
} }
#if os(iOS)
#Preview(as: .accessoryRectangular) { #Preview(as: .accessoryRectangular) {
SolianNotificationWidget() SolianNotificationWidget()
} timeline: { } timeline: {
@@ -694,6 +745,7 @@ struct SolianNotificationWidget: Widget {
isLoading: false isLoading: false
) )
} }
#endif
#Preview(as: .systemSmall) { #Preview(as: .systemSmall) {
SolianNotificationWidget() SolianNotificationWidget()
@@ -846,4 +898,31 @@ struct SolianNotificationWidget: Widget {
isLoading: false isLoading: false
) )
} }
#Preview(as: .accessoryCircular) {
SolianNotificationWidget()
} timeline: {
NotificationEntry(
date: .now,
notifications: [
SnNotification(
id: "1",
topic: "post.replies",
title: "New reply",
subtitle: "Someone replied",
content: "Content",
meta: nil,
priority: 0,
viewedAt: nil,
accountId: "acc-1",
createdAt: ISO8601DateFormatter().string(from: Date()),
updatedAt: ISO8601DateFormatter().string(from: Date()),
deletedAt: nil
)
],
unreadCount: 5,
error: nil,
isLoading: false
)
}
#endif #endif