Adding progress to audio record player

This commit is contained in:
Selim Mustafaev 2025-04-03 22:55:09 +03:00
parent 87074fa61a
commit 49a7e88ee5
8 changed files with 90 additions and 36 deletions

View File

@ -1885,7 +1885,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements; CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 152; CURRENT_PROJECT_VERSION = 153;
DEVELOPMENT_TEAM = 46DTTB8X4S; DEVELOPMENT_TEAM = 46DTTB8X4S;
INFOPLIST_FILE = AutoCat/Info.plist; INFOPLIST_FILE = AutoCat/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = AutoCat; INFOPLIST_KEY_CFBundleDisplayName = AutoCat;
@ -1912,7 +1912,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements; CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 152; CURRENT_PROJECT_VERSION = 153;
DEVELOPMENT_TEAM = 46DTTB8X4S; DEVELOPMENT_TEAM = 46DTTB8X4S;
INFOPLIST_FILE = AutoCat/Info.plist; INFOPLIST_FILE = AutoCat/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = AutoCat; INFOPLIST_KEY_CFBundleDisplayName = AutoCat;

View File

@ -12,10 +12,12 @@ import AutoCatCore
struct AudioRecordView: View { struct AudioRecordView: View {
let record: AudioRecordViewModel let record: AudioRecordViewModel
let progress: Double
var body: some View { var body: some View {
return HStack { ZStack(alignment: .bottom) {
HStack {
playButton playButton
duration duration
Spacer(minLength: 0) Spacer(minLength: 0)
@ -26,6 +28,11 @@ struct AudioRecordView: View {
.monospacedDigit() .monospacedDigit()
} }
.padding(.trailing) .padding(.trailing)
if record.isPlaying {
ProgressView(value: progress)
}
}
} }
var playButton: some View { var playButton: some View {
@ -64,6 +71,7 @@ struct AudioRecordView: View {
raw: "бла-бла", raw: "бла-бла",
duration: 145, duration: 145,
event: nil event: nil
), onPlay: {}) ), onPlay: {}),
progress: 0.5
) )
} }

View File

