Russian UI: settings, notch views, agent steps, errors; visible success after installing hooks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -66,7 +66,7 @@ struct OverviewView: View {
|
||||
Text({ () -> String in
|
||||
switch agent.source {
|
||||
case .claudeCode: return "Claude Code"
|
||||
case .agent: return "Agent"
|
||||
case .agent: return "Агент"
|
||||
case .n8n: return "n8n"
|
||||
}
|
||||
}())
|
||||
@@ -272,14 +272,14 @@ struct EmptyStateView: View {
|
||||
CardBackground(wash: nil)
|
||||
HStack(spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text("Nothing running right now.")
|
||||
Text("Сейчас ничего не запущено.")
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
Text("Drop a file or window, or ask me anything.")
|
||||
Text("Перетащи файл или окно — или спроси что угодно.")
|
||||
.font(.system(size: 13))
|
||||
.foregroundColor(Color(hex: "#9398A1"))
|
||||
}
|
||||
Spacer()
|
||||
PrimaryButton("Ask Claude") {
|
||||
PrimaryButton("Спросить Claude") {
|
||||
state.view = .prompt
|
||||
}
|
||||
}
|
||||
@@ -300,18 +300,18 @@ struct ApprovalView: View {
|
||||
ZStack {
|
||||
CardBackground(wash: .amber)
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
AgentWho(task: state.focusTask, label: "needs permission")
|
||||
AgentWho(task: state.focusTask, label: "просит разрешение")
|
||||
CodeBlock(text: approval?.command ?? approval?.tool ?? "…")
|
||||
HStack(spacing: 8) {
|
||||
SecondaryButton("Deny") {
|
||||
SecondaryButton("Запретить") {
|
||||
HookServer.shared.sendApprovalDecision("deny")
|
||||
}
|
||||
PrimaryButton("Allow") {
|
||||
PrimaryButton("Разрешить") {
|
||||
HookServer.shared.sendApprovalDecision("allow")
|
||||
}
|
||||
// Codex rejects updatedPermissions, so "Always" is not offered
|
||||
if approval?.pillId != "agent_codex" {
|
||||
SecondaryButton("Always") {
|
||||
SecondaryButton("Всегда") {
|
||||
HookServer.shared.sendApprovalDecision("always")
|
||||
}
|
||||
}
|
||||
@@ -356,14 +356,14 @@ struct QuestionView: View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
// Header row: agent name + question counter + "Reply in terminal" link
|
||||
HStack(spacing: 4) {
|
||||
AgentWho(task: nil, label: "Claude Code is asking")
|
||||
AgentWho(task: nil, label: "Claude Code спрашивает")
|
||||
Spacer(minLength: 4)
|
||||
if q.questions.count > 1 {
|
||||
Text("\(qi + 1)/\(q.questions.count)")
|
||||
.font(.system(size: 10))
|
||||
.foregroundColor(Color(hex: "#6B7079"))
|
||||
}
|
||||
Button("Reply in terminal") { HookServer.shared.sendQuestionAsk() }
|
||||
Button("Ответить в терминале") { HookServer.shared.sendQuestionAsk() }
|
||||
.buttonStyle(.plain)
|
||||
.font(.system(size: 10))
|
||||
.foregroundColor(Color(hex: "#6B7079"))
|
||||
@@ -382,7 +382,7 @@ struct QuestionView: View {
|
||||
// Options (wrapping) or "Other…" compact inline row
|
||||
if curOther {
|
||||
HStack(spacing: 6) {
|
||||
TextField("Your answer…", text: Binding(
|
||||
TextField("Твой ответ…", text: Binding(
|
||||
get: { qi < otherTexts.count ? otherTexts[qi] : "" },
|
||||
set: { v in if qi < otherTexts.count { otherTexts[qi] = v } }
|
||||
))
|
||||
@@ -396,7 +396,7 @@ struct QuestionView: View {
|
||||
.padding(.horizontal, 8).padding(.vertical, 5)
|
||||
.background(Color.white.opacity(0.07))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
Button(isLast ? "Send" : "Next") {
|
||||
Button(isLast ? "Отправить" : "Далее") {
|
||||
commitOtherAndProceed(q: q, qi: qi, isLast: isLast)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
@@ -438,14 +438,14 @@ struct QuestionView: View {
|
||||
}
|
||||
}
|
||||
// "Other…" implicit free-text option
|
||||
SecondaryButton("Other…") {
|
||||
SecondaryButton("Другое…") {
|
||||
if qi < showOther.count { showOther[qi] = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
// Send/Next — only for multi-select (and not while "Other…" field is open)
|
||||
if isMulti && !curOther {
|
||||
PrimaryButton(isLast ? "Send" : "Next") {
|
||||
PrimaryButton(isLast ? "Отправить" : "Далее") {
|
||||
proceedFromQuestion(q: q, qi: qi, isLast: isLast)
|
||||
}
|
||||
.disabled(!canProceed)
|
||||
@@ -521,14 +521,14 @@ struct ErrorView: View {
|
||||
CardBackground(wash: .red)
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
AgentWho(task: state.focusTask, label: "n8n")
|
||||
Text("Workflow stopped.")
|
||||
Text("Сценарий остановлен.")
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
Text("Gmail node timed out after 30s. Retry or open n8n.")
|
||||
Text("Узел Gmail не ответил за 30 с. Повтори или открой n8n.")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(Color(hex: "#FF8D97"))
|
||||
HStack(spacing: 8) {
|
||||
PrimaryButton("Retry") { /* retry */ }
|
||||
SecondaryButton("Open in n8n") { /* open */ }
|
||||
PrimaryButton("Повторить") { /* retry */ }
|
||||
SecondaryButton("Открыть в n8n") { /* open */ }
|
||||
}
|
||||
}
|
||||
.padding(.leading, 116)
|
||||
@@ -548,18 +548,18 @@ struct FinishedView: View {
|
||||
ZStack {
|
||||
CardBackground(wash: .green)
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
AgentWho(task: state.focusTask, label: "Claude Code finished")
|
||||
AgentWho(task: state.focusTask, label: "Claude Code закончил")
|
||||
Text({
|
||||
if let fl = state.focusTask?.finalLine { return fl }
|
||||
if let s = state.focusTask?.steps.last(where: { !$0.isDiffStep }) { return s }
|
||||
return "Session finished"
|
||||
return "Сессия завершена"
|
||||
}())
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
HStack(spacing: 8) {
|
||||
#if !APPSTORE
|
||||
PrimaryButton("Open terminal") {
|
||||
PrimaryButton("Открыть терминал") {
|
||||
let terminalBundleIds = ["com.apple.Terminal", "com.googlecode.iterm2", "net.kovidgoyal.kitty", "com.mitchellh.ghostty"]
|
||||
let activated = terminalBundleIds.compactMap { id in
|
||||
NSWorkspace.shared.runningApplications.first { $0.bundleIdentifier == id }
|
||||
@@ -570,7 +570,7 @@ struct FinishedView: View {
|
||||
NotificationCenter.default.post(name: .islandCollapse, object: nil)
|
||||
}
|
||||
#endif
|
||||
SecondaryButton("OK") {
|
||||
SecondaryButton("ОК") {
|
||||
NotificationCenter.default.post(name: .islandCollapse, object: nil)
|
||||
}
|
||||
}
|
||||
@@ -632,11 +632,11 @@ struct DiffCardView: View {
|
||||
|
||||
// Content
|
||||
if diff.tooLarge {
|
||||
Text("Diff too large")
|
||||
Text("Слишком большой diff")
|
||||
.font(.system(size: 10.5))
|
||||
.foregroundColor(Color(hex: "#6B7079"))
|
||||
} else if allLines.isEmpty {
|
||||
Text("No changes")
|
||||
Text("Нет изменений")
|
||||
.font(.system(size: 10.5))
|
||||
.foregroundColor(Color(hex: "#6B7079"))
|
||||
} else {
|
||||
@@ -726,8 +726,8 @@ struct ConfusedView: View {
|
||||
ZStack {
|
||||
CardBackground(wash: .pink)
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text("Too many hits at once.").font(.system(size: 15, weight: .semibold))
|
||||
Text("Give me a sec — back to work in three seconds.")
|
||||
Text("Слишком много тычков разом.").font(.system(size: 15, weight: .semibold))
|
||||
Text("Дай секунду — вернусь к работе через три секунды.")
|
||||
.font(.system(size: 13)).foregroundColor(Color(hex: "#9398A1"))
|
||||
}
|
||||
.padding(.leading, 128)
|
||||
@@ -768,11 +768,11 @@ struct UploadView: View {
|
||||
center: .bottom, startRadius: 0, endRadius: 200
|
||||
))
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Drop your files here")
|
||||
Text("Перетащи файлы сюда")
|
||||
.font(.system(size: 13, weight: .medium))
|
||||
.foregroundColor(state.fileDragOver ? Color(hex: "#34D399") : Color(hex: "#D5D7DB"))
|
||||
HStack(spacing: 6) {
|
||||
ForEach(["PDF", "Images", "Code", "Docs"], id: \.self) { label in
|
||||
ForEach(["PDF", "Фото", "Код", "Документы"], id: \.self) { label in
|
||||
Text(label)
|
||||
.font(.system(size: 11))
|
||||
.padding(.horizontal, 8).padding(.vertical, 3)
|
||||
@@ -879,12 +879,12 @@ struct UploadingView: View {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(Color(hex: "#34D399"))
|
||||
Text(" \(state.droppedFile?.name ?? "File")")
|
||||
Text(" \(state.droppedFile?.name ?? "Файл")")
|
||||
.font(.system(size: 12.5, weight: .semibold))
|
||||
.foregroundColor(Color(hex: "#34D399"))
|
||||
.lineLimit(1).truncationMode(.middle)
|
||||
} else {
|
||||
Text("Uploading \(state.droppedFile?.name ?? "file")")
|
||||
Text("Загружаю \(state.droppedFile?.name ?? "файл")")
|
||||
.font(.system(size: 12.5))
|
||||
.foregroundColor(Color(hex: "#A9ADB5"))
|
||||
.lineLimit(1).truncationMode(.middle)
|
||||
@@ -915,12 +915,12 @@ struct ChooseView: View {
|
||||
ZStack(alignment: .leading) {
|
||||
CardBackground(wash: nil)
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
let fileName = state.droppedFile?.name ?? "file"
|
||||
(Text(fileName).font(.system(size: 14, weight: .semibold)) + Text(" is ready.").font(.system(size: 14, weight: .semibold)))
|
||||
Text("What do you want to do with it?").font(.system(size: 12.5)).foregroundColor(Color(hex: "#9398A1"))
|
||||
let fileName = state.droppedFile?.name ?? "Файл"
|
||||
(Text(fileName).font(.system(size: 14, weight: .semibold)) + Text(" готов.").font(.system(size: 14, weight: .semibold)))
|
||||
Text("Что с ним сделать?").font(.system(size: 12.5)).foregroundColor(Color(hex: "#9398A1"))
|
||||
HStack(spacing: 8) {
|
||||
PrimaryButton("Ask a question") { state.view = .prompt }
|
||||
SecondaryButton("Send by email") { state.view = .mail }
|
||||
PrimaryButton("Задать вопрос") { state.view = .prompt }
|
||||
SecondaryButton("Отправить почтой") { state.view = .mail }
|
||||
}
|
||||
}
|
||||
.padding(.leading, 98)
|
||||
@@ -944,7 +944,7 @@ struct MailView: View {
|
||||
CardBackground(wash: nil)
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack(spacing: 6) {
|
||||
Text("New email").font(.system(size: 12, weight: .semibold))
|
||||
Text("Новое письмо").font(.system(size: 12, weight: .semibold))
|
||||
if let name = state.droppedFile?.name {
|
||||
Text("with").font(.system(size: 12)).foregroundColor(Color(hex: "#8E939C"))
|
||||
Text(name).font(.system(size: 12)).foregroundColor(Color(hex: "#8E939C"))
|
||||
@@ -952,8 +952,8 @@ struct MailView: View {
|
||||
}
|
||||
}
|
||||
|
||||
MailField(label: "To", placeholder: "address@example.com", text: $to)
|
||||
MailField(label: "Subject", placeholder: state.droppedFile?.name ?? "Subject", text: $subject)
|
||||
MailField(label: "Кому", placeholder: "address@example.com", text: $to)
|
||||
MailField(label: "Тема", placeholder: state.droppedFile?.name ?? "Тема", text: $subject)
|
||||
|
||||
// Body — TextEditor scrolls internally when text overflows
|
||||
TextEditor(text: $bodyText)
|
||||
@@ -971,11 +971,11 @@ struct MailView: View {
|
||||
}
|
||||
|
||||
HStack(spacing: 8) {
|
||||
PrimaryButton(isSending ? "Sending…" : "Send") {
|
||||
PrimaryButton(isSending ? "Отправка…" : "Отправить") {
|
||||
guard !isSending else { return }
|
||||
sendMail()
|
||||
}
|
||||
SecondaryButton("Cancel") { state.view = .choose }
|
||||
SecondaryButton("Отмена") { state.view = .choose }
|
||||
}
|
||||
}
|
||||
.padding(.leading, 92)
|
||||
@@ -986,8 +986,8 @@ struct MailView: View {
|
||||
}
|
||||
|
||||
private func sendMail() {
|
||||
guard !to.isEmpty else { statusMsg = "Missing recipient."; return }
|
||||
let subj = subject.isEmpty ? (state.droppedFile?.name ?? "File") : subject
|
||||
guard !to.isEmpty else { statusMsg = "Не указан получатель."; return }
|
||||
let subj = subject.isEmpty ? (state.droppedFile?.name ?? "Файл") : subject
|
||||
|
||||
// Prefer Resend if API key + sender address are configured
|
||||
let apiKey = KeychainStore.shared.get("resend-api-key")
|
||||
@@ -1006,12 +1006,12 @@ struct MailView: View {
|
||||
await MainActor.run {
|
||||
isSending = false
|
||||
if ok { onSuccess(recipient: recipient) }
|
||||
else { statusMsg = "Resend error — check API key & sender." }
|
||||
else { statusMsg = "Ошибка Resend — проверь API-ключ и отправителя." }
|
||||
}
|
||||
}
|
||||
} else if apiKey != nil && fromAddr == nil {
|
||||
// API key set but no sender — guide user instead of silent fallback
|
||||
statusMsg = "Set sender address in Settings."
|
||||
statusMsg = "Укажи адрес отправителя в настройках."
|
||||
} else {
|
||||
// No Resend — fallback to Mail
|
||||
sendViaAppleMail(to: to, subject: subj)
|
||||
@@ -1051,7 +1051,7 @@ struct MailView: View {
|
||||
#if APPSTORE
|
||||
// App Store: no AppleScript — use NSSharingService to compose (user sends manually)
|
||||
guard let service = NSSharingService(named: .composeEmail) else {
|
||||
statusMsg = "Mail not available."
|
||||
statusMsg = "Mail недоступен."
|
||||
return
|
||||
}
|
||||
var items: [Any] = [bodyText.isEmpty ? " " : bodyText]
|
||||
@@ -1097,14 +1097,14 @@ struct MailView: View {
|
||||
var err: NSDictionary?
|
||||
NSAppleScript(source: script)?.executeAndReturnError(&err)
|
||||
if err == nil { onSuccess(recipient: to) }
|
||||
else { statusMsg = "Mail error: \(err?["NSAppleScriptErrorMessage"] as? String ?? "unknown")" }
|
||||
else { statusMsg = "Ошибка Mail: \(err?["NSAppleScriptErrorMessage"] as? String ?? "неизвестно")" }
|
||||
#endif
|
||||
}
|
||||
|
||||
private func onSuccess(recipient: String) {
|
||||
SoundEngine.shared.play("send")
|
||||
NotificationCenter.default.post(name: .triggerEmote, object: BotEmote.wink)
|
||||
state.noteMessage = "Email sent to \(recipient)."
|
||||
state.noteMessage = "Письмо отправлено: \(recipient)."
|
||||
state.view = .note
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
NotificationCenter.default.post(name: .islandCollapse, object: nil)
|
||||
@@ -1198,7 +1198,7 @@ struct PromptView: View {
|
||||
.padding(.horizontal, 10)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
TextField(state.chatHistory.isEmpty ? "Ask me anything…" : "Continue…", text: $text)
|
||||
TextField(state.chatHistory.isEmpty ? "Спроси что угодно…" : "Продолжить…", text: $text)
|
||||
.textFieldStyle(.plain)
|
||||
.font(.system(size: 13))
|
||||
.focused($focused)
|
||||
@@ -1368,7 +1368,7 @@ struct ModelPickerView: View {
|
||||
if state.loadingProviderModels.contains(state.chatProvider) {
|
||||
HStack(spacing: 8) {
|
||||
ProgressView().scaleEffect(0.7)
|
||||
Text("Loading models…")
|
||||
Text("Загрузка моделей…")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(Color(hex: "#8A8F98"))
|
||||
}
|
||||
@@ -1476,9 +1476,9 @@ struct SearchingView: View {
|
||||
|
||||
var label: String {
|
||||
switch state.promptContext {
|
||||
case .window(_, let title, _): return "Claude is reading \(title)…"
|
||||
case .file(let name, _): return "Claude is reading \(name)…"
|
||||
case nil: return "Claude is searching…"
|
||||
case .window(_, let title, _): return "Claude читает \(title)…"
|
||||
case .file(let name, _): return "Claude читает \(name)…"
|
||||
case nil: return "Claude ищет…"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1534,17 +1534,17 @@ struct ResultView: View {
|
||||
// The URL comes from the model, which may have read attacker-controlled
|
||||
// files or pages: only plain web links may leave the app.
|
||||
let openURL = safeWebURL(result.items.first?.url)
|
||||
PrimaryButton("Open") {
|
||||
PrimaryButton("Открыть") {
|
||||
if let openURL { NSWorkspace.shared.open(openURL) }
|
||||
}
|
||||
.disabled(openURL == nil)
|
||||
.help(openURL?.absoluteString ?? "")
|
||||
SecondaryButton("Copy") {
|
||||
SecondaryButton("Копировать") {
|
||||
let text = result.items.map { "\($0.label): \($0.detail)" }.joined(separator: "\n")
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(text, forType: .string)
|
||||
}
|
||||
SecondaryButton("Close") { state.view = state.tasks.isEmpty ? .empty : .overview }
|
||||
SecondaryButton("Закрыть") { state.view = state.tasks.isEmpty ? .empty : .overview }
|
||||
}
|
||||
}
|
||||
.padding(.leading, 84)
|
||||
@@ -1726,12 +1726,12 @@ struct IntegrationCardView: View {
|
||||
private var statusLabel: String {
|
||||
#if !APPSTORE
|
||||
if task.id == "integration_music" {
|
||||
if appState.musicAutomationDenied { return "Automation not allowed" }
|
||||
if appState.musicPlaying { return "Playing · \(MusicController.shared.trackTitle ?? "Unknown")" }
|
||||
return "Not playing"
|
||||
if appState.musicAutomationDenied { return "Автоматизация запрещена" }
|
||||
if appState.musicPlaying { return "Играет · \(MusicController.shared.trackTitle ?? "Неизвестно")" }
|
||||
return "Не играет"
|
||||
}
|
||||
#endif
|
||||
if PillCatalog.definition(for: task.id)?.comingSoon == true { return "Coming soon" }
|
||||
if PillCatalog.definition(for: task.id)?.comingSoon == true { return "Скоро" }
|
||||
let svcErr = task.id == "integration_stripe" ? appState.stripeError
|
||||
: task.id == "integration_calcom" ? appState.calcomError
|
||||
: nil
|
||||
@@ -1739,12 +1739,12 @@ struct IntegrationCardView: View {
|
||||
let isHooks = task.id == "agent_gemini" || task.id == "agent_antigravity"
|
||||
let isAI = ChatProvider(pillID: task.id) != nil
|
||||
if isConfigured {
|
||||
if isHooks { return "Hooks installed" }
|
||||
if isHooks { return "Хуки установлены" }
|
||||
if isAI {
|
||||
let provider = ChatProvider(pillID: task.id)!
|
||||
if provider.isLocal {
|
||||
let model = provider == .ollama ? appState.ollamaChatModel : appState.lmstudioChatModel
|
||||
return "Connected · \(model)"
|
||||
return "Подключено · \(model)"
|
||||
}
|
||||
let model: String
|
||||
switch task.id {
|
||||
@@ -1753,16 +1753,16 @@ struct IntegrationCardView: View {
|
||||
case "ai_openai": model = appState.openAIChatModel
|
||||
default: model = ""
|
||||
}
|
||||
return "Key configured · \(model)"
|
||||
return "Ключ задан · \(model)"
|
||||
}
|
||||
return "Connected · loading…"
|
||||
return "Подключено · загрузка…"
|
||||
} else {
|
||||
if isHooks { return "Hooks not installed" }
|
||||
if isHooks { return "Хуки не установлены" }
|
||||
if isAI {
|
||||
let provider = ChatProvider(pillID: task.id)!
|
||||
return provider.isLocal ? "Not connected" : "Key not configured"
|
||||
return provider.isLocal ? "Не подключено" : "Ключ не задан"
|
||||
}
|
||||
return "Key not configured"
|
||||
return "Ключ не задан"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1836,7 +1836,7 @@ struct IntegrationCardView: View {
|
||||
.foregroundColor(Color(hex: "#F5F6F8"))
|
||||
.lineLimit(1).truncationMode(.tail)
|
||||
.layoutPriority(1)
|
||||
Text(PillCatalog.definition(for: task.id)?.sessionSubtitle ?? "Agent")
|
||||
Text(PillCatalog.definition(for: task.id)?.sessionSubtitle ?? "Агент")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(Color(hex: "#8E939C"))
|
||||
.lineLimit(1).truncationMode(.tail)
|
||||
@@ -1870,7 +1870,7 @@ struct IntegrationCardView: View {
|
||||
Text(PillCatalog.definition(for: task.id)?.name ?? task.name)
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
.foregroundColor(Color(hex: "#F5F6F8"))
|
||||
Text(PillCatalog.definition(for: task.id)?.subtitle ?? "Integration")
|
||||
Text(PillCatalog.definition(for: task.id)?.subtitle ?? "Интеграция")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(Color(hex: "#8E939C"))
|
||||
Spacer(minLength: 2)
|
||||
@@ -1890,7 +1890,7 @@ struct IntegrationCardView: View {
|
||||
|
||||
HStack(spacing: 8) {
|
||||
if task.id == "integration_claude" {
|
||||
Button("Open Visual Studio Code") { openVSCode() }
|
||||
Button("Открыть Visual Studio Code") { openVSCode() }
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.foregroundColor(Color(hex: task.color).opacity(0.7))
|
||||
.buttonStyle(.plain)
|
||||
@@ -1898,7 +1898,7 @@ struct IntegrationCardView: View {
|
||||
#if !APPSTORE
|
||||
if let url = NSWorkspace.shared.urlForApplication(
|
||||
withBundleIdentifier: "com.todesktop.230313mzl4w4u92") {
|
||||
Button("Open Cursor") {
|
||||
Button("Открыть Cursor") {
|
||||
NSWorkspace.shared.openApplication(at: url, configuration: .init(),
|
||||
completionHandler: nil)
|
||||
}
|
||||
@@ -1911,7 +1911,7 @@ struct IntegrationCardView: View {
|
||||
#if !APPSTORE
|
||||
if let url = NSWorkspace.shared.urlForApplication(
|
||||
withBundleIdentifier: "com.openai.codex") {
|
||||
Button("Open Codex") {
|
||||
Button("Открыть Codex") {
|
||||
NSWorkspace.shared.openApplication(at: url, configuration: .init(),
|
||||
completionHandler: nil)
|
||||
}
|
||||
@@ -1922,7 +1922,7 @@ struct IntegrationCardView: View {
|
||||
#endif
|
||||
} else if let provider = ChatProvider(pillID: task.id) {
|
||||
if isConfigured {
|
||||
Button("Chat with \(task.name)") {
|
||||
Button("Чат с \(task.name)") {
|
||||
switchChatProvider(provider)
|
||||
}
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
@@ -1931,12 +1931,12 @@ struct IntegrationCardView: View {
|
||||
}
|
||||
} else if task.id == "integration_music" {
|
||||
#if !APPSTORE
|
||||
Button("Open Music") { MusicController.shared.openMusic() }
|
||||
Button("Открыть Музыку") { MusicController.shared.openMusic() }
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.foregroundColor(Color(hex: task.color).opacity(0.85))
|
||||
.buttonStyle(.plain)
|
||||
if appState.musicAutomationDenied {
|
||||
Button("Open Settings…") { MusicController.shared.openAutomationSettings() }
|
||||
Button("Открыть настройки…") { MusicController.shared.openAutomationSettings() }
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(Color(hex: "#8E939C"))
|
||||
.buttonStyle(.plain)
|
||||
@@ -1951,7 +1951,7 @@ struct IntegrationCardView: View {
|
||||
}) {
|
||||
HStack(spacing: 5) {
|
||||
Circle().fill(accent).frame(width: 5, height: 5)
|
||||
Text(task.steps.first ?? "Workflow")
|
||||
Text(task.steps.first ?? "Сценарий")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(Color(hex: "#C5C8CD"))
|
||||
.lineLimit(1).truncationMode(.tail)
|
||||
@@ -1966,19 +1966,19 @@ struct IntegrationCardView: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
} else if let url = openURL {
|
||||
Button("Open \(task.name)") { NSWorkspace.shared.open(url) }
|
||||
Button("Открыть \(task.name)") { NSWorkspace.shared.open(url) }
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.foregroundColor(Color(hex: task.color).opacity(0.85))
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
if task.id == "integration_stripe" && isConfigured {
|
||||
Button("Refresh") { Task { @MainActor in StripePoller.shared.pollNow() } }
|
||||
Button("Обновить") { Task { @MainActor in StripePoller.shared.pollNow() } }
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.foregroundColor(Color(hex: "#0570DE").opacity(0.85))
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
if task.id == "integration_calcom" && isConfigured {
|
||||
Button("Refresh") { Task { @MainActor in CalcomPoller.shared.pollNow() } }
|
||||
Button("Обновить") { Task { @MainActor in CalcomPoller.shared.pollNow() } }
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.foregroundColor(Color(hex: "#C9956A").opacity(0.85))
|
||||
.buttonStyle(.plain)
|
||||
@@ -1988,7 +1988,7 @@ struct IntegrationCardView: View {
|
||||
&& task.id != "agent_cursor"
|
||||
&& task.id != "agent_codex"
|
||||
&& task.id != "integration_music" {
|
||||
Button("Settings…") {
|
||||
Button("Настройки…") {
|
||||
let section: String
|
||||
switch PillCatalog.definition(for: task.id)?.category {
|
||||
case .workspace, .agent: section = "agents"
|
||||
@@ -2056,7 +2056,7 @@ struct VercelDeploymentListView: View {
|
||||
Text("Vercel")
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
.foregroundColor(Color(hex: "#F5F6F8"))
|
||||
Text("Deployments")
|
||||
Text("Деплои")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(Color(hex: "#8E939C"))
|
||||
}
|
||||
@@ -2171,7 +2171,7 @@ struct VercelDetailView: View {
|
||||
.font(.system(size: 10))
|
||||
.foregroundColor(Color(hex: "#6B7079"))
|
||||
}
|
||||
Text(deployment.timeAgo + " ago")
|
||||
Text(deployment.timeAgo == "только что" ? deployment.timeAgo : deployment.timeAgo + " назад")
|
||||
.font(.system(size: 10))
|
||||
.foregroundColor(Color(hex: "#6B7079"))
|
||||
}
|
||||
@@ -2225,7 +2225,7 @@ struct ResendCardView: View {
|
||||
Text("Resend")
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
.foregroundColor(Color(hex: "#F5F6F8"))
|
||||
Text("Emails")
|
||||
Text("Письма")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(Color(hex: "#8E939C"))
|
||||
if let total {
|
||||
@@ -2367,7 +2367,7 @@ struct GitHubPulseCardView: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
} else {
|
||||
Text("Overview")
|
||||
Text("Обзор")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(Color(hex: "#8E939C"))
|
||||
}
|
||||
@@ -2385,13 +2385,13 @@ struct GitHubPulseCardView: View {
|
||||
if n == 0 { return "0" }
|
||||
let failing = pulse.myPRs.filter { $0.ci == .failure }.count
|
||||
let pending = pulse.myPRs.filter { $0.ci == .pending }.count
|
||||
if failing > 0 { return "\(n) · \(failing) failing" }
|
||||
if pending > 0 { return "\(n) · running" }
|
||||
if failing > 0 { return "\(n) · \(failing) с ошибкой" }
|
||||
if pending > 0 { return "\(n) · идёт" }
|
||||
return "\(n)"
|
||||
}()
|
||||
GitHubStatRow(
|
||||
icon: "arrow.triangle.pull", iconColor: ciColor(prWorst),
|
||||
label: "My PRs", value: prValue
|
||||
label: "Мои PR", value: prValue
|
||||
) { onTapSection(.myPRs) }
|
||||
|
||||
// To review
|
||||
@@ -2399,7 +2399,7 @@ struct GitHubPulseCardView: View {
|
||||
GitHubStatRow(
|
||||
icon: "eye",
|
||||
iconColor: reviewCount > 0 ? "#8AB4F8" : "#6B7079",
|
||||
label: "To review",
|
||||
label: "На ревью",
|
||||
value: "\(reviewCount)"
|
||||
) { onTapSection(.toReview) }
|
||||
|
||||
@@ -2409,18 +2409,18 @@ struct GitHubPulseCardView: View {
|
||||
switch mainWorst {
|
||||
case .failure:
|
||||
let n = pulse.mainCI.filter { $0.ci == .failure }.count
|
||||
return ("xmark.octagon.fill", "#F4505E", "\(n) failing")
|
||||
return ("xmark.octagon.fill", "#F4505E", "\(n) с ошибкой")
|
||||
case .pending:
|
||||
return ("checkmark.seal.fill", "#F5A524", "running")
|
||||
return ("checkmark.seal.fill", "#F5A524", "идёт")
|
||||
case .success:
|
||||
return ("checkmark.seal.fill", "#22C55E", "all green")
|
||||
return ("checkmark.seal.fill", "#22C55E", "всё зелёное")
|
||||
case .unknown:
|
||||
return ("checkmark.seal.fill", "#6B7079", pulse.mainCI.isEmpty ? "no repos" : "unknown")
|
||||
return ("checkmark.seal.fill", "#6B7079", pulse.mainCI.isEmpty ? "нет репо" : "неизвестно")
|
||||
}
|
||||
}()
|
||||
GitHubStatRow(
|
||||
icon: ciIcon, iconColor: ciIconColor,
|
||||
label: "Default branch CI", value: ciValue
|
||||
label: "CI основной ветки", value: ciValue
|
||||
) { onTapSection(.mainCI) }
|
||||
}
|
||||
.padding(.top, 6)
|
||||
@@ -2479,10 +2479,10 @@ struct GitHubDetailView: View {
|
||||
|
||||
private var title: String {
|
||||
switch section {
|
||||
case .myPRs: return "My PRs"
|
||||
case .toReview: return "To review"
|
||||
case .mainCI: return "Default branch CI"
|
||||
case .activity: return "Activity"
|
||||
case .myPRs: return "Мои PR"
|
||||
case .toReview: return "На ревью"
|
||||
case .mainCI: return "CI основной ветки"
|
||||
case .activity: return "Активность"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2530,7 +2530,7 @@ struct GitHubDetailView: View {
|
||||
|
||||
// List
|
||||
if items.isEmpty && repoItems.isEmpty {
|
||||
Text("Nothing here")
|
||||
Text("Пусто")
|
||||
.font(.system(size: 10.5))
|
||||
.foregroundColor(Color(hex: "#6B7079"))
|
||||
.padding(.top, 8)
|
||||
@@ -2619,16 +2619,15 @@ private struct GitHubActivityDetailContent: View {
|
||||
if let day = hoveredDay {
|
||||
let label: String
|
||||
switch day.count {
|
||||
case 0: label = "No contributions"
|
||||
case 1: label = "1 contribution"
|
||||
default: label = "\(day.count) contributions"
|
||||
case 0: label = "Нет вкладов"
|
||||
default: label = "\(day.count) \(ruPlural(day.count, "вклад", "вклада", "вкладов"))"
|
||||
}
|
||||
return "\(activityDateLabel(day.date)) · \(label)"
|
||||
}
|
||||
guard let act = activity else { return "" }
|
||||
let total = activityTotalLabel(act.total)
|
||||
if let s = stats { return "\(total) past year · \(s.totalRepos) repos" }
|
||||
return "\(total) past year"
|
||||
if let s = stats { return "\(total) за год · \(s.totalRepos) репо" }
|
||||
return "\(total) за год"
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -2639,7 +2638,7 @@ private struct GitHubActivityDetailContent: View {
|
||||
HStack(spacing: 3) {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.system(size: 8, weight: .medium))
|
||||
Text("Activity")
|
||||
Text("Активность")
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
}
|
||||
.foregroundColor(Color(hex: "#F5F6F8"))
|
||||
@@ -2691,7 +2690,7 @@ private struct GitHubActivityDetailContent: View {
|
||||
.padding(.leading, 108)
|
||||
.padding(.trailing, 12)
|
||||
} else {
|
||||
Text("Loading…")
|
||||
Text("Загрузка…")
|
||||
.font(.system(size: 10.5))
|
||||
.foregroundColor(Color(hex: "#6B7079"))
|
||||
.padding(.top, 8)
|
||||
@@ -2710,14 +2709,14 @@ private struct GitHubActivityDetailContent: View {
|
||||
guard parts.count == 3,
|
||||
let month = Int(parts[1]), month >= 1 && month <= 12,
|
||||
let day = Int(parts[2]) else { return dateStr }
|
||||
let months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]
|
||||
return "\(months[month - 1]) \(day)"
|
||||
let months = ["янв","фев","мар","апр","мая","июн","июл","авг","сен","окт","ноя","дек"]
|
||||
return "\(day) \(months[month - 1])"
|
||||
}
|
||||
|
||||
private func activityTotalLabel(_ n: Int) -> String {
|
||||
let nf = NumberFormatter()
|
||||
nf.numberStyle = .decimal
|
||||
nf.locale = Locale(identifier: "en_US")
|
||||
nf.locale = Locale(identifier: "ru_RU")
|
||||
return nf.string(from: NSNumber(value: n)) ?? "\(n)"
|
||||
}
|
||||
}
|
||||
@@ -2771,7 +2770,7 @@ private struct GitHubPRRowView: View {
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
if pr.isDraft {
|
||||
Text("Draft")
|
||||
Text("Черновик")
|
||||
.font(.system(size: 9.5))
|
||||
.foregroundColor(Color(hex: "#6B7079"))
|
||||
}
|
||||
@@ -2857,7 +2856,7 @@ struct GitHubStatsCardView: View {
|
||||
Text("GitHub")
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
.foregroundColor(Color(hex: "#F5F6F8"))
|
||||
Text("Overview")
|
||||
Text("Обзор")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(Color(hex: "#8E939C"))
|
||||
}
|
||||
@@ -2868,9 +2867,9 @@ struct GitHubStatsCardView: View {
|
||||
// Stats rows
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
StatRow(icon: "star.fill", color: "#F5A524",
|
||||
label: "Total stars", value: formatCount(stats.totalStars))
|
||||
label: "Всего звёзд", value: formatCount(stats.totalStars))
|
||||
StatRow(icon: "square.stack.fill", color: "#6B7079",
|
||||
label: "Repositories", value: "\(stats.totalRepos)")
|
||||
label: "Репозитории", value: "\(stats.totalRepos)")
|
||||
}
|
||||
.padding(.top, 8)
|
||||
.padding(.leading, 108)
|
||||
@@ -2926,7 +2925,7 @@ struct StripeCardView: View {
|
||||
Text("Stripe")
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
.foregroundColor(Color(hex: "#F5F6F8"))
|
||||
Text("Payments")
|
||||
Text("Платежи")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(Color(hex: "#8E939C"))
|
||||
}
|
||||
@@ -2981,7 +2980,7 @@ private struct StripePaymentRow: View {
|
||||
let accent = payment.isSuccess ? Color(hex: "#22C55E") : Color(hex: "#F4505E")
|
||||
HStack(spacing: 5) {
|
||||
Circle().fill(accent).frame(width: 5, height: 5)
|
||||
Text(payment.description ?? "Payment")
|
||||
Text(payment.description ?? "Платёж")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(Color(hex: "#C5C8CD"))
|
||||
.lineLimit(1).truncationMode(.tail)
|
||||
@@ -3053,7 +3052,7 @@ struct CalcomCalendarView: View {
|
||||
private let cal = Calendar.current
|
||||
|
||||
private var navLabel: String {
|
||||
let f = DateFormatter(); f.dateFormat = "MMMM yyyy"
|
||||
let f = DateFormatter(); f.locale = Locale(identifier: "ru_RU"); f.dateFormat = "LLLL yyyy"
|
||||
return "\(f.string(from: displayMonth)) Q\(displayHalf)"
|
||||
}
|
||||
|
||||
@@ -3111,7 +3110,7 @@ struct CalcomCalendarView: View {
|
||||
HStack(spacing: 6) {
|
||||
Circle().fill(Color(hex: "#C9956A")).frame(width: 7, height: 7)
|
||||
Text("Cal.com").font(.system(size: 12, weight: .semibold)).foregroundColor(Color(hex: "#F5F6F8"))
|
||||
Text("Schedule").font(.system(size: 11)).foregroundColor(Color(hex: "#8E939C"))
|
||||
Text("Расписание").font(.system(size: 11)).foregroundColor(Color(hex: "#8E939C"))
|
||||
}
|
||||
.padding(.top, 6).padding(.leading, 108).padding(.trailing, 36)
|
||||
|
||||
@@ -3150,7 +3149,7 @@ private struct CalcomWeekRow: View {
|
||||
|
||||
private var weekLabel: String {
|
||||
guard let first = week.compactMap({ $0 }).first else { return "" }
|
||||
let f = DateFormatter(); f.dateFormat = "dd/MM"
|
||||
let f = DateFormatter(); f.dateFormat = "dd.MM"
|
||||
return f.string(from: first)
|
||||
}
|
||||
|
||||
@@ -3207,7 +3206,7 @@ struct CalcomDayView: View {
|
||||
.padding(.top, 6).padding(.trailing, 12)
|
||||
|
||||
if bookings.isEmpty {
|
||||
Text("No calls scheduled").font(.system(size: 11)).foregroundColor(Color(hex: "#6B7079"))
|
||||
Text("Звонков нет").font(.system(size: 11)).foregroundColor(Color(hex: "#6B7079"))
|
||||
.padding(.leading, 116).padding(.top, 8)
|
||||
} else {
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
@@ -3237,7 +3236,7 @@ struct CalcomDayView: View {
|
||||
.transition(.opacity)
|
||||
}
|
||||
private var dayLabel: String {
|
||||
let f = DateFormatter(); f.dateFormat = "EEEE d MMMM"; return f.string(from: date)
|
||||
let f = DateFormatter(); f.locale = Locale(identifier: "ru_RU"); f.dateFormat = "EEEE, d MMMM"; return f.string(from: date)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3304,7 +3303,7 @@ struct NotionCardView: View {
|
||||
HStack(spacing: 6) {
|
||||
Circle().fill(Color(hex: "#E8E8E8")).frame(width: 7, height: 7)
|
||||
Text("Notion").font(.system(size: 12, weight: .semibold)).foregroundColor(Color(hex: "#F5F6F8"))
|
||||
Text("Recent").font(.system(size: 11)).foregroundColor(Color(hex: "#8E939C"))
|
||||
Text("Недавние").font(.system(size: 11)).foregroundColor(Color(hex: "#8E939C"))
|
||||
}
|
||||
.padding(.top, 6).padding(.leading, 108).padding(.trailing, 36)
|
||||
|
||||
@@ -3348,7 +3347,7 @@ struct N8nDetailView: View {
|
||||
|
||||
private var success: Bool { task.state == .finished }
|
||||
private var accent: Color { success ? Color(hex: "#22C55E") : Color(hex: "#F4505E") }
|
||||
private var statusLabel: String { success ? "Success" : "Failed" }
|
||||
private var statusLabel: String { success ? "Успех" : "Ошибка" }
|
||||
private var detail: String? { task.steps.dropFirst().first }
|
||||
|
||||
var body: some View {
|
||||
@@ -3367,7 +3366,7 @@ struct N8nDetailView: View {
|
||||
|
||||
Circle().fill(accent).frame(width: 6, height: 6)
|
||||
|
||||
Text(task.steps.first ?? "Workflow")
|
||||
Text(task.steps.first ?? "Сценарий")
|
||||
.font(.system(size: 11, weight: .semibold))
|
||||
.foregroundColor(Color(hex: "#F5F6F8"))
|
||||
.lineLimit(1).truncationMode(.middle)
|
||||
@@ -3395,7 +3394,7 @@ struct N8nDetailView: View {
|
||||
}
|
||||
.frame(maxHeight: 88)
|
||||
} else {
|
||||
Text(success ? "Completed successfully." : "No error details available.")
|
||||
Text(success ? "Успешно завершено." : "Нет подробностей об ошибке.")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(Color(hex: "#6B7079"))
|
||||
}
|
||||
@@ -3891,13 +3890,13 @@ struct MusicCardView: View {
|
||||
.padding(.leading, 108)
|
||||
.padding(.trailing, 36)
|
||||
|
||||
Text("Allow BroV to control Music")
|
||||
Text("Разреши BroV управлять Музыкой")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(Color(hex: "#8E939C"))
|
||||
.padding(.leading, 108)
|
||||
.padding(.trailing, 12)
|
||||
|
||||
Button("Open Settings…") { MusicController.shared.openAutomationSettings() }
|
||||
Button("Открыть настройки…") { MusicController.shared.openAutomationSettings() }
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.foregroundColor(Color(hex: "#FA2D48").opacity(0.85))
|
||||
.buttonStyle(.plain)
|
||||
@@ -4032,8 +4031,8 @@ struct WardrobeView: View {
|
||||
if let h = hoveredOutfit {
|
||||
if h == .auto {
|
||||
let seasonal = Outfit.seasonal(for: Date(), calendar: .current)
|
||||
let name = seasonal == .none ? "None" : seasonal.displayName
|
||||
return "Auto · follows the seasons (now: \(name))"
|
||||
let name = seasonal == .none ? "Нет" : seasonal.displayName
|
||||
return "Авто · по сезону (сейчас: \(name))"
|
||||
}
|
||||
return h.displayName
|
||||
}
|
||||
@@ -4041,8 +4040,8 @@ struct WardrobeView: View {
|
||||
let sel = state.mochiOutfitSelection
|
||||
if sel == .auto {
|
||||
let seasonal = Outfit.seasonal(for: Date(), calendar: .current)
|
||||
let name = seasonal == .none ? "None" : seasonal.displayName
|
||||
return "Auto · \(name)"
|
||||
let name = seasonal == .none ? "Нет" : seasonal.displayName
|
||||
return "Авто · \(name)"
|
||||
}
|
||||
return sel.displayName
|
||||
}
|
||||
@@ -4051,7 +4050,7 @@ struct WardrobeView: View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
// Header
|
||||
HStack(spacing: 6) {
|
||||
Text("Wardrobe")
|
||||
Text("Гардероб")
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
.foregroundColor(Color(hex: "#F5F6F8"))
|
||||
Spacer(minLength: 4)
|
||||
@@ -4211,7 +4210,7 @@ private func drawOutfitIcon(context: GraphicsContext, size: CGSize, outfit: Outf
|
||||
badge.addRoundedRect(in: CGRect(x: -bw/2, y: -bh/2, width: bw, height: bh),
|
||||
cornerSize: CGSize(width: bh/2, height: bh/2))
|
||||
badgeCtx.fill(badge, with: .color(Color.black.opacity(0.60)))
|
||||
badgeCtx.draw(Text("AUTO").font(.system(size: 4.2, weight: .semibold)).foregroundColor(.white),
|
||||
badgeCtx.draw(Text("АВТО").font(.system(size: 4.2, weight: .semibold)).foregroundColor(.white),
|
||||
at: .zero)
|
||||
|
||||
case .none:
|
||||
@@ -4620,7 +4619,7 @@ struct SettingsIslandView: View {
|
||||
.labelsHidden()
|
||||
.scaleEffect(0.75)
|
||||
.frame(width: 44)
|
||||
Text("Sound")
|
||||
Text("Звук")
|
||||
.font(.system(size: 12.5))
|
||||
.foregroundColor(Color(hex: "#C5C8CD"))
|
||||
Slider(value: $state.soundVolume, in: 0...0.2)
|
||||
@@ -4634,13 +4633,13 @@ struct SettingsIslandView: View {
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(Color(hex: "#8E939C"))
|
||||
.frame(width: 16)
|
||||
Text("Auto-close · \(Int(state.autoCloseInterval))s")
|
||||
Text("Автозакрытие · \(Int(state.autoCloseInterval)) с")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(Color(hex: "#C5C8CD"))
|
||||
Spacer()
|
||||
HStack(spacing: 6) {
|
||||
ForEach([10, 15, 30], id: \.self) { s in
|
||||
Button("\(s)s") {
|
||||
Button("\(s) с") {
|
||||
state.autoCloseInterval = Double(s)
|
||||
}
|
||||
.font(.system(size: 11))
|
||||
@@ -4658,7 +4657,7 @@ struct SettingsIslandView: View {
|
||||
StatusBadge(label: "Claude Code", ok: claudeConnected)
|
||||
StatusBadge(label: "API", ok: apiConnected)
|
||||
Spacer()
|
||||
Button("Settings…") {
|
||||
Button("Настройки…") {
|
||||
NotificationCenter.default.post(name: .openFullSettings, object: nil)
|
||||
}
|
||||
.font(.system(size: 11.5))
|
||||
@@ -4718,3 +4717,11 @@ extension Color {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Russian plural form picker: 1 вклад, 2 вклада, 5 вкладов.
|
||||
fileprivate func ruPlural(_ n: Int, _ one: String, _ few: String, _ many: String) -> String {
|
||||
let n10 = abs(n) % 10, n100 = abs(n) % 100
|
||||
if n10 == 1 && n100 != 11 { return one }
|
||||
if (2...4).contains(n10) && !(12...14).contains(n100) { return few }
|
||||
return many
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user