Files
crossdesk/apps/ios/CrossDeskMobile/AnnouncementInboxView.swift
T

194 lines
8.9 KiB
Swift

import SwiftUI
struct AnnouncementBadge: View {
@ObservedObject var inbox: AnnouncementInbox
var body: some View {
Image(systemName: "envelope")
.font(.system(size: 18, weight: .semibold))
.frame(width: 40, height: 40)
.background(Color(.secondarySystemGroupedBackground),
in: RoundedRectangle(cornerRadius: 10, style: .continuous))
.overlay(alignment: .topTrailing) {
if inbox.state.unread > 0 {
Text(inbox.state.unread > 99 ? "99+" : "\(inbox.state.unread)")
.font(.system(size: 10, weight: .bold))
.foregroundStyle(.white)
.padding(.horizontal, 4)
.frame(minWidth: 16, minHeight: 16)
.background(.red, in: Capsule())
.offset(x: 5, y: -3)
}
}
.accessibilityElement(children: .ignore)
.accessibilityLabel(NSLocalizedString("通知公告", comment: ""))
.accessibilityValue(inbox.state.unread > 0 ? String(format: NSLocalizedString("%1$@ 条未读", comment: ""), String(describing: inbox.state.unread)) : NSLocalizedString("无未读公告", comment: ""))
}
}
struct AnnouncementSelection: Hashable {
let id: Int64
let revision: Int64
}
private enum AnnouncementDate {
static let formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm"
return formatter
}()
static func string(_ item: Announcement) -> String { formatter.string(from: item.date) }
}
struct AnnouncementInboxView: View {
@ObservedObject var session: RemoteSessionModel
@ObservedObject var inbox: AnnouncementInbox
@State private var pendingDelete: Announcement?
var body: some View {
List {
if inbox.state.deleteFailed {
Text(NSLocalizedString("无法删除这条公告,请重试。", comment: ""))
.foregroundStyle(.red)
}
if inbox.state.failed && session.announcementsConnected {
HStack {
Text(NSLocalizedString("公告加载失败,请重试。", comment: ""))
Spacer()
Button(NSLocalizedString("重试", comment: "")) { session.refreshAnnouncements() }
.disabled(!session.announcementsConnected)
}
}
Section {
ForEach(inbox.state.items) { item in
NavigationLink(value: HomeDestination.announcement(
AnnouncementSelection(id: item.id, revision: item.revision))) {
HStack(alignment: .top, spacing: 10) {
Circle()
.fill(item.read ? Color.clear : Color.accentColor)
.frame(width: 7, height: 7)
.padding(.top, 7)
.accessibilityHidden(true)
VStack(alignment: .leading, spacing: 7) {
Text(verbatim: item.title)
.fontWeight(item.read ? .regular : .semibold)
.foregroundStyle(.primary)
.lineLimit(2)
Text(AnnouncementDate.string(item))
.font(.caption)
.foregroundStyle(.secondary)
}
}
.padding(.vertical, 5)
.accessibilityElement(children: .combine)
.accessibilityValue(item.read ? NSLocalizedString("已读", comment: "") : NSLocalizedString("未读", comment: ""))
}
.swipeActions {
Button(NSLocalizedString("删除", comment: ""), role: .destructive) { pendingDelete = item }
}
.contextMenu {
Button(NSLocalizedString("删除公告", comment: ""), role: .destructive) { pendingDelete = item }
}
}
if inbox.state.loading {
HStack { Spacer(); ProgressView(NSLocalizedString("加载中…", comment: "")); Spacer() }
} else if inbox.state.items.count < inbox.state.total && !inbox.state.failed {
Button(NSLocalizedString("加载更多", comment: "")) { inbox.loadMore() }
.frame(maxWidth: .infinity)
.disabled(!session.announcementsConnected)
.onAppear { inbox.loadMore() }
} else if inbox.state.loaded && inbox.state.total == 0 &&
!inbox.state.failed && session.announcementsConnected {
Text(NSLocalizedString("暂无公告", comment: ""))
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, minHeight: 120)
}
} header: {
Text(String(format: NSLocalizedString("共 %1$@ 条 · %2$@ 条未读", comment: ""), String(describing: inbox.state.total), String(describing: inbox.state.unread)))
}
}
.readableContentWidth()
.background(Color(.systemGroupedBackground).ignoresSafeArea())
.navigationTitle(NSLocalizedString("通知公告", comment: ""))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button { session.refreshAnnouncements() } label: {
Image(systemName: "arrow.clockwise")
}
.accessibilityLabel(NSLocalizedString("刷新公告", comment: ""))
.disabled(inbox.state.loading || !session.announcementsConnected)
}
}
.refreshable {
session.refreshAnnouncements()
while inbox.state.loading && !Task.isCancelled {
try? await Task.sleep(nanoseconds: 100_000_000)
}
}
.onAppear { session.refreshAnnouncements() }
.alert(NSLocalizedString("删除公告", comment: ""), isPresented: Binding(
get: { pendingDelete != nil },
set: { if !$0 { pendingDelete = nil } }
)) {
Button(NSLocalizedString("取消", comment: ""), role: .cancel) { pendingDelete = nil }
Button(NSLocalizedString("删除", comment: ""), role: .destructive) {
if let item = pendingDelete { inbox.dismiss(item) }
pendingDelete = nil
}
} message: {
Text(NSLocalizedString("删除这条公告?", comment: ""))
}
}
}
struct AnnouncementDetailView: View {
@ObservedObject var inbox: AnnouncementInbox
let selection: AnnouncementSelection
@Environment(\.openURL) private var openURL
private var item: Announcement? {
inbox.state.items.first { $0.id == selection.id && $0.revision == selection.revision }
}
var body: some View {
ScrollView {
if let item {
VStack(alignment: .leading, spacing: 16) {
Text(verbatim: item.title)
.font(.title2.weight(.semibold))
Text("\(AnnouncementDate.string(item)) · \(NSLocalizedString(item.read ? "已读" : "未读", comment: ""))")
.font(.caption)
.foregroundStyle(.secondary)
if inbox.state.readSaveFailed {
Text(NSLocalizedString("无法保存本地阅读状态,请重试。", comment: ""))
.foregroundStyle(.red)
Button(NSLocalizedString("重试保存阅读状态", comment: "")) { inbox.markRead(item) }
}
Text(AnnouncementText.format(item.body))
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
.environment(\.openURL, OpenURLAction { url in
guard AnnouncementText.isWebURL(url) else { return .discarded }
openURL(url)
return .handled
})
}
.padding(20)
.readableContentWidth()
} else {
Text(NSLocalizedString("这条公告已更新或删除,请返回公告列表查看。", comment: ""))
.foregroundStyle(.secondary)
.padding(20)
.readableContentWidth()
}
}
.navigationTitle(NSLocalizedString("公告详情", comment: ""))
.navigationBarTitleDisplayMode(.inline)
.onAppear {
if let item { inbox.markRead(item) }
}
}
}