mirror of
https://github.com/yattee/yattee.git
synced 2026-02-20 17:59:45 +00:00
218 lines
6.9 KiB
Swift
218 lines
6.9 KiB
Swift
//
|
|
// NetworkShareDiscoverySheet.swift
|
|
// Yattee
|
|
//
|
|
// Sheet for discovering and selecting network shares (WebDAV/SMB) via Bonjour.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct NetworkShareDiscoverySheet: View {
|
|
@Environment(\.dismiss) private var dismiss
|
|
@Environment(\.appEnvironment) private var appEnvironment
|
|
|
|
/// Called when user selects a share.
|
|
let onSelect: (DiscoveredShare) -> Void
|
|
|
|
/// Optional filter to show only specific share types.
|
|
let filterType: DiscoveredShare.ShareType?
|
|
|
|
init(filterType: DiscoveredShare.ShareType? = nil, onSelect: @escaping (DiscoveredShare) -> Void) {
|
|
self.filterType = filterType
|
|
self.onSelect = onSelect
|
|
}
|
|
|
|
private var discoveryService: NetworkShareDiscoveryService? {
|
|
appEnvironment?.networkShareDiscoveryService
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
content
|
|
.navigationTitle(String(localized: "discovery.title"))
|
|
#if os(iOS)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
#endif
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button(String(localized: "common.cancel")) {
|
|
discoveryService?.stopDiscovery()
|
|
dismiss()
|
|
}
|
|
}
|
|
}
|
|
.onAppear {
|
|
discoveryService?.startDiscovery()
|
|
}
|
|
.onDisappear {
|
|
discoveryService?.stopDiscovery()
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var content: some View {
|
|
if let service = discoveryService {
|
|
List {
|
|
// Scanning indicator
|
|
if service.isScanning {
|
|
Section {
|
|
HStack {
|
|
ProgressView()
|
|
.controlSize(.small)
|
|
Text(String(localized: "discovery.scanning"))
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Filtered shares based on filterType
|
|
let shares = filteredShares(from: service.discoveredShares)
|
|
|
|
if shares.isEmpty && !service.isScanning {
|
|
// Empty state
|
|
Section {
|
|
ContentUnavailableView {
|
|
Label(String(localized: "discovery.empty.title"), systemImage: "network.slash")
|
|
} description: {
|
|
Text(String(localized: "discovery.empty.description"))
|
|
} actions: {
|
|
Button(String(localized: "discovery.scanAgain")) {
|
|
service.startDiscovery()
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Group shares by type
|
|
let groupedShares = Dictionary(grouping: shares) { $0.type }
|
|
|
|
// WebDAV shares (combined webdav and webdavs)
|
|
let webdavShares = (groupedShares[.webdav] ?? []) + (groupedShares[.webdavs] ?? [])
|
|
if !webdavShares.isEmpty {
|
|
Section {
|
|
ForEach(webdavShares) { share in
|
|
ShareRow(share: share) {
|
|
selectShare(share)
|
|
}
|
|
}
|
|
} header: {
|
|
Text(String(localized: "discovery.section.webdav"))
|
|
}
|
|
}
|
|
|
|
// SMB shares
|
|
if let smbShares = groupedShares[.smb], !smbShares.isEmpty {
|
|
Section {
|
|
ForEach(smbShares) { share in
|
|
ShareRow(share: share) {
|
|
selectShare(share)
|
|
}
|
|
}
|
|
} header: {
|
|
Text(String(localized: "discovery.section.smb"))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#if os(tvOS)
|
|
.listStyle(.grouped)
|
|
#endif
|
|
} else {
|
|
// No environment available
|
|
ContentUnavailableView {
|
|
Label(String(localized: "discovery.unavailable.title"), systemImage: "exclamationmark.triangle")
|
|
} description: {
|
|
Text(String(localized: "discovery.unavailable.description"))
|
|
}
|
|
}
|
|
}
|
|
|
|
private func filteredShares(from shares: [DiscoveredShare]) -> [DiscoveredShare] {
|
|
guard let filterType else { return shares }
|
|
|
|
switch filterType {
|
|
case .webdav, .webdavs:
|
|
// When filtering for WebDAV, include both HTTP and HTTPS variants
|
|
return shares.filter { $0.type == .webdav || $0.type == .webdavs }
|
|
case .smb:
|
|
return shares.filter { $0.type == .smb }
|
|
}
|
|
}
|
|
|
|
private func selectShare(_ share: DiscoveredShare) {
|
|
discoveryService?.stopDiscovery()
|
|
onSelect(share)
|
|
dismiss()
|
|
}
|
|
}
|
|
|
|
// MARK: - Share Row
|
|
|
|
private struct ShareRow: View {
|
|
let share: DiscoveredShare
|
|
let onTap: () -> Void
|
|
|
|
var body: some View {
|
|
Button(action: onTap) {
|
|
HStack {
|
|
Image(systemName: share.type.systemImage)
|
|
.foregroundStyle(iconColor)
|
|
.frame(width: 24)
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(share.name)
|
|
.font(.body)
|
|
.foregroundStyle(.primary)
|
|
|
|
Text(addressDisplay)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
if share.type == .webdavs {
|
|
Image(systemName: "lock.fill")
|
|
.font(.caption)
|
|
.foregroundStyle(.green)
|
|
}
|
|
|
|
Image(systemName: "chevron.right")
|
|
.font(.caption)
|
|
.foregroundStyle(.tertiary)
|
|
}
|
|
.contentShape(Rectangle())
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
|
|
private var iconColor: Color {
|
|
switch share.type {
|
|
case .webdav: .blue
|
|
case .webdavs: .green
|
|
case .smb: .orange
|
|
}
|
|
}
|
|
|
|
private var addressDisplay: String {
|
|
var display = share.host
|
|
|
|
if let port = share.port {
|
|
display += ":\(port)"
|
|
}
|
|
|
|
if let path = share.path, !path.isEmpty {
|
|
display += path.hasPrefix("/") ? path : "/\(path)"
|
|
}
|
|
|
|
return display
|
|
}
|
|
}
|
|
|
|
// MARK: - Preview
|
|
|
|
#Preview {
|
|
NetworkShareDiscoverySheet { _ in }
|
|
.appEnvironment(.preview)
|
|
}
|