@ -19,7 +19,7 @@ struct RecordsScreen: View {
var body: some View { var body: some View {
List(viewModel.recordModels) { record in List(viewModel.recordModels) { record in
AudioRecordView(record: record) AudioRecordView(record: record, progress: viewModel.progress)
.listRowInsets(EdgeInsets()) .listRowInsets(EdgeInsets())
.swipeActions(allowsFullSwipe: false) { .swipeActions(allowsFullSwipe: false) {
makeActions(for: record) makeActions(for: record)

View File

@ -23,6 +23,7 @@ final class RecordsViewModel: ACHudContainer {
var showRecordingAlert: Bool = false var showRecordingAlert: Bool = false
var records: [AudioRecordDto] = [] var records: [AudioRecordDto] = []
var playingRecord: AudioRecordDto? var playingRecord: AudioRecordDto?
var progress: Double = 0
var recordModels: [AudioRecordViewModel] { var recordModels: [AudioRecordViewModel] {
return records.map { record in return records.map { record in
@ -75,13 +76,21 @@ final class RecordsViewModel: ACHudContainer {
func onPlayTapped(record: AudioRecordDto) { func onPlayTapped(record: AudioRecordDto) {
do { do {
playingRecord = record try recordPlayer.play(record: record, onStop: { [weak self] dto, error in
try recordPlayer.play(record: record) { [weak self] dto, error in
self?.playingRecord = nil self?.playingRecord = nil
//self?.progress = 0
if let error { if let error {
self?.hud = .error(error) self?.hud = .error(error)
} }
}, onProgress: { [weak self] progress in
self?.progress = progress
print("==== progress: \(progress)")
})
if playingRecord != record {
progress = 0
} }
playingRecord = record
} catch { } catch {
playingRecord = nil playingRecord = nil
hud = .error(error) hud = .error(error)

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public struct AudioRecordDto: Decodable, Sendable { public struct AudioRecordDto: Decodable, Sendable, Equatable {
public var path: String = "" public var path: String = ""
public var number: String? public var number: String?

View File

@ -13,8 +13,15 @@ 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)?
var onProgress: ((Double) -> Void)?
func playNewRecord(record: AudioRecordDto, onStop: ((AudioRecordDto, Error?) -> Void)?) throws { var progressTimer: Timer?
func playNewRecord(
record: AudioRecordDto,
onStop: ((AudioRecordDto, Error?) -> Void)?,
onProgress: ((Double) -> Void)?
) throws {
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)
@ -23,6 +30,9 @@ public final class RecordPlayerService: NSObject {
self.record = record self.record = record
self.onStop = onStop self.onStop = onStop
activateProgressTimer()
self.onProgress = onProgress
try activateSession() try activateSession()
player?.play() player?.play()
@ -43,30 +53,48 @@ public final class RecordPlayerService: NSObject {
options: .notifyOthersOnDeactivation options: .notifyOthersOnDeactivation
) )
} }
func activateProgressTimer() {
progressTimer?.invalidate()
progressTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { [weak self] _ in
if let player = self?.player {
let progress = player.currentTime/player.duration
self?.onProgress?(progress)
}
}
}
func deactivateProgressTimer() {
progressTimer?.invalidate()
progressTimer = nil
}
} }
extension RecordPlayerService: RecordPlayerServiceProtocol { extension RecordPlayerService: RecordPlayerServiceProtocol {
public func play(record: AudioRecordDto, onStop: ((AudioRecordDto, Error?) -> Void)?) throws { public func play(
record: AudioRecordDto,
onStop: ((AudioRecordDto, Error?) -> Void)?,
onProgress: ((Double) -> Void)?
) throws {
guard let player, self.record?.id == record.id else { guard let player, self.record?.id == record.id else {
try playNewRecord(record: record, onStop: onStop) try playNewRecord(record: record, onStop: onStop, onProgress: onProgress)
return return
} }
if player.isPlaying { if player.isPlaying {
player.pause() player.pause()
deactivateProgressTimer()
try deactivateSession() try deactivateSession()
} else { } else {
try activateSession() try activateSession()
activateProgressTimer()
player.play() player.play()
} }
} }
public func pause() {
player?.pause()
}
public var currentPlayingId: String? { public var currentPlayingId: String? {
guard player?.isPlaying == true else { guard player?.isPlaying == true else {
return nil return nil
@ -102,6 +130,7 @@ extension RecordPlayerService: AVAudioPlayerDelegate {
player = nil player = nil
record = nil record = nil
deactivateProgressTimer()
try? deactivateSession() try? deactivateSession()
} }
} }

View File

@ -12,6 +12,9 @@ public protocol RecordPlayerServiceProtocol {
var currentPlayingId: String? { get } var currentPlayingId: String? { get }
func play(record: AudioRecordDto, onStop: ((AudioRecordDto, Error?) -> Void)?) throws func play(
func pause() record: AudioRecordDto,
onStop: ((AudioRecordDto, Error?) -> Void)?,
onProgress: ((Double) -> Void)?
) throws
} }

View File

@ -35,18 +35,23 @@ extension VehicleService: VehicleServiceProtocol {
number: String, number: String,
forceUpdate: Bool, forceUpdate: Bool,
trackLocation: Bool, trackLocation: Bool,
additionalEvents: [VehicleEventDto], additionalEvent: VehicleEventDto?,
dbUpdatePolicy: DbUpdatePolicy dbUpdatePolicy: DbUpdatePolicy
) async throws -> VehicleWithErrors { ) async throws -> VehicleWithErrors {
var vehicle = (try? await storageService.loadVehicle(number: number)) ?? VehicleDto(number: number) var vehicle = (try? await storageService.loadVehicle(number: number)) ?? VehicleDto(number: number)
var errors: [Error] = [] var errors: [Error] = []
// TODO: Check if additionalEvents already was added earlier (avoid duplication)
let events = vehicle.events + additionalEvents
let notes = vehicle.notes let notes = vehicle.notes
if !additionalEvents.isEmpty { let events: [VehicleEventDto]
if let additionalEvent, !vehicle.events.contains(where: { $0.id == additionalEvent.id }) {
events = vehicle.events + [additionalEvent]
} else {
events = vehicle.events
}
if additionalEvent != nil {
vehicle.events = events vehicle.events = events
vehicle.updatedDate = Date().timeIntervalSince1970 vehicle.updatedDate = Date().timeIntervalSince1970
vehicle.synchronized = false vehicle.synchronized = false
@ -89,7 +94,7 @@ extension VehicleService: VehicleServiceProtocol {
number: number, number: number,
forceUpdate: false, forceUpdate: false,
trackLocation: false, trackLocation: false,
additionalEvents: [], additionalEvent: nil,
dbUpdatePolicy: .always dbUpdatePolicy: .always
) )
#else #else
@ -97,7 +102,7 @@ extension VehicleService: VehicleServiceProtocol {
number: number, number: number,
forceUpdate: false, forceUpdate: false,
trackLocation: true, trackLocation: true,
additionalEvents: [], additionalEvent: nil,
dbUpdatePolicy: .always dbUpdatePolicy: .always
) )
#endif #endif
@ -109,7 +114,7 @@ extension VehicleService: VehicleServiceProtocol {
number: number, number: number,
forceUpdate: true, forceUpdate: true,
trackLocation: false, trackLocation: false,
additionalEvents: [], additionalEvent: nil,
dbUpdatePolicy: .always dbUpdatePolicy: .always
) )
} }
@ -120,7 +125,7 @@ extension VehicleService: VehicleServiceProtocol {
number: number, number: number,
forceUpdate: true, forceUpdate: true,
trackLocation: false, trackLocation: false,
additionalEvents: [], additionalEvent: nil,
dbUpdatePolicy: .ifExists dbUpdatePolicy: .ifExists
) )
} }
@ -131,7 +136,7 @@ extension VehicleService: VehicleServiceProtocol {
number: number, number: number,
forceUpdate: false, forceUpdate: false,
trackLocation: false, trackLocation: false,
additionalEvents: event.flatMap { [$0] } ?? [], additionalEvent: event,
dbUpdatePolicy: .always dbUpdatePolicy: .always
) )
} }