Editing/deleting audio records
This commit is contained in:
parent
c7a2234b3f
commit
d2dcdbff9d
@ -23,6 +23,7 @@ struct AudioRecordView: View {
|
|||||||
Spacer(minLength: 0)
|
Spacer(minLength: 0)
|
||||||
Text(record.date)
|
Text(record.date)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
|
.monospacedDigit()
|
||||||
}
|
}
|
||||||
.padding(.trailing)
|
.padding(.trailing)
|
||||||
}
|
}
|
||||||
@ -34,6 +35,7 @@ struct AudioRecordView: View {
|
|||||||
Image(systemName: record.isPlaying ? "pause.fill" : "play.fill")
|
Image(systemName: record.isPlaying ? "pause.fill" : "play.fill")
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.borderless)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@ -41,6 +43,7 @@ struct AudioRecordView: View {
|
|||||||
if let duration = record.duration {
|
if let duration = record.duration {
|
||||||
Text(duration)
|
Text(duration)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
|
.monospacedDigit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import Foundation
|
|||||||
|
|
||||||
struct AudioRecordViewModel: Identifiable {
|
struct AudioRecordViewModel: Identifiable {
|
||||||
|
|
||||||
var id: TimeInterval
|
var id: String
|
||||||
var isPlaying: Bool
|
var isPlaying: Bool
|
||||||
var duration: String?
|
var duration: String?
|
||||||
var number: String?
|
var number: String?
|
||||||
|
|||||||
@ -7,20 +7,28 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import AutoCatCore
|
||||||
|
|
||||||
struct RecordsScreen: View {
|
struct RecordsScreen: View {
|
||||||
|
|
||||||
@State var viewModel: RecordsViewModel
|
@State var viewModel: RecordsViewModel
|
||||||
|
|
||||||
|
@State var showEditAlert = false
|
||||||
|
@State var numberText = ""
|
||||||
|
@State var selectedRecordId: String = ""
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List(viewModel.recordModels) { record in
|
||||||
ForEach(viewModel.recordModels) { record in
|
AudioRecordView(record: record)
|
||||||
AudioRecordView(record: record)
|
.listRowInsets(EdgeInsets())
|
||||||
.listRowInsets(EdgeInsets())
|
.swipeActions(allowsFullSwipe: false) {
|
||||||
}
|
makeActions(for: record)
|
||||||
|
}
|
||||||
|
.contextMenu {
|
||||||
|
makeActions(for: record, useLabels: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(BorderlessButtonStyle())
|
.listStyle(.inset)
|
||||||
.listStyle(.plain)
|
|
||||||
.hud($viewModel.hud)
|
.hud($viewModel.hud)
|
||||||
.navigationTitle("Voice records")
|
.navigationTitle("Voice records")
|
||||||
.onAppear {
|
.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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -85,4 +85,22 @@ final class RecordsViewModel: ACHudContainer {
|
|||||||
hud = .error(error)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,5 +40,5 @@ public struct AudioRecordDto: Decodable, Sendable {
|
|||||||
|
|
||||||
extension AudioRecordDto: Identifiable {
|
extension AudioRecordDto: Identifiable {
|
||||||
|
|
||||||
public var id: TimeInterval { addedDate }
|
public var id: String { path }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,13 +13,8 @@ public final class RecordPlayerService: NSObject {
|
|||||||
var player: AVAudioPlayer?
|
var player: AVAudioPlayer?
|
||||||
var record: AudioRecordDto?
|
var record: AudioRecordDto?
|
||||||
var onStop: ((AudioRecordDto, Error?) -> Void)?
|
var onStop: ((AudioRecordDto, Error?) -> Void)?
|
||||||
}
|
|
||||||
|
|
||||||
extension RecordPlayerService: RecordPlayerServiceProtocol {
|
func playNewRecord(record: AudioRecordDto, onStop: ((AudioRecordDto, Error?) -> Void)?) throws {
|
||||||
|
|
||||||
public func play(record: AudioRecordDto, onStop: ((AudioRecordDto, Error?) -> Void)?) throws {
|
|
||||||
|
|
||||||
player?.stop()
|
|
||||||
|
|
||||||
let url = try FileManager.default.url(for: record.path, in: Constants.audioRecordsFolder)
|
let url = try FileManager.default.url(for: record.path, in: Constants.audioRecordsFolder)
|
||||||
player = try AVAudioPlayer(contentsOf: url)
|
player = try AVAudioPlayer(contentsOf: url)
|
||||||
@ -28,19 +23,51 @@ extension RecordPlayerService: RecordPlayerServiceProtocol {
|
|||||||
self.record = record
|
self.record = record
|
||||||
self.onStop = onStop
|
self.onStop = onStop
|
||||||
|
|
||||||
// try AVAudioSession.sharedInstance().setCategory(.playback)
|
try activateSession()
|
||||||
// try AVAudioSession.sharedInstance().setActive(true)
|
|
||||||
|
|
||||||
print("==== playing started ====")
|
|
||||||
player?.play()
|
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() {
|
public func pause() {
|
||||||
|
|
||||||
player?.pause()
|
player?.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
public var currentPlayingId: TimeInterval? {
|
public var currentPlayingId: String? {
|
||||||
guard player?.isPlaying == true else {
|
guard player?.isPlaying == true else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -56,7 +83,6 @@ extension RecordPlayerService: AVAudioPlayerDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
print("==== playing stopped ====")
|
|
||||||
onStop?(record, nil)
|
onStop?(record, nil)
|
||||||
reset()
|
reset()
|
||||||
}
|
}
|
||||||
@ -66,7 +92,6 @@ extension RecordPlayerService: AVAudioPlayerDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
print("==== playing error ====")
|
|
||||||
onStop?(record, error)
|
onStop?(record, error)
|
||||||
reset()
|
reset()
|
||||||
}
|
}
|
||||||
@ -76,5 +101,7 @@ extension RecordPlayerService: AVAudioPlayerDelegate {
|
|||||||
onStop = nil
|
onStop = nil
|
||||||
player = nil
|
player = nil
|
||||||
record = nil
|
record = nil
|
||||||
|
|
||||||
|
try? deactivateSession()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import Foundation
|
|||||||
|
|
||||||
public protocol RecordPlayerServiceProtocol {
|
public protocol RecordPlayerServiceProtocol {
|
||||||
|
|
||||||
var currentPlayingId: TimeInterval? { get }
|
var currentPlayingId: String? { get }
|
||||||
|
|
||||||
func play(record: AudioRecordDto, onStop: ((AudioRecordDto, Error?) -> Void)?) throws
|
func play(record: AudioRecordDto, onStop: ((AudioRecordDto, Error?) -> Void)?) throws
|
||||||
func pause()
|
func pause()
|
||||||
|
|||||||
@ -24,7 +24,7 @@ public extension StorageService {
|
|||||||
.map(\.dto)
|
.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 {
|
guard let record = realm.object(ofType: AudioRecord.self, forPrimaryKey: id) else {
|
||||||
throw StorageError.recordNotFound
|
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
|
throw StorageError.recordNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
try await realm.asyncWrite {
|
try await realm.asyncWrite {
|
||||||
realm.add(record, update: .modified)
|
record.number = number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return record.dto
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,6 +35,6 @@ public protocol StorageServiceProtocol: Sendable {
|
|||||||
// Audio records
|
// Audio records
|
||||||
func add(record: AudioRecordDto) async throws
|
func add(record: AudioRecordDto) async throws
|
||||||
func loadRecords() async throws -> [AudioRecordDto]
|
func loadRecords() async throws -> [AudioRecordDto]
|
||||||
func deleteRecord(id: TimeInterval) async throws
|
func deleteRecord(id: String) async throws
|
||||||
func updateRecord(_ record: AudioRecordDto) async throws
|
func updateRecord(id: String, number: String) async throws -> AudioRecordDto
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,7 +36,7 @@ public struct Formatters {
|
|||||||
public static let time: DateComponentsFormatter = {
|
public static let time: DateComponentsFormatter = {
|
||||||
|
|
||||||
let formatter = DateComponentsFormatter()
|
let formatter = DateComponentsFormatter()
|
||||||
formatter.unitsStyle = .abbreviated
|
formatter.unitsStyle = .positional
|
||||||
formatter.allowedUnits = [.minute, .second]
|
formatter.allowedUnits = [.minute, .second]
|
||||||
formatter.zeroFormattingBehavior = .pad
|
formatter.zeroFormattingBehavior = .pad
|
||||||
return formatter
|
return formatter
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user