diff --git a/AutoCat/Screens/RecordsScreen/AudioRecordView/AudioRecordView.swift b/AutoCat/Screens/RecordsScreen/AudioRecordView/AudioRecordView.swift index e940017..c5ceca6 100644 --- a/AutoCat/Screens/RecordsScreen/AudioRecordView/AudioRecordView.swift +++ b/AutoCat/Screens/RecordsScreen/AudioRecordView/AudioRecordView.swift @@ -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() } } diff --git a/AutoCat/Screens/RecordsScreen/AudioRecordView/AudioRecordViewModel.swift b/AutoCat/Screens/RecordsScreen/AudioRecordView/AudioRecordViewModel.swift index de7fc00..cf59466 100644 --- a/AutoCat/Screens/RecordsScreen/AudioRecordView/AudioRecordViewModel.swift +++ b/AutoCat/Screens/RecordsScreen/AudioRecordView/AudioRecordViewModel.swift @@ -11,7 +11,7 @@ import Foundation struct AudioRecordViewModel: Identifiable { - var id: TimeInterval + var id: String var isPlaying: Bool var duration: String? var number: String? diff --git a/AutoCat/Screens/RecordsScreen/RecordsScreen.swift b/AutoCat/Screens/RecordsScreen/RecordsScreen.swift index bf515cd..d7a3f05 100644 --- a/AutoCat/Screens/RecordsScreen/RecordsScreen.swift +++ b/AutoCat/Screens/RecordsScreen/RecordsScreen.swift @@ -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 - AudioRecordView(record: record) - .listRowInsets(EdgeInsets()) - } + 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") + } } } diff --git a/AutoCat/Screens/RecordsScreen/RecordsViewModel.swift b/AutoCat/Screens/RecordsScreen/RecordsViewModel.swift index 67aa6db..a7935b9 100644 --- a/AutoCat/Screens/RecordsScreen/RecordsViewModel.swift +++ b/AutoCat/Screens/RecordsScreen/RecordsViewModel.swift @@ -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 + } + } + } } diff --git a/AutoCatCore/Models/DTO/AudioRecordDto.swift b/AutoCatCore/Models/DTO/AudioRecordDto.swift index 274c529..b610af4 100644 --- a/AutoCatCore/Models/DTO/AudioRecordDto.swift +++ b/AutoCatCore/Models/DTO/AudioRecordDto.swift @@ -40,5 +40,5 @@ public struct AudioRecordDto: Decodable, Sendable { extension AudioRecordDto: Identifiable { - public var id: TimeInterval { addedDate } + public var id: String { path } } diff --git a/AutoCatCore/Services/RecordPlayerService/RecordPlayerService.swift b/AutoCatCore/Services/RecordPlayerService/RecordPlayerService.swift index 700c9fa..ec32978 100644 --- a/AutoCatCore/Services/RecordPlayerService/RecordPlayerService.swift +++ b/AutoCatCore/Services/RecordPlayerService/RecordPlayerService.swift @@ -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() } } diff --git a/AutoCatCore/Services/RecordPlayerService/RecordPlayerServiceProtocol.swift b/AutoCatCore/Services/RecordPlayerService/RecordPlayerServiceProtocol.swift index 8dcdffa..3c2ca5f 100644 --- a/AutoCatCore/Services/RecordPlayerService/RecordPlayerServiceProtocol.swift +++ b/AutoCatCore/Services/RecordPlayerService/RecordPlayerServiceProtocol.swift @@ -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() diff --git a/AutoCatCore/Services/StorageService/StorageService+AudioRecords.swift b/AutoCatCore/Services/StorageService/StorageService+AudioRecords.swift index f949801..511f52b 100644 --- a/AutoCatCore/Services/StorageService/StorageService+AudioRecords.swift +++ b/AutoCatCore/Services/StorageService/StorageService+AudioRecords.swift @@ -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 } } diff --git a/AutoCatCore/Services/StorageService/StorageServiceProtocol.swift b/AutoCatCore/Services/StorageService/StorageServiceProtocol.swift index 7c83173..9f59884 100644 --- a/AutoCatCore/Services/StorageService/StorageServiceProtocol.swift +++ b/AutoCatCore/Services/StorageService/StorageServiceProtocol.swift @@ -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 } diff --git a/AutoCatCore/Utils/Formatters.swift b/AutoCatCore/Utils/Formatters.swift index 386be91..f41c49d 100644 --- a/AutoCatCore/Utils/Formatters.swift +++ b/AutoCatCore/Utils/Formatters.swift @@ -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