YanDisk v1.0.1: тёмная иконка по теме системы, вкладки (Скриншоты скрыт, Файлы→Доступные мне), синк по умолчанию 'В облаке', ПРАВКИ.md

- Иконка: AppIconDark.icns + переключение Dock-иконки по effectiveAppearance (KVO)
- Сайдбар: убран 'Скриншоты', 'Файлы' → 'Доступные мне' (заглушка), первая вкладка — все файлы и папки
- SyncPolicyStore.globalMode по умолчанию .cloudOnly ('В облаке')
- build.sh/smoke_test.sh: SDKROOT из Xcode (фикс SDK-конфликта тулчейна 6.3.3/SDK 6.4)
- ПРАВКИ.md: полный аудит и план доработок
This commit is contained in:
2026-09-15 21:16:22 +03:00
commit 9098340f2a
50 changed files with 3100 additions and 0 deletions
+59
View File
@@ -0,0 +1,59 @@
import SwiftUI
struct LoginView: View {
@Environment(AppStore.self) private var store
var body: some View {
@Bindable var store = store
VStack(spacing: 20) {
Image(systemName: "cloud.circle.fill")
.font(.system(size: 56))
.foregroundStyle(Color(red: 0.93, green: 0.0, blue: 0.21))
Text("YanDisk")
.font(.largeTitle.bold())
Text("Войдите, чтобы синхронизировать файлы")
.foregroundStyle(.secondary)
Button {
store.openAuthorization()
} label: {
Label("Войти через Яндекс", systemImage: "person.crop.circle")
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
.tint(Color(red: 0.93, green: 0.0, blue: 0.21))
VStack(alignment: .leading, spacing: 8) {
Text("Вставьте OAuth-токен")
.font(.caption)
.foregroundStyle(.secondary)
TextField("access_token или ссылка с токеном", text: $store.pasteToken)
.textFieldStyle(.roundedBorder)
Text("После входа скопируйте токен со страницы Яндекса и вставьте сюда.")
.font(.caption2)
.foregroundStyle(.secondary)
}
Button("Продолжить") {
store.login(with: store.pasteToken)
}
.keyboardShortcut(.defaultAction)
.disabled(store.pasteToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
if let error = store.errorMessage {
Text(error)
.font(.caption)
.foregroundStyle(.red)
}
Text("Client ID: \(OAuthManager.clientId)")
.font(.caption2)
.foregroundStyle(.tertiary)
}
.padding(28)
.frame(width: 420)
}
}
+369
View File
@@ -0,0 +1,369 @@
import SwiftUI
import AppKit
struct MainSplitView: View {
@Environment(AppStore.self) private var store
@Environment(\.openWindow) private var openWindow
@State private var showNewFolderAlert = false
@State private var newFolderName = ""
@State private var renameItem: DiskResource?
@State private var renameText = ""
var body: some View {
@Bindable var store = store
NavigationSplitView {
List(AppStore.SidebarItem.displayCases, selection: $store.sidebarSelection) { item in
Label(item.title, systemImage: item.systemImage)
.tag(item)
}
.listStyle(.sidebar)
.safeAreaInset(edge: .bottom) {
if store.diskInfo != nil {
VStack(alignment: .leading, spacing: 4) {
Text("Место на Диске")
.font(.caption)
.foregroundStyle(.secondary)
ProgressView(value: store.usedPercent)
Text("\(store.formattedUsed) / \(store.formattedTotal)")
.font(.caption2)
.foregroundStyle(.secondary)
}
.padding(10)
}
}
} detail: {
VStack(spacing: 0) {
if store.sidebarSelection == .tasks {
TasksView()
} else {
pathBar(store: store)
Divider()
fileBrowser(store: store)
}
if let status = store.statusMessage {
Divider()
HStack {
Image(systemName: "checkmark.circle")
Text(status)
Spacer()
}
.font(.caption)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(.thinMaterial)
}
}
}
.toolbar {
ToolbarItemGroup {
Button {
showNewFolderAlert = true
} label: {
Label("Новая папка", systemImage: "folder.badge.plus")
}
Button {
let panel = NSOpenPanel()
panel.allowsMultipleSelection = true
panel.canChooseDirectories = true
if panel.runModal() == .OK {
Task { await store.uploadFiles(from: panel.urls) }
}
} label: {
Label("Загрузить", systemImage: "square.and.arrow.up")
}
if store.sidebarSelection == .trash {
Button {
Task { await store.emptyTrash() }
} label: {
Label("Очистить Корзину", systemImage: "trash")
}
}
Picker("Режим", selection: Binding(
get: { store.syncEngine.policy.globalMode },
set: { store.syncEngine.setGlobalMode($0) }
)) {
ForEach(SyncMode.allCases) { mode in
Text(mode.title).tag(mode)
}
}
.pickerStyle(.menu)
Button {
NSApp.activate(ignoringOtherApps: true)
NSApp.sendAction(#selector(AppDelegate.showPreferencesWindow(_:)), to: nil, from: nil)
} label: {
Label("Настройки", systemImage: "gearshape")
}
}
}
.sheet(isPresented: $store.showShareSheet) {
ShareSheetView()
.environment(store)
}
.sheet(isPresented: $store.showLoginSheet) {
LoginView()
.environment(store)
}
.alert("Новая папка", isPresented: $showNewFolderAlert) {
TextField("Имя папки", text: $newFolderName)
Button("Создать") {
let name = newFolderName
newFolderName = ""
Task { await store.createFolder(named: name) }
}
Button("Отмена", role: .cancel) {}
}
.alert("Переименовать", isPresented: Binding(
get: { renameItem != nil },
set: { if !$0 { renameItem = nil } }
)) {
TextField("Новое имя", text: $renameText)
Button("OK") {
if let item = renameItem {
let name = renameText
renameItem = nil
Task { await store.rename(item, to: name) }
}
}
Button("Отмена", role: .cancel) { renameItem = nil }
}
.onAppear { store.bootstrap() }
.onChange(of: store.sidebarSelection) { _, newValue in
store.selectSidebar(newValue)
}
}
private func pathBar(store: AppStore) -> some View {
HStack(spacing: 8) {
Button {
store.goBack()
} label: {
Image(systemName: "chevron.left")
}
.disabled(store.pathStack.count <= 1 || store.sidebarSelection == .trash)
Button {
Task { await store.refreshAll() }
} label: {
Image(systemName: "arrow.clockwise")
}
Text(breadcrumb(store: store))
.font(.subheadline)
.lineLimit(1)
Spacer()
TextField("Поиск", text: Binding(
get: { store.searchText },
set: { store.searchText = $0 }
))
.textFieldStyle(.roundedBorder)
.frame(width: 180)
}
.padding(10)
}
private func breadcrumb(store: AppStore) -> String {
switch store.sidebarSelection {
case .trash: return "Корзина"
case .recents: return "Недавние"
case .favorites: return "Избранное"
case .screenshots: return "Скриншоты"
case .files: return "Доступные мне"
default:
if store.currentPath == "/" { return "YanDisk" }
return store.currentPath
}
}
private func visibleItems(store: AppStore) -> [DiskResource] {
switch store.sidebarSelection {
case .trash: return store.trashItems
case .recents: return store.recentItems
case .files: return [] // «Доступные мне» — заглушка до реализации API
default: return store.filteredItems
}
}
@ViewBuilder
private func fileBrowser(store: AppStore) -> some View {
let items = visibleItems(store: store)
if store.isLoading && items.isEmpty {
ProgressView("Загрузка…")
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else if items.isEmpty {
VStack(spacing: 12) {
Image(systemName: store.sidebarSelection == .trash ? "trash" : "folder")
.font(.system(size: 40))
.foregroundStyle(.tertiary)
Text(store.sidebarSelection == .trash ? "Корзина пуста" : "Папка пуста")
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
Table(items, selection: Binding(
get: { store.selection },
set: { store.selection = $0 }
)) {
TableColumn("Имя") { item in
HStack(spacing: 8) {
Image(systemName: icon(for: item))
.foregroundStyle(item.isDirectory ? Color.accentColor : .secondary)
.frame(width: 18)
Text(item.displayName)
.lineLimit(1)
if item.publicUrl != nil {
Image(systemName: "link")
.font(.caption2)
.foregroundStyle(.secondary)
}
}
}
TableColumn("Размер") { item in
Text(item.isDirectory ? "—" : ByteFormatter.string(info: item.displaySize))
.foregroundStyle(.secondary)
}
.width(100)
TableColumn("Изменён") { item in
Text(formattedDate(item.modified))
.foregroundStyle(.secondary)
}
.width(140)
TableColumn("Синхр.") { item in
Text(store.syncEngine.policy.mode(for: item.path).shortTitle)
.foregroundStyle(.secondary)
.font(.caption)
}
.width(100)
}
.contextMenu(forSelectionType: String.self) { ids in
if let path = ids.first, let item = items.first(where: { $0.path == path }) {
contextMenu(store: store, item: item)
}
} primaryAction: { ids in
if let path = ids.first, let item = items.first(where: { $0.path == path }) {
store.open(item)
}
}
.dropDestination(for: URL.self) { urls, _ in
let files = urls.filter { $0.isFileURL }
guard !files.isEmpty else { return false }
Task { await store.uploadFiles(from: files) }
return true
}
}
}
@ViewBuilder
private func contextMenu(store: AppStore, item: DiskResource) -> some View {
if store.sidebarSelection == .trash {
Button("Восстановить") {
Task { await store.restoreFromTrash(item) }
}
} else {
Button("Открыть") { store.open(item) }
Button("Показать в Finder") { store.revealInFinder(item) }
Button("Создать копию на компьютере") {
Task { await store.downloadToComputer(item) }
}
Divider()
Button("Скопировать ссылку") {
Task { await store.copyPublicLink(item) }
}
Button("Настроить общий доступ") {
store.prepareShare(item)
}
Divider()
Menu("Синхронизация") {
ForEach(SyncMode.allCases) { mode in
Button(mode.title) {
Task { await store.applySyncMode(mode, to: item) }
}
}
}
Divider()
Button("Скопировать «\(item.displayName)»") {
Task { await store.duplicate(item) }
}
Button("Переместить в Корзину") {
Task { await store.moveToTrash(item) }
}
Divider()
Button("Переименовать") {
renameItem = item
renameText = item.displayName
}
}
}
private func icon(for item: DiskResource) -> String {
if item.isDirectory { return "folder.fill" }
let ext = (item.displayName as NSString).pathExtension.lowercased()
switch ext {
case "png", "jpg", "jpeg", "gif", "heic", "webp": return "photo"
case "mp4", "mov", "m4v": return "film"
case "mp3", "m4a", "wav": return "music.note"
case "pdf": return "doc.richtext"
case "zip", "rar", "7z": return "doc.zipper"
default: return "doc"
}
}
private func formattedDate(_ raw: String?) -> String {
guard let raw else { return "—" }
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
var date = formatter.date(from: raw)
if date == nil {
formatter.formatOptions = [.withInternetDateTime]
date = formatter.date(from: raw)
}
guard let date else { return raw }
let out = DateFormatter()
out.locale = Locale(identifier: "ru_RU")
out.dateFormat = "dd.MM.yyyy HH:mm"
return out.string(from: date)
}
}
struct TasksView: View {
@Environment(AppStore.self) private var store
var body: some View {
if store.syncEngine.tasks.isEmpty {
VStack(spacing: 10) {
Image(systemName: "arrow.triangle.2.circlepath")
.font(.system(size: 36))
.foregroundStyle(.tertiary)
Text("Нет активных задач")
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
List(store.syncEngine.tasks) { task in
HStack {
Image(systemName: task.kind == .upload ? "arrow.up.circle" : "arrow.down.circle")
.foregroundStyle(task.isError ? Color.red : Color.accentColor)
VStack(alignment: .leading, spacing: 2) {
Text((task.path as NSString).lastPathComponent)
.lineLimit(1)
Text(task.kind.title)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
if task.isFinished {
Image(systemName: task.isError ? "xmark.circle.fill" : "checkmark.circle.fill")
.foregroundStyle(task.isError ? .red : .green)
} else {
ProgressView()
.controlSize(.small)
}
}
.padding(.vertical, 2)
}
}
}
}
+224
View File
@@ -0,0 +1,224 @@
import SwiftUI
import AppKit
struct MenuBarView: View {
@Environment(AppStore.self) private var store
@Environment(\.openWindow) private var openWindow
@State private var showLogin = false
@AppStorage("Appearance") private var appearance = "system"
var body: some View {
VStack(alignment: .leading, spacing: 0) {
header
Divider()
statusSection
Divider()
actionsSection
Divider()
folderSection
Divider()
footerSection
}
.frame(width: 320)
.preferredColorScheme(colorScheme)
.onAppear {
store.bootstrap()
if !store.isAuthorized {
showLogin = true
}
}
.onChange(of: store.showLoginSheet) { _, value in
showLogin = value
}
.sheet(isPresented: $showLogin) {
LoginView()
.environment(store)
}
}
private var colorScheme: ColorScheme? {
switch appearance {
case "light": return .light
case "dark": return .dark
default: return nil
}
}
private var header: some View {
HStack(spacing: 10) {
Image(systemName: "cloud.fill")
.font(.system(size: 22, weight: .medium))
.foregroundStyle(Color(red: 0.93, green: 0.0, blue: 0.21))
VStack(alignment: .leading, spacing: 2) {
Text("YanDisk")
.font(.headline)
Text(store.isAuthorized ? "Подключён" : "Не авторизован")
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
}
.padding(12)
}
private var statusSection: some View {
VStack(alignment: .leading, spacing: 6) {
HStack {
Circle()
.fill(statusColor)
.frame(width: 8, height: 8)
Text(store.syncEngine.state.label)
.font(.subheadline)
Spacer()
}
if store.diskInfo != nil {
Text("\(store.formattedUsed) из \(store.formattedTotal)")
.font(.caption)
.foregroundStyle(.secondary)
ProgressView(value: store.usedPercent)
.progressViewStyle(.linear)
}
HStack {
Text("Режим:")
Picker("", selection: Binding(
get: { store.syncEngine.policy.globalMode },
set: { store.syncEngine.setGlobalMode($0) }
)) {
ForEach(SyncMode.allCases) { mode in
Text(mode.title).tag(mode)
}
}
.labelsHidden()
.fixedSize()
}
.font(.caption)
}
.padding(.horizontal, 12)
.padding(.vertical, 10)
}
private var statusColor: Color {
switch store.syncEngine.state {
case .success: return .green
case .syncing: return .orange
case .offline: return .gray
case .error: return .red
case .idle: return .secondary
}
}
private var actionsSection: some View {
VStack(spacing: 0) {
MenuBarButton(icon: "folder", title: "Открыть YanDisk") {
NSApp.activate(ignoringOtherApps: true)
NSApp.sendAction(#selector(AppDelegate.showMainWindow), to: nil, from: nil)
}
MenuBarButton(icon: "arrow.triangle.2.circlepath", title: "Синхронизировать сейчас") {
Task { await store.syncEngine.syncNow() }
}
MenuBarButton(icon: "rectangle.and.pencil.and.ellipsis", title: "Загрузить файлы…") {
let panel = NSOpenPanel()
panel.allowsMultipleSelection = true
panel.canChooseDirectories = true
panel.canChooseFiles = true
if panel.runModal() == .OK {
Task { await store.uploadFiles(from: panel.urls) }
}
}
MenuBarButton(icon: "folder.badge.gearshape", title: "Показать папку синхронизации") {
try? FileManager.default.createDirectory(
at: store.syncEngine.syncFolderURL,
withIntermediateDirectories: true
)
NSWorkspace.shared.open(store.syncEngine.syncFolderURL)
}
MenuBarButton(icon: "gearshape", title: "Настройки") {
NSApp.activate(ignoringOtherApps: true)
NSApp.sendAction(#selector(AppDelegate.showPreferencesWindow(_:)), to: nil, from: nil)
}
}
.padding(.vertical, 4)
}
private var folderSection: some View {
VStack(alignment: .leading, spacing: 6) {
Text("СИНХРОНИЗАЦИЯ")
.font(.caption2)
.foregroundStyle(.secondary)
.padding(.horizontal, 12)
HStack {
Text("Весь Диск")
.font(.subheadline)
Spacer()
Picker("", selection: Binding(
get: { store.syncEngine.policy.mode(for: "/") },
set: { newValue in
Task { await store.syncEngine.applyModeChange(newValue, path: "/") }
}
)) {
ForEach(SyncMode.allCases) { m in
Text(m.shortTitle).tag(m)
}
}
.labelsHidden()
.fixedSize()
}
.padding(.horizontal, 12)
Toggle("Включить синхронизацию", isOn: Binding(
get: { store.syncEngine.isSyncEnabled },
set: { store.syncEngine.setSyncEnabled($0) }
))
.toggleStyle(.switch)
.controlSize(.small)
.padding(.horizontal, 12)
.padding(.bottom, 6)
}
.padding(.top, 4)
}
private var footerSection: some View {
HStack {
if store.isAuthorized {
Button("Выйти") {
store.logout()
}
} else {
Button("Добавить аккаунт") {
store.openAuthorization()
showLogin = true
}
}
Spacer()
Button("Закрыть") {
NSApp.terminate(nil)
}
}
.buttonStyle(.plain)
.font(.caption)
.foregroundStyle(.secondary)
.padding(10)
}
}
struct MenuBarButton: View {
let icon: String
let title: String
let action: () -> Void
var body: some View {
Button(action: action) {
HStack(spacing: 10) {
Image(systemName: icon)
.frame(width: 16)
.foregroundStyle(.secondary)
Text(title)
.foregroundStyle(.primary)
Spacer()
}
.contentShape(Rectangle())
.padding(.horizontal, 12)
.padding(.vertical, 6)
}
.buttonStyle(.plain)
}
}
+178
View File
@@ -0,0 +1,178 @@
import SwiftUI
import AppKit
import UniformTypeIdentifiers
struct PreferencesView: View {
@Environment(AppStore.self) private var store
@AppStorage("Appearance") private var appearance = "system"
var body: some View {
TabView {
generalTab
.tabItem { Label("Основные", systemImage: "gearshape") }
syncTab
.tabItem { Label("Синхронизация", systemImage: "arrow.triangle.2.circlepath") }
accountTab
.tabItem { Label("Аккаунт", systemImage: "person.crop.circle") }
aboutTab
.tabItem { Label("О программе", systemImage: "info.circle") }
}
.padding(8)
.preferredColorScheme(colorScheme)
}
private var colorScheme: ColorScheme? {
switch appearance {
case "light": return .light
case "dark": return .dark
default: return nil
}
}
private var generalTab: some View {
Form {
Picker("Тема", selection: $appearance) {
Text("Как в системе").tag("system")
Text("Светлая").tag("light")
Text("Тёмная").tag("dark")
}
Toggle("Включить синхронизацию", isOn: Binding(
get: { store.syncEngine.isSyncEnabled },
set: { store.syncEngine.setSyncEnabled($0) }
))
Picker("Режим по умолчанию", selection: Binding(
get: { store.syncEngine.policy.globalMode },
set: { store.syncEngine.setGlobalMode($0) }
)) {
ForEach(SyncMode.allCases) { mode in
Text(mode.title).tag(mode)
}
}
LabeledContent("Папка синхронизации") {
HStack {
Text(store.syncEngine.syncFolderURL.path)
.lineLimit(1)
.truncationMode(.middle)
.foregroundStyle(.secondary)
Button("Изменить…") {
let panel = NSOpenPanel()
panel.canChooseFiles = false
panel.canChooseDirectories = true
panel.canCreateDirectories = true
panel.allowsMultipleSelection = false
panel.prompt = "Выбрать"
if panel.runModal() == .OK, let url = panel.url {
store.syncEngine.setSyncFolder(url)
}
}
}
}
Button("Показать в Finder") {
NSWorkspace.shared.open(store.syncEngine.syncFolderURL)
}
}
.formStyle(.grouped)
}
private var syncTab: some View {
Form {
Section("Режимы синхронизации") {
ForEach(SyncMode.allCases) { mode in
VStack(alignment: .leading, spacing: 4) {
Text(mode.title).font(.headline)
Text(modeHelp(mode))
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(.vertical, 2)
}
}
Section("Переопределения по папкам") {
if store.syncEngine.policy.pathModes.isEmpty {
Text("Нет переопределений")
.foregroundStyle(.secondary)
} else {
ForEach(store.syncEngine.policy.pathModes.sorted(by: { $0.key < $1.key }), id: \.key) { path, mode in
HStack {
Text(path)
Spacer()
Text(mode.title)
.foregroundStyle(.secondary)
Button {
store.syncEngine.policy.clearMode(for: path)
} label: {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
}
}
}
}
Button("Синхронизировать сейчас") {
Task { await store.syncEngine.syncNow() }
}
}
.formStyle(.grouped)
}
private var accountTab: some View {
Form {
if store.isAuthorized {
LabeledContent("Статус") {
Text("Авторизован")
.foregroundStyle(.green)
}
LabeledContent("Client ID") {
Text(OAuthManager.clientId)
.font(.caption)
.foregroundStyle(.secondary)
}
Button("Выйти из аккаунта", role: .destructive) {
store.logout()
}
} else {
Text("Аккаунт не подключён")
Button("Добавить аккаунт") {
store.openAuthorization()
store.showLoginSheet = true
}
.buttonStyle(.borderedProminent)
}
}
.formStyle(.grouped)
}
private var aboutTab: some View {
Form {
LabeledContent("Приложение") {
Text("YanDisk")
}
LabeledContent("Версия") {
Text("1.0.0")
}
LabeledContent("API") {
Text("REST cloud-api.yandex.net")
.font(.caption)
}
Text("Клиент для macOS на Apple Silicon. Чтение, запись, копирование, перемещение, корзина, синхронизация и общий доступ.")
.font(.caption)
.foregroundStyle(.secondary)
}
.formStyle(.grouped)
}
private func modeHelp(_ mode: SyncMode) -> String {
switch mode {
case .everywhere: return "Файлы есть и на компьютере, и в облаке. Двусторонняя синхронизация."
case .computerOnly: return "Сохранять на компьютере; новые файлы из облака также скачиваются."
case .cloudOnly: return "Оставить только в облаке: локальная копия удаляется."
}
}
}
+86
View File
@@ -0,0 +1,86 @@
import SwiftUI
struct ShareSheetView: View {
@Environment(AppStore.self) private var store
@Environment(\.dismiss) private var dismiss
var body: some View {
@Bindable var store = store
VStack(alignment: .leading, spacing: 16) {
HStack {
Image(systemName: "person.2.circle.fill")
.font(.system(size: 28))
.foregroundStyle(Color(red: 0.93, green: 0.0, blue: 0.21))
VStack(alignment: .leading) {
Text("Общий доступ")
.font(.title3.bold())
if let item = store.shareTarget {
Text(item.displayName)
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
Spacer()
}
Divider()
Picker("Права", selection: $store.shareSettings.rights) {
ForEach([ShareRight.read, .write, .readOnly, .readWithPassword, .readWithPasswordNoDownload]) { right in
Text(right.title).tag(right)
}
}
Toggle("Доступен всем, у кого есть ссылка", isOn: $store.shareSettings.everyone)
SecureField("Пароль (необязательно)", text: $store.shareSettings.password)
Picker("Срок действия", selection: Binding(
get: { store.shareSettings.availableUntilSeconds ?? 0 },
set: { store.shareSettings.availableUntilSeconds = $0 == 0 ? nil : $0 }
)) {
Text("Без ограничений").tag(0)
Text("1 час").tag(3600)
Text("1 день").tag(86400)
Text("7 дней").tag(604800)
Text("30 дней").tag(2_592_000)
}
if let url = store.shareURL, !url.isEmpty {
HStack {
TextField("Ссылка", text: .constant(url))
.textFieldStyle(.roundedBorder)
.disabled(true)
Button("Копировать") {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(url, forType: .string)
}
}
}
HStack {
if let item = store.shareTarget, item.publicUrl != nil {
Button("Закрыть доступ", role: .destructive) {
Task {
await store.unpublishShare(item)
dismiss()
}
}
}
Spacer()
Button("Отмена") { dismiss() }
.keyboardShortcut(.cancelAction)
Button("Поделиться") {
Task {
await store.publishShare()
}
}
.keyboardShortcut(.defaultAction)
.buttonStyle(.borderedProminent)
.tint(Color(red: 0.93, green: 0.0, blue: 0.21))
}
}
.padding(20)
.frame(width: 460)
}
}