Yattee v2 rewrite

This commit is contained in:
Arkadiusz Fal
2026-02-08 18:31:16 +01:00
parent 20d0cfc0c7
commit 05f921d605
1043 changed files with 163875 additions and 68430 deletions

View File

@@ -0,0 +1,217 @@
//
// 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)
}