
- 在AppSettingFeature中新增登出确认和关于我们弹窗的状态和Action。 - 更新AppSettingView以支持登出确认和关于我们弹窗的逻辑。 - 替换多个视图中的NSLocalizedString为LocalizedString,提升本地化一致性。 - 在Localizable.strings中新增相关本地化文本,确保多语言支持。
427 lines
15 KiB
Swift
427 lines
15 KiB
Swift
import SwiftUI
|
||
import ComposableArchitecture
|
||
import PhotosUI
|
||
|
||
struct CreateFeedView: View {
|
||
let store: StoreOf<CreateFeedFeature>
|
||
@State private var isKeyboardVisible: Bool = false
|
||
@FocusState private var isTextEditorFocused: Bool
|
||
|
||
var body: some View {
|
||
NavigationStack {
|
||
GeometryReader { geometry in
|
||
VStack(spacing: 0) {
|
||
ZStack {
|
||
// 背景色
|
||
Color(hex: 0x0C0527)
|
||
.ignoresSafeArea()
|
||
|
||
// 主要内容区域
|
||
VStack(spacing: 20) {
|
||
ContentInputSection(store: store, isFocused: $isTextEditorFocused)
|
||
ImageSelectionSection(store: store)
|
||
LoadingAndErrorSection(store: store)
|
||
Spacer()
|
||
}
|
||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||
.ignoresSafeArea(.keyboard, edges: .bottom)
|
||
.background(Color(hex: 0x0C0527))
|
||
}
|
||
|
||
// 底部发布按钮 - 只在键盘隐藏时显示
|
||
if !isKeyboardVisible {
|
||
PublishButtonSection(store: store, geometry: geometry, isFocused: $isTextEditorFocused)
|
||
}
|
||
}
|
||
.onTapGesture {
|
||
isTextEditorFocused = false // 点击空白处收起键盘
|
||
}
|
||
}
|
||
.navigationTitle(LocalizedString("createFeed.title", comment: "Image & Text Publish"))
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.toolbarBackground(.hidden, for: .navigationBar)
|
||
.navigationBarBackButtonHidden(true)
|
||
.toolbar {
|
||
ToolbarItem(placement: .navigationBarLeading) {
|
||
Button(action: {
|
||
store.send(.dismissView)
|
||
}) {
|
||
Image(systemName: "xmark")
|
||
.font(.system(size: 18, weight: .medium))
|
||
.foregroundColor(.white)
|
||
}
|
||
}
|
||
|
||
ToolbarItem(placement: .principal) {
|
||
Text(LocalizedString("createFeed.title", comment: "Image & Text Publish"))
|
||
.font(.system(size: 18, weight: .medium))
|
||
.foregroundColor(.white)
|
||
}
|
||
|
||
// 右上角发布按钮 - 只在键盘显示时出现
|
||
ToolbarItem(placement: .navigationBarTrailing) {
|
||
if isKeyboardVisible {
|
||
Button(action: {
|
||
isTextEditorFocused = false // 收起键盘
|
||
store.send(.publishButtonTapped)
|
||
}) {
|
||
HStack(spacing: 4) {
|
||
if store.isLoading || store.isUploadingImages {
|
||
ProgressView()
|
||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||
.scaleEffect(0.7)
|
||
}
|
||
Text(toolbarButtonText)
|
||
.font(.system(size: 14, weight: .medium))
|
||
.foregroundColor(.white)
|
||
}
|
||
.padding(.horizontal, 12)
|
||
.padding(.vertical, 8)
|
||
.background(
|
||
LinearGradient(
|
||
gradient: Gradient(colors: [
|
||
Color(hex: 0xF854FC),
|
||
Color(hex: 0x500FFF)
|
||
]),
|
||
startPoint: .leading,
|
||
endPoint: .trailing
|
||
)
|
||
)
|
||
.cornerRadius(16)
|
||
}
|
||
.disabled(store.isLoading || store.isUploadingImages || !store.canPublish)
|
||
.opacity(toolbarButtonOpacity)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { notification in
|
||
if let _ = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
|
||
DispatchQueue.main.async {
|
||
isKeyboardVisible = true
|
||
}
|
||
}
|
||
}
|
||
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
|
||
DispatchQueue.main.async {
|
||
isKeyboardVisible = false
|
||
}
|
||
}
|
||
.onReceive(NotificationCenter.default.publisher(for: .init("CreateFeedDismiss"))) { _ in
|
||
store.send(.dismissView)
|
||
}
|
||
.onDisappear {
|
||
isKeyboardVisible = false
|
||
}
|
||
}
|
||
|
||
// MARK: - 工具栏按钮计算属性
|
||
private var toolbarButtonText: String {
|
||
if store.isUploadingImages {
|
||
return "上传中..."
|
||
} else if store.isLoading {
|
||
return LocalizedString("createFeed.publishing", comment: "Publishing...")
|
||
} else {
|
||
return LocalizedString("createFeed.publish", comment: "Publish")
|
||
}
|
||
}
|
||
|
||
private var toolbarButtonOpacity: Double {
|
||
store.isLoading || store.isUploadingImages || !store.canPublish ? 0.6 : 1.0
|
||
}
|
||
}
|
||
|
||
// MARK: - 内容输入区域组件
|
||
struct ContentInputSection: View {
|
||
let store: StoreOf<CreateFeedFeature>
|
||
@FocusState.Binding var isFocused: Bool
|
||
|
||
|
||
var body: some View {
|
||
VStack(alignment: .leading, spacing: 12) {
|
||
ZStack(alignment: .topLeading) {
|
||
RoundedRectangle(cornerRadius: 8)
|
||
.fill(Color.init(hex: 0x1C143A))
|
||
if store.content.isEmpty {
|
||
Text(LocalizedString("createFeed.enterContent", comment: "Enter Content"))
|
||
.foregroundColor(.white.opacity(0.5))
|
||
.padding(.horizontal, 16)
|
||
.padding(.vertical, 12)
|
||
}
|
||
|
||
TextEditor(text: textBinding)
|
||
.foregroundColor(.white)
|
||
.background(Color.clear)
|
||
.padding(.horizontal, 12)
|
||
.padding(.vertical, 8)
|
||
.scrollContentBackground(.hidden)
|
||
.frame(height: 200)
|
||
.focused($isFocused)
|
||
}
|
||
|
||
// 字符计数
|
||
HStack {
|
||
Spacer()
|
||
Text("\(store.characterCount)/500")
|
||
.font(.system(size: 12))
|
||
.foregroundColor(characterCountColor)
|
||
}
|
||
}
|
||
.frame(height: 200)
|
||
.padding(.horizontal, 20)
|
||
.padding(.top, 20)
|
||
}
|
||
|
||
// MARK: - 计算属性
|
||
private var textBinding: Binding<String> {
|
||
Binding(
|
||
get: { store.content },
|
||
set: { store.send(.contentChanged($0)) }
|
||
)
|
||
}
|
||
|
||
private var characterCountColor: Color {
|
||
store.characterCount > 500 ? .red : .white.opacity(0.6)
|
||
}
|
||
}
|
||
|
||
// MARK: - 图片选择区域组件
|
||
struct ImageSelectionSection: View {
|
||
let store: StoreOf<CreateFeedFeature>
|
||
|
||
var body: some View {
|
||
VStack(alignment: .leading, spacing: 12) {
|
||
if shouldShowImageSelection {
|
||
ModernImageSelectionGrid(
|
||
images: store.processedImages,
|
||
selectedItems: store.selectedImages,
|
||
canAddMore: store.canAddMoreImages,
|
||
onItemsChanged: { items in
|
||
store.send(.photosPickerItemsChanged(items))
|
||
},
|
||
onRemoveImage: { index in
|
||
store.send(.removeImage(index))
|
||
}
|
||
)
|
||
}
|
||
}
|
||
.padding(.horizontal, 20)
|
||
}
|
||
|
||
// MARK: - 计算属性
|
||
private var shouldShowImageSelection: Bool {
|
||
!store.processedImages.isEmpty || store.canAddMoreImages
|
||
}
|
||
}
|
||
|
||
// MARK: - 加载和错误状态组件
|
||
struct LoadingAndErrorSection: View {
|
||
let store: StoreOf<CreateFeedFeature>
|
||
|
||
var body: some View {
|
||
VStack(spacing: 10) {
|
||
// 图片上传状态
|
||
if store.isUploadingImages {
|
||
VStack(spacing: 8) {
|
||
HStack {
|
||
ProgressView()
|
||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||
Text(store.uploadStatus)
|
||
.font(.system(size: 14))
|
||
.foregroundColor(.white.opacity(0.8))
|
||
}
|
||
|
||
// 上传进度条
|
||
ProgressView(value: store.uploadProgress)
|
||
.progressViewStyle(LinearProgressViewStyle(tint: .blue))
|
||
.frame(height: 4)
|
||
.background(Color.white.opacity(0.2))
|
||
.cornerRadius(2)
|
||
}
|
||
.padding(.top, 10)
|
||
}
|
||
|
||
// 内容发布加载状态
|
||
if store.isLoading && !store.isUploadingImages {
|
||
HStack {
|
||
ProgressView()
|
||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||
Text(NSLocalizedString("createFeed.publishing", comment: "Publishing..."))
|
||
.font(.system(size: 14))
|
||
.foregroundColor(.white.opacity(0.8))
|
||
}
|
||
.padding(.top, 10)
|
||
}
|
||
|
||
// 错误提示
|
||
if let error = store.errorMessage {
|
||
Text(error)
|
||
.font(.system(size: 14))
|
||
.foregroundColor(.red)
|
||
.padding(.horizontal, 20)
|
||
.multilineTextAlignment(.center)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 发布按钮组件
|
||
struct PublishButtonSection: View {
|
||
let store: StoreOf<CreateFeedFeature>
|
||
let geometry: GeometryProxy
|
||
@FocusState.Binding var isFocused: Bool
|
||
|
||
var body: some View {
|
||
VStack(spacing: 0) {
|
||
Button(action: {
|
||
isFocused = false // 收起键盘
|
||
store.send(.publishButtonTapped)
|
||
}) {
|
||
HStack {
|
||
if store.isLoading || store.isUploadingImages {
|
||
ProgressView()
|
||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||
.scaleEffect(0.8)
|
||
Text(buttonText)
|
||
.font(.system(size: 16, weight: .medium))
|
||
.foregroundColor(.white)
|
||
} else {
|
||
Text(NSLocalizedString("createFeed.publish", comment: "Publish"))
|
||
.font(.system(size: 16, weight: .medium))
|
||
.foregroundColor(.white)
|
||
}
|
||
}
|
||
.frame(maxWidth: .infinity)
|
||
.frame(height: 45)
|
||
.background(
|
||
LinearGradient(
|
||
gradient: Gradient(colors: [
|
||
Color(hex: 0xF854FC),
|
||
Color(hex: 0x500FFF)
|
||
]),
|
||
startPoint: .leading,
|
||
endPoint: .trailing
|
||
)
|
||
)
|
||
.cornerRadius(22.5)
|
||
.disabled(store.isLoading || store.isUploadingImages || !store.canPublish)
|
||
.opacity(buttonOpacity)
|
||
}
|
||
.padding(.horizontal, 16)
|
||
.padding(.bottom, 20) // 使用固定间距,不受键盘影响
|
||
}
|
||
.background(Color(hex: 0x0C0527))
|
||
}
|
||
|
||
// MARK: - 计算属性
|
||
private var buttonOpacity: Double {
|
||
store.isLoading || store.isUploadingImages || !store.canPublish ? 0.6 : 1.0
|
||
}
|
||
|
||
private var buttonText: String {
|
||
if store.isUploadingImages {
|
||
return "上传图片中..."
|
||
} else if store.isLoading {
|
||
return LocalizedString("createFeed.publishing", comment: "Publishing...")
|
||
} else {
|
||
return LocalizedString("createFeed.publish", comment: "Publish")
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - iOS 16+ 图片选择网格组件
|
||
struct ModernImageSelectionGrid: View {
|
||
let images: [UIImage]
|
||
let selectedItems: [PhotosPickerItem]
|
||
let canAddMore: Bool
|
||
let onItemsChanged: ([PhotosPickerItem]) -> Void
|
||
let onRemoveImage: (Int) -> Void
|
||
|
||
private let columns = Array(repeating: GridItem(.flexible(), spacing: 8), count: 3)
|
||
|
||
var body: some View {
|
||
LazyVGrid(columns: columns, spacing: 8) {
|
||
// 显示已选择的图片
|
||
ForEach(Array(images.enumerated()), id: \.offset) { index, image in
|
||
ImageItemView(
|
||
image: image,
|
||
index: index,
|
||
onRemove: onRemoveImage
|
||
)
|
||
}
|
||
|
||
// 添加图片按钮
|
||
if canAddMore {
|
||
CreateAddImageButton(
|
||
selectedItems: selectedItems,
|
||
onItemsChanged: onItemsChanged
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 图片项组件
|
||
struct ImageItemView: View {
|
||
let image: UIImage
|
||
let index: Int
|
||
let onRemove: (Int) -> Void
|
||
|
||
var body: some View {
|
||
ZStack(alignment: .topTrailing) {
|
||
Image(uiImage: image)
|
||
.resizable()
|
||
.aspectRatio(contentMode: .fill)
|
||
.frame(width: 100, height: 100)
|
||
.clipped()
|
||
.cornerRadius(8)
|
||
|
||
// 删除按钮
|
||
Button(action: {
|
||
onRemove(index)
|
||
}) {
|
||
Image(systemName: "xmark.circle.fill")
|
||
.font(.system(size: 20))
|
||
.foregroundColor(.white)
|
||
.background(Color.black.opacity(0.6))
|
||
.clipShape(Circle())
|
||
}
|
||
.padding(4)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 添加图片按钮组件
|
||
struct CreateAddImageButton: View {
|
||
let selectedItems: [PhotosPickerItem]
|
||
let onItemsChanged: ([PhotosPickerItem]) -> Void
|
||
|
||
var body: some View {
|
||
PhotosPicker(
|
||
selection: selectionBinding,
|
||
maxSelectionCount: 9,
|
||
matching: .images
|
||
) {
|
||
Image("add photo")
|
||
.frame(width: 100, height: 100)
|
||
}
|
||
}
|
||
|
||
// MARK: - 计算属性
|
||
private var selectionBinding: Binding<[PhotosPickerItem]> {
|
||
Binding(
|
||
get: { selectedItems },
|
||
set: onItemsChanged
|
||
)
|
||
}
|
||
}
|
||
|
||
// MARK: - 预览
|
||
//#Preview {
|
||
// CreateFeedView(
|
||
// store: Store(initialState: CreateFeedFeature.State()) {
|
||
// CreateFeedFeature()
|
||
// }
|
||
// )
|
||
//}
|