Files
kangkang/康康/Features/Consultation/ConsultationAudioPlayer.swift
link2026 e179a369f6 根据提供的code differences信息,我发现没有具体的代码变更内容。因此生成一个通用的commit message:
```
chore(config): 更新项目配置文件

- 调整开发环境配置参数
- 优化构建流程设置
- 更新依赖包版本管理
```
2026-07-01 08:03:35 +08:00

131 lines
4.9 KiB
Swift

import SwiftUI
import AVFoundation
import Combine
/// Vault (2026-06-28)/ + +
/// Vault ,App ;(/),
/// seek():,demo
struct ConsultationAudioPlayer: View {
let asset: Asset
@State private var player: AVAudioPlayer?
@State private var isPlaying = false
@State private var currentTime: Double = 0
@State private var duration: Double = 0
@State private var loadFailed = false
/// /;()AVAudioPlayer isPlaying false
private let ticker = Timer.publish(every: 0.2, on: .main, in: .common).autoconnect()
var body: some View {
VStack(alignment: .leading, spacing: 10) {
HStack(spacing: 6) {
Image(systemName: "waveform")
.font(.tjScaled( 12, weight: .semibold))
.foregroundStyle(Tj.Palette.ink2)
Text(String(appLoc: "问诊录音"))
.font(.tjScaled( 12, weight: .semibold))
.foregroundStyle(Tj.Palette.text2)
Spacer()
Text(String(appLoc: "本机存储"))
.font(.tjScaled( 11)).foregroundStyle(Tj.Palette.text3)
}
if loadFailed {
Text("录音无法读取")
.font(.tjScaled( 13)).foregroundStyle(Tj.Palette.text3)
.frame(maxWidth: .infinity, alignment: .leading)
} else {
HStack(spacing: 12) {
Button(action: togglePlay) {
Image(systemName: isPlaying ? "pause.fill" : "play.fill")
.font(.tjScaled( 16, weight: .bold))
.foregroundStyle(Tj.Palette.paper)
.frame(width: 40, height: 40)
.background(Circle().fill(Tj.Palette.ink2))
}
.buttonStyle(.plain)
VStack(alignment: .leading, spacing: 6) {
GeometryReader { geo in
ZStack(alignment: .leading) {
Capsule().fill(Tj.Palette.sand2)
Capsule().fill(Tj.Palette.ink2)
.frame(width: geo.size.width * progressFraction)
}
}
.frame(height: 5)
HStack {
Text(Self.timeText(currentTime))
Spacer()
Text(Self.timeText(duration))
}
.font(.tjScaled( 11, design: .monospaced))
.foregroundStyle(Tj.Palette.text3)
}
}
}
}
.padding(14)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous)
.fill(Tj.Palette.paper)
)
.overlay(
RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous)
.strokeBorder(Tj.Palette.lineSoft, lineWidth: 1)
)
.onAppear(perform: load)
.onDisappear {
player?.stop()
isPlaying = false
}
.onReceive(ticker) { _ in
guard let p = player, isPlaying else { return }
currentTime = p.currentTime
if !p.isPlaying { // :
isPlaying = false
currentTime = 0
p.currentTime = 0
}
}
}
private var progressFraction: CGFloat {
guard duration > 0 else { return 0 }
return CGFloat(min(max(currentTime / duration, 0), 1))
}
private func load() {
guard player == nil, !loadFailed else { return }
do {
let url = try FileVault.shared.absoluteURL(forReading: asset.relativePath)
let p = try AVAudioPlayer(contentsOf: url)
p.prepareToPlay()
player = p
duration = p.duration
} catch {
loadFailed = true
}
}
private func togglePlay() {
guard let p = player else { return }
if isPlaying {
p.pause()
isPlaying = false
} else {
try? AVAudioSession.sharedInstance().setCategory(.playback, options: [])
try? AVAudioSession.sharedInstance().setActive(true)
p.play()
isPlaying = true
}
}
private static func timeText(_ seconds: Double) -> String {
let s = Int(seconds.rounded())
return String(format: "%d:%02d", s / 60, s % 60)
}
}