Files
libnovel/ios/LibNovel/LibNovel/Views/Profile/VoiceSelectionView.swift
Admin 7413313100
All checks were successful
CI / Scraper / Lint (push) Successful in 10s
CI / Scraper / Test (push) Successful in 14s
Release / Scraper / Test (push) Successful in 18s
CI / Scraper / Lint (pull_request) Successful in 18s
Release / UI / Build (push) Successful in 23s
CI / Scraper / Test (pull_request) Successful in 15s
CI / UI / Build (pull_request) Successful in 32s
Release / Scraper / Docker (push) Successful in 55s
CI / Scraper / Docker Push (pull_request) Has been skipped
CI / UI / Docker Push (pull_request) Has been skipped
CI / Scraper / Docker Push (push) Successful in 1m5s
Release / UI / Docker (push) Successful in 1m12s
iOS CI / Build (push) Successful in 4m18s
iOS CI / Build (pull_request) Successful in 4m25s
iOS CI / Test (push) Successful in 8m11s
iOS CI / Test (pull_request) Successful in 8m21s
fix: update integration_test.go to match server.New signature (version, commit args)
2026-03-14 14:25:46 +05:00

159 lines
4.8 KiB
Swift

import SwiftUI
struct VoiceSelectionView: View {
@StateObject private var vm = VoiceSelectionViewModel()
@EnvironmentObject var authStore: AuthStore
@Environment(\.dismiss) private var dismiss
@State private var selectedVoice: String
init(currentVoice: String) {
_selectedVoice = State(initialValue: currentVoice)
}
var body: some View {
NavigationStack {
Group {
if vm.isLoading {
ProgressView("Loading voices...")
} else if let error = vm.error {
VStack(spacing: 16) {
Image(systemName: "exclamationmark.triangle")
.font(.system(size: 48))
.foregroundStyle(.amber)
Text(error)
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
}
.padding()
} else {
voiceList
}
}
.navigationTitle("Select Voice")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { dismiss() }
}
ToolbarItem(placement: .confirmationAction) {
Button("Done") {
saveAndDismiss()
}
.fontWeight(.semibold)
.disabled(selectedVoice == authStore.settings.voice)
}
}
.task {
await vm.loadVoices()
}
}
}
// MARK: - Voice List
@ViewBuilder
private var voiceList: some View {
List {
Section {
ForEach(vm.voices, id: \.self) { voice in
VoiceRow(
voice: voice,
isSelected: voice == selectedVoice,
isPlaying: vm.playingVoice == voice,
voiceLabel: vm.voiceLabel(voice),
voiceId: vm.voiceId(voice),
onSelect: {
vm.stopSample()
selectedVoice = voice
},
onPlaySample: {
Task {
await vm.playSample(voice)
}
}
)
}
} header: {
Text("Available Voices")
} footer: {
if selectedVoice != authStore.settings.voice {
Text("New voice will apply to next audio playback")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
}
// MARK: - Actions
private func saveAndDismiss() {
Task {
var settings = authStore.settings
settings.voice = selectedVoice
await authStore.saveSettings(settings)
dismiss()
}
}
}
// MARK: - Voice Row
private struct VoiceRow: View {
let voice: String
let isSelected: Bool
let isPlaying: Bool
let voiceLabel: String
let voiceId: String
let onSelect: () -> Void
let onPlaySample: () -> Void
var body: some View {
HStack(spacing: 12) {
// Selection checkmark
Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
.font(.system(size: 22))
.foregroundStyle(isSelected ? .amber : .secondary.opacity(0.3))
.frame(width: 28)
// Voice info
VStack(alignment: .leading, spacing: 4) {
Text(voiceLabel)
.font(.body)
.fontWeight(isSelected ? .semibold : .regular)
Text(voiceId)
.font(.caption)
.fontDesign(.monospaced)
.foregroundStyle(.secondary)
}
Spacer()
// Play sample button
Button {
onPlaySample()
} label: {
Image(systemName: isPlaying ? "stop.circle.fill" : "play.circle.fill")
.font(.system(size: 28))
.foregroundStyle(isPlaying ? .red : .amber)
.contentTransition(.symbolEffect(.replace))
}
.buttonStyle(.plain)
}
.padding(.vertical, 4)
.contentShape(Rectangle())
.onTapGesture {
onSelect()
}
}
}
// MARK: - Preview
#Preview {
VoiceSelectionView(currentVoice: "af_bella")
.environmentObject(AuthStore())
}