import SwiftUI struct FileView: View { let fileInfo: FileInfo @State private var showingFullScreen = false var body: some View { VStack(alignment: .leading, spacing: 8) { if fileInfo.isImage { AsyncImageView(url: fileInfo.url, contentMode: .fit) .frame(maxHeight: 200) .clipped() .onTapGesture { showingFullScreen = true } } else if fileInfo.isVideo { VStack { Image(systemName: "play.rectangle") .font(.largeTitle) .foregroundColor(.gray) Text(fileInfo.filename) .font(.caption) .foregroundColor(.secondary) } .frame(height: 100) .frame(maxWidth: .infinity) .background(Color.gray.opacity(0.1)) .cornerRadius(8) } else { HStack { Image(systemName: "doc") .foregroundColor(.blue) Text(fileInfo.filename) .font(.caption) .foregroundColor(.secondary) Spacer() } .padding(.vertical, 8) .padding(.horizontal, 12) .background(Color.gray.opacity(0.1)) .cornerRadius(8) } } .fullScreenCover(isPresented: $showingFullScreen) { if fileInfo.isImage { NativeFullScreenImageView(url: fileInfo.url) } } } } struct NativeFullScreenImageView: View { let url: String @Environment(\.dismiss) private var dismiss @State private var scale: CGFloat = 1.0 @State private var lastScale: CGFloat = 1.0 @State private var offset = CGSize.zero @State private var lastOffset = CGSize.zero @State private var dragOffset = CGSize.zero @State private var isDragging = false @State private var showUI = true var body: some View { ZStack { Color.black.ignoresSafeArea() GeometryReader { geometry in AsyncImageView(url: url, contentMode: .fit) .scaleEffect(scale) .offset(offset) .opacity(isDragging ? 0.8 : 1.0) .gesture( MagnificationGesture() .onChanged { value in let delta = value / lastScale lastScale = value scale = min(max(scale * delta, 0.5), 5) } .onEnded { _ in lastScale = 1.0 if scale < 1 { withAnimation(.easeInOut(duration: 0.3)) { scale = 1 offset = .zero } } } ) .gesture( DragGesture() .onChanged { value in if scale > 1 { let delta = CGSize( width: value.translation.width - lastOffset.width, height: value.translation.height - lastOffset.height ) lastOffset = value.translation offset = CGSize( width: offset.width + delta.width, height: offset.height + delta.height ) } else { dragOffset = value.translation isDragging = true } } .onEnded { value in if scale <= 1 { if abs(dragOffset.height) > 100 || abs(dragOffset.width) > 100 { dismiss() } else { withAnimation(.easeInOut(duration: 0.3)) { dragOffset = .zero } } isDragging = false } lastOffset = CGSize.zero } ) .onTapGesture(count: 2) { withAnimation(.easeInOut(duration: 0.3)) { if scale > 1 { scale = 1 offset = .zero } else { scale = 2 } } } .onTapGesture { withAnimation(.easeInOut(duration: 0.2)) { showUI.toggle() } } } } .overlay( VStack { if showUI { HStack { Button(action: { dismiss() }) { Image(systemName: "xmark") .font(.title2) .foregroundColor(.white) .padding(12) .background(Color.black.opacity(0.6)) .clipShape(Circle()) } Spacer() Button(action: { withAnimation(.easeInOut(duration: 0.3)) { scale = 1 offset = .zero } }) { Image(systemName: "arrow.counterclockwise") .font(.title2) .foregroundColor(.white) .padding(12) .background(Color.black.opacity(0.6)) .clipShape(Circle()) } } .padding() .transition(.opacity) } Spacer() } ) .animation(.easeInOut(duration: 0.2), value: showUI) } } struct FilesView: View { let files: [String] var body: some View { if !files.isEmpty { VStack(alignment: .leading, spacing: 12) { Text("Файлы (\(files.count))") .font(.headline) LazyVGrid(columns: [ GridItem(.flexible()), GridItem(.flexible()) ], spacing: 12) { ForEach(files, id: \.self) { file in FileView(fileInfo: FileInfo(filePath: file)) } } } } } }