Editing/deleting audio records

This commit is contained in:
Selim Mustafaev 2025-04-01 00:32:50 +03:00
parent c7a2234b3f
commit d2dcdbff9d
10 changed files with 112 additions and 29 deletions

View File

@ -23,6 +23,7 @@ struct AudioRecordView: View {
Spacer(minLength: 0)
Text(record.date)
.font(.subheadline)
.monospacedDigit()
}
.padding(.trailing)
}
@ -34,6 +35,7 @@ struct AudioRecordView: View {
Image(systemName: record.isPlaying ? "pause.fill" : "play.fill")
.padding()
}
.buttonStyle(.borderless)
}
@ViewBuilder
@ -41,6 +43,7 @@ struct AudioRecordView: View {
if let duration = record.duration {
Text(duration)
.font(.subheadline)
.monospacedDigit()
}
}

View File

@ -11,7 +11,7 @@ import Foundation
struct AudioRecordViewModel: Identifiable {
var id: TimeInterval
var id: String
var isPlaying: Bool
var duration: String?
var number: String?

View File

@ -7,20 +7,28 @@
//
import SwiftUI
import AutoCatCore
struct RecordsScreen: View {
@State var viewModel: RecordsViewModel
@State var showEditAlert = false
@State var numberText = ""
@State var selectedRecordId: String = ""
var body: some View {
List {
ForEach(viewModel.recordModels) { record in
List(viewModel.recordModels) { record in
AudioRecordView(record: record)
.listRowInsets(EdgeInsets())
.swipeActions(allowsFullSwipe: false) {
makeActions(for: record)
}
.contextMenu {
makeActions(for: record, useLabels: true)
}
}
.buttonStyle(BorderlessButtonStyle())
.listStyle(.plain)
.listStyle(.inset)
.hud($viewModel.hud)
.navigationTitle("Voice records")
.onAppear {
@ -43,5 +51,30 @@ struct RecordsScreen: View {
}
}
}
.noteAlert(
title: String(localized: "Edit plate number"),
body: $numberText,
isPresented: $showEditAlert
) { text in
Task { await viewModel.editRecord(id: selectedRecordId, number: numberText) }
}
}
@ViewBuilder
func makeActions(for record: AudioRecordViewModel, useLabels: Bool = false) -> some View {
Button {
selectedRecordId = record.id
numberText = record.number ?? ""
showEditAlert = true
} label: {
Label(useLabels ? "Edit plate number" : "", systemImage: "pencil")
}
Button(role: .destructive) {
Task { await viewModel.deleteRecord(id: record.id) }
} label: {
Label(useLabels ? "Delete" : "", systemImage: "trash")
}
}
}

View File

@ -85,4 +85,22 @@ final class RecordsViewModel: ACHudContainer {
hud = .error(error)
}
}
func deleteRecord(id: String) async {
await wrapWithToast(showProgress: false) { [weak self] in
guard let self else { return }
try await storageService.deleteRecord(id: id)
records.removeAll { $0.id == id }
}
}
func editRecord(id: String, number: String) async {
await wrapWithToast(showProgress: false) { [weak self] in
guard let self else { return }
let updatedRecord = try await storageService.updateRecord(id: id, number: number)
if let index = records.firstIndex(where: { $0.id == id }) {
records[index] = updatedRecord
}
}
}
}

View File

@ -40,5 +40,5 @@ public struct AudioRecordDto: Decodable, Sendable {
extension AudioRecordDto: Identifiable {
public var id: TimeInterval { addedDate }
public var id: String { path }
}

View File

@ -13,13 +13,8 @@ public final class RecordPlayerService: NSObject {
var player: AVAudioPlayer?
var record: AudioRecordDto?
var onStop: ((AudioRecordDto, Error?) -> Void)?
}
extension RecordPlayerService: RecordPlayerServiceProtocol {
public func play(record: AudioRecordDto, onStop: ((AudioRecordDto, Error?) -> Void)?) throws {
player?.stop()
func playNewRecord(record: AudioRecordDto, onStop: ((AudioRecordDto, Error?) -> Void)?) throws {
let url = try FileManager.default.url(for: record.path, in: Constants.audioRecordsFolder)
player = try AVAudioPlayer(contentsOf: url)
@ -28,19 +23,51 @@ extension RecordPlayerService: RecordPlayerServiceProtocol {
self.record = record
self.onStop = onStop
// try AVAudioSession.sharedInstance().setCategory(.playback)
// try AVAudioSession.sharedInstance().setActive(true)
try activateSession()
print("==== playing started ====")
player?.play()
}
func activateSession() throws {
try AVAudioSession.sharedInstance().setCategory(
.playback,
mode: .default,
options: [.duckOthers]
)
try AVAudioSession.sharedInstance().setActive(true)
}
func deactivateSession() throws {
try AVAudioSession.sharedInstance().setActive(
false,
options: .notifyOthersOnDeactivation
)
}
}
extension RecordPlayerService: RecordPlayerServiceProtocol {
public func play(record: AudioRecordDto, onStop: ((AudioRecordDto, Error?) -> Void)?) throws {
guard let player, self.record?.id == record.id else {
try playNewRecord(record: record, onStop: onStop)
return
}
if player.isPlaying {
player.pause()
try deactivateSession()
} else {
try activateSession()
player.play()
}
}
public func pause() {
player?.pause()
}
public var currentPlayingId: TimeInterval? {
public var currentPlayingId: String? {
guard player?.isPlaying == true else {
return nil
}
@ -56,7 +83,6 @@ extension RecordPlayerService: AVAudioPlayerDelegate {
return
}
print("==== playing stopped ====")
onStop?(record, nil)
reset()
}
@ -66,7 +92,6 @@ extension RecordPlayerService: AVAudioPlayerDelegate {
return
}
print("==== playing error ====")
onStop?(record, error)
reset()
}
@ -76,5 +101,7 @@ extension RecordPlayerService: AVAudioPlayerDelegate {
onStop = nil
player = nil
record = nil
try? deactivateSession()
}
}

View File

@ -10,7 +10,7 @@ import Foundation
public protocol RecordPlayerServiceProtocol {
var currentPlayingId: TimeInterval? { get }
var currentPlayingId: String? { get }
func play(record: AudioRecordDto, onStop: ((AudioRecordDto, Error?) -> Void)?) throws
func pause()

View File

@ -24,7 +24,7 @@ public extension StorageService {
.map(\.dto)
}
func deleteRecord(id: TimeInterval) async throws {
func deleteRecord(id: String) async throws {
guard let record = realm.object(ofType: AudioRecord.self, forPrimaryKey: id) else {
throw StorageError.recordNotFound
@ -35,14 +35,16 @@ public extension StorageService {
}
}
func updateRecord(_ record: AudioRecordDto) async throws {
func updateRecord(id: String, number: String) async throws -> AudioRecordDto {
guard let record = realm.object(ofType: AudioRecord.self, forPrimaryKey: record.id) else {
guard let record = realm.object(ofType: AudioRecord.self, forPrimaryKey: id) else {
throw StorageError.recordNotFound
}
try await realm.asyncWrite {
realm.add(record, update: .modified)
}
record.number = number
}
return record.dto
}
}

View File

@ -35,6 +35,6 @@ public protocol StorageServiceProtocol: Sendable {
// Audio records
func add(record: AudioRecordDto) async throws
func loadRecords() async throws -> [AudioRecordDto]
func deleteRecord(id: TimeInterval) async throws
func updateRecord(_ record: AudioRecordDto) async throws
func deleteRecord(id: String) async throws
func updateRecord(id: String, number: String) async throws -> AudioRecordDto
}

View File

@ -36,7 +36,7 @@ public struct Formatters {
public static let time: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .abbreviated
formatter.unitsStyle = .positional
formatter.allowedUnits = [.minute, .second]
formatter.zeroFormattingBehavior = .pad
return formatter