diff --git a/Gemfile b/Gemfile index afad0d89f..b14d63467 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source "https://rubygems.org" gem "fastlane" -gem "cocoapods", '1.11.3' +gem "cocoapods", '1.12.1' plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/Gemfile.lock b/Gemfile.lock index 7632cc028..69afd306a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,12 +3,11 @@ GEM specs: CFPropertyList (3.0.6) rexml - activesupport (6.1.7.2) + activesupport (7.0.5) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - zeitwerk (~> 2.3) addressable (2.8.4) public_suffix (>= 2.0.2, < 6.0) algoliasearch (1.27.5) @@ -17,32 +16,32 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.760.0) - aws-sdk-core (3.171.1) + aws-partitions (1.780.0) + aws-sdk-core (3.175.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.5) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.64.0) - aws-sdk-core (~> 3, >= 3.165.0) + aws-sdk-kms (1.67.0) + aws-sdk-core (~> 3, >= 3.174.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.122.0) - aws-sdk-core (~> 3, >= 3.165.0) + aws-sdk-s3 (1.125.0) + aws-sdk-core (~> 3, >= 3.174.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) aws-sigv4 (1.5.2) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) claide (1.1.0) - cocoapods (1.11.3) + cocoapods (1.12.1) addressable (~> 2.8) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.11.3) + cocoapods-core (= 1.12.1) cocoapods-deintegrate (>= 1.0.3, < 2.0) - cocoapods-downloader (>= 1.4.0, < 2.0) + cocoapods-downloader (>= 1.6.0, < 2.0) cocoapods-plugins (>= 1.0.0, < 2.0) cocoapods-search (>= 1.0.0, < 2.0) - cocoapods-trunk (>= 1.4.0, < 2.0) + cocoapods-trunk (>= 1.6.0, < 2.0) cocoapods-try (>= 1.1.0, < 2.0) colored2 (~> 3.1) escape (~> 0.0.4) @@ -50,10 +49,10 @@ GEM gh_inspector (~> 1.0) molinillo (~> 0.8.0) nap (~> 1.0) - ruby-macho (>= 1.0, < 3.0) + ruby-macho (>= 2.3.0, < 3.0) xcodeproj (>= 1.21.0, < 2.0) - cocoapods-core (1.11.3) - activesupport (>= 5.0, < 7) + cocoapods-core (1.12.1) + activesupport (>= 5.0, < 8) addressable (~> 2.8) algoliasearch (~> 1.0) concurrent-ruby (~> 1.1) @@ -75,7 +74,7 @@ GEM colored2 (3.1.2) commander (4.6.0) highline (~> 2.0.0) - concurrent-ruby (1.2.0) + concurrent-ruby (1.2.2) declarative (0.0.20) digest-crc (0.6.4) rake (>= 12.0.0, < 14.0.0) @@ -86,7 +85,7 @@ GEM escape (0.0.4) ethon (0.16.0) ffi (>= 1.15.0) - excon (0.99.0) + excon (0.100.0) faraday (1.10.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -115,8 +114,8 @@ GEM faraday-retry (1.0.3) faraday_middleware (1.2.0) faraday (~> 1.0) - fastimage (2.2.6) - fastlane (2.212.2) + fastimage (2.2.7) + fastlane (2.213.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -140,7 +139,7 @@ GEM json (< 3.0.0) jwt (>= 2.1.0, < 3) mini_magick (>= 4.9.4, < 5.0.0) - multipart-post (~> 2.0.0) + multipart-post (>= 2.0.0, < 3.0.0) naturally (~> 2.2) optparse (~> 0.1.1) plist (>= 3.1.0, < 4.0.0) @@ -155,12 +154,12 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) - fastlane-plugin-firebase_app_distribution (0.5.0) + fastlane-plugin-firebase_app_distribution (0.6.1) ffi (1.15.5) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.41.0) + google-apis-androidpublisher_v3 (0.43.0) google-apis-core (>= 0.11.0, < 2.a) google-apis-core (0.11.0) addressable (~> 2.5, >= 2.5.1) @@ -202,18 +201,18 @@ GEM http-cookie (1.0.5) domain_name (~> 0.5) httpclient (2.8.3) - i18n (1.12.0) + i18n (1.14.1) concurrent-ruby (~> 1.0) jmespath (1.6.2) json (2.6.3) - jwt (2.7.0) + jwt (2.7.1) memoist (0.16.2) mini_magick (4.12.0) mini_mime (1.1.2) - minitest (5.17.0) + minitest (5.18.1) molinillo (0.8.0) multi_json (1.15.0) - multipart-post (2.0.0) + multipart-post (2.3.0) nanaimo (0.3.0) nap (1.1.0) naturally (2.2.1) @@ -272,16 +271,15 @@ GEM rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) xcpretty (~> 0.2, >= 0.0.7) - zeitwerk (2.6.7) PLATFORMS ruby DEPENDENCIES - cocoapods (= 1.11.3) + cocoapods (= 1.12.1) fastlane fastlane-plugin-firebase_app_distribution i18n (~> 1.8) BUNDLED WITH - 2.4.5 + 2.4.13 diff --git a/Modules/RuuviDiscover/RuuviDiscover.podspec b/Modules/RuuviDiscover/RuuviDiscover.podspec index 1a1cdc052..cc3b626dc 100644 --- a/Modules/RuuviDiscover/RuuviDiscover.podspec +++ b/Modules/RuuviDiscover/RuuviDiscover.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'RuuviDiscover' - s.version = '0.0.1' + s.version = '0.0.2' s.summary = 'Ruuvi Discover' s.homepage = 'https://ruuvi.com' s.author = { 'Rinat Enikeev' => 'rinat@ruuvi.com' } diff --git a/Modules/RuuviDiscover/Sources/RuuviDiscover/VMP/Presenter/DiscoverPresenter.swift b/Modules/RuuviDiscover/Sources/RuuviDiscover/VMP/Presenter/DiscoverPresenter.swift index 3eb022095..10cd4f613 100644 --- a/Modules/RuuviDiscover/Sources/RuuviDiscover/VMP/Presenter/DiscoverPresenter.swift +++ b/Modules/RuuviDiscover/Sources/RuuviDiscover/VMP/Presenter/DiscoverPresenter.swift @@ -40,12 +40,40 @@ class DiscoverPresenter: NSObject, RuuviDiscover { var ruuviOwnershipService: RuuviServiceOwnership! private weak var view: DiscoverViewInput? - private var ruuviTags = Set() + private var accessQueue = DispatchQueue( + label: "com.ruuviDiscover.accessQueue", attributes: .concurrent + ) + private var _persistedSensors: [RuuviTagSensor] = [] private var persistedSensors: [RuuviTagSensor]! { - didSet { - updateViewDevices() + get { + return accessQueue.sync { + _persistedSensors + } + } + set { + accessQueue.async(flags: .barrier) { + self._persistedSensors = newValue + DispatchQueue.main.async { + self.updateViewDevices() + } + } } } + + private var _ruuviTags = Set() + private var ruuviTags: Set { + get { + return accessQueue.sync { + _ruuviTags + } + } + set { + accessQueue.async(flags: .barrier) { + self._ruuviTags = newValue + } + } + } + private var reloadTimer: Timer? private var scanToken: ObservationToken? private var stateToken: ObservationToken? @@ -196,7 +224,10 @@ extension DiscoverPresenter { } private func startReloading() { - reloadTimer = Timer.scheduledTimer(withTimeInterval: 3, repeats: true, block: { [weak self] (_) in + reloadTimer = Timer.scheduledTimer( + withTimeInterval: 3, + repeats: true, + block: { [weak self] (_) in self?.updateViewDevices() }) // don't wait for timer, reload after 0.5 sec diff --git a/Packages/RuuviCloud/RuuviCloud.podspec b/Packages/RuuviCloud/RuuviCloud.podspec index 34bb3834d..54cb4bd5a 100644 --- a/Packages/RuuviCloud/RuuviCloud.podspec +++ b/Packages/RuuviCloud/RuuviCloud.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'RuuviCloud' - s.version = '0.0.7' + s.version = '0.0.8' s.summary = 'Ruuvi Cloud' s.homepage = 'https://ruuvi.com' s.author = { 'Rinat Enikeev' => 'rinat@ruuvi.com' } diff --git a/Packages/RuuviCloud/Sources/RuuviCloud/RuuviCloud.swift b/Packages/RuuviCloud/Sources/RuuviCloud/RuuviCloud.swift index 3f68e1c49..71d6b9760 100644 --- a/Packages/RuuviCloud/Sources/RuuviCloud/RuuviCloud.swift +++ b/Packages/RuuviCloud/Sources/RuuviCloud/RuuviCloud.swift @@ -28,7 +28,8 @@ public protocol RuuviCloud { func registerPNToken(token: String, type: String, name: String?, - data: String?) -> Future + data: String?, + params: [String: String]?) -> Future @discardableResult func unregisterPNToken(token: String?, @@ -61,6 +62,12 @@ public protocol RuuviCloud { macId: MACIdentifier ) -> Future + @discardableResult + func contest( + macId: MACIdentifier, + secret: String + ) -> Future + @discardableResult func unclaim(macId: MACIdentifier) -> Future @@ -145,6 +152,12 @@ public protocol RuuviCloud { @discardableResult func set(dashboardTapActionType: DashboardTapActionType) -> Future + @discardableResult + func set(emailAlert: Bool) -> Future + + @discardableResult + func set(pushAlert: Bool) -> Future + @discardableResult func update( temperatureOffset: Double?, diff --git a/Packages/RuuviCloud/Sources/RuuviCloudApi/RuuviCloudApi.swift b/Packages/RuuviCloud/Sources/RuuviCloudApi/RuuviCloudApi.swift index f67009afa..e1a90b6dd 100644 --- a/Packages/RuuviCloud/Sources/RuuviCloudApi/RuuviCloudApi.swift +++ b/Packages/RuuviCloud/Sources/RuuviCloudApi/RuuviCloudApi.swift @@ -39,6 +39,11 @@ public protocol RuuviCloudApi { authorization: String ) -> Future + func contest( + _ requestModel: RuuviCloudApiContestRequest, + authorization: String + ) -> Future + func unclaim( _ requestModel: RuuviCloudApiClaimRequest, authorization: String diff --git a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Request/RuuviCloudApiContestRequest.swift b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Request/RuuviCloudApiContestRequest.swift new file mode 100644 index 000000000..1203e0b61 --- /dev/null +++ b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Request/RuuviCloudApiContestRequest.swift @@ -0,0 +1,11 @@ +import Foundation + +public struct RuuviCloudApiContestRequest: Codable { + let sensor: String + let secret: String + + public init(sensor: String, secret: String) { + self.sensor = sensor + self.secret = secret + } +} diff --git a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Request/RuuviCloudPNTokenRegisterRequest.swift b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Request/RuuviCloudPNTokenRegisterRequest.swift index 3c9f77b91..ad2acdb55 100644 --- a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Request/RuuviCloudPNTokenRegisterRequest.swift +++ b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Request/RuuviCloudPNTokenRegisterRequest.swift @@ -1,18 +1,26 @@ import Foundation +// swiftlint:disable:next type_name +public enum RuuviCloudPNTokenRegisterRequestParamsKey: String { + case sound = "soundFile" +} + public struct RuuviCloudPNTokenRegisterRequest: Encodable { let token: String let type: String let name: String? let data: String? + let params: [String: String]? public init(token: String, type: String, name: String? = nil, - data: String? = nil) { + data: String? = nil, + params: [String: String]? = nil) { self.token = token self.type = type self.name = name self.data = data + self.params = params } } diff --git a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiContestResponse.swift b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiContestResponse.swift new file mode 100644 index 000000000..584de6766 --- /dev/null +++ b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiContestResponse.swift @@ -0,0 +1,5 @@ +import Foundation + +public struct RuuviCloudApiContestResponse: Decodable { + public let sensor: String +} diff --git a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiGetSettingsResponse.swift b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiGetSettingsResponse.swift index 8ffceb0d0..4c249bf7e 100644 --- a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiGetSettingsResponse.swift +++ b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiGetSettingsResponse.swift @@ -46,6 +46,12 @@ public struct RuuviCloudApiSettings: Decodable, RuuviCloudSettings { public var dashboardTapActionType: DashboardTapActionType? { return dashboardTapActionTypeString?.ruuviCloudApiSettingsDashboardTapActionType } + public var pushAlertEnabled: Bool? { + return pushAlertEnabledString?.ruuviCloudApiSettingBoolean + } + public var emailAlertEnabled: Bool? { + return emailAlertEnabledString?.ruuviCloudApiSettingBoolean + } var unitTemperatureString: String? var accuracyTemperatureString: String? @@ -60,6 +66,8 @@ public struct RuuviCloudApiSettings: Decodable, RuuviCloudSettings { var dashboardEnabledString: String? var dashboardTypeString: String? var dashboardTapActionTypeString: String? + var pushAlertEnabledString: String? + var emailAlertEnabledString: String? enum CodingKeys: String, CodingKey { case unitTemperatureString = "UNIT_TEMPERATURE" @@ -75,5 +83,7 @@ public struct RuuviCloudApiSettings: Decodable, RuuviCloudSettings { case dashboardEnabledString = "DASHBOARD_ENABLED" case dashboardTypeString = "DASHBOARD_TYPE" case dashboardTapActionTypeString = "DASHBOARD_TAP_ACTION" + case pushAlertEnabledString = "ALERT_PUSH_ENABLED" + case emailAlertEnabledString = "ALERT_EMAIL_ENABLED" } } diff --git a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Settings/RuuviCloudApiSettings.swift b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Settings/RuuviCloudApiSettings.swift index 7112516ab..cf0055fb9 100644 --- a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Settings/RuuviCloudApiSettings.swift +++ b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Settings/RuuviCloudApiSettings.swift @@ -15,6 +15,8 @@ public enum RuuviCloudApiSetting: String, CaseIterable, Codable { case dashboardEnabled = "DASHBOARD_ENABLED" case dashboardType = "DASHBOARD_TYPE" case dashboardTapActionType = "DASHBOARD_TAP_ACTION" + case pushAlertEnabled = "ALERT_PUSH_ENABLED" + case emailAlertEnabled = "ALERT_EMAIL_ENABLED" } extension TemperatureUnit { @@ -154,16 +156,14 @@ extension String { } } - // swiftlint:disable switch_case_alignment public var ruuviCloudApiSettingsDashboardTapActionType: DashboardTapActionType { switch self { - case "card": - return .card - case "chart": - return .chart - default: - return .card + case "card": + return .card + case "chart": + return .chart + default: + return .card } } - // swiftlint:enable switch_case_alignment } diff --git a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/RuuviCloudApiURLSession.swift b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/RuuviCloudApiURLSession.swift index 513378b8b..97430eb72 100644 --- a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/RuuviCloudApiURLSession.swift +++ b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/RuuviCloudApiURLSession.swift @@ -13,6 +13,7 @@ extension RuuviCloudApiURLSession { case unregisterPNToken = "push-unregister" case PNTokens = "push-list" case claim + case contest = "contest-sensor" case unclaim case share case unshare @@ -112,6 +113,16 @@ public final class RuuviCloudApiURLSession: NSObject, RuuviCloudApi { authorization: authorization) } + public func contest( + _ requestModel: RuuviCloudApiContestRequest, + authorization: String + ) -> Future { + return request(endpoint: Routes.contest, + with: requestModel, + method: .post, + authorization: authorization) + } + public func unclaim( _ requestModel: RuuviCloudApiClaimRequest, authorization: String diff --git a/Packages/RuuviCloud/Sources/RuuviCloudPure/RuuviCloudPure.swift b/Packages/RuuviCloud/Sources/RuuviCloudPure/RuuviCloudPure.swift index b37d97c3b..8b5dd143d 100644 --- a/Packages/RuuviCloud/Sources/RuuviCloudPure/RuuviCloudPure.swift +++ b/Packages/RuuviCloud/Sources/RuuviCloudPure/RuuviCloudPure.swift @@ -423,6 +423,58 @@ public final class RuuviCloudPure: RuuviCloud { return promise.future } + @discardableResult + public func set(emailAlert: Bool) -> Future { + let promise = Promise() + guard let apiKey = user.apiKey else { + promise.fail(error: .notAuthorized) + return promise.future + } + let request = RuuviCloudApiPostSettingRequest( + name: .emailAlertEnabled, + value: emailAlert.chartBoolSettingString, + timestamp: Int(Date().timeIntervalSince1970) + ) + api.postSetting(request, authorization: apiKey) + .on(success: { _ in + promise.succeed(value: emailAlert) + }, failure: { [weak self] error in + self?.createQueuedRequest( + from: request, + type: .settings, + uniqueKey: RuuviCloudApiSetting.emailAlertEnabled.rawValue + ) + promise.fail(error: .api(error)) + }) + return promise.future + } + + @discardableResult + public func set(pushAlert: Bool) -> Future { + let promise = Promise() + guard let apiKey = user.apiKey else { + promise.fail(error: .notAuthorized) + return promise.future + } + let request = RuuviCloudApiPostSettingRequest( + name: .pushAlertEnabled, + value: pushAlert.chartBoolSettingString, + timestamp: Int(Date().timeIntervalSince1970) + ) + api.postSetting(request, authorization: apiKey) + .on(success: { _ in + promise.succeed(value: pushAlert) + }, failure: { [weak self] error in + self?.createQueuedRequest( + from: request, + type: .settings, + uniqueKey: RuuviCloudApiSetting.pushAlertEnabled.rawValue + ) + promise.fail(error: .api(error)) + }) + return promise.future + } + @discardableResult public func getCloudSettings() -> Future { let promise = Promise() @@ -724,6 +776,26 @@ public final class RuuviCloudPure: RuuviCloud { return promise.future } + @discardableResult + public func contest( + macId: MACIdentifier, + secret: String + ) -> Future { + let promise = Promise() + guard let apiKey = user.apiKey else { + promise.fail(error: .notAuthorized) + return promise.future + } + let request = RuuviCloudApiContestRequest(sensor: macId.value, secret: secret) + api.contest(request, authorization: apiKey) + .on(success: { response in + promise.succeed(value: response.sensor.mac) + }, failure: { error in + promise.fail(error: .api(error)) + }) + return promise.future + } + public func unclaim(macId: MACIdentifier) -> Future { let promise = Promise() guard let apiKey = user.apiKey else { @@ -794,16 +866,20 @@ public final class RuuviCloudPure: RuuviCloud { public func registerPNToken(token: String, type: String, name: String?, - data: String?) -> Future { + data: String?, + params: [String: String]?) -> Future { let promise = Promise() guard let apiKey = user.apiKey else { promise.fail(error: .notAuthorized) return promise.future } - let request = RuuviCloudPNTokenRegisterRequest(token: token, - type: type, - name: name, - data: data) + let request = RuuviCloudPNTokenRegisterRequest( + token: token, + type: type, + name: name, + data: data, + params: params + ) api.registerPNToken(request, authorization: apiKey) .on(success: { response in diff --git a/Packages/RuuviDaemon/Sources/RuuviDaemon/RuuviDaemonWorker.swift b/Packages/RuuviDaemon/Sources/RuuviDaemon/RuuviDaemonWorker.swift index b81218995..42b5fa3e4 100644 --- a/Packages/RuuviDaemon/Sources/RuuviDaemon/RuuviDaemonWorker.swift +++ b/Packages/RuuviDaemon/Sources/RuuviDaemon/RuuviDaemonWorker.swift @@ -2,13 +2,13 @@ import Foundation open class RuuviDaemonWorker: NSObject { public var thread: Thread! - private var block: (() -> Void)! + private var block: (() -> Void)? override public init() {} @objc internal func runBlock() { autoreleasepool { - block() + block?() } } @@ -19,12 +19,12 @@ open class RuuviDaemonWorker: NSObject { .components(separatedBy: .punctuationCharacters)[1] thread = Thread { [weak self] in - while self != nil && !self!.thread.isCancelled { + defer { self?.block = nil } + while !(self?.thread.isCancelled ?? true) { RunLoop.current.run( mode: RunLoop.Mode.default, before: Date.distantFuture) } - Thread.exit() } thread.name = "\(threadName)-\(UUID().uuidString)" thread.start() @@ -37,6 +37,7 @@ open class RuuviDaemonWorker: NSObject { } public func stopWork() { + block = nil perform(#selector(stopThread), on: thread, with: nil, @@ -45,7 +46,6 @@ open class RuuviDaemonWorker: NSObject { } @objc func stopThread() { - Thread.exit() + thread.cancel() } - } diff --git a/Packages/RuuviDaemon/Sources/RuuviDaemon/RuuviTagAdvertisementDaemon.swift b/Packages/RuuviDaemon/Sources/RuuviDaemon/RuuviTagAdvertisementDaemon.swift index 87e8c5d2c..5a4ab1bc5 100644 --- a/Packages/RuuviDaemon/Sources/RuuviDaemon/RuuviTagAdvertisementDaemon.swift +++ b/Packages/RuuviDaemon/Sources/RuuviDaemon/RuuviTagAdvertisementDaemon.swift @@ -1,7 +1,12 @@ import Foundation extension Notification.Name { - public static let RuuviTagAdvertisementDaemonDidFail = Notification.Name("RuuviTagAdvertisementDaemonDidFail") + public static let RuuviTagAdvertisementDaemonDidFail = Notification.Name( + "RuuviTagAdvertisementDaemonDidFail" + ) + public static let RuuviTagAdvertisementDaemonShouldRestart = Notification.Name( + "RuuviTagAdvertisementDaemonShouldRestart" + ) } public enum RuuviTagAdvertisementDaemonDidFailKey: String { diff --git a/Packages/RuuviDaemon/Sources/RuuviDaemon/RuuviTagHeartbeatDaemon.swift b/Packages/RuuviDaemon/Sources/RuuviDaemon/RuuviTagHeartbeatDaemon.swift index 08ec2bf97..bb169f982 100644 --- a/Packages/RuuviDaemon/Sources/RuuviDaemon/RuuviTagHeartbeatDaemon.swift +++ b/Packages/RuuviDaemon/Sources/RuuviDaemon/RuuviTagHeartbeatDaemon.swift @@ -3,6 +3,7 @@ import Future extension Notification.Name { public static let RuuviTagHeartbeatDaemonDidFail = Notification.Name("RuuviTagHeartbeatDaemonDidFail") + public static let RuuviTagHeartBeatDaemonShouldRestart = Notification.Name("RuuviTagHeartBeatDaemonShouldRestart") } public enum RuuviTagHeartbeatDaemonDidFailKey: String { diff --git a/Packages/RuuviDaemon/Sources/RuuviDaemonRuuviTag/Advertisement/RuuviTagAdvertisementDaemonBTKit.swift b/Packages/RuuviDaemon/Sources/RuuviDaemonRuuviTag/Advertisement/RuuviTagAdvertisementDaemonBTKit.swift index 00c40153f..cf1de7750 100644 --- a/Packages/RuuviDaemon/Sources/RuuviDaemonRuuviTag/Advertisement/RuuviTagAdvertisementDaemonBTKit.swift +++ b/Packages/RuuviDaemon/Sources/RuuviDaemonRuuviTag/Advertisement/RuuviTagAdvertisementDaemonBTKit.swift @@ -8,6 +8,7 @@ import RuuviPool import RuuviPersistence import RuuviDaemon +// swiftlint:disable:next type_body_length public final class RuuviTagAdvertisementDaemonBTKit: RuuviDaemonWorker, RuuviTagAdvertisementDaemon { private let ruuviPool: RuuviPool private let ruuviStorage: RuuviStorage @@ -23,6 +24,7 @@ public final class RuuviTagAdvertisementDaemonBTKit: RuuviDaemonWorker, RuuviTag private var savedDate = [String: Date]() // uuid:date private var isOnToken: NSObjectProtocol? private var cloudModeOnToken: NSObjectProtocol? + private var daemonRestartToken: NSObjectProtocol? private var saveInterval: TimeInterval { return TimeInterval(settings.advertisementDaemonIntervalMinutes * 60) } @@ -47,6 +49,9 @@ public final class RuuviTagAdvertisementDaemonBTKit: RuuviDaemonWorker, RuuviTag if let cloudModeOnToken = cloudModeOnToken { NotificationCenter.default.removeObserver(cloudModeOnToken) } + if let daemonRestartToken = daemonRestartToken { + NotificationCenter.default.removeObserver(daemonRestartToken) + } } public init( @@ -83,6 +88,15 @@ public final class RuuviTagAdvertisementDaemonBTKit: RuuviDaemonWorker, RuuviTag guard let sSelf = self else { return } sSelf.restartObserving() } + + daemonRestartToken = NotificationCenter + .default + .addObserver(forName: .RuuviTagAdvertisementDaemonShouldRestart, + object: nil, + queue: .main) { [weak self] _ in + guard let sSelf = self else { return } + sSelf.restart() + } } public func start() { @@ -121,8 +135,10 @@ public final class RuuviTagAdvertisementDaemonBTKit: RuuviDaemonWorker, RuuviTag } public func restart() { - stop() - start() + ruuviStorage.readAll().on(success: { [weak self] sensors in + self?.ruuviTags = sensors + self?.restartObserving() + }) } @objc private func stopDaemon() { diff --git a/Packages/RuuviDaemon/Sources/RuuviDaemonRuuviTag/Heartbeat/RuuviTagHeartbeatDaemonBTKit.swift b/Packages/RuuviDaemon/Sources/RuuviDaemonRuuviTag/Heartbeat/RuuviTagHeartbeatDaemonBTKit.swift index 295cf88a2..e0e8b3d05 100644 --- a/Packages/RuuviDaemon/Sources/RuuviDaemonRuuviTag/Heartbeat/RuuviTagHeartbeatDaemonBTKit.swift +++ b/Packages/RuuviDaemon/Sources/RuuviDaemonRuuviTag/Heartbeat/RuuviTagHeartbeatDaemonBTKit.swift @@ -33,6 +33,7 @@ public final class RuuviTagHeartbeatDaemonBTKit: RuuviDaemonWorker, RuuviTagHear private var ruuviTagsToken: RuuviReactorToken? private var sensorSettingsTokens = [String: RuuviReactorToken]() private var cloudModeOnToken: NSObjectProtocol? + private var daemonRestartToken: NSObjectProtocol? // swiftlint:disable:next function_body_length public init( @@ -101,6 +102,15 @@ public final class RuuviTagHeartbeatDaemonBTKit: RuuviDaemonWorker, RuuviTagHear guard let sSelf = self else { return } sSelf.handleRuuviTagsChange() } + + daemonRestartToken = NotificationCenter + .default + .addObserver(forName: .RuuviTagHeartBeatDaemonShouldRestart, + object: nil, + queue: .main) { [weak self] _ in + guard let sSelf = self else { return } + sSelf.handleRuuviTagsChange() + } } deinit { @@ -114,6 +124,9 @@ public final class RuuviTagHeartbeatDaemonBTKit: RuuviDaemonWorker, RuuviTagHear if let cloudModeOnToken = cloudModeOnToken { NotificationCenter.default.removeObserver(cloudModeOnToken) } + if let daemonRestartToken = daemonRestartToken { + NotificationCenter.default.removeObserver(daemonRestartToken) + } } public func start() { @@ -153,8 +166,10 @@ public final class RuuviTagHeartbeatDaemonBTKit: RuuviDaemonWorker, RuuviTagHear } public func restart() { - stop() - start() + ruuviStorage.readAll().on(success: { [weak self] sensors in + self?.ruuviTags = sensors + self?.handleRuuviTagsChange() + }) } @objc private func stopDaemon() { diff --git a/Packages/RuuviLocal/Sources/RuuviLocal/RuuviLocalSettings.swift b/Packages/RuuviLocal/Sources/RuuviLocal/RuuviLocalSettings.swift index 5c725ad5c..ed4535807 100644 --- a/Packages/RuuviLocal/Sources/RuuviLocal/RuuviLocalSettings.swift +++ b/Packages/RuuviLocal/Sources/RuuviLocal/RuuviLocalSettings.swift @@ -21,6 +21,9 @@ extension Notification.Name { public static let DashboardTypeDidChange = Notification.Name("DashboardTypeDidChange") public static let DashboardTapActionTypeDidChange = Notification.Name("DashboardTapActionTypeDidChange") public static let AppearanceSettingsDidChange = Notification.Name("AppearanceSettingsDidChange") + public static let AlertSoundSettingsDidChange = Notification.Name("AlertSoundSettingsDidChange") + public static let EmailAlertSettingsDidChange = Notification.Name("EmailAlertSettingsDidChange") + public static let PushAlertSettingsDidChange = Notification.Name("PushAlertSettingsDidChange") } public enum DashboardTypeKey: String { @@ -77,6 +80,12 @@ public protocol RuuviLocalSettings { var dashboardType: DashboardType { get set } var dashboardTapActionType: DashboardTapActionType { get set } var theme: RuuviTheme { get set } + var hideNFCForSensorContest: Bool { get set } + var alertSound: RuuviAlertSound { get set } + var showEmailAlertSettings: Bool { get set } + var emailAlertEnabled: Bool { get set } + var showPushAlertSettings: Bool { get set } + var pushAlertEnabled: Bool { get set } func keepConnectionDialogWasShown(for luid: LocalIdentifier) -> Bool func setKeepConnectionDialogWasShown(for luid: LocalIdentifier) @@ -94,6 +103,9 @@ public protocol RuuviLocalSettings { func lastOpenedChart() -> String? func setLastOpenedChart(with id: String) - func setOwnerCheckDate(for macId: MACIdentifier, value: Date) - func ownerCheckDate(for macId: MACIdentifier) -> Date? + func setOwnerCheckDate(for macId: MACIdentifier?, value: Date?) + func ownerCheckDate(for macId: MACIdentifier?) -> Date? + + func syncDialogHidden(for luid: LocalIdentifier) -> Bool + func setSyncDialogHidden(for luid: LocalIdentifier) } diff --git a/Packages/RuuviLocal/Sources/RuuviLocalUserDefaults/RuuviLocalSettingsUserDefaults.swift b/Packages/RuuviLocal/Sources/RuuviLocalUserDefaults/RuuviLocalSettingsUserDefaults.swift index 5cddef7e4..db2aa5f63 100644 --- a/Packages/RuuviLocal/Sources/RuuviLocalUserDefaults/RuuviLocalSettingsUserDefaults.swift +++ b/Packages/RuuviLocal/Sources/RuuviLocalUserDefaults/RuuviLocalSettingsUserDefaults.swift @@ -418,12 +418,18 @@ final class RuuviLocalSettingsUserDefaults: RuuviLocalSettings { } private let ownerCheckDateKey = "SettingsUserDefaults.ownerCheckDate" - func setOwnerCheckDate(for macId: MACIdentifier, value: Date) { - UserDefaults.standard.set(value, forKey: ownerCheckDateKey + macId.mac) + func setOwnerCheckDate(for macId: MACIdentifier?, value: Date?) { + guard let macId = macId else { return } + if let value = value { + UserDefaults.standard.set(value, forKey: ownerCheckDateKey + macId.mac) + } else { + UserDefaults.standard.removeObject(forKey: ownerCheckDateKey + macId.mac) + } } - func ownerCheckDate(for macId: MACIdentifier) -> Date? { - UserDefaults.standard.value(forKey: ownerCheckDateKey + macId.mac) as? Date + func ownerCheckDate(for macId: MACIdentifier?) -> Date? { + guard let macId = macId else { return nil } + return UserDefaults.standard.value(forKey: ownerCheckDateKey + macId.mac) as? Date } @UserDefault("SettingsUserDefaults.dashboardEnabled", defaultValue: true) @@ -543,5 +549,66 @@ final class RuuviLocalSettingsUserDefaults: RuuviLocalSettings { userInfo: [AppearanceTypeKey.style: newValue]) } } + + private let syncDialogHiddenKey = "SettingsUserDefaults.syncDialogHiddenKey." + func syncDialogHidden(for luid: LocalIdentifier) -> Bool { + return UserDefaults.standard.bool(forKey: syncDialogHiddenKey + luid.value) + } + + func setSyncDialogHidden(for luid: LocalIdentifier) { + UserDefaults.standard.set(true, forKey: syncDialogHiddenKey + luid.value) + } + + @UserDefault("SettingsUserDefaults.hideNFCForSensorContest", defaultValue: false) + var hideNFCForSensorContest: Bool + private let ruuviAlertSoundKey = "SettingsUserDefaults.ruuviAlertSoundKey" + var alertSound: RuuviAlertSound { + get { + if let key = UserDefaults.standard.string(forKey: ruuviAlertSoundKey) { + return RuuviAlertSound(rawValue: key) ?? .ruuviSpeak + } + return .ruuviSpeak + } + set { + UserDefaults.standard.set(newValue.rawValue, forKey: ruuviAlertSoundKey) + NotificationCenter + .default + .post(name: .AlertSoundSettingsDidChange, + object: self, + userInfo: [AppearanceTypeKey.style: newValue]) + } + } + + @UserDefault("SettingsUserDefaults.showEmailAlertSettings", defaultValue: false) + var showEmailAlertSettings: Bool + + @UserDefault("SettingsUserDefaults.emailAlertEnabled", defaultValue: false) + var emailAlertEnabled: Bool { + didSet { + DispatchQueue.global(qos: .userInitiated).async { + NotificationCenter + .default + .post(name: .EmailAlertSettingsDidChange, + object: self, + userInfo: nil) + } + } + } + + @UserDefault("SettingsUserDefaults.showPushAlertSettings", defaultValue: false) + var showPushAlertSettings: Bool + + @UserDefault("SettingsUserDefaults.pushAlertEnabled", defaultValue: false) + var pushAlertEnabled: Bool { + didSet { + DispatchQueue.global(qos: .userInitiated).async { + NotificationCenter + .default + .post(name: .PushAlertSettingsDidChange, + object: self, + userInfo: nil) + } + } + } } // swiftlint:enable type_body_length file_length diff --git a/Packages/RuuviNotification/RuuviNotification.podspec b/Packages/RuuviNotification/RuuviNotification.podspec index 82f05d075..9e9e44856 100644 --- a/Packages/RuuviNotification/RuuviNotification.podspec +++ b/Packages/RuuviNotification/RuuviNotification.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'RuuviNotification' - s.version = '0.0.1' + s.version = '0.0.2' s.summary = 'Ruuvi Notification' s.homepage = 'https://ruuvi.com' s.author = { 'Rinat Enikeev' => 'rinat@ruuvi.com' } diff --git a/Packages/RuuviNotification/Sources/RuuviNotificationLocal/RuuviNotificationLocalImpl.swift b/Packages/RuuviNotification/Sources/RuuviNotificationLocal/RuuviNotificationLocalImpl.swift index a24c80b8c..58728096e 100644 --- a/Packages/RuuviNotification/Sources/RuuviNotificationLocal/RuuviNotificationLocalImpl.swift +++ b/Packages/RuuviNotification/Sources/RuuviNotificationLocal/RuuviNotificationLocalImpl.swift @@ -103,7 +103,14 @@ public final class RuuviNotificationLocalImpl: NSObject, RuuviNotificationLocal let content = UNMutableNotificationContent() content.title = title - content.sound = .default + switch settings.alertSound { + case .systemDefault: + content.sound = .default + default: + content.sound = UNNotificationSound( + named: UNNotificationSoundName(rawValue: settings.alertSound.rawValue) + ) + } content.userInfo = [blast.uuidKey: uuid, blast.typeKey: BlastNotificationType.connection.rawValue] content.categoryIdentifier = blast.id @@ -123,7 +130,14 @@ public final class RuuviNotificationLocalImpl: NSObject, RuuviNotificationLocal return // muted } let content = UNMutableNotificationContent() - content.sound = .default + switch settings.alertSound { + case .systemDefault: + content.sound = .default + default: + content.sound = UNNotificationSound( + named: UNNotificationSoundName(rawValue: settings.alertSound.rawValue) + ) + } content.userInfo = [blast.uuidKey: uuid, blast.typeKey: BlastNotificationType.connection.rawValue] content.categoryIdentifier = blast.id content.title = title @@ -145,7 +159,14 @@ public final class RuuviNotificationLocalImpl: NSObject, RuuviNotificationLocal } let content = UNMutableNotificationContent() - content.sound = .default + switch settings.alertSound { + case .systemDefault: + content.sound = .default + default: + content.sound = UNNotificationSound( + named: UNNotificationSoundName(rawValue: settings.alertSound.rawValue) + ) + } content.userInfo = [blast.uuidKey: uuid, blast.typeKey: BlastNotificationType.movement.rawValue] content.categoryIdentifier = blast.id @@ -218,7 +239,14 @@ extension RuuviNotificationLocalImpl { } if needsToShow { let content = UNMutableNotificationContent() - content.sound = .default + switch settings.alertSound { + case .systemDefault: + content.sound = .default + default: + content.sound = UNNotificationSound( + named: UNNotificationSoundName(rawValue: settings.alertSound.rawValue) + ) + } content.title = title content.userInfo = [lowHigh.uuidKey: uuid, lowHigh.typeKey: type.rawValue] content.categoryIdentifier = lowHigh.id diff --git a/Packages/RuuviOntology/RuuviOntology.podspec b/Packages/RuuviOntology/RuuviOntology.podspec index 9281de7d1..276692d69 100644 --- a/Packages/RuuviOntology/RuuviOntology.podspec +++ b/Packages/RuuviOntology/RuuviOntology.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'RuuviOntology' - s.version = '0.0.5' + s.version = '0.0.6' s.summary = 'Ruuvi Ontology' s.homepage = 'https://ruuvi.com' s.author = { 'Rinat Enikeev' => 'rinat@ruuvi.com' } diff --git a/Packages/RuuviOntology/Sources/RuuviOntology/Alert/RuuviAlertSound.swift b/Packages/RuuviOntology/Sources/RuuviOntology/Alert/RuuviAlertSound.swift new file mode 100644 index 000000000..7201a0ad3 --- /dev/null +++ b/Packages/RuuviOntology/Sources/RuuviOntology/Alert/RuuviAlertSound.swift @@ -0,0 +1,15 @@ +import Foundation + +public enum RuuviAlertSound: String { + case systemDefault = "default" + case ruuviSpeak = "ruuvi_speak.caf" + + public var fileName: String { + switch self { + case .systemDefault: + return "default" + case .ruuviSpeak: + return "ruuvi_speak" + } + } +} diff --git a/Packages/RuuviOntology/Sources/RuuviOntology/Common/RuuviCloudSettings.swift b/Packages/RuuviOntology/Sources/RuuviOntology/Common/RuuviCloudSettings.swift index 4535ea827..e9a18808a 100644 --- a/Packages/RuuviOntology/Sources/RuuviOntology/Common/RuuviCloudSettings.swift +++ b/Packages/RuuviOntology/Sources/RuuviOntology/Common/RuuviCloudSettings.swift @@ -14,4 +14,6 @@ public protocol RuuviCloudSettings { var dashboardEnabled: Bool? { get } var dashboardType: DashboardType? { get } var dashboardTapActionType: DashboardTapActionType? { get } + var pushAlertEnabled: Bool? { get } + var emailAlertEnabled: Bool? { get } } diff --git a/Packages/RuuviPersistence/Sources/RuuviPersistenceSQLite/RuuviPersistenceSQLite.swift b/Packages/RuuviPersistence/Sources/RuuviPersistenceSQLite/RuuviPersistenceSQLite.swift index 168ca8a3b..cf89fdbf7 100644 --- a/Packages/RuuviPersistence/Sources/RuuviPersistenceSQLite/RuuviPersistenceSQLite.swift +++ b/Packages/RuuviPersistence/Sources/RuuviPersistenceSQLite/RuuviPersistenceSQLite.swift @@ -30,7 +30,7 @@ public class RuuviPersistenceSQLite: RuuviPersistence, DatabaseService { private let context: SQLiteContext private let readQueue: DispatchQueue = DispatchQueue(label: "RuuviTagPersistenceSQLite.readQueue", - qos: .userInitiated) + qos: .default) public init(context: SQLiteContext) { self.context = context } diff --git a/Packages/RuuviReactor/Sources/RuuviReactorImpl/RuuviReactorCombine/RuuviTagSubjectCombine.swift b/Packages/RuuviReactor/Sources/RuuviReactorImpl/RuuviReactorCombine/RuuviTagSubjectCombine.swift index c4c41132b..df1ea5123 100644 --- a/Packages/RuuviReactor/Sources/RuuviReactorImpl/RuuviReactorCombine/RuuviTagSubjectCombine.swift +++ b/Packages/RuuviReactor/Sources/RuuviReactorImpl/RuuviReactorCombine/RuuviTagSubjectCombine.swift @@ -27,7 +27,7 @@ final class RuuviTagSubjectCombine { ruuviTagsRealmToken?.invalidate() } - // swiftlint:disable:next cyclomatic_complexity + // swiftlint:disable:next cyclomatic_complexity function_body_length init(sqlite: SQLiteContext, realm: RealmContext) { self.sqlite = sqlite self.realm = realm @@ -40,11 +40,17 @@ final class RuuviTagSubjectCombine { guard let sSelf = self else { return } switch event { case .insertion: - sSelf.insertSubject.send(record.any) + DispatchQueue.main.async { + sSelf.insertSubject.send(record.any) + } case .update: - sSelf.updateSubject.send(record.any) + DispatchQueue.main.async { + sSelf.updateSubject.send(record.any) + } case .deletion: - sSelf.deleteSubject.send(record.any) + DispatchQueue.main.async { + sSelf.deleteSubject.send(record.any) + } case .move: break } diff --git a/Packages/RuuviService/Sources/RuuviService/RuuviServiceAppSettings.swift b/Packages/RuuviService/Sources/RuuviService/RuuviServiceAppSettings.swift index 58294c2d2..9af214996 100644 --- a/Packages/RuuviService/Sources/RuuviService/RuuviServiceAppSettings.swift +++ b/Packages/RuuviService/Sources/RuuviService/RuuviServiceAppSettings.swift @@ -41,4 +41,10 @@ public protocol RuuviServiceAppSettings { @discardableResult func set(dashboardTapActionType: DashboardTapActionType) -> Future + + @discardableResult + func set(emailAlert: Bool) -> Future + + @discardableResult + func set(pushAlert: Bool) -> Future } diff --git a/Packages/RuuviService/Sources/RuuviService/RuuviServiceCloudNotification.swift b/Packages/RuuviService/Sources/RuuviService/RuuviServiceCloudNotification.swift index 1c17a6d44..7d3b4d73e 100644 --- a/Packages/RuuviService/Sources/RuuviService/RuuviServiceCloudNotification.swift +++ b/Packages/RuuviService/Sources/RuuviService/RuuviServiceCloudNotification.swift @@ -7,13 +7,19 @@ public protocol RuuviServiceCloudNotification { @discardableResult func set(token: String?, name: String?, - data: String?) -> Future + data: String?, + sound: RuuviAlertSound) -> Future + + @discardableResult + func set(sound: RuuviAlertSound, + deviceName: String?) -> Future @discardableResult func register(token: String, type: String, name: String?, - data: String?) -> Future + data: String?, + params: [String: String]?) -> Future @discardableResult func unregister(token: String?, diff --git a/Packages/RuuviService/Sources/RuuviService/RuuviServiceOwnership.swift b/Packages/RuuviService/Sources/RuuviService/RuuviServiceOwnership.swift index d788a7de1..393881c1b 100644 --- a/Packages/RuuviService/Sources/RuuviService/RuuviServiceOwnership.swift +++ b/Packages/RuuviService/Sources/RuuviService/RuuviServiceOwnership.swift @@ -15,6 +15,9 @@ public protocol RuuviServiceOwnership { @discardableResult func claim(sensor: RuuviTagSensor) -> Future + @discardableResult + func contest(sensor: RuuviTagSensor, secret: String) -> Future + @discardableResult func unclaim(sensor: RuuviTagSensor) -> Future diff --git a/Packages/RuuviService/Sources/RuuviServiceAppSettings/RuuviServiceAppSettingsImpl.swift b/Packages/RuuviService/Sources/RuuviServiceAppSettings/RuuviServiceAppSettingsImpl.swift index bfb8faba2..36c203d7f 100644 --- a/Packages/RuuviService/Sources/RuuviServiceAppSettings/RuuviServiceAppSettingsImpl.swift +++ b/Packages/RuuviService/Sources/RuuviServiceAppSettings/RuuviServiceAppSettingsImpl.swift @@ -185,4 +185,28 @@ public final class RuuviServiceAppSettingsImpl: RuuviServiceAppSettings { }) return promise.future } + + @discardableResult + public func set(emailAlert: Bool) -> Future { + let promise = Promise() + cloud.set(emailAlert: emailAlert) + .on(success: { enabled in + promise.succeed(value: enabled) + }, failure: { error in + promise.fail(error: .ruuviCloud(error)) + }) + return promise.future + } + + @discardableResult + public func set(pushAlert: Bool) -> Future { + let promise = Promise() + cloud.set(pushAlert: pushAlert) + .on(success: { enabled in + promise.succeed(value: enabled) + }, failure: { error in + promise.fail(error: .ruuviCloud(error)) + }) + return promise.future + } } diff --git a/Packages/RuuviService/Sources/RuuviServiceCloudNotification/RuuviServiceCloudNotificationImpl.swift b/Packages/RuuviService/Sources/RuuviServiceCloudNotification/RuuviServiceCloudNotificationImpl.swift index ec7014b53..fb2151571 100644 --- a/Packages/RuuviService/Sources/RuuviServiceCloudNotification/RuuviServiceCloudNotificationImpl.swift +++ b/Packages/RuuviService/Sources/RuuviServiceCloudNotification/RuuviServiceCloudNotificationImpl.swift @@ -34,7 +34,8 @@ public final class RuuviServiceCloudNotificationImpl: RuuviServiceCloudNotificat @discardableResult public func set(token: String?, name: String?, - data: String?) -> Future { + data: String?, + sound: RuuviAlertSound) -> Future { let promise = Promise() guard ruuviUser.isAuthorized, let token = token else { return promise.future @@ -54,10 +55,43 @@ public final class RuuviServiceCloudNotificationImpl: RuuviServiceCloudNotificat return promise.future } - register(token: token, - type: "ios", - name: name, - data: data) + register( + token: token, + type: "ios", + name: name, + data: data, + params: [ + RuuviCloudPNTokenRegisterRequestParamsKey.sound.rawValue: sound.rawValue + ] + ) + .on(success: { [weak self] tokenId in + self?.pnManager.fcmTokenId = tokenId + self?.pnManager.fcmToken = token + self?.pnManager.fcmTokenLastRefreshed = Date() + promise.succeed(value: tokenId) + }, failure: { error in + promise.fail(error: error) + }) + return promise.future + } + + @discardableResult + public func set(sound: RuuviAlertSound, + deviceName: String?) -> Future { + let promise = Promise() + guard ruuviUser.isAuthorized, let token = pnManager.fcmToken else { + return promise.future + } + + register( + token: token, + type: "ios", + name: deviceName, + data: nil, + params: [ + RuuviCloudPNTokenRegisterRequestParamsKey.sound.rawValue: sound.rawValue + ] + ) .on(success: { [weak self] tokenId in self?.pnManager.fcmTokenId = tokenId self?.pnManager.fcmToken = token @@ -73,12 +107,14 @@ public final class RuuviServiceCloudNotificationImpl: RuuviServiceCloudNotificat public func register(token: String, type: String, name: String?, - data: String?) -> Future { + data: String?, + params: [String: String]?) -> Future { let promise = Promise() cloud.registerPNToken(token: token, type: type, name: name, - data: data) + data: data, + params: params) .on(success: { tokenId in promise.succeed(value: tokenId) }, failure: { error in diff --git a/Packages/RuuviService/Sources/RuuviServiceCloudSync/RuuviServiceCloudSyncImpl.swift b/Packages/RuuviService/Sources/RuuviServiceCloudSync/RuuviServiceCloudSyncImpl.swift index a3f900569..bb0d51514 100644 --- a/Packages/RuuviService/Sources/RuuviServiceCloudSync/RuuviServiceCloudSyncImpl.swift +++ b/Packages/RuuviService/Sources/RuuviServiceCloudSync/RuuviServiceCloudSyncImpl.swift @@ -104,6 +104,14 @@ public final class RuuviServiceCloudSyncImpl: RuuviServiceCloudSync { dashboardTapActionType != sSelf.ruuviLocalSettings.dashboardTapActionType { sSelf.ruuviLocalSettings.dashboardTapActionType = dashboardTapActionType } + if let pushAlertEnabled = cloudSettings.pushAlertEnabled, + pushAlertEnabled != sSelf.ruuviLocalSettings.pushAlertEnabled { + sSelf.ruuviLocalSettings.pushAlertEnabled = pushAlertEnabled + } + if let emailAlertEnabled = cloudSettings.emailAlertEnabled, + emailAlertEnabled != sSelf.ruuviLocalSettings.emailAlertEnabled { + sSelf.ruuviLocalSettings.emailAlertEnabled = emailAlertEnabled + } promise.succeed(value: cloudSettings) }, failure: { error in diff --git a/Packages/RuuviService/Sources/RuuviServiceOwnership/RuuviServiceOwnershipImpl.swift b/Packages/RuuviService/Sources/RuuviServiceOwnership/RuuviServiceOwnershipImpl.swift index 516660c81..13b16377b 100644 --- a/Packages/RuuviService/Sources/RuuviServiceOwnership/RuuviServiceOwnershipImpl.swift +++ b/Packages/RuuviService/Sources/RuuviServiceOwnership/RuuviServiceOwnershipImpl.swift @@ -75,7 +75,6 @@ public final class RuuviServiceOwnershipImpl: RuuviServiceOwnership { } @discardableResult - // swiftlint:disable:next function_body_length public func claim(sensor: RuuviTagSensor) -> Future { let promise = Promise() guard let macId = sensor.macId else { @@ -88,58 +87,42 @@ public final class RuuviServiceOwnershipImpl: RuuviServiceOwnership { } cloud.claim(name: sensor.name, macId: macId) .on(success: { [weak self] _ in - guard let sSelf = self else { return } - let claimedSensor = sensor - .with(owner: owner) - .with(isClaimed: true) - .with(isCloudSensor: true) - sSelf.pool - .update(claimedSensor) - .on(success: { [weak sSelf] _ in - guard let ssSelf = sSelf else { return } - if let customImage = ssSelf.localImages.getCustomBackground(for: macId) { - if let jpegData = customImage.jpegData(compressionQuality: 1.0) { - let remote = ssSelf.cloud.upload( - imageData: jpegData, - mimeType: .jpg, - progress: nil, - for: macId - ) - remote.on(success: { _ in - promise.succeed(value: claimedSensor.any) - }, failure: { error in - promise.fail(error: .ruuviCloud(error)) - }) - } else { - promise.fail(error: .failedToGetJpegRepresentation) - } - } else { - promise.succeed(value: claimedSensor.any) - } - - ssSelf.storage - .readSensorSettings(sensor) - .on { [weak ssSelf] settings in - guard let sssSelf = ssSelf else { return } - sssSelf.cloud.update( - temperatureOffset: settings?.temperatureOffset ?? 0, - humidityOffset: (settings?.humidityOffset ?? 0) * 100, // fraction local, % on cloud - pressureOffset: (settings?.pressureOffset ?? 0) * 100, // hPA local, Pa on cloud - for: sensor - ).on() - } - - AlertType.allCases.forEach { type in - if let alert = ssSelf.alertService.alert(for: sensor, of: type) { - ssSelf.alertService.register(type: alert, ruuviTag: claimedSensor) - } - } - }, failure: { error in - promise.fail(error: .ruuviPool(error)) - }) + self?.handleSensorClaimed( + sensor: sensor, + owner: owner, + macId: macId, + promise: promise + ) + }, failure: { error in + promise.fail(error: .ruuviCloud(error)) + }) + return promise.future + } + + @discardableResult + public func contest( + sensor: RuuviTagSensor, + secret: String + ) -> Future { + let promise = Promise() + guard let macId = sensor.macId else { + promise.fail(error: .macIdIsNil) + return promise.future + } + + guard let owner = ruuviUser.email else { + promise.fail(error: .ruuviCloud(.notAuthorized)) + return promise.future + } + + cloud.contest(macId: macId, secret: secret) + .on(success: { [weak self] _ in + self?.handleSensorClaimed( + sensor: sensor, + owner: owner, + macId: macId, + promise: promise) }, failure: { error in - // TODO: @rinat check on use cases - // if error.errorDescription == "Sensor already claimed" { promise.fail(error: .ruuviCloud(error)) }) return promise.future @@ -251,3 +234,73 @@ public final class RuuviServiceOwnershipImpl: RuuviServiceOwnership { return promise.future } } + +extension RuuviServiceOwnershipImpl { + private func handleSensorClaimed( + sensor: RuuviTagSensor, + owner: String, + macId: MACIdentifier, + promise: Promise + ) { + let claimedSensor = sensor + .with(owner: owner) + .with(isClaimed: true) + .with(isCloudSensor: true) + .with(isOwner: true) + pool + .update(claimedSensor) + .on(success: { [weak self] _ in + self?.handleUpdatedSensor( + sensor: claimedSensor, + promise: promise, + macId: macId + ) + }, failure: { error in + promise.fail(error: .ruuviPool(error)) + }) + } + + private func handleUpdatedSensor( + sensor: RuuviTagSensor, + promise: Promise, + macId: MACIdentifier + ) { + if let customImage = localImages.getCustomBackground(for: macId) { + if let jpegData = customImage.jpegData(compressionQuality: 1.0) { + let remote = cloud.upload( + imageData: jpegData, + mimeType: .jpg, + progress: nil, + for: macId + ) + remote.on(success: { _ in + promise.succeed(value: sensor.any) + }, failure: { error in + promise.fail(error: .ruuviCloud(error)) + }) + } else { + promise.fail(error: .failedToGetJpegRepresentation) + } + } else { + promise.succeed(value: sensor.any) + } + + storage + .readSensorSettings(sensor) + .on { [weak self] settings in + guard let sSelf = self else { return } + sSelf.cloud.update( + temperatureOffset: settings?.temperatureOffset ?? 0, + humidityOffset: (settings?.humidityOffset ?? 0) * 100, // fraction local, % on cloud + pressureOffset: (settings?.pressureOffset ?? 0) * 100, // hPA local, Pa on cloud + for: sensor + ).on() + } + + AlertType.allCases.forEach { type in + if let alert = alertService.alert(for: sensor, of: type) { + alertService.register(type: alert, ruuviTag: sensor) + } + } + } +} diff --git a/Podfile.lock b/Podfile.lock index bcd1f95be..20f3ec2a4 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - BTKit (0.4.1) + - BTKit (0.4.2) - Firebase (10.7.0): - Firebase/Core (= 10.7.0) - Firebase/Analytics (10.7.0): @@ -178,9 +178,9 @@ PODS: - RuuviBundleUtils/RuuviBundleUtils (= 0.0.1) - RuuviBundleUtils/RuuviBundleUtils (0.0.1) - RuuviBundleUtils/Tests (0.0.1) - - RuuviCloud (0.0.7): - - RuuviCloud/Contract (= 0.0.7) - - RuuviCloud/Api (0.0.7): + - RuuviCloud (0.0.8): + - RuuviCloud/Contract (= 0.0.8) + - RuuviCloud/Api (0.0.8): - BTKit - FutureX - RuuviCloud/Contract @@ -188,13 +188,13 @@ PODS: - RuuviOntology/Mappers - RuuviPersistence - RuuviPool - - RuuviCloud/Contract (0.0.7): + - RuuviCloud/Contract (0.0.8): - FutureX - RuuviOntology - RuuviPersistence - RuuviPool - RuuviUser - - RuuviCloud/Pure (0.0.7): + - RuuviCloud/Pure (0.0.8): - FutureX - RuuviCloud/Api - RuuviCloud/Contract @@ -202,7 +202,7 @@ PODS: - RuuviPersistence - RuuviPool - RuuviUser - - RuuviCloud/Tests (0.0.7) + - RuuviCloud/Tests (0.0.8) - RuuviContext (0.0.1): - RuuviContext/SQLite (= 0.0.1) - RuuviContext/Contract (0.0.1) @@ -276,9 +276,9 @@ PODS: - RuuviDFU/Impl (0.0.1): - iOSDFULibrary - RuuviDFU/Tests (0.0.1) - - RuuviDiscover (0.0.1): - - RuuviDiscover/RuuviDiscover (= 0.0.1) - - RuuviDiscover/RuuviDiscover (0.0.1): + - RuuviDiscover (0.0.2): + - RuuviDiscover/RuuviDiscover (= 0.0.2) + - RuuviDiscover/RuuviDiscover (0.0.2): - BTKit - RuuviContext - RuuviCore @@ -288,7 +288,7 @@ PODS: - RuuviReactor - RuuviService - RuuviVirtual - - RuuviDiscover/Tests (0.0.1) + - RuuviDiscover/Tests (0.0.2) - RuuviLocal (0.0.3): - RuuviLocal/Contract (= 0.0.3) - RuuviLocal/Contract (0.0.3): @@ -330,17 +330,17 @@ PODS: - RuuviStorage - RuuviVirtual - RuuviMigration/Tests (0.0.1) - - RuuviNotification (0.0.1): - - RuuviNotification/Contract (= 0.0.1) - - RuuviNotification/Contract (0.0.1) - - RuuviNotification/Local (0.0.1): + - RuuviNotification (0.0.2): + - RuuviNotification/Contract (= 0.0.2) + - RuuviNotification/Contract (0.0.2) + - RuuviNotification/Local (0.0.2): - RuuviLocal - RuuviNotification/Contract - RuuviOntology - RuuviService - RuuviStorage - RuuviVirtual - - RuuviNotification/Tests (0.0.1) + - RuuviNotification/Tests (0.0.2) - RuuviNotifier (0.0.1): - RuuviNotifier/Contract (= 0.0.1) - RuuviNotifier/Contract (0.0.1) @@ -356,20 +356,20 @@ PODS: - RuuviBundleUtils - RuuviUser - RuuviOnboard/Tests (0.0.4) - - RuuviOntology (0.0.5): - - RuuviOntology/Contract (= 0.0.5) - - RuuviOntology/Contract (0.0.5): + - RuuviOntology (0.0.6): + - RuuviOntology/Contract (= 0.0.6) + - RuuviOntology/Contract (0.0.6): - Humidity - - RuuviOntology/Mappers (0.0.5): + - RuuviOntology/Mappers (0.0.6): - BTKit - Humidity - RuuviOntology/Contract - - RuuviOntology/Realm (0.0.5): + - RuuviOntology/Realm (0.0.6): - Humidity - Realm - RealmSwift (~> 10.33.0) - RuuviOntology/Contract - - RuuviOntology/SQLite (0.0.5): + - RuuviOntology/SQLite (0.0.6): - GRDB.swift - Humidity - RuuviOntology/Contract @@ -808,7 +808,7 @@ EXTERNAL SOURCES: CHECKOUT OPTIONS: BTKit: - :commit: c1c41194a14d2c2594e7658c7088f8262813707a + :commit: ae35464de55347e4d021659f4cb483d96c9802b2 :git: https://github.com/ruuvi/BTKit.git Humidity: :commit: fb909b332a1f17a6453587dbeaebcfe7c6712a72 @@ -821,7 +821,7 @@ CHECKOUT OPTIONS: :git: https://github.com/rinat-enikeev/RangeSeekSlider SPEC CHECKSUMS: - BTKit: 752fd4c1aaf8f4bc3ea3cd3df55b816bebd59dc0 + BTKit: 3df5d1542352cb23fad21c5cda55f93843515d79 Firebase: 0219acf760880eeec8ce479895bd7767466d9f81 FirebaseABTesting: 76c8297fd026074e0366dc941d265d1be80a56d5 FirebaseAnalytics: f8133442ee6f8512e28ff19e62ce15398bfaeace @@ -856,21 +856,21 @@ SPEC CHECKSUMS: RealmSwift: cef9946f09f2333a8f2ac8bac4f8de52fb9f5ac3 RuuviAnalytics: 1442f702d42ca8a7c4000394a534cd54bf5fd67a RuuviBundleUtils: b143f4bb7bbb9b173527bec8836b3a507b6ca8f2 - RuuviCloud: 12ff06453a24a8a86d25ae83b62f55b46150bc83 + RuuviCloud: 8bbfbfe12801da753dc60100cfdff579e5588361 RuuviContext: 3c3a03e1791189e57d35252cac2448b30cb5c8c4 RuuviCore: c42d46fd24adec33663aa61a7b73430b448a56e4 RuuviDaemon: 029c2bcced7d9fdfc3aa32d452701a7d1306f293 RuuviDFU: f032417ccbb62cbaa35d62918cdb51b420010c78 - RuuviDiscover: 1e6354187fbf85c84a752408577f977375696ab2 + RuuviDiscover: 6b710fc2c5124dba67807aec126c99d1d6e723c3 RuuviLocal: 5d711ed6933bbea2c958bd805186bbde012bc4e3 RuuviLocalization: 04e829bac8113cc8edb49c22a243da88b8383210 RuuviLocation: 39860779e7ea8330ef0c9bbddc0d6088687767d8 RuuviLocationPicker: ec6fb89f5b0a5ff85d00036a2847e0ffac3b9076 RuuviMigration: e2d397eba79436eef6e8b982b1e188a5f6426a2e - RuuviNotification: 0fc88ecc79eca0dfc6bc7252c1cc13822f6c257a + RuuviNotification: 4907402a0962abd2513dc511ade32ee6a05da6a1 RuuviNotifier: 2fdb2579b48c2ff5ad28ae5d651b01831e8b1b90 RuuviOnboard: a17e7b9ded33af7c7f3c622444fcf3d5b8770ad1 - RuuviOntology: c9553c9c5f9cef49aa829434fd576eb557746457 + RuuviOntology: 174c688dc2083e7afe1d98d941c9db0fd6c7a063 RuuviPersistence: 0be00598293866c79a94636247e9365b2a2e3da5 RuuviPool: 663c3b125b2f865b0e5174abcd88c148c4c7ed6b RuuviPresenters: 9e7ab27726604db02b29bf8c8a8289bbc624e303 @@ -886,4 +886,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: ab0aab8c409b0583cdc89f48cf4164faf8b6f712 -COCOAPODS: 1.11.3 +COCOAPODS: 1.12.1 diff --git a/station.localization b/station.localization index b5f9a6342..b960f2fd1 160000 --- a/station.localization +++ b/station.localization @@ -1 +1 @@ -Subproject commit b5f9a63427b946015ad119581f6f7fa27a052c3d +Subproject commit b960f2fd1d1dad5f24e996f4d17dc6ea46daa26c diff --git a/station.xcodeproj/project.pbxproj b/station.xcodeproj/project.pbxproj index a4051a4d2..f873325a5 100644 --- a/station.xcodeproj/project.pbxproj +++ b/station.xcodeproj/project.pbxproj @@ -692,6 +692,9 @@ E116708D29634D91002DF7BF /* BackgroundSelectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E116708B29634D91002DF7BF /* BackgroundSelectionViewModel.swift */; }; E116708F29635B53002DF7BF /* BackgroundSelectionUploadProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E116708E29635B53002DF7BF /* BackgroundSelectionUploadProgressView.swift */; }; E116709029635B53002DF7BF /* BackgroundSelectionUploadProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E116708E29635B53002DF7BF /* BackgroundSelectionUploadProgressView.swift */; }; + E116AED22A059578003EF65A /* ruuvi_speak.caf in Resources */ = {isa = PBXBuildFile; fileRef = E116AED12A059578003EF65A /* ruuvi_speak.caf */; }; + E116AED32A059578003EF65A /* ruuvi_speak.caf in Resources */ = {isa = PBXBuildFile; fileRef = E116AED12A059578003EF65A /* ruuvi_speak.caf */; }; + E116AED42A059578003EF65A /* ruuvi_speak.caf in Resources */ = {isa = PBXBuildFile; fileRef = E116AED12A059578003EF65A /* ruuvi_speak.caf */; }; E11989F529B7A46E002245CF /* UIView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11989F429B7A46E002245CF /* UIView+Extension.swift */; }; E11989F629B7A46E002245CF /* UIView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11989F429B7A46E002245CF /* UIView+Extension.swift */; }; E11989FF29BA60CE002245CF /* AppearanceSettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11989FE29BA60CE002245CF /* AppearanceSettingsTableViewController.swift */; }; @@ -882,6 +885,46 @@ E191F21F2969FF6900F1FEA6 /* TagSettingsSwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E191F21D2969FF6900F1FEA6 /* TagSettingsSwitchCell.swift */; }; E191F221296A00CE00F1FEA6 /* TagSettingsFooterCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E191F220296A00CE00F1FEA6 /* TagSettingsFooterCell.swift */; }; E191F222296A00CE00F1FEA6 /* TagSettingsFooterCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E191F220296A00CE00F1FEA6 /* TagSettingsFooterCell.swift */; }; + E19691502A06DCA400DC360E /* NotificationsSettingsModuleFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E196914F2A06DCA400DC360E /* NotificationsSettingsModuleFactory.swift */; }; + E19691512A06DCA400DC360E /* NotificationsSettingsModuleFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E196914F2A06DCA400DC360E /* NotificationsSettingsModuleFactory.swift */; }; + E19691542A06DCBB00DC360E /* NotificationsSettingsRouterInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19691522A06DCBB00DC360E /* NotificationsSettingsRouterInput.swift */; }; + E19691552A06DCBB00DC360E /* NotificationsSettingsRouterInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19691522A06DCBB00DC360E /* NotificationsSettingsRouterInput.swift */; }; + E19691562A06DCBB00DC360E /* NotificationsSettingsRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19691532A06DCBB00DC360E /* NotificationsSettingsRouter.swift */; }; + E19691572A06DCBB00DC360E /* NotificationsSettingsRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19691532A06DCBB00DC360E /* NotificationsSettingsRouter.swift */; }; + E196915B2A06DCD500DC360E /* NotificationsSettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19691582A06DCD400DC360E /* NotificationsSettingsTableViewController.swift */; }; + E196915C2A06DCD500DC360E /* NotificationsSettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19691582A06DCD400DC360E /* NotificationsSettingsTableViewController.swift */; }; + E196915D2A06DCD500DC360E /* NotificationsSettingsTextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19691592A06DCD500DC360E /* NotificationsSettingsTextCell.swift */; }; + E196915E2A06DCD500DC360E /* NotificationsSettingsTextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19691592A06DCD500DC360E /* NotificationsSettingsTextCell.swift */; }; + E196915F2A06DCD500DC360E /* NotificationsSettingsSwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E196915A2A06DCD500DC360E /* NotificationsSettingsSwitchCell.swift */; }; + E19691602A06DCD500DC360E /* NotificationsSettingsSwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E196915A2A06DCD500DC360E /* NotificationsSettingsSwitchCell.swift */; }; + E19691642A06DCE500DC360E /* NotificationsSettingsViewInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19691612A06DCE500DC360E /* NotificationsSettingsViewInput.swift */; }; + E19691652A06DCE500DC360E /* NotificationsSettingsViewInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19691612A06DCE500DC360E /* NotificationsSettingsViewInput.swift */; }; + E19691662A06DCE500DC360E /* NotificationsSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19691622A06DCE500DC360E /* NotificationsSettingsViewModel.swift */; }; + E19691672A06DCE500DC360E /* NotificationsSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19691622A06DCE500DC360E /* NotificationsSettingsViewModel.swift */; }; + E19691682A06DCE500DC360E /* NotificationsSettingsViewOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19691632A06DCE500DC360E /* NotificationsSettingsViewOutput.swift */; }; + E19691692A06DCE500DC360E /* NotificationsSettingsViewOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19691632A06DCE500DC360E /* NotificationsSettingsViewOutput.swift */; }; + E196916C2A06DCEF00DC360E /* NotificationsSettingsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E196916A2A06DCEF00DC360E /* NotificationsSettingsPresenter.swift */; }; + E196916D2A06DCEF00DC360E /* NotificationsSettingsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E196916A2A06DCEF00DC360E /* NotificationsSettingsPresenter.swift */; }; + E196916E2A06DCEF00DC360E /* NotificationsSettingsModuleInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E196916B2A06DCEF00DC360E /* NotificationsSettingsModuleInput.swift */; }; + E196916F2A06DCEF00DC360E /* NotificationsSettingsModuleInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E196916B2A06DCEF00DC360E /* NotificationsSettingsModuleInput.swift */; }; + E196917C2A06E03300DC360E /* PushAlertSoundSelectionModuleFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19691722A06E03300DC360E /* PushAlertSoundSelectionModuleFactory.swift */; }; + E196917D2A06E03300DC360E /* PushAlertSoundSelectionModuleFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19691722A06E03300DC360E /* PushAlertSoundSelectionModuleFactory.swift */; }; + E196917E2A06E03300DC360E /* PushAlertSoundSelectionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19691742A06E03300DC360E /* PushAlertSoundSelectionPresenter.swift */; }; + E196917F2A06E03300DC360E /* PushAlertSoundSelectionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19691742A06E03300DC360E /* PushAlertSoundSelectionPresenter.swift */; }; + E19691802A06E03300DC360E /* PushAlertSoundSelectionModuleInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19691752A06E03300DC360E /* PushAlertSoundSelectionModuleInput.swift */; }; + E19691812A06E03300DC360E /* PushAlertSoundSelectionModuleInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19691752A06E03300DC360E /* PushAlertSoundSelectionModuleInput.swift */; }; + E19691822A06E03300DC360E /* PushAlertSoundSelectionTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19691782A06E03300DC360E /* PushAlertSoundSelectionTableViewController.swift */; }; + E19691832A06E03300DC360E /* PushAlertSoundSelectionTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19691782A06E03300DC360E /* PushAlertSoundSelectionTableViewController.swift */; }; + E19691842A06E03300DC360E /* PushAlertSoundSelectionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19691792A06E03300DC360E /* PushAlertSoundSelectionTableViewCell.swift */; }; + E19691852A06E03300DC360E /* PushAlertSoundSelectionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19691792A06E03300DC360E /* PushAlertSoundSelectionTableViewCell.swift */; }; + E19691862A06E03300DC360E /* PushAlertSoundSelectionViewInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E196917A2A06E03300DC360E /* PushAlertSoundSelectionViewInput.swift */; }; + E19691872A06E03300DC360E /* PushAlertSoundSelectionViewInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E196917A2A06E03300DC360E /* PushAlertSoundSelectionViewInput.swift */; }; + E19691882A06E03300DC360E /* PushAlertSoundSelectionViewOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E196917B2A06E03300DC360E /* PushAlertSoundSelectionViewOutput.swift */; }; + E19691892A06E03300DC360E /* PushAlertSoundSelectionViewOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E196917B2A06E03300DC360E /* PushAlertSoundSelectionViewOutput.swift */; }; + E196918B2A06E15F00DC360E /* PushAlertSoundSelectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E196918A2A06E15F00DC360E /* PushAlertSoundSelectionViewModel.swift */; }; + E196918C2A06E15F00DC360E /* PushAlertSoundSelectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E196918A2A06E15F00DC360E /* PushAlertSoundSelectionViewModel.swift */; }; + E196918E2A06E2E100DC360E /* RuuviAlertSound+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E196918D2A06E2E100DC360E /* RuuviAlertSound+Extension.swift */; }; + E196918F2A06E2E100DC360E /* RuuviAlertSound+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E196918D2A06E2E100DC360E /* RuuviAlertSound+Extension.swift */; }; E1972BE129587615000E2AEC /* CardsLargeImageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1972BE029587615000E2AEC /* CardsLargeImageCell.swift */; }; E1972BE229587615000E2AEC /* CardsLargeImageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1972BE029587615000E2AEC /* CardsLargeImageCell.swift */; }; E19EAF4C299679D8005827E4 /* pnservice.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = E17C469929956732008CFDD7 /* pnservice.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -923,6 +966,24 @@ E19EAFAE299E82D1005827E4 /* SignInPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19EAFAC299E82D1005827E4 /* SignInPresenter.swift */; }; E19EAFB0299EB46D005827E4 /* NoSensorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19EAFAF299EB46D005827E4 /* NoSensorView.swift */; }; E19EAFB1299EB46D005827E4 /* NoSensorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19EAFAF299EB46D005827E4 /* NoSensorView.swift */; }; + E1AB90552A0BFECE00543F61 /* RuuviLinkTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AB90542A0BFECE00543F61 /* RuuviLinkTextView.swift */; }; + E1AB90562A0BFECE00543F61 /* RuuviLinkTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AB90542A0BFECE00543F61 /* RuuviLinkTextView.swift */; }; + E1AB905E2A0EB0D000543F61 /* SensorForceClaimModuleFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AB905D2A0EB0D000543F61 /* SensorForceClaimModuleFactory.swift */; }; + E1AB905F2A0EB0D000543F61 /* SensorForceClaimModuleFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AB905D2A0EB0D000543F61 /* SensorForceClaimModuleFactory.swift */; }; + E1AB90612A0EB11800543F61 /* SensorForceClaimPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AB90602A0EB11800543F61 /* SensorForceClaimPresenter.swift */; }; + E1AB90622A0EB11800543F61 /* SensorForceClaimPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AB90602A0EB11800543F61 /* SensorForceClaimPresenter.swift */; }; + E1AB90642A0EB13400543F61 /* SensorForceClaimModuleInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AB90632A0EB13400543F61 /* SensorForceClaimModuleInput.swift */; }; + E1AB90652A0EB13400543F61 /* SensorForceClaimModuleInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AB90632A0EB13400543F61 /* SensorForceClaimModuleInput.swift */; }; + E1AB90672A0EB1AD00543F61 /* SensorForceClaimRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AB90662A0EB1AD00543F61 /* SensorForceClaimRouter.swift */; }; + E1AB90682A0EB1AD00543F61 /* SensorForceClaimRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AB90662A0EB1AD00543F61 /* SensorForceClaimRouter.swift */; }; + E1AB906A2A0EB1BC00543F61 /* SensorForceClaimRouterInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AB90692A0EB1BC00543F61 /* SensorForceClaimRouterInput.swift */; }; + E1AB906B2A0EB1BC00543F61 /* SensorForceClaimRouterInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AB90692A0EB1BC00543F61 /* SensorForceClaimRouterInput.swift */; }; + E1AB90732A0EB24600543F61 /* SensorForceClaimViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AB90722A0EB24600543F61 /* SensorForceClaimViewController.swift */; }; + E1AB90742A0EB24600543F61 /* SensorForceClaimViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AB90722A0EB24600543F61 /* SensorForceClaimViewController.swift */; }; + E1AB907B2A0ECB6D00543F61 /* SensorForceClaimViewOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AB907A2A0ECB6D00543F61 /* SensorForceClaimViewOutput.swift */; }; + E1AB907C2A0ECB6D00543F61 /* SensorForceClaimViewOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AB907A2A0ECB6D00543F61 /* SensorForceClaimViewOutput.swift */; }; + E1AB907E2A0ECB7600543F61 /* SensorForceClaimViewInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AB907D2A0ECB7600543F61 /* SensorForceClaimViewInput.swift */; }; + E1AB907F2A0ECB7600543F61 /* SensorForceClaimViewInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AB907D2A0ECB7600543F61 /* SensorForceClaimViewInput.swift */; }; E1B20C872926CDE10023D739 /* UITextField+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B20C862926CDE10023D739 /* UITextField+Extension.swift */; }; E1B20C882926CDE10023D739 /* UITextField+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B20C862926CDE10023D739 /* UITextField+Extension.swift */; }; E1B20C8A2926D2FC0023D739 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B20C892926D2FC0023D739 /* Double+Extension.swift */; }; @@ -993,6 +1054,8 @@ E1CE5E4029F994DE00391109 /* AppGroupConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CE5E3E29F994DE00391109 /* AppGroupConstants.swift */; }; E1CE5E4229FC39D000391109 /* DefaultsModuleOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CE5E4129FC39D000391109 /* DefaultsModuleOutput.swift */; }; E1CE5E4329FC39D000391109 /* DefaultsModuleOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CE5E4129FC39D000391109 /* DefaultsModuleOutput.swift */; }; + E1CE5E472A016D4000391109 /* RuuviCustomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CE5E462A016D4000391109 /* RuuviCustomButton.swift */; }; + E1CE5E482A016D4000391109 /* RuuviCustomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CE5E462A016D4000391109 /* RuuviCustomButton.swift */; }; E1D0238B29EB02C600EC0FFD /* RuuviUISwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D0238A29EB02C600EC0FFD /* RuuviUISwitch.swift */; }; E1D0238C29EB02C600EC0FFD /* RuuviUISwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D0238A29EB02C600EC0FFD /* RuuviUISwitch.swift */; }; E1D0238E29EB3F7D00EC0FFD /* YAxisValueFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D0238D29EB3F7D00EC0FFD /* YAxisValueFormatter.swift */; }; @@ -1512,6 +1575,7 @@ E116708729634815002DF7BF /* BackgroundSelectionModuleFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundSelectionModuleFactory.swift; sourceTree = ""; }; E116708B29634D91002DF7BF /* BackgroundSelectionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundSelectionViewModel.swift; sourceTree = ""; }; E116708E29635B53002DF7BF /* BackgroundSelectionUploadProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundSelectionUploadProgressView.swift; sourceTree = ""; }; + E116AED12A059578003EF65A /* ruuvi_speak.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = ruuvi_speak.caf; sourceTree = ""; }; E11989F429B7A46E002245CF /* UIView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Extension.swift"; sourceTree = ""; }; E11989FE29BA60CE002245CF /* AppearanceSettingsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceSettingsTableViewController.swift; sourceTree = ""; }; E1198A0129BA6193002245CF /* AppearanceSettingsTableViewBasicCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceSettingsTableViewBasicCell.swift; sourceTree = ""; }; @@ -1610,6 +1674,26 @@ E191F21A2969EF7B00F1FEA6 /* TagSettingsModuleFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagSettingsModuleFactory.swift; sourceTree = ""; }; E191F21D2969FF6900F1FEA6 /* TagSettingsSwitchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagSettingsSwitchCell.swift; sourceTree = ""; }; E191F220296A00CE00F1FEA6 /* TagSettingsFooterCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagSettingsFooterCell.swift; sourceTree = ""; }; + E196914F2A06DCA400DC360E /* NotificationsSettingsModuleFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationsSettingsModuleFactory.swift; sourceTree = ""; }; + E19691522A06DCBB00DC360E /* NotificationsSettingsRouterInput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationsSettingsRouterInput.swift; sourceTree = ""; }; + E19691532A06DCBB00DC360E /* NotificationsSettingsRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationsSettingsRouter.swift; sourceTree = ""; }; + E19691582A06DCD400DC360E /* NotificationsSettingsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationsSettingsTableViewController.swift; sourceTree = ""; }; + E19691592A06DCD500DC360E /* NotificationsSettingsTextCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationsSettingsTextCell.swift; sourceTree = ""; }; + E196915A2A06DCD500DC360E /* NotificationsSettingsSwitchCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationsSettingsSwitchCell.swift; sourceTree = ""; }; + E19691612A06DCE500DC360E /* NotificationsSettingsViewInput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationsSettingsViewInput.swift; sourceTree = ""; }; + E19691622A06DCE500DC360E /* NotificationsSettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationsSettingsViewModel.swift; sourceTree = ""; }; + E19691632A06DCE500DC360E /* NotificationsSettingsViewOutput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationsSettingsViewOutput.swift; sourceTree = ""; }; + E196916A2A06DCEF00DC360E /* NotificationsSettingsPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationsSettingsPresenter.swift; sourceTree = ""; }; + E196916B2A06DCEF00DC360E /* NotificationsSettingsModuleInput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationsSettingsModuleInput.swift; sourceTree = ""; }; + E19691722A06E03300DC360E /* PushAlertSoundSelectionModuleFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushAlertSoundSelectionModuleFactory.swift; sourceTree = ""; }; + E19691742A06E03300DC360E /* PushAlertSoundSelectionPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushAlertSoundSelectionPresenter.swift; sourceTree = ""; }; + E19691752A06E03300DC360E /* PushAlertSoundSelectionModuleInput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushAlertSoundSelectionModuleInput.swift; sourceTree = ""; }; + E19691782A06E03300DC360E /* PushAlertSoundSelectionTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushAlertSoundSelectionTableViewController.swift; sourceTree = ""; }; + E19691792A06E03300DC360E /* PushAlertSoundSelectionTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushAlertSoundSelectionTableViewCell.swift; sourceTree = ""; }; + E196917A2A06E03300DC360E /* PushAlertSoundSelectionViewInput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushAlertSoundSelectionViewInput.swift; sourceTree = ""; }; + E196917B2A06E03300DC360E /* PushAlertSoundSelectionViewOutput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushAlertSoundSelectionViewOutput.swift; sourceTree = ""; }; + E196918A2A06E15F00DC360E /* PushAlertSoundSelectionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushAlertSoundSelectionViewModel.swift; sourceTree = ""; }; + E196918D2A06E2E100DC360E /* RuuviAlertSound+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RuuviAlertSound+Extension.swift"; sourceTree = ""; }; E1972BE029587615000E2AEC /* CardsLargeImageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardsLargeImageCell.swift; sourceTree = ""; }; E19EAF502996BAAF005827E4 /* pnservice.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = pnservice.entitlements; sourceTree = ""; }; E19EAF512996C828005827E4 /* Montserrat-ExtraBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Montserrat-ExtraBold.ttf"; sourceTree = ""; }; @@ -1634,6 +1718,15 @@ E19EAFA9299E62C0005827E4 /* SignInBenefitsViewOutput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInBenefitsViewOutput.swift; sourceTree = ""; }; E19EAFAC299E82D1005827E4 /* SignInPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInPresenter.swift; sourceTree = ""; }; E19EAFAF299EB46D005827E4 /* NoSensorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoSensorView.swift; sourceTree = ""; }; + E1AB90542A0BFECE00543F61 /* RuuviLinkTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuuviLinkTextView.swift; sourceTree = ""; }; + E1AB905D2A0EB0D000543F61 /* SensorForceClaimModuleFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorForceClaimModuleFactory.swift; sourceTree = ""; }; + E1AB90602A0EB11800543F61 /* SensorForceClaimPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorForceClaimPresenter.swift; sourceTree = ""; }; + E1AB90632A0EB13400543F61 /* SensorForceClaimModuleInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorForceClaimModuleInput.swift; sourceTree = ""; }; + E1AB90662A0EB1AD00543F61 /* SensorForceClaimRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorForceClaimRouter.swift; sourceTree = ""; }; + E1AB90692A0EB1BC00543F61 /* SensorForceClaimRouterInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorForceClaimRouterInput.swift; sourceTree = ""; }; + E1AB90722A0EB24600543F61 /* SensorForceClaimViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorForceClaimViewController.swift; sourceTree = ""; }; + E1AB907A2A0ECB6D00543F61 /* SensorForceClaimViewOutput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorForceClaimViewOutput.swift; sourceTree = ""; }; + E1AB907D2A0ECB7600543F61 /* SensorForceClaimViewInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorForceClaimViewInput.swift; sourceTree = ""; }; E1B20C862926CDE10023D739 /* UITextField+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextField+Extension.swift"; sourceTree = ""; }; E1B20C892926D2FC0023D739 /* Double+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = ""; }; E1B57FEA29859CB800B441FB /* DevicesModuleFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesModuleFactory.swift; sourceTree = ""; }; @@ -1669,6 +1762,7 @@ E1CE4C762959DF01005C023F /* DashboardIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardIndicatorView.swift; sourceTree = ""; }; E1CE5E3E29F994DE00391109 /* AppGroupConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroupConstants.swift; sourceTree = ""; }; E1CE5E4129FC39D000391109 /* DefaultsModuleOutput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsModuleOutput.swift; sourceTree = ""; }; + E1CE5E462A016D4000391109 /* RuuviCustomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuuviCustomButton.swift; sourceTree = ""; }; E1D0238A29EB02C600EC0FFD /* RuuviUISwitch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuuviUISwitch.swift; sourceTree = ""; }; E1D0238D29EB3F7D00EC0FFD /* YAxisValueFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YAxisValueFormatter.swift; sourceTree = ""; }; E1E102ED28F348B700815508 /* TagChartsHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagChartsHelper.swift; sourceTree = ""; }; @@ -1942,6 +2036,7 @@ 0E09671722AE762900E85F48 /* Resources */ = { isa = PBXGroup; children = ( + E116AED02A05955B003EF65A /* Sounds */, E1597A5A295E24BB00DFB70B /* Assets */, 0EB8ED412692004300C6B0FA /* Colors */, 0EB48DE4261A17F4008E0D2D /* JSONs */, @@ -2064,6 +2159,7 @@ A9A48A5B244CD9E70004FD50 /* UIWindow+Shake.swift */, A91D031925113EAA00694733 /* UnitPressure+Extension.swift */, E1198A2029BA6EC6002245CF /* RuuviTheme+Extension.swift */, + E196918D2A06E2E100DC360E /* RuuviAlertSound+Extension.swift */, 0E0A381823616AC3003A0364 /* UserDefaults+Optional.swift */, 3490A4C227D9F2C70032BBAB /* UINavigationController.swift */, ); @@ -2676,6 +2772,7 @@ 0EBAF063232001080025A191 /* Submodules */ = { isa = PBXGroup; children = ( + E196914A2A059C7200DC360E /* Notifications */, E11989F829BA605F002245CF /* Appearance */, E1B57FE629859C7000B441FB /* Devices */, A9828E54247BAC0700E7E9D4 /* Chart */, @@ -3174,6 +3271,7 @@ 66BC44812657AED400A03253 /* Submodules */ = { isa = PBXGroup; children = ( + E1AB90572A0EAF4F00543F61 /* Force Claim */, 340BE37727B54F37006D6C34 /* Owner */, 0E97D782268C826400FE9D5B /* DFU */, 66BC44822657AED400A03253 /* OffsetCorrection */, @@ -3723,6 +3821,14 @@ path = UI; sourceTree = ""; }; + E116AED02A05955B003EF65A /* Sounds */ = { + isa = PBXGroup; + children = ( + E116AED12A059578003EF65A /* ruuvi_speak.caf */, + ); + path = Sounds; + sourceTree = ""; + }; E11989F829BA605F002245CF /* Appearance */ = { isa = PBXGroup; children = ( @@ -3832,6 +3938,8 @@ children = ( E1395BF0294F793000C403C6 /* AppDateFormatter.swift */, E1D0238A29EB02C600EC0FFD /* RuuviUISwitch.swift */, + E1CE5E462A016D4000391109 /* RuuviCustomButton.swift */, + E1AB90542A0BFECE00543F61 /* RuuviLinkTextView.swift */, ); path = Classess; sourceTree = ""; @@ -4056,6 +4164,102 @@ path = Presenter; sourceTree = ""; }; + E196914A2A059C7200DC360E /* Notifications */ = { + isa = PBXGroup; + children = ( + E196914E2A059CAD00DC360E /* Assembly */, + E196914D2A059C8D00DC360E /* Router */, + E196914C2A059C8800DC360E /* View */, + E196914B2A059C8000DC360E /* Presenter */, + E19691702A06E03300DC360E /* Selection */, + ); + path = Notifications; + sourceTree = ""; + }; + E196914B2A059C8000DC360E /* Presenter */ = { + isa = PBXGroup; + children = ( + E196916B2A06DCEF00DC360E /* NotificationsSettingsModuleInput.swift */, + E196916A2A06DCEF00DC360E /* NotificationsSettingsPresenter.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + E196914C2A059C8800DC360E /* View */ = { + isa = PBXGroup; + children = ( + E19691612A06DCE500DC360E /* NotificationsSettingsViewInput.swift */, + E19691622A06DCE500DC360E /* NotificationsSettingsViewModel.swift */, + E19691632A06DCE500DC360E /* NotificationsSettingsViewOutput.swift */, + E1BCF0B52A06390A005729F4 /* UI */, + ); + path = View; + sourceTree = ""; + }; + E196914D2A059C8D00DC360E /* Router */ = { + isa = PBXGroup; + children = ( + E19691532A06DCBB00DC360E /* NotificationsSettingsRouter.swift */, + E19691522A06DCBB00DC360E /* NotificationsSettingsRouterInput.swift */, + ); + path = Router; + sourceTree = ""; + }; + E196914E2A059CAD00DC360E /* Assembly */ = { + isa = PBXGroup; + children = ( + E196914F2A06DCA400DC360E /* NotificationsSettingsModuleFactory.swift */, + ); + path = Assembly; + sourceTree = ""; + }; + E19691702A06E03300DC360E /* Selection */ = { + isa = PBXGroup; + children = ( + E19691712A06E03300DC360E /* Assembly */, + E19691732A06E03300DC360E /* Presenter */, + E19691762A06E03300DC360E /* View */, + ); + path = Selection; + sourceTree = ""; + }; + E19691712A06E03300DC360E /* Assembly */ = { + isa = PBXGroup; + children = ( + E19691722A06E03300DC360E /* PushAlertSoundSelectionModuleFactory.swift */, + ); + path = Assembly; + sourceTree = ""; + }; + E19691732A06E03300DC360E /* Presenter */ = { + isa = PBXGroup; + children = ( + E19691742A06E03300DC360E /* PushAlertSoundSelectionPresenter.swift */, + E19691752A06E03300DC360E /* PushAlertSoundSelectionModuleInput.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + E19691762A06E03300DC360E /* View */ = { + isa = PBXGroup; + children = ( + E19691772A06E03300DC360E /* UI */, + E196917A2A06E03300DC360E /* PushAlertSoundSelectionViewInput.swift */, + E196917B2A06E03300DC360E /* PushAlertSoundSelectionViewOutput.swift */, + E196918A2A06E15F00DC360E /* PushAlertSoundSelectionViewModel.swift */, + ); + path = View; + sourceTree = ""; + }; + E19691772A06E03300DC360E /* UI */ = { + isa = PBXGroup; + children = ( + E19691782A06E03300DC360E /* PushAlertSoundSelectionTableViewController.swift */, + E19691792A06E03300DC360E /* PushAlertSoundSelectionTableViewCell.swift */, + ); + path = UI; + sourceTree = ""; + }; E19EAF81299C1310005827E4 /* UI */ = { isa = PBXGroup; children = ( @@ -4121,6 +4325,61 @@ path = Presenter; sourceTree = ""; }; + E1AB90572A0EAF4F00543F61 /* Force Claim */ = { + isa = PBXGroup; + children = ( + E1AB905C2A0EAFA600543F61 /* Router */, + E1AB905A2A0EAF9400543F61 /* View */, + E1AB90592A0EAF8900543F61 /* Presenter */, + E1AB90582A0EAF8200543F61 /* Assembly */, + ); + path = "Force Claim"; + sourceTree = ""; + }; + E1AB90582A0EAF8200543F61 /* Assembly */ = { + isa = PBXGroup; + children = ( + E1AB905D2A0EB0D000543F61 /* SensorForceClaimModuleFactory.swift */, + ); + path = Assembly; + sourceTree = ""; + }; + E1AB90592A0EAF8900543F61 /* Presenter */ = { + isa = PBXGroup; + children = ( + E1AB90602A0EB11800543F61 /* SensorForceClaimPresenter.swift */, + E1AB90632A0EB13400543F61 /* SensorForceClaimModuleInput.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + E1AB905A2A0EAF9400543F61 /* View */ = { + isa = PBXGroup; + children = ( + E1AB90792A0EB26900543F61 /* UI */, + E1AB907A2A0ECB6D00543F61 /* SensorForceClaimViewOutput.swift */, + E1AB907D2A0ECB7600543F61 /* SensorForceClaimViewInput.swift */, + ); + path = View; + sourceTree = ""; + }; + E1AB905C2A0EAFA600543F61 /* Router */ = { + isa = PBXGroup; + children = ( + E1AB90662A0EB1AD00543F61 /* SensorForceClaimRouter.swift */, + E1AB90692A0EB1BC00543F61 /* SensorForceClaimRouterInput.swift */, + ); + path = Router; + sourceTree = ""; + }; + E1AB90792A0EB26900543F61 /* UI */ = { + isa = PBXGroup; + children = ( + E1AB90722A0EB24600543F61 /* SensorForceClaimViewController.swift */, + ); + path = UI; + sourceTree = ""; + }; E1B57FE629859C7000B441FB /* Devices */ = { isa = PBXGroup; children = ( @@ -4179,6 +4438,16 @@ path = Interactor; sourceTree = ""; }; + E1BCF0B52A06390A005729F4 /* UI */ = { + isa = PBXGroup; + children = ( + E196915A2A06DCD500DC360E /* NotificationsSettingsSwitchCell.swift */, + E19691582A06DCD400DC360E /* NotificationsSettingsTableViewController.swift */, + E19691592A06DCD500DC360E /* NotificationsSettingsTextCell.swift */, + ); + path = UI; + sourceTree = ""; + }; E1CA28AC29201EF0009E4423 /* RUAlertExpandButton */ = { isa = PBXGroup; children = ( @@ -4568,6 +4837,7 @@ E19EAF5C2996CF38005827E4 /* Muli-SemiBoldItalic.ttf in Resources */, 0E8BD3EB238566AB008B31EF /* Localizable.strings in Resources */, E1CD77E828781A0E00F1F0EB /* UnitSettings.storyboard in Resources */, + E116AED32A059578003EF65A /* ruuvi_speak.caf in Resources */, 0E8BD3EF238566AB008B31EF /* Muli-Regular.ttf in Resources */, E19EAF542996C828005827E4 /* Montserrat-ExtraBold.ttf in Resources */, 0E8BD3F0238566AB008B31EF /* Oswald-ExtraLight.ttf in Resources */, @@ -4632,6 +4902,7 @@ E19EAF5B2996CF38005827E4 /* Muli-SemiBoldItalic.ttf in Resources */, 0EEB20C722B7A7200015F9E0 /* Localizable.strings in Resources */, E1CD77E728781A0E00F1F0EB /* UnitSettings.storyboard in Resources */, + E116AED22A059578003EF65A /* ruuvi_speak.caf in Resources */, 64678192225D03330072856A /* Muli-Regular.ttf in Resources */, E19EAF532996C828005827E4 /* Montserrat-ExtraBold.ttf in Resources */, 643EEC2B2266435100D4E837 /* Oswald-ExtraLight.ttf in Resources */, @@ -4672,6 +4943,7 @@ buildActionMask = 2147483647; files = ( E17C46A429957763008CFDD7 /* Localizable.strings in Resources */, + E116AED42A059578003EF65A /* ruuvi_speak.caf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4963,6 +5235,8 @@ 0EE36E4326957E200021B746 /* LatestRelease.swift in Sources */, 0E8BD2B3238566AB008B31EF /* Optional.swift in Sources */, 0EA7968A2664B8CB002BA25D /* RuuviServiceError+LocalizedError.swift in Sources */, + E1AB90742A0EB24600543F61 /* SensorForceClaimViewController.swift in Sources */, + E1AB905F2A0EB0D000543F61 /* SensorForceClaimModuleFactory.swift in Sources */, 0E8BD2B8238566AB008B31EF /* CALayer+IB.swift in Sources */, 0E8BD2BA238566AB008B31EF /* MenuTablePresentationController.swift in Sources */, E1972BE229587615000E2AEC /* CardsLargeImageCell.swift in Sources */, @@ -4975,9 +5249,11 @@ E1597A44295CD58600DFB70B /* DashboardRouterDelegate.swift in Sources */, E10E18B8297D67E8002C78C3 /* RuuviCloudViewInput.swift in Sources */, E16B8F4028D10CF50025B92D /* MyRuuviAccountViewController.swift in Sources */, + E1CE5E482A016D4000391109 /* RuuviCustomButton.swift in Sources */, 0E8BD2BF238566AB008B31EF /* MenuPresenter.swift in Sources */, E16B8F4328D10DF90025B92D /* MyRuuviAccountViewInput.swift in Sources */, 340BE39327B54F37006D6C34 /* OwnerViewController.swift in Sources */, + E1AB906B2A0EB1BC00543F61 /* SensorForceClaimRouterInput.swift in Sources */, E1395BF2294F793000C403C6 /* AppDateFormatter.swift in Sources */, E1B57FF829859CEC00B441FB /* DevicesTableViewCell.swift in Sources */, 0E8BD2C1238566AB008B31EF /* MenuTableViewController.swift in Sources */, @@ -5003,12 +5279,14 @@ 0E8BD2CE238566AB008B31EF /* Language+Localization.swift in Sources */, A91D02F82511207300694733 /* SelectionModuleInput.swift in Sources */, 0E8BD2D0238566AB008B31EF /* WebTagSettingsRouter.swift in Sources */, + E19691512A06DCA400DC360E /* NotificationsSettingsModuleFactory.swift in Sources */, E10E18AF297D6672002C78C3 /* RuuviCloudPresenter.swift in Sources */, E19EAF76299AE3B0005827E4 /* SignInView.swift in Sources */, A92A66BE2450C64A002918E7 /* UITableViewCell+ReusableView.swift in Sources */, 0E8BD2D1238566AB008B31EF /* DefaultsPresenter.swift in Sources */, E1198A3029BA76A8002245CF /* ASSelectionTableViewCell.swift in Sources */, 0E8BD2D4238566AB008B31EF /* SettingsPresenter.swift in Sources */, + E1AB90652A0EB13400543F61 /* SensorForceClaimModuleInput.swift in Sources */, A91D02F52511207300694733 /* SelectionPresenter.swift in Sources */, E116708629634708002DF7BF /* BackgroundSelectionPresenter.swift in Sources */, 0E197C6C23C4A52A0074015B /* MailComposerPresenterMessageUI.swift in Sources */, @@ -5029,6 +5307,7 @@ 0E62299626AAA0570041DCDD /* DiscoverRouter.swift in Sources */, 0E8BD2DD238566AB008B31EF /* Localizable.swift in Sources */, 0E97D7A9268C922C00FE9D5B /* DFUInteractor.swift in Sources */, + E19691572A06DCBB00DC360E /* NotificationsSettingsRouter.swift in Sources */, 0EE36E4026957E010021B746 /* DFUInteractorInput.swift in Sources */, 0E8BD2DE238566AB008B31EF /* LocationPickerAppleInitializer.swift in Sources */, E19EAF79299AE5EA005827E4 /* SignInVerifyView.swift in Sources */, @@ -5038,6 +5317,7 @@ E1198A0029BA60CE002245CF /* AppearanceSettingsTableViewController.swift in Sources */, 340BE39727B54F38006D6C34 /* OwnerRouter.swift in Sources */, 66BC44B12657AED400A03253 /* OffsetCorrectionViewOutput.swift in Sources */, + E19691672A06DCE500DC360E /* NotificationsSettingsViewModel.swift in Sources */, E1198A3C29BA76F1002245CF /* ASSelectionModuleInput.swift in Sources */, E19EAF6A29997711005827E4 /* SignInViewController.swift in Sources */, 0E8BD2E0238566AB008B31EF /* DefaultsList.swift in Sources */, @@ -5065,7 +5345,10 @@ E1CD7810287830BF00F1F0EB /* UnitSettingsRouterInput.swift in Sources */, E1B57FFF29859D1800B441FB /* DevicesViewInput.swift in Sources */, 0EB48D962619D5E5008E0D2D /* FeatureToggleProvider.swift in Sources */, + E196915C2A06DCD500DC360E /* NotificationsSettingsTableViewController.swift in Sources */, + E19691812A06E03300DC360E /* PushAlertSoundSelectionModuleInput.swift in Sources */, 0EA7967A2664B37F002BA25D /* RuuviLocalError+LocalizedError.swift in Sources */, + E19691832A06E03300DC360E /* PushAlertSoundSelectionTableViewController.swift in Sources */, A91D0317251124F900694733 /* SelectionItem.swift in Sources */, 0E8BD2F0238566AB008B31EF /* SettingsRouter.swift in Sources */, 340BE37627B54EE2006D6C34 /* AppAssemblyConstants.swift in Sources */, @@ -5120,6 +5403,7 @@ 0EB48DDD2619E306008E0D2D /* FallbackFeatureToggleProvider.swift in Sources */, A9646482247BAE6B0001D55D /* ChartModuleInput.swift in Sources */, 0E8BD320238566AB008B31EF /* DefaultsInitializer.swift in Sources */, + E19691652A06DCE500DC360E /* NotificationsSettingsViewInput.swift in Sources */, E16B8F5228D113440025B92D /* MyRuuviAccountConfigurator.swift in Sources */, E116708D29634D91002DF7BF /* BackgroundSelectionViewModel.swift in Sources */, E18D04C528E9DF77008EF5EC /* TagChartsView.swift in Sources */, @@ -5144,9 +5428,11 @@ E18D04BB28E8B34F008EF5EC /* TagChartsViewInteractor.swift in Sources */, E19EAFA8299E62A7005827E4 /* SignInBenefitsViewInput.swift in Sources */, 0EB8ED3D268F8CD900C6B0FA /* FirmwareRepository.swift in Sources */, + E19691872A06E03300DC360E /* PushAlertSoundSelectionViewInput.swift in Sources */, E168B4AF2886AE4E00D6B5C6 /* MeasurementAccuracyType+Extension.swift in Sources */, E1597A47295CD5E400DFB70B /* DashboardModuleInput.swift in Sources */, 0E8BD330238566AB008B31EF /* AboutInitializer.swift in Sources */, + E19691692A06DCE500DC360E /* NotificationsSettingsViewOutput.swift in Sources */, 0E8BD332238566AB008B31EF /* MenuTableEmbededViewController.swift in Sources */, A907BB1224AE620D009DA3DB /* UIWindow+Orientation.swift in Sources */, E1198A1929BA6A61002245CF /* AppearanceSettingsViewInput.swift in Sources */, @@ -5156,6 +5442,7 @@ 0EAD33DD2399273D00EC5BAA /* HeartbeatRouter.swift in Sources */, 0E2B339C26A2BC3F00366B01 /* AppRouter.swift in Sources */, 660EB29D266928E6000FD22B /* UIViewController+Alert.swift in Sources */, + E196917D2A06E03300DC360E /* PushAlertSoundSelectionModuleFactory.swift in Sources */, E1D0238F29EB3F7D00EC0FFD /* YAxisValueFormatter.swift in Sources */, 0E8BD335238566AB008B31EF /* Date+Ruuvi.swift in Sources */, 0EA796862664B84D002BA25D /* RuuviReactorError+LocalizedError.swift in Sources */, @@ -5168,6 +5455,7 @@ A91D02FB2511207300694733 /* SelectionViewInput.swift in Sources */, 0E8BD343238566AB008B31EF /* SettingsTableViewController.swift in Sources */, 3414BFCC2806D1B300C63BE9 /* RuuviCodeTextField.swift in Sources */, + E1AB90562A0BFECE00543F61 /* RuuviLinkTextView.swift in Sources */, 0EAD33DC2399273D00EC5BAA /* HeartbeatRouterInput.swift in Sources */, E1CD780D287830B100F1F0EB /* UnitSettingsRouter.swift in Sources */, E19EAFB1299EB46D005827E4 /* NoSensorView.swift in Sources */, @@ -5188,6 +5476,7 @@ E1198A0329BA6193002245CF /* AppearanceSettingsTableViewBasicCell.swift in Sources */, 0E8BD34C238566AB008B31EF /* DefaultsRouter.swift in Sources */, 340BE38927B54F37006D6C34 /* OwnerInitializer.swift in Sources */, + E19691892A06E03300DC360E /* PushAlertSoundSelectionViewOutput.swift in Sources */, 0EB8ED30268F12FA00C6B0FA /* DFUModuleFactory.swift in Sources */, A91D03042511207300694733 /* SelectionViewOutput.swift in Sources */, E18D04D528F1D52A008EF5EC /* TagChartsViewOutput.swift in Sources */, @@ -5220,7 +5509,9 @@ E1395C072958A33400C403C6 /* CardsBackgroundView.swift in Sources */, E191F1FF2969D6C900F1FEA6 /* TagSettingsPlainCell.swift in Sources */, E1597A60295F803400DFB70B /* UIColor+Extension.swift in Sources */, + E19691552A06DCBB00DC360E /* NotificationsSettingsRouterInput.swift in Sources */, 0E8BD35D238566AB008B31EF /* AboutModuleInput.swift in Sources */, + E1AB907F2A0ECB7600543F61 /* SensorForceClaimViewInput.swift in Sources */, 0EAD33DA2399273D00EC5BAA /* HeartbeatModuleInput.swift in Sources */, 0EB8ED1B268EF36200C6B0FA /* DFUViewModel.swift in Sources */, 340BE37327B54E26006D6C34 /* PresentationConstants.swift in Sources */, @@ -5238,6 +5529,7 @@ 0E8BD368238566AB008B31EF /* Double+Temperature.swift in Sources */, E1395BFD2954DAAD00C403C6 /* CardsInteractor.swift in Sources */, 0EB8ED3A268F6A6900C6B0FA /* ProgressBar.swift in Sources */, + E196916D2A06DCEF00DC360E /* NotificationsSettingsPresenter.swift in Sources */, E191F21C2969EF7B00F1FEA6 /* TagSettingsModuleFactory.swift in Sources */, 0E8BD36B238566AB008B31EF /* AppStateServiceImpl.swift in Sources */, E1CD78022878302100F1F0EB /* UnitSettingsViewInput.swift in Sources */, @@ -5251,7 +5543,9 @@ 0E8BD372238566AB008B31EF /* SwipeDownToDismissTransitioningDelegate.swift in Sources */, E19EAF95299E6038005827E4 /* SignInBenefitsModuleFactory.swift in Sources */, 0E8BD373238566AB008B31EF /* MenuViewInput.swift in Sources */, + E196918F2A06E2E100DC360E /* RuuviAlertSound+Extension.swift in Sources */, A9E6774925A33081000B75A3 /* String+Characters.swift in Sources */, + E196915E2A06DCD500DC360E /* NotificationsSettingsTextCell.swift in Sources */, A91D030A2511207300694733 /* SelectionRouterInput.swift in Sources */, 0EAD33E42399273D00EC5BAA /* HeartbeatViewController.swift in Sources */, E191F20D2969E14600F1FEA6 /* TagSettingsRouter.swift in Sources */, @@ -5267,6 +5561,7 @@ 0E8BD37F238566AB008B31EF /* CardsViewInput.swift in Sources */, 0E8BD382238566AB008B31EF /* AboutRouter.swift in Sources */, 66718A6D266BD0E800A380F8 /* Color+Ruuvi.swift in Sources */, + E1AB90622A0EB11800543F61 /* SensorForceClaimPresenter.swift in Sources */, A964647F247BAE6B0001D55D /* ChartSettingsInitializer.swift in Sources */, A93CDCD025659BA600018C6C /* AlertPresenter.swift in Sources */, 0E8BD389238566AB008B31EF /* PhotoPickerPresenter.swift in Sources */, @@ -5277,6 +5572,7 @@ E1ED427128FF261D00302179 /* XAxisValueFormatter.swift in Sources */, A964646D247BAE6B0001D55D /* ChartSettingsStepperTableViewCell.swift in Sources */, 0E8BD38D238566AB008B31EF /* DefaultsModuleInput.swift in Sources */, + E196918C2A06E15F00DC360E /* PushAlertSoundSelectionViewModel.swift in Sources */, E1CD78092878308E00F1F0EB /* UnitSettingsItem.swift in Sources */, E1B57FFC29859D0700B441FB /* DevicesViewModel.swift in Sources */, 0E8BD392238566AB008B31EF /* HumidityUnit+Localization.swift in Sources */, @@ -5298,6 +5594,7 @@ 0E8BD39A238566AB008B31EF /* AppStateService.swift in Sources */, A9646467247BAE6B0001D55D /* ChartSettingsViewInput.swift in Sources */, 0E8BD39B238566AB008B31EF /* MenuTableDismissTransitionAnimation.swift in Sources */, + E1AB90682A0EB1AD00543F61 /* SensorForceClaimRouter.swift in Sources */, 0E8BD39E238566AB008B31EF /* SettingsTableConfigurator.swift in Sources */, 0E8BD3A1238566AB008B31EF /* LocationPickerModuleOutput.swift in Sources */, E18D04A628E8A5A3008EF5EC /* TagChartsViewConfigurator.swift in Sources */, @@ -5308,6 +5605,7 @@ 0E8BD3A5238566AB008B31EF /* SettingsTableInitializer.swift in Sources */, 0EA7967E2664B50A002BA25D /* RuuviPersistenceError+LocalizedError.swift in Sources */, 0EAD33E12399273D00EC5BAA /* HeartbeatViewInput.swift in Sources */, + E1AB907C2A0ECB6D00543F61 /* SensorForceClaimViewOutput.swift in Sources */, 340BE39C27B54FEC006D6C34 /* String+Email.swift in Sources */, E10E18B5297D67B8002C78C3 /* RuuviCloudViewModel.swift in Sources */, E1198A2229BA6EC6002245CF /* RuuviTheme+Extension.swift in Sources */, @@ -5315,6 +5613,7 @@ E16B8F4928D10E7A0025B92D /* MyRuuviAccountViewModel.swift in Sources */, E1B57FEC29859CB800B441FB /* DevicesModuleFactory.swift in Sources */, E18FD50E28DF7E7100289359 /* UIView+Layout.swift in Sources */, + E19691602A06DCD500DC360E /* NotificationsSettingsSwitchCell.swift in Sources */, E11FDA7129A3F78D003ADA7B /* RuuviSimpleViewCompositionalLayout.swift in Sources */, E1198A3929BA76DD002245CF /* ASSelectionPresenter.swift in Sources */, 340BE38B27B54F37006D6C34 /* OwnerConfigurator.swift in Sources */, @@ -5359,6 +5658,7 @@ 0E8BD3CC238566AB008B31EF /* CardsRouterInput.swift in Sources */, A91D031B25113EAA00694733 /* UnitPressure+Extension.swift in Sources */, E1395BFA2954DAA200C403C6 /* CardsInteractorInput.swift in Sources */, + E196917F2A06E03300DC360E /* PushAlertSoundSelectionPresenter.swift in Sources */, A9E599622557346600F9E5CC /* ShareSendButtonTableViewCell.swift in Sources */, A9646479247BAE6B0001D55D /* ChartSettingsViewModel.swift in Sources */, E1CE4C752959D08D005C023F /* DashboardImageCell.swift in Sources */, @@ -5389,6 +5689,7 @@ E1198A1229BA6910002245CF /* AppearanceSettingsPresenter.swift in Sources */, 4F8FB6FCCB69D64B47FF440B /* ShareConfigurator.swift in Sources */, 898D45D0BF0ABBCB2E68FBE9 /* ShareInitializer.swift in Sources */, + E196916F2A06DCEF00DC360E /* NotificationsSettingsModuleInput.swift in Sources */, C9D6ACA6CB9694F6C33414CE /* ShareModuleInput.swift in Sources */, 248444E4B7F8C3D6454D4D1B /* ShareModuleOutput.swift in Sources */, 26BA929FDA200CCAC9964651 /* SharePresenter.swift in Sources */, @@ -5398,6 +5699,7 @@ B6FA27492FB1F63393783EFC /* ShareViewInput.swift in Sources */, E1B20C882926CDE10023D739 /* UITextField+Extension.swift in Sources */, 3BA1E7A8054CF95D80BAE3F9 /* ShareViewOutput.swift in Sources */, + E19691852A06E03300DC360E /* PushAlertSoundSelectionTableViewCell.swift in Sources */, 2BB81E4733D8EC40BECC68EF /* ShareViewModel.swift in Sources */, E18FD50B28DF7B7900289359 /* TagChartsViewController.swift in Sources */, E1597A53295CD6BB00DFB70B /* DashboardViewInput.swift in Sources */, @@ -5458,6 +5760,7 @@ 0EA7967D2664B50A002BA25D /* RuuviPersistenceError+LocalizedError.swift in Sources */, 0EC50F5622CCE46D00172EEB /* Optional.swift in Sources */, E16B8F5128D113440025B92D /* MyRuuviAccountConfigurator.swift in Sources */, + E1AB907E2A0ECB7600543F61 /* SensorForceClaimViewInput.swift in Sources */, A9B57442253B994700DB7353 /* SignInRouter.swift in Sources */, E1597A752962046500DFB70B /* BackgroundSelectionViewHeader.swift in Sources */, E116708C29634D91002DF7BF /* BackgroundSelectionViewModel.swift in Sources */, @@ -5474,6 +5777,7 @@ A9646472247BAE6B0001D55D /* ChartSettingsTableViewController.swift in Sources */, 0EF4E34026824EF500D83CC7 /* DfuFirmware+Log.swift in Sources */, E11989F529B7A46E002245CF /* UIView+Extension.swift in Sources */, + E19691542A06DCBB00DC360E /* NotificationsSettingsRouterInput.swift in Sources */, E191F1F52968D32000F1FEA6 /* TagSettingsExpandableSectionHeader.swift in Sources */, 0EF2863122CBB00D0026C7A5 /* TagSettingsViewInput.swift in Sources */, E19EAF9A299E608E005827E4 /* SignInBenefitsRouter.swift in Sources */, @@ -5483,7 +5787,9 @@ E1597A722962000B00DFB70B /* BackgroundSelectionViewCell.swift in Sources */, 0E1C1E0022B400130032F6CA /* MenuPresenter.swift in Sources */, A91D03062511207300694733 /* SelectionRouter.swift in Sources */, + E196918B2A06E15F00DC360E /* PushAlertSoundSelectionViewModel.swift in Sources */, 0E1C1DF822B3FF480032F6CA /* MenuTableViewController.swift in Sources */, + E196915D2A06DCD500DC360E /* NotificationsSettingsTextCell.swift in Sources */, 0EEB20F922B7D28C0015F9E0 /* AboutRouterInput.swift in Sources */, E1CD77F828782F2200F1F0EB /* UnitSettingsPresenter.swift in Sources */, 0E84BF572397F33E00A37E1A /* HeartbeatViewInput.swift in Sources */, @@ -5491,6 +5797,7 @@ 0EEB20F522B7D1A40015F9E0 /* AboutViewOutput.swift in Sources */, A98D3F15256CBD600066588B /* ShareViewOutput.swift in Sources */, E1167082296346D5002DF7BF /* BackgroundSelectionModuleInput.swift in Sources */, + E1AB906A2A0EB1BC00543F61 /* SensorForceClaimRouterInput.swift in Sources */, E1198A3529BA76CB002245CF /* ASSelectionViewInput.swift in Sources */, 0EB48D952619D5E5008E0D2D /* FeatureToggleProvider.swift in Sources */, A9E5994A2557341F00F9E5CC /* ShareDescriptionTableViewCell.swift in Sources */, @@ -5548,6 +5855,7 @@ 0E0501212685E895007060C4 /* HeartbeatDaemonTitles.swift in Sources */, 0EEB20CD22B7BD6C0015F9E0 /* MenuModuleOutput.swift in Sources */, 0E70A46322AF959E006CB87C /* Localizable.swift in Sources */, + E1AB90672A0EB1AD00543F61 /* SensorForceClaimRouter.swift in Sources */, 66BC44B02657AED400A03253 /* OffsetCorrectionViewOutput.swift in Sources */, E1CD78012878302100F1F0EB /* UnitSettingsViewInput.swift in Sources */, E1198A1129BA6910002245CF /* AppearanceSettingsPresenter.swift in Sources */, @@ -5569,6 +5877,7 @@ E1395BF1294F793000C403C6 /* AppDateFormatter.swift in Sources */, E18D04C028E8B3B0008EF5EC /* TagChartsViewInteractorOutput.swift in Sources */, E19EAF72299ADB2F005827E4 /* UIButton+Extension.swift in Sources */, + E1AB905E2A0EB0D000543F61 /* SensorForceClaimModuleFactory.swift in Sources */, 66718A6C266BD0E800A380F8 /* Color+Ruuvi.swift in Sources */, 0E046F5122F1828900BD4E9C /* LocationPickerRouter.swift in Sources */, 0E046F3422F057FC00BD4E9C /* WebTagSettingsModuleInput.swift in Sources */, @@ -5590,6 +5899,7 @@ 0EA796712664A7E0002BA25D /* RuuviCloudError+LocalizedError.swift in Sources */, E1E3C333298D7FA500A59CB8 /* UIImage+Extension.swift in Sources */, E11989FF29BA60CE002245CF /* AppearanceSettingsTableViewController.swift in Sources */, + E196916C2A06DCEF00DC360E /* NotificationsSettingsPresenter.swift in Sources */, 0EEB20FF22B7D2DD0015F9E0 /* AboutPresenter.swift in Sources */, E1B5800529859E8900B441FB /* DevicesInteractor.swift in Sources */, 0E84BF612397F73100A37E1A /* HeartbeatEnvironmentObject.swift in Sources */, @@ -5602,6 +5912,7 @@ E19EAFA4299E6211005827E4 /* SignInBenefitsModuleOutput.swift in Sources */, E116708529634708002DF7BF /* BackgroundSelectionPresenter.swift in Sources */, 0EC50F5022CCB92000172EEB /* Observable.swift in Sources */, + E1AB90552A0BFECE00543F61 /* RuuviLinkTextView.swift in Sources */, E1198A1829BA6A61002245CF /* AppearanceSettingsViewInput.swift in Sources */, A9BD38BA24F6108300904BBE /* Humidity+Offset.swift in Sources */, 0EF2863322CBB0250026C7A5 /* TagSettingsViewOutput.swift in Sources */, @@ -5635,6 +5946,7 @@ E1D0238E29EB3F7D00EC0FFD /* YAxisValueFormatter.swift in Sources */, E1CE5E3F29F994DE00391109 /* AppGroupConstants.swift in Sources */, 0E84BF712398035C00A37E1A /* HeartbeatConfigurator.swift in Sources */, + E1AB907B2A0ECB6D00543F61 /* SensorForceClaimViewOutput.swift in Sources */, E19EAFA1299E61C5005827E4 /* SignInBenefitsModuleInput.swift in Sources */, E191F2092969E11F00F1FEA6 /* TagSettingsRouterInput.swift in Sources */, 0EB8ED2F268F12FA00C6B0FA /* DFUModuleFactory.swift in Sources */, @@ -5662,6 +5974,7 @@ A93CDCCF25659BA600018C6C /* AlertPresenter.swift in Sources */, E1597A2D295B6E6B00DFB70B /* UIFont+Extension.swift in Sources */, A9646487247BAE6B0001D55D /* ChartSettingsRouterInput.swift in Sources */, + E196917C2A06E03300DC360E /* PushAlertSoundSelectionModuleFactory.swift in Sources */, 0EEB210122B7D2F50015F9E0 /* AboutInitializer.swift in Sources */, E16B8F4B28D1114F0025B92D /* MyRuuviAccountModuleInput.swift in Sources */, 0EEB20CB22B7B37C0015F9E0 /* MenuTableEmbededViewController.swift in Sources */, @@ -5671,14 +5984,17 @@ 0E84BF5F2397F6BE00A37E1A /* HeartbeatViewController.swift in Sources */, E191F1F82968D33800F1FEA6 /* TagSettingsSimpleSectionHeader.swift in Sources */, E191F1EC2968BCF300F1FEA6 /* TagSettingsBasicCell.swift in Sources */, + E1AB90612A0EB11800543F61 /* SensorForceClaimPresenter.swift in Sources */, 0E84BF67239801AF00A37E1A /* HeartbeatRouterInput.swift in Sources */, 0EB48DDC2619E306008E0D2D /* FallbackFeatureToggleProvider.swift in Sources */, 0EF5B72B22D62CD900D9D14A /* Date+Ruuvi.swift in Sources */, + E19691842A06E03300DC360E /* PushAlertSoundSelectionTableViewCell.swift in Sources */, A98D3F0F256CBD600066588B /* ShareViewInput.swift in Sources */, 0EB8ED3F26916D1700C6B0FA /* LargeButtonStyle.swift in Sources */, E1CD77F128782E8000F1F0EB /* UnitSettingsTableConfigurator.swift in Sources */, 0EB8ED25268F083700C6B0FA /* DFUUIView.swift in Sources */, E1597A5C295E24D400DFB70B /* RuuviAssets.swift in Sources */, + E19691682A06DCE500DC360E /* NotificationsSettingsViewOutput.swift in Sources */, E1E3C342299007B700A59CB8 /* TagSettingsModuleInput.swift in Sources */, E1ED427028FF261D00302179 /* XAxisValueFormatter.swift in Sources */, A980573825807118000D03AB /* AboutViewModel.swift in Sources */, @@ -5687,6 +6003,7 @@ E1E3C345299007DE00A59CB8 /* TagSettingsModuleOutput.swift in Sources */, E1B57FEB29859CB800B441FB /* DevicesModuleFactory.swift in Sources */, E1B5800E2986AE0800B441FB /* RuuviContextMenuButton.swift in Sources */, + E1CE5E472A016D4000391109 /* RuuviCustomButton.swift in Sources */, A9646478247BAE6B0001D55D /* ChartSettingsViewModel.swift in Sources */, A9B57441253B994700DB7353 /* SignInRouterInput.swift in Sources */, E1B5800B29859EEB00B441FB /* DevicesInteractorOutput.swift in Sources */, @@ -5694,6 +6011,7 @@ E16B8F5428D113750025B92D /* MyRuuviAccountPresenter.swift in Sources */, A9646484247BAE6B0001D55D /* ChartSettingsPresenter.swift in Sources */, E19EAFA7299E62A7005827E4 /* SignInBenefitsViewInput.swift in Sources */, + E196917E2A06E03300DC360E /* PushAlertSoundSelectionPresenter.swift in Sources */, E1B5800129859D2300B441FB /* DevicesViewOutput.swift in Sources */, 0E046F5322F182AB00BD4E9C /* LocationPickerModuleInput.swift in Sources */, A91D031A25113EAA00694733 /* UnitPressure+Extension.swift in Sources */, @@ -5706,8 +6024,10 @@ A9E6774825A33081000B75A3 /* String+Characters.swift in Sources */, 0EEB20DD22B7C8650015F9E0 /* SettingsRouterInput.swift in Sources */, 0EEB215A22BA28860015F9E0 /* MenuTableTransitionManager.swift in Sources */, + E196915F2A06DCD500DC360E /* NotificationsSettingsSwitchCell.swift in Sources */, A91D02FD2511207300694733 /* SelectionTableViewCell.swift in Sources */, E10E18BA297D67FC002C78C3 /* RuuviCloudViewOutput.swift in Sources */, + E19691562A06DCBB00DC360E /* NotificationsSettingsRouter.swift in Sources */, E10E18722978AF3D002C78C3 /* TagSettingsViewModel.swift in Sources */, 340BE39627B54F38006D6C34 /* OwnerRouter.swift in Sources */, E18D04AF28E8A747008EF5EC /* TagChartsViewModuleOutput.swift in Sources */, @@ -5721,9 +6041,11 @@ E1CD78082878308E00F1F0EB /* UnitSettingsItem.swift in Sources */, 0E9D0AB1231EBEFD00C6BDA7 /* LocalizationService.swift in Sources */, E16B8F3F28D10CF50025B92D /* MyRuuviAccountViewController.swift in Sources */, + E19691662A06DCE500DC360E /* NotificationsSettingsViewModel.swift in Sources */, 0E02ABBA237598C600ED4629 /* RURangeSeekSlider.swift in Sources */, E191F1FB2969CD0100F1FEA6 /* TagSettingsBackgroundSelectionView.swift in Sources */, E1CD78042878302C00F1F0EB /* UnitSettingsViewOutput.swift in Sources */, + E19691642A06DCE500DC360E /* NotificationsSettingsViewInput.swift in Sources */, E16051EC285CBA57003FCA70 /* FileManager+Date.swift in Sources */, 0EA796892664B8CB002BA25D /* RuuviServiceError+LocalizedError.swift in Sources */, 0E1C1DDD22B3C2160032F6CA /* CardsModuleInput.swift in Sources */, @@ -5732,6 +6054,7 @@ 0E25135F2684AEAD004A522A /* RuuviNotifierTitlesImpl.swift in Sources */, E18D04A928E8A6C0008EF5EC /* TagChartsViewPresenter.swift in Sources */, A98D3F0C256CBD600066588B /* ShareConfigurator.swift in Sources */, + E19691502A06DCA400DC360E /* NotificationsSettingsModuleFactory.swift in Sources */, 0EA7AB7A2680A68200C137AD /* RuuviCoreError+LocalizedError.swift in Sources */, E16051E9285CB384003FCA70 /* AppStoreReviewHelper.swift in Sources */, E18FD50D28DF7E7100289359 /* UIView+Layout.swift in Sources */, @@ -5755,6 +6078,7 @@ 0EB48D8B2619D5AC008E0D2D /* FeatureToggleService.swift in Sources */, 0EE98A7926493C5000AAB3ED /* FLEXFeatureTogglesViewController.swift in Sources */, E1198A3229BA76C1002245CF /* ASSelectionViewOutput.swift in Sources */, + E19691882A06E03300DC360E /* PushAlertSoundSelectionViewOutput.swift in Sources */, 0E5C302822D0B1C600B52E39 /* AppStateServiceImpl.swift in Sources */, 0E1C1DF522B3FF1D0032F6CA /* MenuViewOutput.swift in Sources */, 0E046F4D22F1823C00BD4E9C /* LocationPickerAppleViewController.swift in Sources */, @@ -5767,16 +6091,20 @@ E1597A5F295F803400DFB70B /* UIColor+Extension.swift in Sources */, 66BC44A12657AED400A03253 /* OffsetCorrectionPresenter.swift in Sources */, E1CD780F287830BF00F1F0EB /* UnitSettingsRouterInput.swift in Sources */, + E1AB90732A0EB24600543F61 /* SensorForceClaimViewController.swift in Sources */, 0EEB20F722B7D2090015F9E0 /* AboutViewController.swift in Sources */, E116706F29620AD4002DF7BF /* BackgroundSelectionButtonView.swift in Sources */, 0E8A10202384633200A9CBA6 /* DefaultsEnvironmentObject.swift in Sources */, A98D3F14256CBD600066588B /* ShareModuleOutput.swift in Sources */, E191F21E2969FF6900F1FEA6 /* TagSettingsSwitchCell.swift in Sources */, E1CE4C742959D08D005C023F /* DashboardImageCell.swift in Sources */, + E19691862A06E03300DC360E /* PushAlertSoundSelectionViewInput.swift in Sources */, A98D3F10256CBD600066588B /* ShareViewModel.swift in Sources */, 0E70A46022AF9567006CB87C /* ViewInput.swift in Sources */, + E19691802A06E03300DC360E /* PushAlertSoundSelectionModuleInput.swift in Sources */, 0E0A381923616AC3003A0364 /* UserDefaults+Optional.swift in Sources */, 340BE39227B54F37006D6C34 /* OwnerViewController.swift in Sources */, + E196915B2A06DCD500DC360E /* NotificationsSettingsTableViewController.swift in Sources */, E1198A2629BA763F002245CF /* ASSelectionModuleFactory.swift in Sources */, 0E1C1DD322B3BF180032F6CA /* CardsViewInput.swift in Sources */, E1198A2C29BA769A002245CF /* ASSelectionTableViewController.swift in Sources */, @@ -5796,6 +6124,7 @@ E1597A40295CD55300DFB70B /* DashboardRouter.swift in Sources */, 0EB48DA32619D7EE008E0D2D /* FirebaseFeatureToggleProvider.swift in Sources */, E168B4B62886AF1200D6B5C6 /* UnitSettingsType.swift in Sources */, + E19691822A06E03300DC360E /* PushAlertSoundSelectionTableViewController.swift in Sources */, E1395BFC2954DAAD00C403C6 /* CardsInteractor.swift in Sources */, 3414BFC82806D19C00C63BE9 /* RuuviCodeView.swift in Sources */, 0E53DA6422CC943900BC6D64 /* SwipeDownToDismissInteractiveTransition.swift in Sources */, @@ -5848,6 +6177,7 @@ 3490A4C327D9F2C80032BBAB /* UINavigationController.swift in Sources */, E1C395D329B26044009301D3 /* UICollectionView+Extension.swift in Sources */, 0EEB20E122B7C8A90015F9E0 /* SettingsModuleInput.swift in Sources */, + E196916E2A06DCEF00DC360E /* NotificationsSettingsModuleInput.swift in Sources */, E1E3C33C298ED3E300A59CB8 /* CardsPresenter.swift in Sources */, E16B8F4528D10E2C0025B92D /* MyRuuviAccountViewOutput.swift in Sources */, A9B5744A253B994700DB7353 /* SignInViewOutput.swift in Sources */, @@ -5872,6 +6202,7 @@ 0EB8ED36268F685500C6B0FA /* URLSession+downloadTaskPublisher.swift in Sources */, 0E84BF5B2397F3DF00A37E1A /* HeartbeatViewOutput.swift in Sources */, 0EEB20D622B7C7E70015F9E0 /* SettingsViewInput.swift in Sources */, + E196918E2A06E2E100DC360E /* RuuviAlertSound+Extension.swift in Sources */, 0E62299526AAA0570041DCDD /* DiscoverRouter.swift in Sources */, 340BE39427B54F37006D6C34 /* OwnerViewOutput.swift in Sources */, E10E18AB297D6664002C78C3 /* RuuviCloudModuleFactory.swift in Sources */, @@ -5882,6 +6213,7 @@ A9646475247BAE6B0001D55D /* ChartSettingsViewOutput.swift in Sources */, E1B5800829859EDE00B441FB /* DevicesInteractorInput.swift in Sources */, A91D03002511207300694733 /* SelectionTableViewController.swift in Sources */, + E1AB90642A0EB13400543F61 /* SensorForceClaimModuleInput.swift in Sources */, A964646F247BAE6B0001D55D /* ChartSettingsSwitchTableViewCell.swift in Sources */, A98D3F0D256CBD600066588B /* ShareInitializer.swift in Sources */, 0E84BF632397F76A00A37E1A /* HeartbeatList.swift in Sources */, @@ -6031,7 +6363,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 375; + CURRENT_PROJECT_VERSION = 386; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 4MUYJ4YYH4; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 4MUYJ4YYH4; @@ -6041,7 +6373,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.1.2; + MARKETING_VERSION = 2.2.0; OTHER_SWIFT_FLAGS = "$(inherited) -D COCOAPODS -D DEVELOPMENT"; PRODUCT_BUNDLE_IDENTIFIER = com.ruuvi.station; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6063,7 +6395,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 375; + CURRENT_PROJECT_VERSION = 386; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 4MUYJ4YYH4; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 4MUYJ4YYH4; @@ -6073,7 +6405,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.1.2; + MARKETING_VERSION = 2.2.0; OTHER_SWIFT_FLAGS = "$(inherited) -D COCOAPODS"; PRODUCT_BUNDLE_IDENTIFIER = com.ruuvi.station; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6095,7 +6427,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 375; + CURRENT_PROJECT_VERSION = 386; DEVELOPMENT_TEAM = 4MUYJ4YYH4; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 4MUYJ4YYH4; GENERATE_INFOPLIST_FILE = YES; @@ -6108,7 +6440,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.1.2; + MARKETING_VERSION = 2.2.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.ruuvi.station.widgets; @@ -6132,7 +6464,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 375; + CURRENT_PROJECT_VERSION = 386; DEVELOPMENT_TEAM = 4MUYJ4YYH4; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 4MUYJ4YYH4; GENERATE_INFOPLIST_FILE = YES; @@ -6145,7 +6477,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.1.2; + MARKETING_VERSION = 2.2.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.ruuvi.station.widgets; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6167,7 +6499,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 375; + CURRENT_PROJECT_VERSION = 386; DEVELOPMENT_TEAM = 4MUYJ4YYH4; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 4MUYJ4YYH4; GENERATE_INFOPLIST_FILE = YES; @@ -6180,7 +6512,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.1.2; + MARKETING_VERSION = 2.2.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.ruuvi.station.intents; @@ -6203,7 +6535,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 375; + CURRENT_PROJECT_VERSION = 386; DEVELOPMENT_TEAM = 4MUYJ4YYH4; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 4MUYJ4YYH4; GENERATE_INFOPLIST_FILE = YES; @@ -6216,7 +6548,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.1.2; + MARKETING_VERSION = 2.2.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.ruuvi.station.intents; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6355,7 +6687,7 @@ CODE_SIGN_ENTITLEMENTS = station/station.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 375; + CURRENT_PROJECT_VERSION = 386; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 4MUYJ4YYH4; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 4MUYJ4YYH4; @@ -6397,7 +6729,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.1.2; + MARKETING_VERSION = 2.2.0; OTHER_SWIFT_FLAGS = "$(inherited) -D COCOAPODS"; PRODUCT_BUNDLE_IDENTIFIER = com.ruuvi.station; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6417,7 +6749,7 @@ CODE_SIGN_ENTITLEMENTS = station/station.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 375; + CURRENT_PROJECT_VERSION = 386; DEVELOPMENT_TEAM = 4MUYJ4YYH4; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 4MUYJ4YYH4; INFOPLIST_FILE = "$(SRCROOT)/station/Resources/Plists/Info.plist"; @@ -6426,7 +6758,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.1.2; + MARKETING_VERSION = 2.2.0; OTHER_SWIFT_FLAGS = "$(inherited) -D COCOAPODS"; PRODUCT_BUNDLE_IDENTIFIER = com.ruuvi.station; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6531,7 +6863,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 375; + CURRENT_PROJECT_VERSION = 386; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 4MUYJ4YYH4; GENERATE_INFOPLIST_FILE = YES; @@ -6544,7 +6876,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.1.1; + MARKETING_VERSION = 2.2.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.ruuvi.station.pnservice; @@ -6566,7 +6898,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 375; + CURRENT_PROJECT_VERSION = 386; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 4MUYJ4YYH4; GENERATE_INFOPLIST_FILE = YES; @@ -6579,7 +6911,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.1.1; + MARKETING_VERSION = 2.2.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.ruuvi.station.pnservice; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/station/Classes/Application/AppDelegate.swift b/station/Classes/Application/AppDelegate.swift index e63b1a81e..add717760 100644 --- a/station/Classes/Application/AppDelegate.swift +++ b/station/Classes/Application/AppDelegate.swift @@ -136,7 +136,8 @@ extension AppDelegate: MessagingDelegate { cloudNotificationService.set(token: fcmToken, name: UIDevice.modelName, - data: nil) + data: nil, + sound: settings.alertSound) } } diff --git a/station/Classes/Presentation/Modules/Dashboard/Background/View/UI/BackgroundSelectionViewController.swift b/station/Classes/Presentation/Modules/Dashboard/Background/View/UI/BackgroundSelectionViewController.swift index 280dad4c5..73062b9e6 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Background/View/UI/BackgroundSelectionViewController.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Background/View/UI/BackgroundSelectionViewController.swift @@ -89,8 +89,8 @@ extension BackgroundSelectionViewController { leading: leftBarButtonView.leadingAnchor, bottom: leftBarButtonView.bottomAnchor, trailing: leftBarButtonView.trailingAnchor, - padding: .init(top: 0, left: -8, bottom: 0, right: 0), - size: .init(width: 32, height: 32)) + padding: .init(top: 0, left: -12, bottom: 0, right: 0), + size: .init(width: 40, height: 40)) navigationItem.leftBarButtonItem = UIBarButtonItem(customView: leftBarButtonView) } diff --git a/station/Classes/Presentation/Modules/Dashboard/Cards/Presenter/CardsPresenter.swift b/station/Classes/Presentation/Modules/Dashboard/Cards/Presenter/CardsPresenter.swift index ea21bf6f4..c24dfb477 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Cards/Presenter/CardsPresenter.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Cards/Presenter/CardsPresenter.swift @@ -237,6 +237,8 @@ extension CardsPresenter { guard let sSelf = self else { return } switch change { case .insert(let sensor): + sSelf.notifyRestartAdvertisementDaemon() + sSelf.notifyRestartHeartBeatDaemon() sSelf.checkFirmwareVersion(for: sensor) sSelf.ruuviTags.append(sensor.any) sSelf.syncViewModels() @@ -1403,5 +1405,23 @@ extension CardsPresenter { observable.value = isOn } } + + private func notifyRestartAdvertisementDaemon() { + // Notify daemon to restart + NotificationCenter + .default + .post(name: .RuuviTagAdvertisementDaemonShouldRestart, + object: nil, + userInfo: nil) + } + + private func notifyRestartHeartBeatDaemon() { + // Notify daemon to restart + NotificationCenter + .default + .post(name: .RuuviTagHeartBeatDaemonShouldRestart, + object: nil, + userInfo: nil) + } } // swiftlint:enable file_length trailing_whitespace diff --git a/station/Classes/Presentation/Modules/Dashboard/Cards/View/UI/CardsViewController.swift b/station/Classes/Presentation/Modules/Dashboard/Cards/View/UI/CardsViewController.swift index ba7382324..ccb60f985 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Cards/View/UI/CardsViewController.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Cards/View/UI/CardsViewController.swift @@ -6,13 +6,6 @@ import RuuviLocal import RuuviService import GestureInstructions -enum CardsSection: CaseIterable { - case main -} - -typealias CardsSnapshot = NSDiffableDataSourceSnapshot -typealias CardsDataSource = UICollectionViewDiffableDataSource - class CardsViewController: UIViewController { // Configuration @@ -31,20 +24,7 @@ class CardsViewController: UIViewController { var scrollIndex: Int = 0 private var currentPage: Int = 0 - private lazy var datasource = makeDatasource() private static let reuseIdentifier: String = "reuseIdentifier" - // MARK: - Datasource - private func makeDatasource() -> CardsDataSource { - let datasource = CardsDataSource( - collectionView: collectionView, - cellProvider: { [unowned self] (collectionView, indexPath, viewModel) in - return self.cell(collectionView: collectionView, - indexPath: indexPath, - viewModel: viewModel) - } - ) - return datasource - } func cell(collectionView: UICollectionView, indexPath: IndexPath, @@ -58,13 +38,7 @@ class CardsViewController: UIViewController { } func applySnapshot() { - var snapshot = CardsSnapshot() - snapshot.appendSections([.main]) - snapshot.appendItems(viewModels, toSection: .main) currentPage = scrollIndex - datasource.apply(snapshot, - animatingDifferences: false) - // Forces collection view to reload. collectionView.reloadWithoutAnimation() if currentPage < viewModels.count { collectionView.scrollTo(index: currentPage) @@ -90,7 +64,7 @@ class CardsViewController: UIViewController { private lazy var backButton: UIButton = { let button = UIButton() button.tintColor = .white - let buttonImage = UIImage(named: "chevron_back") + let buttonImage = RuuviAssets.backButtonImage button.setImage(buttonImage, for: .normal) button.setImage(buttonImage, for: .highlighted) button.imageView?.tintColor = .white @@ -99,11 +73,10 @@ class CardsViewController: UIViewController { return button }() - private lazy var alertButton: UIImageView = { - let iv = UIImageView(image: nil, - contentMode: .scaleAspectFit) - iv.tintColor = .white - return iv + private lazy var alertButton: RuuviCustomButton = { + let button = RuuviCustomButton(icon: nil) + button.backgroundColor = .clear + return button }() /// This button is used to be able to tap the alert button when @@ -115,22 +88,31 @@ class CardsViewController: UIViewController { return button }() - private lazy var chartButton: UIImageView = { - let iv = UIImageView(image: RuuviAssets.chartsIcon, - contentMode: .scaleAspectFit) - iv.tintColor = .white - iv.addGestureRecognizer(UITapGestureRecognizer(target: self, - action: #selector(chartButtonDidTap))) - return iv + private lazy var chartButton: RuuviCustomButton = { + let button = RuuviCustomButton(icon: RuuviAssets.chartsIcon) + button.backgroundColor = .clear + button.addGestureRecognizer( + UITapGestureRecognizer( + target: self, + action: #selector(chartButtonDidTap) + ) + ) + return button }() - private lazy var settingsButton: UIImageView = { - let iv = UIImageView(image: RuuviAssets.settingsIcon, - contentMode: .scaleAspectFit) - iv.tintColor = .white - iv.addGestureRecognizer(UITapGestureRecognizer(target: self, - action: #selector(settingsButtonDidTap))) - return iv + private lazy var settingsButton: RuuviCustomButton = { + let button = RuuviCustomButton( + icon: RuuviAssets.settingsIcon, + iconSize: .init(width: 26, height: 25) + ) + button.backgroundColor = .clear + button.addGestureRecognizer( + UITapGestureRecognizer( + target: self, + action: #selector(settingsButtonDidTap) + ) + ) + return button }() // BODY @@ -143,6 +125,7 @@ class CardsViewController: UIViewController { cv.isPagingEnabled = true cv.alwaysBounceVertical = false cv.delegate = self + cv.dataSource = self cv.register(CardsLargeImageCell.self, forCellWithReuseIdentifier: Self.reuseIdentifier) return cv @@ -201,10 +184,17 @@ extension CardsViewController { // Scroll to current Item after the orientation change. coordinator.animate(alongsideTransition: { [weak self] _ in guard let sSelf = self else { return } - sSelf.collectionView.collectionViewLayout.invalidateLayout() - if sSelf.currentPage < sSelf.viewModels.count { - sSelf.collectionView.scrollTo(index: sSelf.currentPage) - } + let flowLayout = sSelf.createLayout() + sSelf.collectionView.setCollectionViewLayout( + flowLayout, + animated: false, + completion: { _ in + guard sSelf.viewModels.count > 0 else { return } + if sSelf.currentPage < sSelf.viewModels.count { + sSelf.collectionView.scrollTo(index: sSelf.currentPage) + } + } + ) }) } } @@ -235,15 +225,15 @@ extension CardsViewController { leading: leftBarButtonView.leadingAnchor, bottom: leftBarButtonView.bottomAnchor, trailing: nil, - padding: .init(top: 0, left: -8, bottom: 0, right: 0), - size: .init(width: 32, height: 32)) + padding: .init(top: 0, left: -12, bottom: 0, right: 0), + size: .init(width: 40, height: 40)) leftBarButtonView.addSubview(ruuviLogoView) ruuviLogoView.anchor(top: nil, leading: backButton.trailingAnchor, bottom: nil, trailing: leftBarButtonView.trailingAnchor, - padding: .init(top: 0, left: 16, bottom: 0, right: 0), + padding: .init(top: 0, left: 12, bottom: 0, right: 0), size: .init(width: 110, height: 22)) ruuviLogoView.centerYInSuperview() @@ -253,8 +243,7 @@ extension CardsViewController { alertButton.anchor(top: rightBarButtonView.topAnchor, leading: rightBarButtonView.leadingAnchor, bottom: rightBarButtonView.bottomAnchor, - trailing: nil, - size: .init(width: 20, height: 20)) + trailing: nil) alertButton.centerYInSuperview() rightBarButtonView.addSubview(alertButtonHidden) @@ -264,9 +253,7 @@ extension CardsViewController { chartButton.anchor(top: nil, leading: alertButton.trailingAnchor, bottom: nil, - trailing: nil, - padding: .init(top: 0, left: 22, bottom: 0, right: 0), - size: .init(width: 20, height: 20)) + trailing: nil) chartButton.centerYInSuperview() rightBarButtonView.addSubview(settingsButton) @@ -274,8 +261,7 @@ extension CardsViewController { leading: chartButton.trailingAnchor, bottom: nil, trailing: rightBarButtonView.trailingAnchor, - padding: .init(top: 0, left: 16, bottom: 0, right: 0), - size: .init(width: 26, height: 25)) + padding: .init(top: 0, left: 0, bottom: 0, right: -14)) settingsButton.centerYInSuperview() navigationItem.leftBarButtonItem = UIBarButtonItem(customView: leftBarButtonView) @@ -288,8 +274,6 @@ extension CardsViewController { leading: view.safeLeftAnchor, bottom: view.safeBottomAnchor, trailing: view.safeRightAnchor) - - collectionView.dataSource = datasource } fileprivate func createLayout() -> UICollectionViewLayout { @@ -341,17 +325,36 @@ extension CardsViewController: UICollectionViewDelegate { let xPoint = scrollView.contentOffset.x + scrollView.frame.size.width / 2 let yPoint = scrollView.frame.size.height / 2 let center = CGPoint(x: xPoint, y: yPoint) - if let currentIndexPath = collectionView.indexPathForItem(at: center), - let currentVisibleItem = datasource.itemIdentifier(for: currentIndexPath) { + if let currentIndexPath = collectionView.indexPathForItem(at: center) { currentPage = currentIndexPath.row - self.currentVisibleItem = currentVisibleItem + let currentItem = viewModels[currentPage] + self.currentVisibleItem = currentItem restartAnimations() - output.viewDidScroll(to: currentVisibleItem) - output.viewDidTriggerFirmwareUpdateDialog(for: currentVisibleItem) + output.viewDidScroll(to: currentItem) + output.viewDidTriggerFirmwareUpdateDialog(for: currentItem) } } } +extension CardsViewController: UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, + numberOfItemsInSection section: Int) -> Int { + return viewModels.count + } + + func collectionView(_ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = cell( + collectionView: collectionView, + indexPath: indexPath, + viewModel: viewModels[indexPath.item] + ) else { + fatalError() + } + return cell + } +} + extension CardsViewController { @objc fileprivate func backButtonDidTap() { // TODO: Handle the case when chart visible and sync ongoing. @@ -427,17 +430,18 @@ extension CardsViewController: CardsViewInput { } func applyUpdate(to viewModel: CardsViewModel) { - var snapshot = datasource.snapshot() - if let index = snapshot.indexOfItem(viewModel), - var item = datasource.itemIdentifier(for: IndexPath(item: index, - section: 0)) { - if viewModel == currentVisibleItem { - item = viewModel + if let index = viewModels.firstIndex(where: { vm in + vm.luid.value != nil && vm.luid.value == viewModel.luid.value || + vm.mac.value != nil && vm.mac.value == viewModel.mac.value + }) { + let indexPath = IndexPath(item: index, section: 0) + if let cell = collectionView + .cellForItem(at: indexPath) as? CardsLargeImageCell { + cell.configure( + with: viewModel, measurementService: measurementService + ) restartAnimations() updateTopActionButtonVisibility() - snapshot.reloadItems([item]) - datasource.apply(snapshot, - animatingDifferences: false) } } } @@ -484,8 +488,7 @@ extension CardsViewController: CardsViewInput { func scroll(to index: Int) { guard viewModels.count > 0, - index < viewModels.count, - index < datasource.snapshot().numberOfItems else { + index < viewModels.count else { return } let viewModel = viewModels[index] diff --git a/station/Classes/Presentation/Modules/Dashboard/Charts/Interactor/TagChartsViewInteractor.swift b/station/Classes/Presentation/Modules/Dashboard/Charts/Interactor/TagChartsViewInteractor.swift index cc371658d..67d85ff15 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Charts/Interactor/TagChartsViewInteractor.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Charts/Interactor/TagChartsViewInteractor.swift @@ -24,6 +24,7 @@ class TagChartsViewInteractor { var ruuviAppSettingsService: RuuviServiceAppSettings! var lastMeasurement: RuuviMeasurement? + var lastMeasurementRecord: RuuviTagSensorRecord? var ruuviTagData: [RuuviMeasurement] = [] private var ruuviTagSensorObservationToken: RuuviReactorToken? @@ -78,6 +79,7 @@ extension TagChartsViewInteractor: TagChartsViewInteractorInput { ruuviTagSensor = ruuviTag sensorSettings = settings lastMeasurement = nil + lastMeasurementRecord = nil restartScheduler() fetchLast() @@ -240,6 +242,7 @@ extension TagChartsViewInteractor { return } sSelf.lastMeasurement = record.measurement + sSelf.lastMeasurementRecord = record var chartsCases = MeasurementType.chartsCases if record.humidity == nil { chartsCases.remove(at: 1) @@ -247,24 +250,35 @@ extension TagChartsViewInteractor { chartsCases.remove(at: 2) } sSelf.presenter.createChartModules(from: chartsCases) + sSelf.presenter.updateLatestRecord(record) }, failure: {[weak self] (error) in self?.presenter.interactorDidError(.ruuviStorage(error)) }) } private func fetchLastFromDate() { - guard let lastDate = lastMeasurement?.date else { + guard let lastMeasurement = lastMeasurement, + let lastMeasurementRecord = lastMeasurementRecord else { return } - let op = ruuviStorage.readLast(ruuviTagSensor.id, from: lastDate.timeIntervalSince1970) + let op = ruuviStorage.readLast( + ruuviTagSensor.id, + from: lastMeasurement.date.timeIntervalSince1970 + ) op.on(success: { [weak self] (results) in guard results.count > 0, - let last = results.last else { return } + let last = results.last else { + self?.presenter.updateLatestRecord(lastMeasurementRecord) + return + } guard let sSelf = self else { return } sSelf.lastMeasurement = last.measurement + sSelf.lastMeasurementRecord = last sSelf.ruuviTagData.append(last.measurement) sSelf.insertMeasurements([last.measurement]) + sSelf.presenter.updateLatestRecord(last) }, failure: {[weak self] (error) in + self?.presenter.updateLatestRecord(lastMeasurementRecord) self?.presenter.interactorDidError(.ruuviStorage(error)) }) } diff --git a/station/Classes/Presentation/Modules/Dashboard/Charts/Interactor/TagChartsViewInteractorOutput.swift b/station/Classes/Presentation/Modules/Dashboard/Charts/Interactor/TagChartsViewInteractorOutput.swift index 1919bd204..e68d3c750 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Charts/Interactor/TagChartsViewInteractorOutput.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Charts/Interactor/TagChartsViewInteractorOutput.swift @@ -4,6 +4,7 @@ import RuuviOntology protocol TagChartsViewInteractorOutput: AnyObject { var isLoading: Bool { get set } func insertMeasurements(_ newValues: [RuuviMeasurement]) + func updateLatestRecord(_ record: RuuviTagSensorRecord) func interactorDidError(_ error: RUError) func createChartModules(from: [MeasurementType]) func interactorDidUpdate(sensor: AnyRuuviTagSensor) diff --git a/station/Classes/Presentation/Modules/Dashboard/Charts/Presenter/TagChartsViewPresenter.swift b/station/Classes/Presentation/Modules/Dashboard/Charts/Presenter/TagChartsViewPresenter.swift index fb44d7418..f1ed14fb6 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Charts/Presenter/TagChartsViewPresenter.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Charts/Presenter/TagChartsViewPresenter.swift @@ -173,7 +173,17 @@ extension TagChartsViewPresenter: TagChartsViewOutput { } func viewDidTriggerSync(for viewModel: TagChartsViewModel) { - view?.showSyncConfirmationDialog(for: viewModel) + viewDidStartSync(for: viewModel) + + guard let luid = ruuviTag.luid else { return } + if !settings.syncDialogHidden(for: luid) { + view?.showSyncConfirmationDialog(for: viewModel) + } + } + + func viewDidTriggerDoNotShowSyncDialog() { + guard let luid = ruuviTag.luid else { return } + settings.setSyncDialogHidden(for: luid) } func viewDidStartSync(for viewModel: TagChartsViewModel) { @@ -673,6 +683,10 @@ extension TagChartsViewPresenter { } } + func updateLatestRecord(_ record: RuuviTagSensorRecord) { + view?.updateLatestRecordStatus(with: record) + } + private func createChartData() { guard view != nil else { return } datasource.removeAll() diff --git a/station/Classes/Presentation/Modules/Dashboard/Charts/View/TagChartsViewInput.swift b/station/Classes/Presentation/Modules/Dashboard/Charts/View/TagChartsViewInput.swift index ff271bac6..81b4211c3 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Charts/View/TagChartsViewInput.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Charts/View/TagChartsViewInput.swift @@ -2,6 +2,7 @@ import Foundation import BTKit import Charts import RuuviLocal +import RuuviOntology protocol TagChartsViewInput: ViewInput { var viewModel: TagChartsViewModel { get set } @@ -20,6 +21,7 @@ protocol TagChartsViewInput: ViewInput { humidity: ChartDataEntry?, pressure: ChartDataEntry?, settings: RuuviLocalSettings) + func updateLatestRecordStatus(with record: RuuviTagSensorRecord) func showBluetoothDisabled(userDeclined: Bool) func showClearConfirmationDialog(for viewModel: TagChartsViewModel) func setSync(progress: BTServiceProgress?, for viewModel: TagChartsViewModel) diff --git a/station/Classes/Presentation/Modules/Dashboard/Charts/View/TagChartsViewOutput.swift b/station/Classes/Presentation/Modules/Dashboard/Charts/View/TagChartsViewOutput.swift index d28cede04..7dc2623e5 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Charts/View/TagChartsViewOutput.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Charts/View/TagChartsViewOutput.swift @@ -8,6 +8,7 @@ protocol TagChartsViewOutput { func viewDidTransition() func viewDidTriggerSync(for viewModel: TagChartsViewModel) func viewDidStartSync(for viewModel: TagChartsViewModel) + func viewDidTriggerDoNotShowSyncDialog() func viewDidTriggerStopSync(for viewModel: TagChartsViewModel) func viewDidTriggerClear(for viewModel: TagChartsViewModel) func viewDidConfirmToClear(for viewModel: TagChartsViewModel) diff --git a/station/Classes/Presentation/Modules/Dashboard/Charts/View/UI/TagChartsViewController.swift b/station/Classes/Presentation/Modules/Dashboard/Charts/View/UI/TagChartsViewController.swift index b68a49b09..bfaa9d1b9 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Charts/View/UI/TagChartsViewController.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Charts/View/UI/TagChartsViewController.swift @@ -112,12 +112,14 @@ class TagChartsViewController: UIViewController { }() private lazy var syncButton: RuuviContextMenuButton = { - let button = RuuviContextMenuButton(menu: nil, - titleColor: .white, - title: "TagCharts.Sync.title".localized(), - icon: UIImage(systemName: "arrow.triangle.2.circlepath"), - iconTintColor: .white, - preccedingIcon: true) + let button = RuuviContextMenuButton( + menu: nil, + titleColor: .white, + title: "TagCharts.Sync.title".localized(), + icon: UIImage(named: "icon_sync_bt"), + iconTintColor: .white, + preccedingIcon: true + ) button.button.showsMenuAsPrimaryAction = false button.button.addTarget(self, action: #selector(syncButtonDidTap), for: .touchUpInside) @@ -133,10 +135,31 @@ class TagChartsViewController: UIViewController { return button }() + private lazy var updatedAtLabel: UILabel = { + let label = UILabel() + label.textColor = .white.withAlphaComponent(0.8) + label.textAlignment = .right + label.numberOfLines = 0 + label.font = UIFont.Muli(.regular, size: 14) + return label + }() + + private lazy var dataSourceIconView: UIImageView = { + let iv = UIImageView() + iv.contentMode = .scaleAspectFit + iv.backgroundColor = .clear + iv.alpha = 0.7 + return iv + }() // UI END private let minimumHistoryLimit: Int = 1 // Day private let maximumHistoryLimit: Int = 10 // Days + private var timer: Timer? + + deinit { + timer?.invalidate() + } // MARK: - LIFECYCLE override func viewDidLoad() { @@ -148,6 +171,7 @@ class TagChartsViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + hideNoDataLabel() output.viewWillAppear() } @@ -157,6 +181,14 @@ class TagChartsViewController: UIViewController { output.viewWillDisappear() } + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + updateChartsCollectionConstaints( + from: chartModules, + withAnimation: false + ) + } + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { coordinator.animate(alongsideTransition: { _ in }, completion: { [weak self] (_) in @@ -251,8 +283,9 @@ class TagChartsViewController: UIViewController { leading: chartToolbarView.leadingAnchor, bottom: nil, trailing: nil, + padding: .init(top: 0, left: 8, bottom: 0, right: 0), size: .init(width: 0, - height: 24)) + height: 28)) syncButton.centerYInSuperview() syncButton.alpha = 1 @@ -261,7 +294,7 @@ class TagChartsViewController: UIViewController { leading: view.safeLeftAnchor, bottom: view.safeBottomAnchor, trailing: view.safeRightAnchor, - padding: .init(top: 6, left: 0, bottom: 0, right: 0)) + padding: .init(top: 6, left: 0, bottom: 28, right: 0)) scrollView.addSubview(temperatureChartView) temperatureChartView.anchor(top: scrollView.topAnchor, @@ -301,6 +334,40 @@ class TagChartsViewController: UIViewController { noDataLabel.centerYInSuperview() noDataLabel.alpha = 0 + let footerView = UIView(color: .clear) + view.addSubview(footerView) + footerView.anchor(top: scrollView.bottomAnchor, + leading: view.safeLeftAnchor, + bottom: view.safeBottomAnchor, + trailing: view.safeRightAnchor, + padding: .init(top: 4, + left: 16, + bottom: 8, + right: 16), + size: .init(width: 0, height: 24)) + + footerView.addSubview(updatedAtLabel) + updatedAtLabel.anchor(top: footerView.topAnchor, + leading: nil, + bottom: footerView.bottomAnchor, + trailing: nil, + padding: .init(top: 0, + left: 12, + bottom: 0, + right: 0)) + + footerView.addSubview(dataSourceIconView) + dataSourceIconView.anchor(top: nil, + leading: updatedAtLabel.trailingAnchor, + bottom: nil, + trailing: footerView.trailingAnchor, + padding: .init(top: 0, + left: 6, + bottom: 0, + right: 0), + size: .init(width: 20, height: 20)) + dataSourceIconView.centerYInSuperview() + } @objc fileprivate func syncButtonDidTap() { @@ -502,6 +569,28 @@ extension TagChartsViewController: TagChartsViewInput { unit: settings.pressureUnit.symbol) } + func updateLatestRecordStatus(with record: RuuviTagSensorRecord) { + // Ago + let date = record.date.ruuviAgo() + updatedAtLabel.text = date + startTimer(with: record.date) + // Source + switch record.source { + case .unknown: + dataSourceIconView.image = nil + case .advertisement: + dataSourceIconView.image = RuuviAssets.advertisementImage + case .heartbeat: + dataSourceIconView.image = RuuviAssets.heartbeatImage + case .log: + dataSourceIconView.image = RuuviAssets.heartbeatImage + case .ruuviNetwork: + dataSourceIconView.image = RuuviAssets.ruuviNetworkImage + case .weatherProvider: + dataSourceIconView.image = RuuviAssets.weatherProviderImage + } + } + func localize() { syncButton.updateTitle(with: "TagCharts.Sync.title".localized()) } @@ -581,15 +670,15 @@ extension TagChartsViewController: TagChartsViewInput { } func showSyncConfirmationDialog(for viewModel: TagChartsViewModel) { - let title = "bluetooth_download".localized() - let message = "bluetooth_download_description".localized() + let title = "synchronisation".localized() + let message = "gatt_sync_description".localized() let alertVC = UIAlertController(title: title, message: message, preferredStyle: .alert) alertVC.addAction(UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: nil)) - let actionTitle = "download".localized() + let actionTitle = "do_not_show_again".localized() alertVC.addAction(UIAlertAction(title: actionTitle, style: .default, handler: { [weak self] _ in - self?.output.viewDidStartSync(for: viewModel) + self?.output.viewDidTriggerDoNotShowSyncDialog() })) present(alertVC, animated: true) @@ -657,8 +746,6 @@ extension TagChartsViewController { noDataLabel.alpha = 0 chartViews.removeAll() - view.setNeedsLayout() - view.layoutIfNeeded() let scrollViewHeight = scrollView.frame.height guard viewIsVisible && scrollViewHeight > 0 && from.count > 0 else { return @@ -808,6 +895,17 @@ extension TagChartsViewController { noDataLabel.alpha = 1 } } + + private func startTimer(with date: Date?) { + timer?.invalidate() + timer = nil + + timer = Timer.scheduledTimer(withTimeInterval: 1, + repeats: true, + block: { [weak self] (_) in + self?.updatedAtLabel.text = date?.ruuviAgo() ?? "Cards.UpdatedLabel.NoData.message".localized() + }) + } } extension TagChartsViewController: RuuviServiceMeasurementDelegate { diff --git a/station/Classes/Presentation/Modules/Dashboard/Home/Presenter/DashboardPresenter.swift b/station/Classes/Presentation/Modules/Dashboard/Home/Presenter/DashboardPresenter.swift index b7448e442..8ce736d69 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Home/Presenter/DashboardPresenter.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Home/Presenter/DashboardPresenter.swift @@ -341,7 +341,7 @@ extension DashboardPresenter: MenuModuleOutput { func menu(module: MenuModuleInput, didSelectGetMoreSensors sender: Any?) { module.dismiss() - router.openRuuviProductsPage() + router.openRuuviProductsPageFromMenu() } func menu(module: MenuModuleInput, didSelectFeedback sender: Any?) { @@ -982,6 +982,8 @@ extension DashboardPresenter { sSelf.startObservingWebTags() sSelf.restartObservingRuuviTagLastRecords() case .insert(let sensor): + sSelf.notifyRestartAdvertisementDaemon() + sSelf.notifyRestartHeartBeatDaemon() sSelf.checkFirmwareVersion(for: sensor) sSelf.ruuviTags.append(sensor.any) @@ -1800,5 +1802,23 @@ extension DashboardPresenter { private func notifyViewModelUpdate(for viewModel: CardsViewModel) { view?.applyUpdate(to: viewModel) } + + private func notifyRestartAdvertisementDaemon() { + // Notify daemon to restart + NotificationCenter + .default + .post(name: .RuuviTagAdvertisementDaemonShouldRestart, + object: nil, + userInfo: nil) + } + + private func notifyRestartHeartBeatDaemon() { + // Notify daemon to restart + NotificationCenter + .default + .post(name: .RuuviTagHeartBeatDaemonShouldRestart, + object: nil, + userInfo: nil) + } } // swiftlint:enable file_length trailing_whitespace diff --git a/station/Classes/Presentation/Modules/Dashboard/Home/Router/DashboardRouter.swift b/station/Classes/Presentation/Modules/Dashboard/Home/Router/DashboardRouter.swift index cd8138362..5ac142de9 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Home/Router/DashboardRouter.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Home/Router/DashboardRouter.swift @@ -71,6 +71,13 @@ class DashboardRouter: NSObject, DashboardRouterInput { UIApplication.shared.open(url, options: [:], completionHandler: nil) } + func openRuuviProductsPageFromMenu() { + guard let url = URL(string: "Ruuvi.BuySensors.Menu.URL.IOS".localized()) else { + return + } + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } + func openSignIn(output: SignInBenefitsModuleOutput) { let factory: SignInBenefitsModuleFactory = SignInPromoModuleFactoryImpl() let module = factory.create() diff --git a/station/Classes/Presentation/Modules/Dashboard/Home/Router/DashboardRouterInput.swift b/station/Classes/Presentation/Modules/Dashboard/Home/Router/DashboardRouterInput.swift index caf422ecc..7b86cfcfb 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Home/Router/DashboardRouterInput.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Home/Router/DashboardRouterInput.swift @@ -9,6 +9,7 @@ protocol DashboardRouterInput { func openAbout() func openWhatToMeasurePage() func openRuuviProductsPage() + func openRuuviProductsPageFromMenu() func openSignIn(output: SignInBenefitsModuleOutput) // swiftlint:disable:next function_parameter_count func openCardImageView(with viewModels: [CardsViewModel], diff --git a/station/Classes/Presentation/Modules/Dashboard/Home/View/RuuviContextMenuButton.swift b/station/Classes/Presentation/Modules/Dashboard/Home/View/RuuviContextMenuButton.swift index 18790b5f3..60a856f23 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Home/View/RuuviContextMenuButton.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Home/View/RuuviContextMenuButton.swift @@ -65,10 +65,10 @@ extension RuuviContextMenuButton { ]) } buttonIconView.heightAnchor.constraint( - lessThanOrEqualToConstant: 12 + lessThanOrEqualToConstant: 16 ).isActive = true buttonIconView.widthAnchor.constraint( - lessThanOrEqualToConstant: 12 + lessThanOrEqualToConstant: 16 ).isActive = true stackView.axis = .horizontal stackView.spacing = 6 diff --git a/station/Classes/Presentation/Modules/Settings/Module/Presenter/SettingsPresenter.swift b/station/Classes/Presentation/Modules/Settings/Module/Presenter/SettingsPresenter.swift index 548fa7dab..ef97ad6d3 100644 --- a/station/Classes/Presentation/Modules/Settings/Module/Presenter/SettingsPresenter.swift +++ b/station/Classes/Presentation/Modules/Settings/Module/Presenter/SettingsPresenter.swift @@ -139,6 +139,10 @@ extension SettingsPresenter: SettingsViewOutput { func viewDidTapAppearance() { router.openAppearance() } + + func viewDidTapAlertNotifications() { + router.openAlertNotificationsSettings() + } } extension SettingsPresenter: DefaultsModuleOutput { diff --git a/station/Classes/Presentation/Modules/Settings/Module/Router/SettingsRouter.swift b/station/Classes/Presentation/Modules/Settings/Module/Router/SettingsRouter.swift index 21d8eea06..b4d92da96 100644 --- a/station/Classes/Presentation/Modules/Settings/Module/Router/SettingsRouter.swift +++ b/station/Classes/Presentation/Modules/Settings/Module/Router/SettingsRouter.swift @@ -96,4 +96,15 @@ class SettingsRouter: SettingsRouterInput { animated: true ) } + + func openAlertNotificationsSettings() { + let factory: NotificationsSettingsModuleFactory = NotificationsSettingsModuleFactoryImpl() + let module = factory.create() + transitionHandler + .navigationController? + .pushViewController( + module, + animated: true + ) + } } diff --git a/station/Classes/Presentation/Modules/Settings/Module/Router/SettingsRouterInput.swift b/station/Classes/Presentation/Modules/Settings/Module/Router/SettingsRouterInput.swift index 28d26cc9f..1e723ef50 100644 --- a/station/Classes/Presentation/Modules/Settings/Module/Router/SettingsRouterInput.swift +++ b/station/Classes/Presentation/Modules/Settings/Module/Router/SettingsRouterInput.swift @@ -10,4 +10,5 @@ protocol SettingsRouterInput { func openUnitSettings(with viewModel: UnitSettingsViewModel, output: UnitSettingsModuleOutput?) func openRuuviCloud() func openAppearance() + func openAlertNotificationsSettings() } diff --git a/station/Classes/Presentation/Modules/Settings/Module/Settings.storyboard b/station/Classes/Presentation/Modules/Settings/Module/Settings.storyboard index 65a821d9d..cf0f9f5dd 100644 --- a/station/Classes/Presentation/Modules/Settings/Module/Settings.storyboard +++ b/station/Classes/Presentation/Modules/Settings/Module/Settings.storyboard @@ -57,9 +57,30 @@ - + + + + + + + + + + + + + + + + + @@ -79,7 +100,7 @@ - + @@ -100,7 +121,7 @@ - + @@ -122,7 +143,7 @@ - + @@ -144,7 +165,7 @@ - + @@ -166,14 +187,14 @@ - + - + - + @@ -210,7 +231,7 @@ - + @@ -231,7 +252,7 @@ - + @@ -252,7 +273,7 @@ - + @@ -290,6 +311,8 @@ + + diff --git a/station/Classes/Presentation/Modules/Settings/Module/View/SettingsViewOutput.swift b/station/Classes/Presentation/Modules/Settings/Module/View/SettingsViewOutput.swift index b12624f30..0dfaa5511 100644 --- a/station/Classes/Presentation/Modules/Settings/Module/View/SettingsViewOutput.swift +++ b/station/Classes/Presentation/Modules/Settings/Module/View/SettingsViewOutput.swift @@ -16,4 +16,5 @@ protocol SettingsViewOutput { func viewDidTapRuuviCloud() func viewDidSelectChangeLanguage() func viewDidTapAppearance() + func viewDidTapAlertNotifications() } diff --git a/station/Classes/Presentation/Modules/Settings/Module/View/Table/SettingsTableViewController.swift b/station/Classes/Presentation/Modules/Settings/Module/View/Table/SettingsTableViewController.swift index 2f31e832a..457a56786 100644 --- a/station/Classes/Presentation/Modules/Settings/Module/View/Table/SettingsTableViewController.swift +++ b/station/Classes/Presentation/Modules/Settings/Module/View/Table/SettingsTableViewController.swift @@ -4,6 +4,9 @@ import RuuviOntology class SettingsTableViewController: UITableViewController { var output: SettingsViewOutput! + @IBOutlet weak var alertNotificationsCell: UITableViewCell! + @IBOutlet weak var alertNotificationsTitleLabel: UILabel! + @IBOutlet weak var appearanceCell: UITableViewCell! @IBOutlet weak var appearanceTitleLabel: UILabel! @@ -80,6 +83,7 @@ extension SettingsTableViewController: SettingsViewInput { chartTitleLabel.text = "Settings.Label.Chart".localized() ruuviCloudTitleLabel.text = "ruuvi_cloud".localized() appearanceTitleLabel.text = "settings_appearance".localized() + alertNotificationsTitleLabel.text = "settings_alert_notifications".localized() updateUILanguage() tableView.reloadData() } @@ -174,6 +178,8 @@ extension SettingsTableViewController { output.viewDidTapRuuviCloud() case appearanceCell: output.viewDidTapAppearance() + case alertNotificationsCell: + output.viewDidTapAlertNotifications() default: break } diff --git a/station/Classes/Presentation/Modules/Settings/Submodules/Defaults/Presenter/DefaultsPresenter.swift b/station/Classes/Presentation/Modules/Settings/Submodules/Defaults/Presenter/DefaultsPresenter.swift index aa64e5549..d7e4a1649 100644 --- a/station/Classes/Presentation/Modules/Settings/Submodules/Defaults/Presenter/DefaultsPresenter.swift +++ b/station/Classes/Presentation/Modules/Settings/Submodules/Defaults/Presenter/DefaultsPresenter.swift @@ -31,8 +31,12 @@ class DefaultsPresenter: NSObject, DefaultsModuleInput { buildAskForReviewLater(), buildDashboardCardTapAction(), buildConnectToDevServer(), + buildHideNFCButtonInSensorContents(), buildIsAuthorized(), - buildAuthToken()] + buildAuthToken(), + buildShowEmailAlertSettings(), + buildShowPushAlertSettings(), + buildIsAuthorized()] self.output = output } @@ -288,6 +292,47 @@ extension DefaultsPresenter { return viewModel } + private func buildHideNFCButtonInSensorContents() -> DefaultsViewModel { + let viewModel = DefaultsViewModel() + viewModel.title = "Defaults.HideNFC.title".localized() + viewModel.boolean.value = settings.hideNFCForSensorContest + viewModel.type.value = .switcher + + bind(viewModel.boolean, + fire: false) { observer, hideNFC in + observer.settings.hideNFCForSensorContest = hideNFC.bound + } + return viewModel + } + + private func buildShowEmailAlertSettings() -> DefaultsViewModel { + + let viewModel = DefaultsViewModel() + viewModel.title = "Defaults.ShowEmailAlertsSettings.title".localized() + viewModel.boolean.value = settings.showEmailAlertSettings + viewModel.type.value = .switcher + + bind(viewModel.boolean, fire: false) { observer, show in + observer.settings.showEmailAlertSettings = GlobalHelpers.getBool(from: show) + } + + return viewModel + } + + private func buildShowPushAlertSettings() -> DefaultsViewModel { + + let viewModel = DefaultsViewModel() + viewModel.title = "Defaults.ShowPushAlertsSettings.title".localized() + viewModel.boolean.value = settings.showPushAlertSettings + viewModel.type.value = .switcher + + bind(viewModel.boolean, fire: false) { observer, show in + observer.settings.showPushAlertSettings = GlobalHelpers.getBool(from: show) + } + + return viewModel + } + } extension DefaultsPresenter { diff --git a/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Assembly/NotificationsSettingsModuleFactory.swift b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Assembly/NotificationsSettingsModuleFactory.swift new file mode 100644 index 000000000..8e2b97d41 --- /dev/null +++ b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Assembly/NotificationsSettingsModuleFactory.swift @@ -0,0 +1,29 @@ +import UIKit +import RuuviLocal +import RuuviService + +protocol NotificationsSettingsModuleFactory { + func create() -> NotificationsSettingsTableViewController +} + +final class NotificationsSettingsModuleFactoryImpl: NotificationsSettingsModuleFactory { + func create() -> NotificationsSettingsTableViewController { + let r = AppAssembly.shared.assembler.resolver + + let view = NotificationsSettingsTableViewController( + title: "settings_alert_notifications".localized() + ) + let router = NotificationsSettingsRouter() + router.transitionHandler = view + + let presenter = NotificationsSettingsPresenter() + presenter.view = view + presenter.settings = r.resolve(RuuviLocalSettings.self) + presenter.ruuviAppSettingsService = r.resolve(RuuviServiceAppSettings.self) + presenter.cloudNotificationService = r.resolve(RuuviServiceCloudNotification.self) + presenter.router = router + + view.output = presenter + return view + } +} diff --git a/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Presenter/NotificationsSettingsModuleInput.swift b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Presenter/NotificationsSettingsModuleInput.swift new file mode 100644 index 000000000..658300036 --- /dev/null +++ b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Presenter/NotificationsSettingsModuleInput.swift @@ -0,0 +1,3 @@ +import Foundation + +protocol NotificationsSettingsModuleInput: AnyObject {} diff --git a/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Presenter/NotificationsSettingsPresenter.swift b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Presenter/NotificationsSettingsPresenter.swift new file mode 100644 index 000000000..8d3509adf --- /dev/null +++ b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Presenter/NotificationsSettingsPresenter.swift @@ -0,0 +1,173 @@ +import Foundation +import UIKit +import RuuviOntology +import RuuviLocal +import RuuviService + +class NotificationsSettingsPresenter: NSObject, NotificationsSettingsModuleInput { + weak var view: NotificationsSettingsViewInput? + var router: NotificationsSettingsRouterInput! + + var settings: RuuviLocalSettings! + var ruuviAppSettingsService: RuuviServiceAppSettings! + var cloudNotificationService: RuuviServiceCloudNotification! + + private var settingsViewModels: [NotificationsSettingsViewModel] = [] { + didSet { + view?.viewModels = settingsViewModels + } + } + + private var soundSettingsToken: NSObjectProtocol? + private var emailAlertsSettingsToken: NSObjectProtocol? + private var pushAlertsSettingsToken: NSObjectProtocol? + + deinit { + soundSettingsToken?.invalidate() + emailAlertsSettingsToken?.invalidate() + pushAlertsSettingsToken?.invalidate() + } +} + +extension NotificationsSettingsPresenter: NotificationsSettingsViewOutput { + func viewDidLoad() { + configure() + startObservingAlertSoundSetting() + startObservingEmailAlertSetting() + startObservingPushAlertSetting() + } + + func viewDidTapSoundSelection() { + let pushAlertSoundViewModel = PushAlertSoundSelectionViewModel( + title: "settings_alert_sound".localized(), + items: [ + RuuviAlertSound.systemDefault, + RuuviAlertSound.ruuviSpeak + ], + selection: settings.alertSound + ) + router.openSelection(with: pushAlertSoundViewModel) + } +} + +extension NotificationsSettingsPresenter { + fileprivate func configure() { + var viewModels: [NotificationsSettingsViewModel] = [] + + if settings.showEmailAlertSettings { + viewModels.append(buildEmailAlertSettings()) + } + + if settings.showPushAlertSettings { + viewModels.append(buildPushSettings()) + } + + viewModels.append(buildSoundSettings()) + + settingsViewModels = viewModels + } + + private func buildEmailAlertSettings() -> NotificationsSettingsViewModel { + let viewModel = NotificationsSettingsViewModel() + viewModel.title = "settings_email_alerts".localized() + viewModel.subtitle = "settings_email_alerts_description".localized() + viewModel.boolean.value = settings.emailAlertEnabled + viewModel.configType.value = .switcher + viewModel.settingsType.value = .email + + bind(viewModel.boolean, fire: false) { observer, enabled in + let alertEnabled = GlobalHelpers.getBool(from: enabled) + observer.settings.emailAlertEnabled = alertEnabled + observer.ruuviAppSettingsService.set(emailAlert: alertEnabled) + } + + return viewModel + } + + private func buildPushSettings() -> NotificationsSettingsViewModel { + let viewModel = NotificationsSettingsViewModel() + viewModel.title = "settings_push_alerts".localized() + viewModel.subtitle = "settings_push_alerts_description".localized() + viewModel.boolean.value = settings.pushAlertEnabled + viewModel.configType.value = .switcher + viewModel.settingsType.value = .push + + bind(viewModel.boolean, fire: false) { observer, enabled in + let alertEnabled = GlobalHelpers.getBool(from: enabled) + observer.settings.pushAlertEnabled = alertEnabled + observer.ruuviAppSettingsService.set(pushAlert: alertEnabled) + } + + return viewModel + } + + private func buildSoundSettings() -> NotificationsSettingsViewModel { + let viewModel = NotificationsSettingsViewModel() + viewModel.title = "settings_alert_sound".localized() + viewModel.subtitle = "settings_alert_sound_description".localized() + viewModel.value.value = settings.alertSound.title + viewModel.configType.value = .plain + viewModel.settingsType.value = .alertSound + return viewModel + } + + private func startObservingAlertSoundSetting() { + soundSettingsToken = NotificationCenter + .default + .addObserver(forName: .AlertSoundSettingsDidChange, + object: nil, + queue: .main, + using: { [weak self] (_) in + self?.configure() + guard let sSelf = self else { return } + DispatchQueue.main.async { + sSelf.cloudNotificationService.set( + sound: sSelf.settings.alertSound, + deviceName: UIDevice.modelName + ) + } + }) + } + + private func startObservingEmailAlertSetting() { + emailAlertsSettingsToken = NotificationCenter + .default + .addObserver(forName: .EmailAlertSettingsDidChange, + object: nil, + queue: .main, + using: { [weak self] (_) in + self?.updateEmailViewModel() + }) + } + + private func startObservingPushAlertSetting() { + pushAlertsSettingsToken = NotificationCenter + .default + .addObserver(forName: .PushAlertSettingsDidChange, + object: nil, + queue: .main, + using: { [weak self] (_) in + self?.updatePushViewModel() + }) + } + + private func updateEmailViewModel() { + if let viewModel = settingsViewModels.first(where: { vm in + vm.settingsType.value == .email + }) { + if viewModel.boolean.value != settings.emailAlertEnabled { + configure() + } + } + } + + private func updatePushViewModel() { + if let viewModel = settingsViewModels.first(where: { vm in + vm.settingsType.value == .push + }) { + if viewModel.boolean.value != settings.pushAlertEnabled { + configure() + } + } + } +} diff --git a/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Router/NotificationsSettingsRouter.swift b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Router/NotificationsSettingsRouter.swift new file mode 100644 index 000000000..7676cfb00 --- /dev/null +++ b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Router/NotificationsSettingsRouter.swift @@ -0,0 +1,25 @@ +import LightRoute + +class NotificationsSettingsRouter: NotificationsSettingsRouterInput { + weak var transitionHandler: UIViewController? + + func dismiss() { + try? transitionHandler?.closeCurrentModule().perform() + } + + func openSelection(with viewModel: PushAlertSoundSelectionViewModel) { + let factory: PushAlertSoundSelectionModuleFactory = PushAlertSoundSelectionModuleFactoryImpl() + let module = factory.create(with: viewModel.title) + + transitionHandler? + .navigationController? + .pushViewController( + module, + animated: true + ) + + if let output = module.output as? PushAlertSoundSelectionModuleInput { + output.configure(viewModel: viewModel) + } + } +} diff --git a/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Router/NotificationsSettingsRouterInput.swift b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Router/NotificationsSettingsRouterInput.swift new file mode 100644 index 000000000..53e8e15c8 --- /dev/null +++ b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Router/NotificationsSettingsRouterInput.swift @@ -0,0 +1,6 @@ +import Foundation + +protocol NotificationsSettingsRouterInput { + func dismiss() + func openSelection(with viewModel: PushAlertSoundSelectionViewModel) +} diff --git a/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Selection/Assembly/PushAlertSoundSelectionModuleFactory.swift b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Selection/Assembly/PushAlertSoundSelectionModuleFactory.swift new file mode 100644 index 000000000..ec9360721 --- /dev/null +++ b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Selection/Assembly/PushAlertSoundSelectionModuleFactory.swift @@ -0,0 +1,23 @@ +import UIKit +import RuuviLocal + +protocol PushAlertSoundSelectionModuleFactory { + func create(with title: String) -> PushAlertSoundSelectionTableViewController +} + +final class PushAlertSoundSelectionModuleFactoryImpl: PushAlertSoundSelectionModuleFactory { + func create(with title: String) -> PushAlertSoundSelectionTableViewController { + let r = AppAssembly.shared.assembler.resolver + + let view = PushAlertSoundSelectionTableViewController( + title: title + ) + + let presenter = PushAlertSoundSelectionPresenter() + presenter.view = view + presenter.settings = r.resolve(RuuviLocalSettings.self) + + view.output = presenter + return view + } +} diff --git a/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Selection/Presenter/PushAlertSoundSelectionModuleInput.swift b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Selection/Presenter/PushAlertSoundSelectionModuleInput.swift new file mode 100644 index 000000000..8ee468b05 --- /dev/null +++ b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Selection/Presenter/PushAlertSoundSelectionModuleInput.swift @@ -0,0 +1,6 @@ +import Foundation +import UIKit + +protocol PushAlertSoundSelectionModuleInput: AnyObject { + func configure(viewModel: PushAlertSoundSelectionViewModel) +} diff --git a/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Selection/Presenter/PushAlertSoundSelectionPresenter.swift b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Selection/Presenter/PushAlertSoundSelectionPresenter.swift new file mode 100644 index 000000000..7c11258f9 --- /dev/null +++ b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Selection/Presenter/PushAlertSoundSelectionPresenter.swift @@ -0,0 +1,42 @@ +import Foundation +import UIKit +import RuuviOntology +import RuuviLocal + +class PushAlertSoundSelectionPresenter: NSObject { + weak var view: PushAlertSoundSelectionViewInput? + + var settings: RuuviLocalSettings! + + private var viewModel: PushAlertSoundSelectionViewModel? { + didSet { + view?.viewModel = viewModel + } + } +} + +extension PushAlertSoundSelectionPresenter: PushAlertSoundSelectionViewOutput { + func viewDidLoad() { + // No op. + } + + func viewDidSelectItem(item: SelectionItemProtocol) { + if let selectedSound = item as? RuuviAlertSound, + let viewModel = viewModel { + settings.alertSound = selectedSound + let updatedViewModel = PushAlertSoundSelectionViewModel( + title: viewModel.title, + items: viewModel.items, + selection: selectedSound + ) + self.viewModel = updatedViewModel + view?.playSelectedSound(from: selectedSound) + } + } +} + +extension PushAlertSoundSelectionPresenter: PushAlertSoundSelectionModuleInput { + func configure(viewModel: PushAlertSoundSelectionViewModel) { + self.viewModel = viewModel + } +} diff --git a/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Selection/View/PushAlertSoundSelectionViewInput.swift b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Selection/View/PushAlertSoundSelectionViewInput.swift new file mode 100644 index 000000000..52c28e762 --- /dev/null +++ b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Selection/View/PushAlertSoundSelectionViewInput.swift @@ -0,0 +1,7 @@ +import Foundation +import RuuviOntology + +protocol PushAlertSoundSelectionViewInput: ViewInput { + var viewModel: PushAlertSoundSelectionViewModel? { get set } + func playSelectedSound(from sound: RuuviAlertSound) +} diff --git a/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Selection/View/PushAlertSoundSelectionViewModel.swift b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Selection/View/PushAlertSoundSelectionViewModel.swift new file mode 100644 index 000000000..9a9f9223a --- /dev/null +++ b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Selection/View/PushAlertSoundSelectionViewModel.swift @@ -0,0 +1,7 @@ +import Foundation + +struct PushAlertSoundSelectionViewModel { + let title: String + let items: [SelectionItemProtocol] + let selection: SelectionItemProtocol +} diff --git a/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Selection/View/PushAlertSoundSelectionViewOutput.swift b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Selection/View/PushAlertSoundSelectionViewOutput.swift new file mode 100644 index 000000000..d4c25be35 --- /dev/null +++ b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Selection/View/PushAlertSoundSelectionViewOutput.swift @@ -0,0 +1,6 @@ +import Foundation + +protocol PushAlertSoundSelectionViewOutput { + func viewDidLoad() + func viewDidSelectItem(item: SelectionItemProtocol) +} diff --git a/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Selection/View/UI/PushAlertSoundSelectionTableViewCell.swift b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Selection/View/UI/PushAlertSoundSelectionTableViewCell.swift new file mode 100644 index 000000000..0a81233ab --- /dev/null +++ b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Selection/View/UI/PushAlertSoundSelectionTableViewCell.swift @@ -0,0 +1,53 @@ +import UIKit + +class PushAlertSelectionTableViewCell: UITableViewCell { + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.textColor = RuuviColor.ruuviTextColor + label.textAlignment = .left + label.numberOfLines = 1 + label.font = UIFont.Muli(.regular, size: 16) + return label + }() + + override init(style: UITableViewCell.CellStyle, + reuseIdentifier: String?) { + super.init(style: style, + reuseIdentifier: reuseIdentifier) + setUpUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + fileprivate func setUpUI() { + backgroundColor = .clear + tintColor = RuuviColor.ruuviTintColor + + addSubview(titleLabel) + titleLabel.anchor(top: safeTopAnchor, + leading: safeLeftAnchor, + bottom: safeBottomAnchor, + trailing: contentView.safeRightAnchor, + padding: .init(top: 12, left: 20, bottom: 12, right: 8)) + } +} + +// MARK: - SETTERS + +extension PushAlertSelectionTableViewCell { + func configure(title: String?, selection: String?) { + titleLabel.text = title + + let isSelected = title == selection + titleLabel.font = isSelected ? + UIFont.Muli(.bold, size: 16) : + UIFont.Muli(.regular, size: 16) + titleLabel.textColor = isSelected ? + RuuviColor.ruuviMenuTextColor : + RuuviColor.ruuviTextColor + accessoryType = isSelected ? .checkmark : .none + } +} diff --git a/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Selection/View/UI/PushAlertSoundSelectionTableViewController.swift b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Selection/View/UI/PushAlertSoundSelectionTableViewController.swift new file mode 100644 index 000000000..ea6a7cee8 --- /dev/null +++ b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/Selection/View/UI/PushAlertSoundSelectionTableViewController.swift @@ -0,0 +1,137 @@ +import UIKit +import Foundation +import AVFoundation +import RuuviOntology + +// swiftlint:disable:next type_name +class PushAlertSoundSelectionTableViewController: UITableViewController { + var output: PushAlertSoundSelectionViewOutput! + var viewModel: PushAlertSoundSelectionViewModel? { + didSet { + updateUI() + } + } + + private var audioPlayer: AVAudioPlayer? + + init(title: String) { + super.init(style: .grouped) + self.title = title + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + audioPlayer?.invalidate() + audioPlayer = nil + } + + private let reuseIdentifier: String = "reuseIdentifier" +} + +// MARK: - LIFECYCLE +extension PushAlertSoundSelectionTableViewController { + override func viewDidLoad() { + super.viewDidLoad() + setupLocalization() + setUpUI() + output.viewDidLoad() + } +} + +extension PushAlertSoundSelectionTableViewController: PushAlertSoundSelectionViewInput { + func localize() { + // no op. + } + + func playSelectedSound(from sound: RuuviAlertSound) { + switch sound { + case .systemDefault: + break + default: + playSound(from: sound) + } + } +} + +extension PushAlertSoundSelectionTableViewController { + fileprivate func setUpUI() { + view.backgroundColor = RuuviColor.ruuviPrimary + setUpTableView() + } + + fileprivate func setUpTableView() { + tableView.sectionFooterHeight = UITableView.automaticDimension + tableView.register(PushAlertSelectionTableViewCell.self, + forCellReuseIdentifier: reuseIdentifier) + } + + fileprivate func updateUI() { + if isViewLoaded { + DispatchQueue.main.async(execute: { [weak self] in + self?.tableView.reloadData() + }) + } + } +} + +// MARK: - UITableViewDataSource +extension PushAlertSoundSelectionTableViewController { + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return viewModel?.items.count ?? 0 + } + + override func tableView(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell( + withIdentifier: reuseIdentifier, + for: indexPath + ) as? PushAlertSelectionTableViewCell else { + fatalError() + } + if let viewModel = viewModel { + let item = viewModel.items[indexPath.row] + cell.configure( + title: item.title, + selection: viewModel.selection.title + ) + } + return cell + } +} + +// MARK: - UITableViewDelegate +extension PushAlertSoundSelectionTableViewController { + override func tableView(_ tableView: UITableView, + didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + if let viewModel = viewModel { + output.viewDidSelectItem(item: viewModel.items[indexPath.row]) + } + } +} + +// MARK: - Audio Player +extension PushAlertSoundSelectionTableViewController { + func playSound(from sound: RuuviAlertSound) { + audioPlayer?.invalidate() + audioPlayer = nil + + guard let audioURL = Bundle.main.url( + forResource: sound.fileName, + withExtension: "caf" + ) else { + return + } + + do { + audioPlayer = try? AVAudioPlayer(contentsOf: audioURL) + audioPlayer?.prepareToPlay() + audioPlayer?.play() + } + } + +} diff --git a/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/View/NotificationsSettingsViewInput.swift b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/View/NotificationsSettingsViewInput.swift new file mode 100644 index 000000000..ff01c5790 --- /dev/null +++ b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/View/NotificationsSettingsViewInput.swift @@ -0,0 +1,5 @@ +import Foundation + +protocol NotificationsSettingsViewInput: ViewInput { + var viewModels: [NotificationsSettingsViewModel] { get set } +} diff --git a/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/View/NotificationsSettingsViewModel.swift b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/View/NotificationsSettingsViewModel.swift new file mode 100644 index 000000000..5c854e961 --- /dev/null +++ b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/View/NotificationsSettingsViewModel.swift @@ -0,0 +1,28 @@ +import Foundation +import RuuviOntology + +enum NotificationsSettingsConfigType { + case switcher + case plain +} + +enum NotificationsSettingsType { + case email + case push + case alertSound +} + +class NotificationsSettingsViewModel: Identifiable { + var id = UUID().uuidString + + var title: String? + var subtitle: String? + var configType: Observable = + Observable() + var settingsType: Observable = + Observable() + // Value for switcher type + var boolean: Observable = Observable() + // Value for plain type + var value: Observable = Observable() +} diff --git a/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/View/NotificationsSettingsViewOutput.swift b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/View/NotificationsSettingsViewOutput.swift new file mode 100644 index 000000000..4099e5b41 --- /dev/null +++ b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/View/NotificationsSettingsViewOutput.swift @@ -0,0 +1,6 @@ +import Foundation + +protocol NotificationsSettingsViewOutput { + func viewDidLoad() + func viewDidTapSoundSelection() +} diff --git a/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/View/UI/NotificationsSettingsSwitchCell.swift b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/View/UI/NotificationsSettingsSwitchCell.swift new file mode 100644 index 000000000..fe5e03526 --- /dev/null +++ b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/View/UI/NotificationsSettingsSwitchCell.swift @@ -0,0 +1,103 @@ +import UIKit + +protocol NotificationsSettingsSwitchCellDelegate: NSObjectProtocol { + func didToggleSwitch(isOn: Bool, sender: NotificationsSettingsSwitchCell) +} + +class NotificationsSettingsSwitchCell: UITableViewCell { + + weak var delegate: NotificationsSettingsSwitchCellDelegate? + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.textColor = RuuviColor.ruuviTextColor + label.textAlignment = .left + label.numberOfLines = 1 + label.font = UIFont.Muli(.bold, size: 16) + return label + }() + + private lazy var subtitleLabel: UILabel = { + let label = UILabel() + label.textColor = RuuviColor + .dashboardIndicatorTextColor? + .withAlphaComponent(0.6) + label.textAlignment = .left + label.numberOfLines = 0 + label.font = UIFont.Muli(.regular, size: 13) + return label + }() + + lazy var statusSwitch: RuuviUISwitch = { + let toggle = RuuviUISwitch() + toggle.isOn = false + toggle.addTarget(self, + action: #selector(handleStatusToggle), + for: .valueChanged) + return toggle + }() + + override init(style: UITableViewCell.CellStyle, + reuseIdentifier: String?) { + super.init(style: style, + reuseIdentifier: reuseIdentifier) + setUpUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + fileprivate func setUpUI() { + selectionStyle = .none + contentView.isUserInteractionEnabled = true + + backgroundColor = .clear + + let textStack = UIStackView(arrangedSubviews: [ + titleLabel, subtitleLabel + ]) + textStack.spacing = 4 + textStack.distribution = .fillProportionally + textStack.axis = .vertical + contentView.addSubview(textStack) + textStack.anchor( + top: contentView.safeTopAnchor, + leading: contentView.safeLeftAnchor, + bottom: contentView.safeBottomAnchor, + trailing: nil, + padding: .init(top: 12, + left: 20, + bottom: 12, + right: 0) + ) + + contentView.addSubview(statusSwitch) + statusSwitch.anchor( + top: nil, + leading: textStack.trailingAnchor, + bottom: nil, + trailing: contentView.safeRightAnchor, + padding: .init(top: 0, left: 8, bottom: 0, right: 12) + ) + statusSwitch.centerYInSuperview() + } + + @objc private func handleStatusToggle(_ sender: RuuviUISwitch) { + delegate?.didToggleSwitch(isOn: sender.isOn, sender: self) + } +} + + // MARK: - SETTERS +extension NotificationsSettingsSwitchCell { + + func configure(title: String?, subtitle: String?, value: Bool?) { + titleLabel.text = title + subtitleLabel.text = subtitle + if let value = value { + statusSwitch.setOn(value, animated: false) + } else { + statusSwitch.setOn(false, animated: false) + } + } +} diff --git a/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/View/UI/NotificationsSettingsTableViewController.swift b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/View/UI/NotificationsSettingsTableViewController.swift new file mode 100644 index 000000000..9f917daf7 --- /dev/null +++ b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/View/UI/NotificationsSettingsTableViewController.swift @@ -0,0 +1,166 @@ +import UIKit +import Foundation + +class NotificationsSettingsTableViewController: UITableViewController { + var output: NotificationsSettingsViewOutput? + var viewModels = [NotificationsSettingsViewModel]() { + didSet { + updateUI() + } + } + + init(title: String) { + super.init(style: .grouped) + self.title = title + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private static let reuseIdentifierTextCell: String = "reuseIdentifierTextCell" + private static let reuseIdentifierSwitchCell: String = "reuseIdentifierSwitchCell" +} + +// MARK: - LIFECYCLE +extension NotificationsSettingsTableViewController { + override func viewDidLoad() { + super.viewDidLoad() + setupLocalization() + setUpUI() + output?.viewDidLoad() + } +} + +extension NotificationsSettingsTableViewController: NotificationsSettingsViewInput { + func localize() { + // no op. + } +} + +extension NotificationsSettingsTableViewController { + fileprivate func setUpUI() { + view.backgroundColor = RuuviColor.ruuviPrimary + setUpTableView() + } + + fileprivate func setUpTableView() { + tableView.rowHeight = UITableView.automaticDimension + tableView.estimatedRowHeight = 120 + tableView.sectionFooterHeight = UITableView.automaticDimension + tableView.register( + NotificationsSettingsTextCell.self, + forCellReuseIdentifier: Self.reuseIdentifierTextCell + ) + tableView.register( + NotificationsSettingsSwitchCell.self, + forCellReuseIdentifier: Self.reuseIdentifierSwitchCell + ) + } + + fileprivate func updateUI() { + if isViewLoaded { + DispatchQueue.main.async(execute: { [weak self] in + self?.tableView.reloadData() + }) + } + } +} + + // MARK: - UITableViewDataSource +extension NotificationsSettingsTableViewController { + + override func tableView(_ tableView: UITableView, + numberOfRowsInSection section: Int) -> Int { + return viewModels.count + } + + override func tableView(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let viewModel = viewModels[indexPath.row] + switch viewModel.configType.value { + case .plain: + guard let cell = tableView.dequeueReusableCell( + withIdentifier: Self.reuseIdentifierTextCell, + for: indexPath + ) as? NotificationsSettingsTextCell else { + fatalError() + } + cell.configure( + title: viewModel.title, + subtitle: viewModel.subtitle, + value: viewModel.value.value + ) + return cell + case .switcher: + guard let cell = tableView.dequeueReusableCell( + withIdentifier: Self.reuseIdentifierSwitchCell, + for: indexPath + ) as? NotificationsSettingsSwitchCell else { + fatalError() + } + cell.configure( + title: viewModel.title, + subtitle: viewModel.subtitle, + value: viewModel.boolean.value + ) + cell.delegate = self + return cell + default: + return UITableViewCell() + } + + } + + override func tableView(_ tableView: UITableView, + estimatedHeightForFooterInSection section: Int) -> CGFloat { + return 100 + } + + override func tableView(_ tableView: UITableView, + viewForFooterInSection section: Int) -> UIView? { + let footerView = UIView() + let footerTextView = RuuviLinkTextView( + fullTextString: "settings_alerts_footer_description".localized(), + linkString: "settings_alerts_footer_description_link_mask".localized(), + link: UIApplication.openSettingsURLString + ) + footerTextView.linkDelegate = self + footerView.addSubview(footerTextView) + footerTextView.fillSuperview(padding: .init(top: 0, left: 16, bottom: 0, right: 16)) + return footerView + } +} + +// MARK: - UITableViewDelegate +extension NotificationsSettingsTableViewController { + override func tableView(_ tableView: UITableView, + didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + let viewModel = viewModels[indexPath.row] + switch viewModel.settingsType.value { + case .alertSound: + output?.viewDidTapSoundSelection() + default: + break + } + } +} + +// MARK: - NotificationsSettingsSwitchCellDelegate +extension NotificationsSettingsTableViewController: NotificationsSettingsSwitchCellDelegate { + func didToggleSwitch(isOn: Bool, sender: NotificationsSettingsSwitchCell) { + if let indexPath = tableView.indexPath(for: sender) { + viewModels[indexPath.row].boolean.value = isOn + } + } +} + +extension NotificationsSettingsTableViewController: RuuviLinkTextViewDelegate { + func didTapLink(url: String) { + guard let settingsURL = URL(string: url) else { + return + } + UIApplication.shared.open(settingsURL) + } +} diff --git a/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/View/UI/NotificationsSettingsTextCell.swift b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/View/UI/NotificationsSettingsTextCell.swift new file mode 100644 index 000000000..fba531810 --- /dev/null +++ b/station/Classes/Presentation/Modules/Settings/Submodules/Notifications/View/UI/NotificationsSettingsTextCell.swift @@ -0,0 +1,77 @@ +import UIKit + +class NotificationsSettingsTextCell: UITableViewCell { + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.textColor = RuuviColor.ruuviMenuTextColor + label.textAlignment = .left + label.numberOfLines = 1 + label.font = UIFont.Muli(.bold, size: 16) + return label + }() + + private lazy var subtitleLabel: UILabel = { + let label = UILabel() + label.textColor = RuuviColor + .dashboardIndicatorTextColor? + .withAlphaComponent(0.6) + label.textAlignment = .left + label.numberOfLines = 0 + label.font = UIFont.Muli(.regular, size: 13) + return label + }() + + private lazy var valueLabel: UILabel = { + let label = UILabel() + label.textColor = RuuviColor.ruuviMenuTextColor + label.textAlignment = .right + label.numberOfLines = 1 + label.font = UIFont.Muli(.regular, size: 16) + return label + }() + + override init(style: UITableViewCell.CellStyle, + reuseIdentifier: String?) { + super.init(style: style, + reuseIdentifier: reuseIdentifier) + setUpUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + fileprivate func setUpUI() { + backgroundColor = .clear + accessoryType = .disclosureIndicator + + let leftStack = UIStackView(arrangedSubviews: [ + titleLabel, subtitleLabel + ]) + leftStack.spacing = 4 + leftStack.distribution = .fillProportionally + leftStack.axis = .vertical + + let fullStack = UIStackView(arrangedSubviews: [ + leftStack, valueLabel + ]) + fullStack.spacing = 4 + fullStack.distribution = .fillProportionally + fullStack.axis = .horizontal + + contentView.addSubview(fullStack) + fullStack.fillSuperviewToSafeArea( + padding: .init(top: 12, left: 20, bottom: 12, right: 8) + ) + } +} + + // MARK: - SETTERS +extension NotificationsSettingsTextCell { + func configure(title: String?, subtitle: String?, value: String?) { + titleLabel.text = title + subtitleLabel.text = subtitle + valueLabel.text = value + } +} diff --git a/station/Classes/Presentation/Modules/Share/View/ViewController/ShareViewController.swift b/station/Classes/Presentation/Modules/Share/View/ViewController/ShareViewController.swift index d688c589f..597ff4d4b 100644 --- a/station/Classes/Presentation/Modules/Share/View/ViewController/ShareViewController.swift +++ b/station/Classes/Presentation/Modules/Share/View/ViewController/ShareViewController.swift @@ -38,7 +38,7 @@ class ShareViewController: UITableViewController { private lazy var backButton: UIButton = { let button = UIButton() button.tintColor = .label - let buttonImage = UIImage(named: "chevron_back") + let buttonImage = RuuviAssets.backButtonImage button.setImage(buttonImage, for: .normal) button.setImage(buttonImage, for: .highlighted) button.imageView?.tintColor = .label @@ -239,8 +239,8 @@ extension ShareViewController { leading: backBarButtonItemView.leadingAnchor, bottom: backBarButtonItemView.bottomAnchor, trailing: backBarButtonItemView.trailingAnchor, - padding: .init(top: 0, left: -8, bottom: 0, right: 0), - size: .init(width: 32, height: 32)) + padding: .init(top: 0, left: -12, bottom: 0, right: 0), + size: .init(width: 40, height: 40)) navigationItem.leftBarButtonItem = UIBarButtonItem(customView: backBarButtonItemView) } diff --git a/station/Classes/Presentation/Modules/SignIn/Presenter/SignInPresenter.swift b/station/Classes/Presentation/Modules/SignIn/Presenter/SignInPresenter.swift index e9213b494..fc50ea3a9 100644 --- a/station/Classes/Presentation/Modules/SignIn/Presenter/SignInPresenter.swift +++ b/station/Classes/Presentation/Modules/SignIn/Presenter/SignInPresenter.swift @@ -255,10 +255,12 @@ extension SignInPresenter { } private func registerFCMToken() { + let sound = settings.alertSound Messaging.messaging().token { [weak self] fcmToken, _ in self?.cloudNotificationService.set(token: fcmToken, name: UIDevice.modelName, - data: nil) + data: nil, + sound: sound) } } } diff --git a/station/Classes/Presentation/Modules/TagSettings/Assembly/TagSettingsModuleFactory.swift b/station/Classes/Presentation/Modules/TagSettings/Assembly/TagSettingsModuleFactory.swift index eb315e5d7..3d9461e96 100644 --- a/station/Classes/Presentation/Modules/TagSettings/Assembly/TagSettingsModuleFactory.swift +++ b/station/Classes/Presentation/Modules/TagSettings/Assembly/TagSettingsModuleFactory.swift @@ -47,8 +47,6 @@ final class TagSettingsModuleFactoryImpl: TagSettingsModuleFactory { presenter.ruuviPool = r.resolve(RuuviPool.self) presenter.localSyncState = r.resolve(RuuviLocalSyncState.self) presenter.alertHandler = r.resolve(RuuviNotifier.self) - presenter.advertisementDaemon = r.resolve(RuuviTagAdvertisementDaemon.self) - presenter.heartbeatDaemon = r.resolve(RuuviTagHeartbeatDaemon.self) view.measurementService = r.resolve(RuuviServiceMeasurement.self) diff --git a/station/Classes/Presentation/Modules/TagSettings/Presenter/TagSettingsPresenter.swift b/station/Classes/Presentation/Modules/TagSettings/Presenter/TagSettingsPresenter.swift index 1fdfbce0e..b5b52e43d 100644 --- a/station/Classes/Presentation/Modules/TagSettings/Presenter/TagSettingsPresenter.swift +++ b/station/Classes/Presentation/Modules/TagSettings/Presenter/TagSettingsPresenter.swift @@ -39,8 +39,6 @@ class TagSettingsPresenter: NSObject, TagSettingsModuleInput { var exportService: RuuviServiceExport! var localSyncState: RuuviLocalSyncState! var alertHandler: RuuviNotifier! - var advertisementDaemon: RuuviTagAdvertisementDaemon! - var heartbeatDaemon: RuuviTagHeartbeatDaemon! private static let lowUpperDebounceDelay: TimeInterval = 0.3 @@ -247,19 +245,20 @@ extension TagSettingsPresenter: TagSettingsViewOutput { let isConnected = sSelf.viewModel.isConnected.value, isConnected { sSelf.connectionPersistence.setKeepConnection(false, for: luid) - sSelf.heartbeatDaemon.restart() + sSelf.notifyRestartHeartBeatDaemon() } if sSelf.ruuviTag.isOwner { - sSelf.advertisementDaemon.restart() + sSelf.notifyRestartAdvertisementDaemon() if let isConnected = sSelf.viewModel.isConnected.value, isConnected { - sSelf.heartbeatDaemon.restart() + sSelf.notifyRestartHeartBeatDaemon() } } sSelf.viewModel.reset() sSelf.localSyncState.setSyncDate(nil, for: sSelf.ruuviTag.macId) sSelf.localSyncState.setGattSyncDate(nil, for: sSelf.ruuviTag.macId) + sSelf.settings.setOwnerCheckDate(for: sSelf.ruuviTag.macId, value: nil) sSelf.output?.tagSettingsDidDeleteTag(module: sSelf, ruuviTag: sSelf.ruuviTag) }, failure: { [weak self] error in @@ -415,9 +414,11 @@ extension TagSettingsPresenter: TagSettingsViewOutput { } func viewDidTapOnOwner() { - guard let isOwner = viewModel.isOwner.value, isOwner else { return } if viewModel.isClaimedTag.value == false { router.openOwner(ruuviTag: ruuviTag) + } else { + guard let isOwner = viewModel.isOwner.value, !isOwner else { return } + router.openContest(ruuviTag: ruuviTag) } } } @@ -1524,6 +1525,24 @@ extension TagSettingsPresenter { for: ruuviTag ) } + + private func notifyRestartAdvertisementDaemon() { + // Notify daemon to restart + NotificationCenter + .default + .post(name: .RuuviTagAdvertisementDaemonShouldRestart, + object: nil, + userInfo: nil) + } + + private func notifyRestartHeartBeatDaemon() { + // Notify daemon to restart + NotificationCenter + .default + .post(name: .RuuviTagHeartBeatDaemonShouldRestart, + object: nil, + userInfo: nil) + } } extension TagSettingsPresenter { diff --git a/station/Classes/Presentation/Modules/TagSettings/Router/TagSettingsRouter.swift b/station/Classes/Presentation/Modules/TagSettings/Router/TagSettingsRouter.swift index 59e5ebecb..f7e7cf5d9 100644 --- a/station/Classes/Presentation/Modules/TagSettings/Router/TagSettingsRouter.swift +++ b/station/Classes/Presentation/Modules/TagSettings/Router/TagSettingsRouter.swift @@ -76,6 +76,20 @@ class TagSettingsRouter: NSObject, TagSettingsRouterInput { module.configure(ruuviTag: ruuviTag) }) } + + func openContest(ruuviTag: RuuviTagSensor) { + let factory: SensorForceClaimModuleFactory = SensorForceClaimModuleFactoryImpl() + let module = factory.create() + transitionHandler + .navigationController? + .pushViewController( + module, + animated: true + ) + if let presenter = module.output as? SensorForceClaimModuleInput { + presenter.configure(ruuviTag: ruuviTag) + } + } } extension TagSettingsRouter: UIAdaptivePresentationControllerDelegate { diff --git a/station/Classes/Presentation/Modules/TagSettings/Router/TagSettingsRouterInput.swift b/station/Classes/Presentation/Modules/TagSettings/Router/TagSettingsRouterInput.swift index 7c188d36c..2142530df 100644 --- a/station/Classes/Presentation/Modules/TagSettings/Router/TagSettingsRouterInput.swift +++ b/station/Classes/Presentation/Modules/TagSettings/Router/TagSettingsRouterInput.swift @@ -11,6 +11,7 @@ protocol TagSettingsRouterInput { sensorSettings: SensorSettings?) func openUpdateFirmware(ruuviTag: RuuviTagSensor) func openOwner(ruuviTag: RuuviTagSensor) + func openContest(ruuviTag: RuuviTagSensor) } extension TagSettingsRouterInput { diff --git a/station/Classes/Presentation/Modules/TagSettings/Submodules/DFU/Interactor/DFUInteractor.swift b/station/Classes/Presentation/Modules/TagSettings/Submodules/DFU/Interactor/DFUInteractor.swift index bd5c8f179..dff55dbbf 100644 --- a/station/Classes/Presentation/Modules/TagSettings/Submodules/DFU/Interactor/DFUInteractor.swift +++ b/station/Classes/Presentation/Modules/TagSettings/Submodules/DFU/Interactor/DFUInteractor.swift @@ -8,6 +8,8 @@ final class DFUInteractor { var ruuviDFU: RuuviDFU! var background: BTBackground! private let firmwareRepository: FirmwareRepository = FirmwareRepositoryImpl() + private var timer: Timer? + private var timeoutDuration: Double = 15 } extension DFUInteractor: DFUInteractorInput { @@ -132,6 +134,15 @@ extension DFUInteractor: DFUInteractorInput { promise(.failure(DFUError.failedToGetLuid)) return } + + sSelf.invalidateTimer() + sSelf.timer = Timer.scheduledTimer( + withTimeInterval: sSelf.timeoutDuration, repeats: false + ) { _ in + sSelf.invalidateTimer() + promise(.failure(BTError.logic(.connectionTimedOut))) + } + sSelf.background.services.gatt.firmwareRevision( for: sSelf, uuid: uuid, @@ -168,3 +179,10 @@ extension DFUInteractor: DFUInteractorInput { } } } + +extension DFUInteractor { + private func invalidateTimer() { + timer?.invalidate() + timer = nil + } +} diff --git a/station/Classes/Presentation/Modules/TagSettings/Submodules/DFU/View/DFUViewModel.swift b/station/Classes/Presentation/Modules/TagSettings/Submodules/DFU/View/DFUViewModel.swift index dbbae43c2..79a3e50df 100644 --- a/station/Classes/Presentation/Modules/TagSettings/Submodules/DFU/View/DFUViewModel.swift +++ b/station/Classes/Presentation/Modules/TagSettings/Submodules/DFU/View/DFUViewModel.swift @@ -31,6 +31,7 @@ final class DFUViewModel: ObservableObject { private let activityPresenter: ActivityPresenter private var ruuviTagObserveToken: ObservationToken? private var isMigrating: Bool = false + private let timeoutDuration: Int = 15 var isLoading: Bool = false { didSet { diff --git a/station/Classes/Presentation/Modules/TagSettings/Submodules/Force Claim/Assembly/SensorForceClaimModuleFactory.swift b/station/Classes/Presentation/Modules/TagSettings/Submodules/Force Claim/Assembly/SensorForceClaimModuleFactory.swift new file mode 100644 index 000000000..15566ba23 --- /dev/null +++ b/station/Classes/Presentation/Modules/TagSettings/Submodules/Force Claim/Assembly/SensorForceClaimModuleFactory.swift @@ -0,0 +1,37 @@ +import Foundation +import RuuviService +import RuuviUser +import RuuviPool +import RuuviPresenters +import RuuviDFU +import BTKit +import RuuviLocal + +protocol SensorForceClaimModuleFactory { + func create() -> SensorForceClaimViewController +} + +final class SensorForceClaimModuleFactoryImpl: SensorForceClaimModuleFactory { + func create() -> SensorForceClaimViewController { + let r = AppAssembly.shared.assembler.resolver + + let view = SensorForceClaimViewController() + let router = SensorForceClaimRouter() + router.transitionHandler = view + + let presenter = SensorForceClaimPresenter() + presenter.view = view + presenter.router = router + presenter.ruuviOwnershipService = r.resolve(RuuviServiceOwnership.self) + presenter.activityPresenter = r.resolve(ActivityPresenter.self) + presenter.ruuviUser = r.resolve(RuuviUser.self) + presenter.ruuviPool = r.resolve(RuuviPool.self) + presenter.background = r.resolve(BTBackground.self) + presenter.errorPresenter = r.resolve(ErrorPresenter.self) + presenter.settings = r.resolve(RuuviLocalSettings.self) + + view.output = presenter + + return view + } +} diff --git a/station/Classes/Presentation/Modules/TagSettings/Submodules/Force Claim/Presenter/SensorForceClaimModuleInput.swift b/station/Classes/Presentation/Modules/TagSettings/Submodules/Force Claim/Presenter/SensorForceClaimModuleInput.swift new file mode 100644 index 000000000..ccc622744 --- /dev/null +++ b/station/Classes/Presentation/Modules/TagSettings/Submodules/Force Claim/Presenter/SensorForceClaimModuleInput.swift @@ -0,0 +1,6 @@ +import Foundation +import RuuviOntology + +protocol SensorForceClaimModuleInput: AnyObject { + func configure(ruuviTag: RuuviTagSensor) +} diff --git a/station/Classes/Presentation/Modules/TagSettings/Submodules/Force Claim/Presenter/SensorForceClaimPresenter.swift b/station/Classes/Presentation/Modules/TagSettings/Submodules/Force Claim/Presenter/SensorForceClaimPresenter.swift new file mode 100644 index 000000000..bbd9d27d0 --- /dev/null +++ b/station/Classes/Presentation/Modules/TagSettings/Submodules/Force Claim/Presenter/SensorForceClaimPresenter.swift @@ -0,0 +1,166 @@ +import Foundation +import RuuviOntology +import RuuviService +import RuuviUser +import RuuviPool +import RuuviPresenters +import BTKit +import CoreNFC +import RuuviLocal + +final class SensorForceClaimPresenter: SensorForceClaimModuleInput { + weak var view: SensorForceClaimViewInput? + var router: SensorForceClaimRouterInput? + var ruuviOwnershipService: RuuviServiceOwnership! + var activityPresenter: ActivityPresenter! + var ruuviUser: RuuviUser! + var ruuviPool: RuuviPool! + var background: BTBackground! + var errorPresenter: ErrorPresenter! + var settings: RuuviLocalSettings! + + private var ruuviTag: RuuviTagSensor? + private var secret: String? + private var isLoading: Bool = false { + didSet { + if isLoading { + activityPresenter.increment() + } else { + activityPresenter.decrement() + } + } + } + private var timer: Timer? + private var gattTimeoutSeconds: Double = 15 + + func configure(ruuviTag: RuuviTagSensor) { + self.ruuviTag = ruuviTag + } + + deinit { + timer?.invalidate() + } +} + +extension SensorForceClaimPresenter: SensorForceClaimViewOutput { + func viewDidLoad() { + if settings.hideNFCForSensorContest { + view?.hideNFCButton() + } + } + + func viewDidTapUseNFC() { + view?.startNFCSession() + } + + func viewDidTapUseBluetooth() { + setUpTimeoutTimerForGATTSecret() + getTagSecretFromGatt() + } + + func viewDidReceiveNFCMessages(messages: [NFCNDEFMessage]) { + // Stop NFC session + view?.stopNFCSession() + // Parse the message + for message in messages { + for record in message.records { + if let (key, value) = parse(record: record) { + switch key { + case "idID": + secret = value + default: + break + } + } + } + } + // Claim + contestSensor(with: secret) + } + + func viewDidDismiss() { + router?.dismiss() + } +} + +extension SensorForceClaimPresenter { + /// Sets up a 15 seconds timer to attempt GATT connection and get secret. + private func setUpTimeoutTimerForGATTSecret() { + timer = Timer.scheduledTimer( + withTimeInterval: gattTimeoutSeconds, + repeats: true, + block: { [weak self] (_) in + self?.isLoading = false + self?.invalidateTimer() + self?.view?.showGATTConnectionTimeoutDialog() + }) + } + + /// Invalidates the running timer + private func invalidateTimer() { + timer?.invalidate() + timer = nil + } + + private func getTagSecretFromGatt() { + guard let luid = ruuviTag?.luid else { + return + } + isLoading = true + // TODO: Check the timeout issue. Timeout not trigerred now. + background.services.gatt.serialRevision( + for: self, + uuid: luid.value, + options: [.connectionTimeout(gattTimeoutSeconds)] // Doesn't work now. + ) { [weak self] _, result in + switch result { + case .success(let secret): + self?.contestSensor(with: secret) + case .failure(let error): + self?.errorPresenter.present(error: error) + self?.isLoading = false + } + } + } + + /// Contest sensor with tag secret. + private func contestSensor(with secret: String?) { + guard let ruuviTag = ruuviTag, + let secret = secret else { return } + + isLoading = true + ruuviOwnershipService + .contest(sensor: ruuviTag, secret: secret) + .on(success: { [weak self] _ in + self?.router?.dismiss() + }, failure: { [weak self] error in + self?.errorPresenter.present(error: error) + }, completion: { [weak self] in + self?.isLoading = false + }) + } + + /// Parse the NFC payload + private func parse(record: NFCNDEFPayload) -> (String, String)? { + let payload = record.payload + let prefix = payload.prefix(1) + let rest = payload.dropFirst(1) + + switch prefix { + case .init([0x02]): + guard let restString = String( + data: rest, encoding: .utf8 + ) else { return nil } + + let components = restString.components(separatedBy: ": ") + if components.count == 2 { + let key = components[0] + let value = components[1].trimmingCharacters(in: .whitespacesAndNewlines) + return (key, value) + } + default: + return nil + } + return nil + } +} diff --git a/station/Classes/Presentation/Modules/TagSettings/Submodules/Force Claim/Router/SensorForceClaimRouter.swift b/station/Classes/Presentation/Modules/TagSettings/Submodules/Force Claim/Router/SensorForceClaimRouter.swift new file mode 100644 index 000000000..6e71ebc32 --- /dev/null +++ b/station/Classes/Presentation/Modules/TagSettings/Submodules/Force Claim/Router/SensorForceClaimRouter.swift @@ -0,0 +1,9 @@ +import LightRoute + +class SensorForceClaimRouter: SensorForceClaimRouterInput { + weak var transitionHandler: UIViewController? + + func dismiss() { + try? transitionHandler?.closeCurrentModule().perform() + } +} diff --git a/station/Classes/Presentation/Modules/TagSettings/Submodules/Force Claim/Router/SensorForceClaimRouterInput.swift b/station/Classes/Presentation/Modules/TagSettings/Submodules/Force Claim/Router/SensorForceClaimRouterInput.swift new file mode 100644 index 000000000..527f6cfc6 --- /dev/null +++ b/station/Classes/Presentation/Modules/TagSettings/Submodules/Force Claim/Router/SensorForceClaimRouterInput.swift @@ -0,0 +1,5 @@ +import Foundation + +protocol SensorForceClaimRouterInput { + func dismiss() +} diff --git a/station/Classes/Presentation/Modules/TagSettings/Submodules/Force Claim/View/SensorForceClaimViewInput.swift b/station/Classes/Presentation/Modules/TagSettings/Submodules/Force Claim/View/SensorForceClaimViewInput.swift new file mode 100644 index 000000000..d4a736a4c --- /dev/null +++ b/station/Classes/Presentation/Modules/TagSettings/Submodules/Force Claim/View/SensorForceClaimViewInput.swift @@ -0,0 +1,8 @@ +import Foundation + +protocol SensorForceClaimViewInput: ViewInput { + func startNFCSession() + func stopNFCSession() + func hideNFCButton() + func showGATTConnectionTimeoutDialog() +} diff --git a/station/Classes/Presentation/Modules/TagSettings/Submodules/Force Claim/View/SensorForceClaimViewOutput.swift b/station/Classes/Presentation/Modules/TagSettings/Submodules/Force Claim/View/SensorForceClaimViewOutput.swift new file mode 100644 index 000000000..b9657430a --- /dev/null +++ b/station/Classes/Presentation/Modules/TagSettings/Submodules/Force Claim/View/SensorForceClaimViewOutput.swift @@ -0,0 +1,10 @@ +import Foundation +import CoreNFC + +protocol SensorForceClaimViewOutput { + func viewDidLoad() + func viewDidTapUseNFC() + func viewDidTapUseBluetooth() + func viewDidReceiveNFCMessages(messages: [NFCNDEFMessage]) + func viewDidDismiss() +} diff --git a/station/Classes/Presentation/Modules/TagSettings/Submodules/Force Claim/View/UI/SensorForceClaimViewController.swift b/station/Classes/Presentation/Modules/TagSettings/Submodules/Force Claim/View/UI/SensorForceClaimViewController.swift new file mode 100644 index 000000000..5543a2fe3 --- /dev/null +++ b/station/Classes/Presentation/Modules/TagSettings/Submodules/Force Claim/View/UI/SensorForceClaimViewController.swift @@ -0,0 +1,310 @@ +import UIKit +import CoreNFC +import RuuviOntology + +class SensorForceClaimViewController: UIViewController { + + private lazy var backButton: UIButton = { + let button = UIButton() + button.tintColor = .label + let buttonImage = RuuviAssets.backButtonImage + button.setImage(buttonImage, for: .normal) + button.setImage(buttonImage, for: .highlighted) + button.imageView?.tintColor = .label + button.backgroundColor = .clear + button.addTarget(self, action: #selector(backButtonDidTap), for: .touchUpInside) + return button + }() + + private lazy var messageLabel: UILabel = { + let label = UILabel() + label.textColor = RuuviColor.ruuviTextColor + label.textAlignment = .left + label.numberOfLines = 0 + label.text = "force_claim_sensor_description1".localized() + label.font = UIFont.Muli(.regular, size: 16) + return label + }() + + private lazy var claimSensorButton: UIButton = { + let button = UIButton(color: RuuviColor.ruuviTintColor, + cornerRadius: 25) + button.setTitle("force_claim".localized(), for: .normal) + button.setTitleColor(.white, for: .normal) + button.titleLabel?.font = UIFont.Muli(.bold, size: 16) + button.addTarget(self, + action: #selector(handleClaimSensorTap), + for: .touchUpInside) + return button + }() + + private lazy var sensorClaimNotesViewContainer: UIView = UIView( + color: RuuviColor.ruuviPrimary + ) + private lazy var sensorClaimNotesView: UITextView = { + let tv = UITextView() + tv.isSelectable = false + tv.isEditable = false + tv.textAlignment = .left + tv.text = "force_claim_sensor_description2".localized() + tv.textColor = RuuviColor.ruuviTextColor + tv.backgroundColor = .clear + tv.font = UIFont.Muli(.regular, size: 16) + tv.isScrollEnabled = true + return tv + }() + + private lazy var useNFCButton: UIButton = { + let button = UIButton(color: RuuviColor.ruuviTintColor, + cornerRadius: 25) + button.setTitle( + "use_nfc".localized(), + for: .normal + ) + button.setTitleColor(.white, for: .normal) + button.titleLabel?.font = UIFont.Muli(.bold, size: 16) + button.addTarget(self, + action: #selector(handleUseNFCButtonTap), + for: .touchUpInside) + return button + }() + + private lazy var useBluetoothButton: UIButton = { + let button = UIButton(color: RuuviColor.ruuviTintColor, + cornerRadius: 25) + button.setTitle( + "use_bluetooth".localized(), + for: .normal + ) + button.setTitleColor(.white, for: .normal) + button.titleLabel?.font = UIFont.Muli(.bold, size: 16) + button.addTarget(self, + action: #selector(handleUseBluetoothButtonTap), + for: .touchUpInside) + return button + }() + + // Implementation + private var isNFCAvailable: Bool { + return NFCNDEFReaderSession.readingAvailable + } + private var session: NFCNDEFReaderSession? + + // Constraints + private var bluetoothButtonRegularLeadingConstraint: NSLayoutConstraint! + private var bluetoothButtonRegularTrailingConstraint: NSLayoutConstraint! + private var bluetoothButtonRegularWidthConstraint: NSLayoutConstraint! + private var bluetoothButtonNoNFCWidthConstraint: NSLayoutConstraint! + private var bluetoothButtonNoNFCCenterXConstraint: NSLayoutConstraint! + + // Output + var output: SensorForceClaimViewOutput? + +} + +// MARK: - VIEW LIFECYCLE +extension SensorForceClaimViewController { + override func viewDidLoad() { + super.viewDidLoad() + setUpUI() + output?.viewDidLoad() + } +} + +// MARK: - SensorForceClaimViewInput +extension SensorForceClaimViewController: SensorForceClaimViewInput { + func localize() { + // No op. + } + + func hideNFCButton() { + hideNFCButton(hide: true) + } + + func startNFCSession() { + session = NFCNDEFReaderSession( + delegate: self, + queue: nil, + invalidateAfterFirstRead: false + ) + session?.begin() + } + + func stopNFCSession() { + session?.invalidate() + } + + func showGATTConnectionTimeoutDialog() { + let message = "sensor_not_found_error".localized() + let controller = UIAlertController( + title: nil, message: message, preferredStyle: .alert + ) + controller.addAction( + UIAlertAction(title: "OK".localized(), style: .cancel, handler: nil) + ) + present(controller, animated: true) + } +} + +// MARK: - PRIVATE SET UI +extension SensorForceClaimViewController { + private func setUpUI() { + setUpBase() + setUpClaimIntroView() + setUpClaimNoteView() + } + + private func setUpBase() { + self.title = "force_claim_sensor".localized() + + view.backgroundColor = RuuviColor.ruuviPrimary + + let backBarButtonItemView = UIView() + backBarButtonItemView.addSubview(backButton) + backButton.anchor(top: backBarButtonItemView.topAnchor, + leading: backBarButtonItemView.leadingAnchor, + bottom: backBarButtonItemView.bottomAnchor, + trailing: backBarButtonItemView.trailingAnchor, + padding: .init(top: 0, left: -12, bottom: 0, right: 0), + size: .init(width: 40, height: 40)) + navigationItem.leftBarButtonItem = UIBarButtonItem(customView: backBarButtonItemView) + } + + private func setUpClaimIntroView() { + view.addSubview(messageLabel) + messageLabel.anchor( + top: view.safeTopAnchor, + leading: view.safeLeftAnchor, + bottom: nil, + trailing: view.safeRightAnchor, + padding: .init(top: 16, left: 12, bottom: 0, right: 12) + ) + + view.addSubview(claimSensorButton) + claimSensorButton.anchor( + top: messageLabel.bottomAnchor, + leading: nil, + bottom: nil, + trailing: nil, + padding: .init(top: 40, left: 0, bottom: 0, right: 0), + size: .init(width: 200, height: 50) + ) + claimSensorButton.centerXInSuperview() + } + + // swiftlint:disable:next function_body_length + private func setUpClaimNoteView() { + view.addSubview(sensorClaimNotesViewContainer) + sensorClaimNotesViewContainer.fillSuperviewToSafeArea() + + // Text view + sensorClaimNotesViewContainer.addSubview(sensorClaimNotesView) + sensorClaimNotesView.anchor( + top: sensorClaimNotesViewContainer.topAnchor, + leading: sensorClaimNotesViewContainer.leadingAnchor, + bottom: nil, + trailing: sensorClaimNotesViewContainer.trailingAnchor, + padding: .init(top: 16, left: 12, bottom: 0, right: 12) + ) + + // Footer + let footerView = UIView(color: .clear) + sensorClaimNotesViewContainer.addSubview(footerView) + footerView.anchor( + top: sensorClaimNotesView.bottomAnchor, + leading: sensorClaimNotesView.leadingAnchor, + bottom: sensorClaimNotesViewContainer.bottomAnchor, + trailing: sensorClaimNotesView.trailingAnchor + ) + + // Scan buttons + footerView.addSubview(useNFCButton) + useNFCButton.anchor( + top: footerView.topAnchor, + leading: footerView.leadingAnchor, + bottom: footerView.bottomAnchor, + trailing: nil, + padding: .init(top: 16, left: 0, bottom: 16, right: 0), + size: .init(width: 0, height: 50) + ) + + footerView.addSubview(useBluetoothButton) + useBluetoothButton.anchor( + top: useNFCButton.topAnchor, + leading: nil, + bottom: useNFCButton.bottomAnchor, + trailing: nil + ) + + bluetoothButtonRegularLeadingConstraint = useBluetoothButton + .leadingAnchor + .constraint( + equalTo: useNFCButton.trailingAnchor, + constant: 12 + ) + bluetoothButtonRegularTrailingConstraint = useBluetoothButton + .trailingAnchor + .constraint( + equalTo: footerView.trailingAnchor + ) + bluetoothButtonRegularWidthConstraint = useBluetoothButton + .widthAnchor + .constraint(equalTo: useNFCButton.widthAnchor) + + bluetoothButtonNoNFCWidthConstraint = useBluetoothButton + .widthAnchor + .constraint(equalToConstant: 180) + bluetoothButtonNoNFCCenterXConstraint = useBluetoothButton + .centerXAnchor + .constraint(equalTo: footerView.centerXAnchor) + hideNFCButton(hide: !isNFCAvailable) + + sensorClaimNotesViewContainer.alpha = 0 + } +} + +// MARK: - IBACTIONS +extension SensorForceClaimViewController { + @objc fileprivate func backButtonDidTap() { + _ = navigationController?.popViewController(animated: true) + } + + @objc private func handleClaimSensorTap() { + sensorClaimNotesViewContainer.alpha = 1 + } + + @objc private func handleUseNFCButtonTap() { + output?.viewDidTapUseNFC() + } + + @objc private func handleUseBluetoothButtonTap() { + output?.viewDidTapUseBluetooth() + } +} + +// MARK: - PRIVATE +extension SensorForceClaimViewController { + fileprivate func hideNFCButton(hide: Bool) { + useNFCButton.alpha = hide ? 0 : 1 + bluetoothButtonRegularLeadingConstraint.isActive = !hide + bluetoothButtonRegularTrailingConstraint.isActive = !hide + bluetoothButtonRegularWidthConstraint.isActive = !hide + bluetoothButtonNoNFCWidthConstraint.isActive = hide + bluetoothButtonNoNFCCenterXConstraint.isActive = hide + } +} + +// MARK: - NFCNDEFReaderSessionDelegate +extension SensorForceClaimViewController: NFCNDEFReaderSessionDelegate { + func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) { + DispatchQueue.main.async { [weak self] in + self?.stopNFCSession() + } + } + + func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) { + DispatchQueue.main.async { [weak self] in + self?.output?.viewDidReceiveNFCMessages(messages: messages) + } + } +} diff --git a/station/Classes/Presentation/Modules/TagSettings/Submodules/OffsetCorrection/View/Apple/OffsetCorrectionAppleViewController.swift b/station/Classes/Presentation/Modules/TagSettings/Submodules/OffsetCorrection/View/Apple/OffsetCorrectionAppleViewController.swift index 05d188d08..2172cfd55 100644 --- a/station/Classes/Presentation/Modules/TagSettings/Submodules/OffsetCorrection/View/Apple/OffsetCorrectionAppleViewController.swift +++ b/station/Classes/Presentation/Modules/TagSettings/Submodules/OffsetCorrection/View/Apple/OffsetCorrectionAppleViewController.swift @@ -16,7 +16,7 @@ class OffsetCorrectionAppleViewController: UIViewController { private lazy var backButton: UIButton = { let button = UIButton() button.tintColor = .label - let buttonImage = UIImage(named: "chevron_back") + let buttonImage = RuuviAssets.backButtonImage button.setImage(buttonImage, for: .normal) button.setImage(buttonImage, for: .highlighted) button.imageView?.tintColor = .label @@ -60,8 +60,8 @@ class OffsetCorrectionAppleViewController: UIViewController { leading: backBarButtonItemView.leadingAnchor, bottom: backBarButtonItemView.bottomAnchor, trailing: backBarButtonItemView.trailingAnchor, - padding: .init(top: 0, left: -8, bottom: 0, right: 0), - size: .init(width: 32, height: 32)) + padding: .init(top: 0, left: -12, bottom: 0, right: 0), + size: .init(width: 40, height: 40)) navigationItem.leftBarButtonItem = UIBarButtonItem(customView: backBarButtonItemView) output.viewDidLoad() diff --git a/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/Presenter/OwnerPresenter.swift b/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/Presenter/OwnerPresenter.swift index 1be2b9a28..5b1bfb76b 100644 --- a/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/Presenter/OwnerPresenter.swift +++ b/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/Presenter/OwnerPresenter.swift @@ -57,8 +57,9 @@ extension OwnerPresenter: OwnerViewOutput { self?.isLoading = false }) } + /// Update the tag with owner information - func update(with email: String) { + func updateOwnerInfo(with email: String) { ruuviStorage.readAll().on(success: { [weak self] localSensors in guard let sSelf = self else { return } if let sensor = localSensors.first(where: {$0.id == sSelf.ruuviTag.id }) { diff --git a/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/View/OwnerViewController.swift b/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/View/OwnerViewController.swift index 51018887e..257940d8c 100644 --- a/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/View/OwnerViewController.swift +++ b/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/View/OwnerViewController.swift @@ -37,7 +37,11 @@ extension OwnerViewController: OwnerViewInput { message: "UserApiError.ER_SENSOR_ALREADY_CLAIMED_NO_EMAIL".localized(), preferredStyle: .alert ) - alertVC.addAction(UIAlertAction(title: "OK".localized(), style: .default, handler: nil)) + alertVC.addAction(UIAlertAction(title: "OK".localized(), style: .default, handler: { + [weak self] _ in + // TODO: - Update with masked email once backend is adjusted. + self?.output.updateOwnerInfo(with: "*****") + })) present(alertVC, animated: true) } func localize() { @@ -80,8 +84,8 @@ extension OwnerViewController { leading: backBarButtonItemView.leadingAnchor, bottom: backBarButtonItemView.bottomAnchor, trailing: backBarButtonItemView.trailingAnchor, - padding: .init(top: 0, left: -8, bottom: 0, right: 0), - size: .init(width: 32, height: 32)) + padding: .init(top: 0, left: -12, bottom: 0, right: 0), + size: .init(width: 40, height: 40)) navigationItem.leftBarButtonItem = UIBarButtonItem(customView: backBarButtonItemView) } diff --git a/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/View/OwnerViewOutput.swift b/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/View/OwnerViewOutput.swift index c35c6c4b6..08ae1708d 100644 --- a/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/View/OwnerViewOutput.swift +++ b/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/View/OwnerViewOutput.swift @@ -3,7 +3,7 @@ import RuuviOntology protocol OwnerViewOutput: AnyObject { func viewDidTapOnClaim() - func update(with email: String) + func updateOwnerInfo(with email: String) func viewDidTriggerFirmwareUpdateDialog() func viewDidConfirmFirmwareUpdate() /// Trigger this method when user cancel the legacy firmware update dialog for the first time diff --git a/station/Classes/Presentation/Modules/TagSettings/View/UI/TagSettingsViewController.swift b/station/Classes/Presentation/Modules/TagSettings/View/UI/TagSettingsViewController.swift index 9125cface..94fc2d31f 100644 --- a/station/Classes/Presentation/Modules/TagSettings/View/UI/TagSettingsViewController.swift +++ b/station/Classes/Presentation/Modules/TagSettings/View/UI/TagSettingsViewController.swift @@ -567,9 +567,14 @@ extension TagSettingsViewController { } } + let isClaimed = viewModel.isClaimedTag.value + let isOwner = viewModel.isOwner.value if let tagOwnerCell = tagOwnerCell { tagOwnerCell.bind(viewModel.owner) { cell, owner in cell.configure(value: owner) + if let isClaimed = isClaimed, let isOwner = isOwner { + cell.setAccessory(type: (isClaimed && isOwner) ? .none : .chevron) + } } } @@ -626,13 +631,14 @@ extension TagSettingsViewController { } private func tagOwnerSettingItem() -> TagSettingsItem { + let isClaimed = GlobalHelpers.getBool(from: viewModel?.isClaimedTag.value) + let isOwner = GlobalHelpers.getBool(from: viewModel?.isOwner.value) let settingItem = TagSettingsItem( identifier: .generalOwner, createdCell: { [weak self] in self?.tagOwnerCell?.configure(title: "TagSettings.NetworkInfo.Owner".localized(), value: self?.viewModel?.owner.value) - let isClaimed = GlobalHelpers.getBool(from: self?.viewModel?.isClaimedTag.value) - self?.tagOwnerCell?.setAccessory(type: isClaimed ? .none : .chevron ) + self?.tagOwnerCell?.setAccessory(type: (isClaimed && isOwner) ? .none : .chevron ) self?.tagOwnerCell?.hideSeparator(hide: !GlobalHelpers.getBool(from: self?.showShare())) return self?.tagOwnerCell ?? UITableViewCell() }, @@ -3061,18 +3067,18 @@ extension TagSettingsViewController { leading: backBarButtonItemView.leadingAnchor, bottom: backBarButtonItemView.bottomAnchor, trailing: backBarButtonItemView.trailingAnchor, - padding: .init(top: 0, left: -8, bottom: 0, right: 0), - size: .init(width: 32, height: 32)) + padding: .init(top: 0, left: -12, bottom: 0, right: 0), + size: .init(width: 40, height: 40)) navigationItem.leftBarButtonItem = UIBarButtonItem(customView: backBarButtonItemView) let rightBarButtonItemView = UIView() rightBarButtonItemView.addSubview(exportButton) exportButton.anchor(top: rightBarButtonItemView.topAnchor, - leading: rightBarButtonItemView.leadingAnchor, - bottom: rightBarButtonItemView.bottomAnchor, - trailing: rightBarButtonItemView.trailingAnchor, - padding: .init(top: 0, left: 0, bottom: 0, right: 0), - size: .init(width: 32, height: 32)) + leading: rightBarButtonItemView.leadingAnchor, + bottom: rightBarButtonItemView.bottomAnchor, + trailing: rightBarButtonItemView.trailingAnchor, + padding: .init(top: 0, left: 0, bottom: 0, right: -8), + size: .init(width: 40, height: 40)) navigationItem.rightBarButtonItem = UIBarButtonItem(customView: rightBarButtonItemView) let container = UIView(color: .clear) diff --git a/station/Extensions/Classess/RuuviCustomButton.swift b/station/Extensions/Classess/RuuviCustomButton.swift new file mode 100644 index 000000000..a8a273158 --- /dev/null +++ b/station/Extensions/Classess/RuuviCustomButton.swift @@ -0,0 +1,54 @@ +import UIKit + +class RuuviCustomButton: UIView { + + var image: UIImage? { + didSet { + iconView.image = image + } + } + + private lazy var iconView: UIImageView = { + let iv = UIImageView( + image: nil, + contentMode: .scaleAspectFit + ) + return iv + }() + + private var iconSize: CGSize = .zero + + convenience init( + icon: UIImage?, + tintColor: UIColor = .white, + iconSize: CGSize = .init(width: 20, height: 20) + ) { + self.init() + self.iconView.image = icon + self.iconView.tintColor = tintColor + self.iconSize = iconSize + self.setUpUI() + } + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension RuuviCustomButton { + private func setUpUI() { + addSubview(iconView) + iconView.size(width: iconSize.width, height: iconSize.height) + iconView.fillSuperview(padding: .init(top: 6, left: 12, bottom: 6, right: 12)) + } +} + +extension RuuviCustomButton { + func setImage(image: UIImage?) { + iconView.image = image + } +} diff --git a/station/Extensions/Classess/RuuviLinkTextView.swift b/station/Extensions/Classess/RuuviLinkTextView.swift new file mode 100644 index 000000000..5dc1b6958 --- /dev/null +++ b/station/Extensions/Classess/RuuviLinkTextView.swift @@ -0,0 +1,101 @@ +import UIKit + +protocol RuuviLinkTextViewDelegate: NSObjectProtocol { + func didTapLink(url: String) +} + +class RuuviLinkTextView: UITextView { + + private var textRegularColor: UIColor? = RuuviColor + .dashboardIndicatorTextColor? + .withAlphaComponent(0.6) + private var textLinkColor: UIColor? = RuuviColor.ruuviTextColor + private var fullTextString: String? + private var linkString: String? + private var link: String? + + weak var linkDelegate: RuuviLinkTextViewDelegate? + + convenience init( + textColor: UIColor? = RuuviColor.dashboardIndicatorTextColor?.withAlphaComponent(0.6), + linkColor: UIColor? = RuuviColor.ruuviTextColor, + fullTextString: String?, + linkString: String?, + link: String? + ) { + self.init() + self.textRegularColor = textColor + self.textLinkColor = linkColor + self.fullTextString = fullTextString + self.linkString = linkString + self.link = link + setUpUI() + } + + override init(frame: CGRect, textContainer: NSTextContainer?) { + super.init(frame: frame, textContainer: textContainer) + setUpUI() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setUpUI() + } + + private func setUpUI() { + isEditable = false + isScrollEnabled = false + isUserInteractionEnabled = true + backgroundColor = .clear + isSelectable = false + + let regularAttributes: [NSAttributedString.Key: Any] = [ + .font: UIFont.Muli(.regular, size: 13), + .foregroundColor: textRegularColor ?? .secondaryLabel + ] + + let tappableAttributes: [NSAttributedString.Key: Any] = [ + .font: UIFont.Muli(.bold, size: 13), + .foregroundColor: textLinkColor ?? .secondaryLabel + ] + + guard let fullTextString = fullTextString, + let linkString = linkString else { return } + + let attributedText = NSMutableAttributedString( + string: fullTextString, attributes: regularAttributes + ) + + if let tappableTextRange = fullTextString.range(of: linkString) { + let nsRange = NSRange(tappableTextRange, in: fullTextString) + attributedText.addAttributes(tappableAttributes, range: nsRange) + } + + self.attributedText = attributedText + + let tapGesture = UITapGestureRecognizer( + target: self, action: #selector(handleTap) + ) + addGestureRecognizer(tapGesture) + } + + @objc private func handleTap(gestureRecognizer: UITapGestureRecognizer) { + let location = gestureRecognizer.location(in: self) + let position = layoutManager.characterIndex( + for: location, in: textContainer, + fractionOfDistanceBetweenInsertionPoints: nil + ) + + guard let fullTextString = fullTextString, + let linkString = linkString, + let link = link else { return } + + if let tappableTextRange = fullTextString.range(of: linkString) { + let nsRange = NSRange(tappableTextRange, in: fullTextString) + + if NSLocationInRange(position, nsRange) { + linkDelegate?.didTapLink(url: link) + } + } + } +} diff --git a/station/Extensions/RuuviAlertSound+Extension.swift b/station/Extensions/RuuviAlertSound+Extension.swift new file mode 100644 index 000000000..e2abff838 --- /dev/null +++ b/station/Extensions/RuuviAlertSound+Extension.swift @@ -0,0 +1,13 @@ +import Foundation +import RuuviOntology + +extension RuuviAlertSound: SelectionItemProtocol { + var title: String { + switch self { + case .systemDefault: + return "settings_alert_sound_default".localized() + case .ruuviSpeak: + return "settings_alert_sound_ruuvi_speak".localized() + } + } +} diff --git a/station/Resources/Images/Assets.xcassets/icon_sync_bt.imageset/Contents.json b/station/Resources/Images/Assets.xcassets/icon_sync_bt.imageset/Contents.json new file mode 100644 index 000000000..03a84a3fe --- /dev/null +++ b/station/Resources/Images/Assets.xcassets/icon_sync_bt.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icon_sync_bt.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/station/Resources/Images/Assets.xcassets/icon_sync_bt.imageset/icon_sync_bt.png b/station/Resources/Images/Assets.xcassets/icon_sync_bt.imageset/icon_sync_bt.png new file mode 100644 index 000000000..526aea7c4 Binary files /dev/null and b/station/Resources/Images/Assets.xcassets/icon_sync_bt.imageset/icon_sync_bt.png differ diff --git a/station/Resources/Plists/DevInfo.plist b/station/Resources/Plists/DevInfo.plist index b68649176..9f80e8939 100644 --- a/station/Resources/Plists/DevInfo.plist +++ b/station/Resources/Plists/DevInfo.plist @@ -24,11 +24,13 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 375 + 386 FirebaseMessagingAutoInitEnabled LSRequiresIPhoneOS + NFCReaderUsageDescription + Allows user to claim a RuuviTag using NFC when the user has physical access to the sensor NSBluetoothAlwaysUsageDescription The app uses Bluetooth LE to read data from RuuviTag sensors. NSBluetoothPeripheralUsageDescription diff --git a/station/Resources/Plists/Info.plist b/station/Resources/Plists/Info.plist index 2e4929c9a..eb71fcd36 100644 --- a/station/Resources/Plists/Info.plist +++ b/station/Resources/Plists/Info.plist @@ -24,7 +24,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - $(CURRENT_PROJECT_VERSION) + 384 FirebaseMessagingAutoInitEnabled LSApplicationQueriesSchemes @@ -35,6 +35,8 @@ LSRequiresIPhoneOS + NFCReaderUsageDescription + Allows user to claim a RuuviTag using NFC when the user has physical access to the sensor NSBluetoothAlwaysUsageDescription The app uses Bluetooth LE to read data from Ruuvi Sensors NSBluetoothPeripheralUsageDescription diff --git a/station/Resources/Sounds/ruuvi_speak.caf b/station/Resources/Sounds/ruuvi_speak.caf new file mode 100644 index 000000000..8f7e2d9ab Binary files /dev/null and b/station/Resources/Sounds/ruuvi_speak.caf differ diff --git a/station/Resources/Strings/de.lproj/Localizable.strings b/station/Resources/Strings/de.lproj/Localizable.strings index 6731886d9..518037240 100644 --- a/station/Resources/Strings/de.lproj/Localizable.strings +++ b/station/Resources/Strings/de.lproj/Localizable.strings @@ -258,20 +258,20 @@ "TagSettings.ConnectStatus.Disconnected" = "Getrennt"; "TagCharts.Export.title" = "EXPORT"; "h" = "h"; -"About.AboutHelp.contents" = "Ruuvi Station ist ein benutzerfreundliches, aber leistungsstarkes Werkzeug, um Ihre Ruuvi-Sensoren aufzuladen. Fügen Sie einfach einige Sensoren hinzu und schon können Sie loslegen."; +"About.AboutHelp.contents" = "Ruuvi Station ist eine benutzerfreundliche Anwendung, mit der Sie die Messdaten von Ruuvi-Sensoren überwachen können."; "About.AboutHelp.header" = "Über / Hilfe"; "About.TagsCount.text" = "Sensoren hinzugefügt: %d"; "About.MeasurementsCount.text" = "Anzahl lokal gespeicherter Messungen: %d"; "About.DatabaseSize.text" = "Datenbankgröße: %@"; "About.More.contents" = "Ruuvi-Website: ruuvi.com\nRuuvi-Forum: f.ruuvi.com\nRuuvi-Blog: ruuvi.com/blog\nRuuvi auf Twitter: twitter.com/ruuvicom"; "About.More.header" = "Mehr erfahren"; -"About.OpenSource.contents" = "Genau wie Ruuvi-Sensoren ist auch Ruuvi Station Open Source: github.com/ruuvi"; +"About.OpenSource.contents" = "Genau wie Ruuvi-Sensoren sind auch die Ruuvi Station-Apps Open Source. Verfolgen Sie die Entwicklung und leisten Sie einen Beitrag unter: github.com/ruuvi"; "About.OpenSource.header" = "Open-source"; -"About.OperationsManual.contents" = "Wir haben die App so gestaltet, dass sie selbsterklärend ist, aber sowohl für Ruuvi-Sensoren als auch für die Ruuvi-Station sind umfassende Handbücher verfügbar: ruuvi.com/support"; +"About.OperationsManual.contents" = "Beginnen Sie mit der Nutzung der mobilen Anwendung Ruuvi Station mit unseren Online-Anleitungen: ruuvi.com/support/station-mobile"; "About.OperationsManual.header" = "Anleitung"; "About.Privacy.contents" = "Durch die Nutzung der Anwendung akzeptieren Sie die allgemeinen Geschäftsbedingungen von Ruuvi: ruuvi.com/terms"; "About.Privacy.header" = "Datenschutz-bestimmungen"; -"About.Troubleshooting.contents" = "Wenn Sie Probleme haben und unsere Handbücher nicht die gewünschte Hilfe bieten, finden Sie weitere Hilfe zur Fehlerbehebung hier: ruuvi.com/support"; +"About.Troubleshooting.contents" = "Hilfe bei der Verwendung der Ruuvi Station-Apps, Ruuvi-Produkte und des Ruuvi Cloud-Dienstes finden Sie in unserem Support-Center: ruuvi.com/support"; "About.Troubleshooting.header" = "Fehlerbehebung"; "Interval.Hour.string" = "Stunden"; "Interval.Days.string" = "Tage"; @@ -286,6 +286,7 @@ "Menu.Label.AppSettings.text" = "App Einstellungen"; "Menu.Label.GetMoreSensors.text" = "Sensoren kaufen"; "Ruuvi.BuySensors.URL.IOS" = "https://ruuvi.com/products?utm_campaign=app_ua&utm_medium=referral&utm_source=ios"; +"Ruuvi.BuySensors.Menu.URL.IOS" = "https://ruuvi.com/products?utm_campaign=app_ua_nav&utm_medium=referral&utm_source=ios"; "Menu.Label.BuyRuuviGateway.text" = "Ruuvi Gateway kaufen"; "Menu.BuyGateway.URL.IOS" = "https://ruuvi.com/gateway?utm_campaign=app_ua&utm_medium=referral&utm_source=ios"; "Menu.Label.WhatToMeasure.text" = "Was mit Ruuvi messen?"; @@ -371,7 +372,6 @@ "SignIn.CodeHint" = "Code"; "TagsManagerPresenter.SignOutConfirmAlert.Message" = "Wenn Sie sich abmelden, werden Sensoren, deren Eigentum Sie mit den Sensoreinstellungen bestätigt haben, automatisch aus der App entfernt. Wenn Sie sich erneut mit derselben E-Mail-Adresse anmelden, werden die Sensoren aus der Cloud zurückgegeben.\n\nMöchten Sie sich abmelden?"; "TagSettings.ClaimTagButton.Claim" = "Inhaberschaft beanspruchen"; -"TagSettings.ClaimTagButton.Unclaim" = "Eigentumsanspruch aufheben"; "TagSettings.ShareButton" = "Teilen"; "Syncing..." = "Synchronisierung..."; "Synchronized" = "Synchronisiert"; @@ -511,7 +511,7 @@ Ihr RuuviTag-Sensor ist einsatzbereit!"; "TagSettings.NotShared.title" = "Nicht geteilt"; "Owner.title" = "Sensor beanspruchen"; "Owner.ClaimOwnership.button" = "Inhaberschaft beanspruchen"; -"Owner.Claim.description" = "Besitzen Sie diesen Sensor? Wenn ja, beanspruchen Sie bitte den Besitz des Sensors und er wird Ihrem Ruuvi-Konto hinzugefügt. Jeder Ruuvi-Sensor kann nur einen Besitzer haben.\n\nVorteile:\n\n ● Sensornamen, Hintergrundbilder, Offsets und Alarmeinstellungen werden sicher in der Cloud gespeichert\n\n ● Fernzugriff auf Sensoren über das Internet (erfordert ein Ruuvi Gateway)\n\n ● Teilen Sie Sensoren mit Freunden und Familie (erfordert ein Ruuvi Gateway)\n\n ● Sehen Sie bis zu 2 Jahre Geschichtsdaten auf station.ruuvi.com (erfordert ein Ruuvi-Gateway)"; +"Owner.Claim.description" = "Besitzen Sie diesen Sensor? Wenn ja, beanspruchen Sie bitte den Besitz des Sensors und er wird Ihrem Ruuvi-Konto hinzugefügt. Jeder Ruuvi-Sensor kann nur einen Besitzer haben. Um Eigentum zu beanspruchen, müssen Sie angemeldet sein.\n\nVorteile:\n\n ● Sensornamen, Hintergrundbilder, Offsets und Alarmeinstellungen werden sicher in der Cloud gespeichert\n\n ● Fernzugriff auf Sensoren über das Internet (erfordert ein Ruuvi Gateway)\n\n ● Teilen Sie Sensoren mit Freunden und Familie (erfordert ein Ruuvi Gateway)\n\n ● Sehen Sie bis zu 2 Jahre Geschichtsdaten auf station.ruuvi.com (erfordert ein Ruuvi-Gateway)"; "TagSettings.confirmTagUnclaimAndRemoveDialog.message" = "Durch das Entfernen des Sensors wird Ihr Sensor-Eigentumsstatus widerrufen. Nach dem Entfernen kann eine andere Person das Eigentum an dem Sensor beanspruchen. Jeder Ruuvi-Sensor kann nur einen Besitzer haben."; "TagSettings.confirmSharedTagRemovalDialog.message" = "Wenn Sie den Sensor entfernen, wird der Besitzer des Sensors benachrichtigt und Sie können nicht mehr auf den Sensor zugreifen."; "TagSettings.General.Owner.none" = "Keiner"; @@ -589,7 +589,7 @@ Ihr RuuviTag-Sensor ist einsatzbereit!"; "settings_and_alerts" = "Einstellungen & Benachrichtigungen"; "change_background" = "Einstellungen und Warnungen"; "check_claim_state" = "Prüfung des Antragsstatus"; -"claim_in_progress" = "Claiming in progress"; +"claim_in_progress" = "Anspruchserhebung läuft"; "force_claim_sensor" = "Antragstellung in Bearbeitung"; "force_claim_sensor_description1" = "Dieser Sensor wurde von einem anderen Benutzer beansprucht. Sie können die Eigentümerschaft für Ihr Konto erzwingen, wenn Sie physischen Zugang zu diesem Sensor haben. Jeder Ruuvi-Sensor kann nur einen Besitzer haben."; "force_claim_sensor_description2" = "Die Zwangsanforderung erfolgt über die Nahfeldkommunikation (NFC). Vergewissern Sie sich, dass NFC auf Ihrem Mobilgerät aktiviert ist.\ @@ -597,7 +597,7 @@ Ihr RuuviTag-Sensor ist einsatzbereit!"; 2. Nach erfolgreicher Beanspruchung werden Sie zu den Sensoreinstellungen zurückgeschickt.\ \ Wenn die Beanspruchung nicht erfolgreich war oder NFC auf Ihrem Gerät nicht verfügbar ist:\n\n 1. Öffnen Sie die Abdeckung Ihres Ruuvi-Sensors.\ - 2. Suchen Sie die runde schwarze Taste (oder die Taste \"B\", falls Ihr Sensor 2 Tasten hat) auf der weißen Platine und drücken Sie sie kurz, um den Beanspruchungsprozess zu starten.\ + 2. Suchen Sie die runde schwarze Taste (oder die Taste \"B\", falls Ihr Sensor 2 Tasten hat) auf der weißen Platine und drücken Sie sie kurz, und tippen Sie dann auf die Schaltfläche „Verwenden Sie BT“, um den Beanspruchungsprozess zu starten.\ \n\t3. Nach erfolgreicher Beanspruchung werden Sie zu den Sensoreinstellungen zurückgeschickt."; "force_claim" = "Kraft-Anspruch"; "claim_wrong_sensor_scanned" = "Sie scannen gerade verschiedene RuuviTag"; @@ -678,22 +678,41 @@ Wenn die Beanspruchung nicht erfolgreich war oder NFC auf Ihrem Gerät nicht ver "changelog_ios_url" = "https://f.ruuvi.com/t/3192"; "shared_to_x" = "Freigegebene %d/%d"; "settings_alert_notifications" = "Alarmbenachrichtigungen"; -"settings_alert_sound" = "Alert Sound"; -"settings_alert_sound_description" = "Select push notification alert sound."; -"settings_alerts_footer_description" = "You can also adjust Notification settings under iOS Settings -> Notifications"; -"settings_alerts_footer_description_link_mask" = "iOS Settings -> Notifications"; -"settings_email_alerts" = "Email Alerts"; -"settings_email_alerts_description" = "If you are using Ruuvi Cloud and Ruuvi Gateway, you will be able to receive email alerts by enabling this."; -"settings_push_alerts" = "Push Alerts"; -"settings_push_alerts_description" = "If you are using Ruuvi Cloud and Ruuvi Gateway, you will be able to receive push alerts by enabling this."; +"settings_alert_sound" = "Alarmton"; +"settings_alert_sound_description" = "Wählen Sie den Push-Benachrichtigungston aus."; +"settings_alerts_footer_description" = "Sie können die Benachrichtigungseinstellungen auch unter iOS-Einstellungen -> Benachrichtigungen anpassen"; +"settings_alerts_footer_description_link_mask" = "iOS-Einstellungen -> Benachrichtigungen"; +"settings_email_alerts" = "E-Mail-Benachrichtigungen"; +"settings_email_alerts_description" = "Wenn Sie Ruuvi Cloud und Ruuvi Gateway verwenden, können Sie durch Aktivieren dieser Funktion E-Mail-Benachrichtigungen erhalten."; +"settings_push_alerts" = "Push-Benachrichtigungen"; +"settings_push_alerts_description" = "Wenn Sie Ruuvi Cloud und Ruuvi Gateway verwenden, können Sie Push-Benachrichtigungen erhalten, indem Sie diese aktivieren."; "synchronisation" = "Synchronisation"; "gatt_sync_description" = "Ruuvi Station lädt den internen Verlauf des Sensors für die letzten 10 Tage herunter, wenn der Messverlauf verfügbar ist.\n\nDer Verlauf wird über eine Bluetooth-Verbindung heruntergeladen. Stellen Sie sicher, dass Sie sich in der Nähe des Sensors befinden."; "do_not_show_again" = "Zeige das nicht noch einmal"; "sign_in_continue" = "Weitermachen"; "signing_in_is_optional" = "(Die Anmeldung ist optional)"; -"Defaults.UserAuthorized.title" = "User Authorized"; -"Defaults.DashboardTapActionChart.title" = "Show Chart on Dashboard Card Tap"; -"Defaults.DevServer.title" = "Use Dev Server"; -"Defaults.DevServer.message" = "Changing Ruuvi Cloud endpoint requires signing out from current session and restart the app. Are you sure?"; -"Defaults.ShowEmailAlertsSettings.title" = "Show email alerts settings"; -"Defaults.ShowPushAlertsSettings.title" = "Show push alerts settings"; +"Defaults.UserAuthorized.title" = "Benutzerautorisiert"; +"Defaults.DashboardTapActionChart.title" = "Tippen Sie auf die Dashboard-Karte, um das Diagramm anzuzeigen"; +"Defaults.DevServer.title" = "Verwenden Sie einen Entwicklungsserver"; +"Defaults.DevServer.message" = "Um den Ruuvi Cloud-Endpunkt zu ändern, müssen Sie sich von der aktuellen Sitzung abmelden und die App neu starten. Bist du dir sicher?"; +"Defaults.ShowEmailAlertsSettings.title" = "Einstellungen für E-Mail-Benachrichtigungen anzeigen"; +"Defaults.ShowPushAlertsSettings.title" = "Einstellungen für Push-Benachrichtigungen anzeigen"; +"use_nfc" = "Verwenden Sie NFC"; +"use_bluetooth" = "Verwenden Sie BT"; +"sensor_not_found_error" = "Sensor nicht gefunden. Versuchen Sie es erneut."; +"Defaults.HideNFC.title" = "Verstecken Sie die NFC-Option vor einem erzwungenen Eigentümerwechsel"; +"settings_alert_sound_default" = "Systemfehler"; +"settings_alert_sound_ruuvi_speak" = "Ruuvi Alarm"; +"add_with_nfc" = "Add with NFC"; +"sensor_details" = "Sensor Details"; +"add_sensor" = "Add Sensor"; +"copy_details" = "Copy Details"; +"name" = "Name:"; +"mac_address" = "Mac Address:"; +"unique_id" = "Unique ID:"; +"firmware_version" = "Firmware Version:"; +"close" = "Close"; +"add_sensor_nfc_df3_error" = "This tag cannot be added with NFC due to old firmware. Please add the tag with Bluetooth and update firmware."; +"unclaim_sensor" = "Unclaim sensor"; +"unclaim" = "Unclaim"; +"unclaim_sensor_description" = "Ownership of this sensor has been claimed to your Ruuvi account. Press Unclaim to remove this sensor's settings and related data from your Ruuvi account."; diff --git a/station/Resources/Strings/en.lproj/Localizable.strings b/station/Resources/Strings/en.lproj/Localizable.strings index 0f698157d..fe3a935ca 100644 --- a/station/Resources/Strings/en.lproj/Localizable.strings +++ b/station/Resources/Strings/en.lproj/Localizable.strings @@ -259,20 +259,20 @@ If you cannot see the Language option in the settings, make sure that you have a "TagSettings.ConnectStatus.Disconnected" = "Disconnected"; "TagCharts.Export.title" = "EXPORT"; "h" = "h"; -"About.AboutHelp.contents" = "Ruuvi Station is an easy-to-use but powerful tool to supercharge your Ruuvi sensors. Simply add some sensors and you're ready to go."; +"About.AboutHelp.contents" = "Ruuvi Station is an easy-to-use application that allows you to monitor the measurement data of Ruuvi sensors."; "About.AboutHelp.header" = "About / Help"; "About.TagsCount.text" = "Added sensors: %d"; "About.MeasurementsCount.text" = "Number of locally stored measurements: %d"; "About.DatabaseSize.text" = "Database size: %@"; "About.More.contents" = "Ruuvi's website: ruuvi.com\nRuuvi Forum: f.ruuvi.com\nRuuvi Blog: ruuvi.com/blog\nRuuvi on Twitter: twitter.com/ruuvicom"; "About.More.header" = "More to read"; -"About.OpenSource.contents" = "Just like Ruuvi sensors, Ruuvi Station is also open-source: github.com/ruuvi"; +"About.OpenSource.contents" = "Just like Ruuvi sensors, Ruuvi Station apps are open source. Follow the development and contribute at: github.com/ruuvi"; "About.OpenSource.header" = "Open-source"; -"About.OperationsManual.contents" = "We've designed the app to be self-explanatory but comprehensive manuals are also available for both Ruuvi sensors and Ruuvi Station: ruuvi.com/support"; +"About.OperationsManual.contents" = "Get started using the Ruuvi Station mobile application with our online guides: ruuvi.com/support/station-mobile"; "About.OperationsManual.header" = "Operations manual"; "About.Privacy.contents" = "By using the application, you accept Ruuvi's standard terms and conditions: ruuvi.com/terms"; "About.Privacy.header" = "Privacy policy"; -"About.Troubleshooting.contents" = "If you’re experiencing problems and our manuals didn’t provide the help you need, check out our many troubleshooting topics, here: ruuvi.com/support"; +"About.Troubleshooting.contents" = "Find help using the Ruuvi Station apps, Ruuvi products and Ruuvi Cloud service from our support center: ruuvi.com/support"; "About.Troubleshooting.header" = "Troubleshooting"; "Interval.Hour.string" = "Hours"; "Interval.Days.string" = "Days"; @@ -287,6 +287,7 @@ If you cannot see the Language option in the settings, make sure that you have a "Menu.Label.AppSettings.text" = "App Settings"; "Menu.Label.GetMoreSensors.text" = "Buy Ruuvi Sensors"; "Ruuvi.BuySensors.URL.IOS" = "https://ruuvi.com/products?utm_campaign=app_ua&utm_medium=referral&utm_source=ios"; +"Ruuvi.BuySensors.Menu.URL.IOS" = "https://ruuvi.com/products?utm_campaign=app_ua_nav&utm_medium=referral&utm_source=ios"; "Menu.Label.BuyRuuviGateway.text" = "Buy Ruuvi Gateway"; "Menu.BuyGateway.URL.IOS" = "https://ruuvi.com/gateway?utm_campaign=app_ua&utm_medium=referral&utm_source=ios"; "Menu.Label.WhatToMeasure.text" = "What to measure with Ruuvi?"; @@ -372,7 +373,6 @@ If you cannot see the Language option in the settings, make sure that you have a "SignIn.CodeHint" = "Code"; "TagsManagerPresenter.SignOutConfirmAlert.Message" = "When you sign out, sensors the ownerships of which you've claimed on the sensor Settings page will be automatically removed from the app. When you sign in again using the same email address, the sensors will be returned from the cloud.\n\nDo you want to sign out?"; "TagSettings.ClaimTagButton.Claim" = "Claim ownership"; -"TagSettings.ClaimTagButton.Unclaim" = "Unclaim ownership"; "TagSettings.ShareButton" = "Share"; "Syncing..." = "Loading history from the cloud..."; "Synchronized" = "Synchronised"; @@ -512,7 +512,7 @@ Your RuuviTag sensor is ready for use!"; "TagSettings.NotShared.title" = "Not shared"; "Owner.title" = "Claim sensor"; "Owner.ClaimOwnership.button" = "Claim ownership"; -"Owner.Claim.description" = "Do you own this sensor? If yes, please claim ownership of the sensor and it'll be added to your Ruuvi account. Each Ruuvi sensor can have only one owner.\n\nBenefits:\n\n ● Sensor names, background images, offsets and alert settings will be securely stored on the cloud\n\n ● Access sensors remotely over the Internet (requires a Ruuvi Gateway)\n\n ● Share sensors with friends and family (requires a Ruuvi Gateway)\n\n ● Browse up to 2 years of history on station.ruuvi.com (requires a Ruuvi Gateway)"; +"Owner.Claim.description" = "Do you own this sensor? If yes, please claim ownership of the sensor and it'll be added to your Ruuvi account. Each Ruuvi sensor can have only one owner. To claim ownership, you need to be signed in.\n\nBenefits:\n\n ● Sensor names, background images, offsets and alert settings will be securely stored on the cloud\n\n ● Access sensors remotely over the Internet (requires a Ruuvi Gateway)\n\n ● Share sensors with friends and family (requires a Ruuvi Gateway)\n\n ● Browse up to 2 years of history on station.ruuvi.com (requires a Ruuvi Gateway)"; "TagSettings.confirmTagUnclaimAndRemoveDialog.message" = "By removing the sensor, your sensor ownership status will be revoked. After removal, someone else can claim ownership of the sensor. Each Ruuvi sensor can have only one owner."; "TagSettings.confirmSharedTagRemovalDialog.message" = "If you remove the sensor, the owner of the sensor will be notified and you will not be able to access the sensor anymore."; "TagSettings.General.Owner.none" = "No owner"; @@ -593,7 +593,7 @@ Your RuuviTag sensor is ready for use!"; "claim_in_progress" = "Claiming in progress"; "force_claim_sensor" = "Force Claim Sensor"; "force_claim_sensor_description1" = "This sensor has been claimed by another user. You can force the ownership to your account if you have physical access to this sensor. Each Ruuvi sensor can have only one owner."; -"force_claim_sensor_description2" = "Force Claim is done by using Near-Field Communication (NFC). Make sure NFC is enabled on your mobile device.\n\n\t1. Touch your Ruuvi sensor with your mobile device to start the claiming process.\n\n\t2. When successfully claimed, you will be sent back to Sensor Settings.\n\nIf claiming was unsuccessful or NFC is not available on your device:\n\n\t1. Open the cover of your Ruuvi sensor.\n\n\t2. Locate the round black button (or button \"B\" in case your sensor has 2 buttons) on the white circuit board and press it briefly to start the claiming process.\n\n\t3. When successfully claimed you will be sent back to Sensor Settings."; +"force_claim_sensor_description2" = "Force Claim is done by using Near-Field Communication (NFC). Make sure NFC is enabled on your mobile device.\n\n\t1. Touch your Ruuvi sensor with your mobile device to start the claiming process.\n\n\t2. When successfully claimed, you will be sent back to Sensor Settings.\n\nIf claiming was unsuccessful or NFC is not available on your device:\n\n\t1. Open the cover of your Ruuvi sensor.\n\n\t2. Locate the round black button (or button \"B\" in case your sensor has 2 buttons) on the white circuit board and press it briefly, then tap on Use BT button to start the claiming process.\n\n\t3. When successfully claimed you will be sent back to Sensor Settings."; "force_claim" = "Force Claim"; "claim_wrong_sensor_scanned" = "You are scanning different RuuviTag"; "view" = "View"; @@ -692,3 +692,22 @@ Your RuuviTag sensor is ready for use!"; "Defaults.DevServer.message" = "Changing Ruuvi Cloud endpoint requires signing out from current session and restart the app. Are you sure?"; "Defaults.ShowEmailAlertsSettings.title" = "Show email alerts settings"; "Defaults.ShowPushAlertsSettings.title" = "Show push alerts settings"; +"use_nfc" = "Use NFC"; +"use_bluetooth" = "Use BT"; +"sensor_not_found_error" = "Sensor not found. Try again."; +"Defaults.HideNFC.title" = "Hide NFC Option for sensor contest"; +"settings_alert_sound_default" = "System Default"; +"settings_alert_sound_ruuvi_speak" = "Ruuvi Alert"; +"add_with_nfc" = "Add with NFC"; +"sensor_details" = "Sensor Details"; +"add_sensor" = "Add Sensor"; +"copy_details" = "Copy Details"; +"name" = "Name:"; +"mac_address" = "Mac Address:"; +"unique_id" = "Unique ID:"; +"firmware_version" = "Firmware Version:"; +"close" = "Close"; +"add_sensor_nfc_df3_error" = "This tag cannot be added with NFC due to old firmware. Please add the tag with Bluetooth and update firmware."; +"unclaim_sensor" = "Unclaim sensor"; +"unclaim" = "Unclaim"; +"unclaim_sensor_description" = "Ownership of this sensor has been claimed to your Ruuvi account. Press Unclaim to remove this sensor's settings and related data from your Ruuvi account."; diff --git a/station/Resources/Strings/fi.lproj/Localizable.strings b/station/Resources/Strings/fi.lproj/Localizable.strings index 0415312fb..30bf3bab9 100644 --- a/station/Resources/Strings/fi.lproj/Localizable.strings +++ b/station/Resources/Strings/fi.lproj/Localizable.strings @@ -259,20 +259,20 @@ Mikäli et näe Kieli-valintaa asetuksissa, varmista, että sinulla on vähintä "TagSettings.ConnectStatus.Disconnected" = "Ei yhteyttä"; "TagCharts.Export.title" = "LATAA"; "h" = "t"; -"About.AboutHelp.contents" = "Ruuvi Station on helppokäyttöinen sovellus Ruuvin tuotteiden hallinnointiin. Aloita lisäämällä antureita."; +"About.AboutHelp.contents" = "Ruuvi Station on kätevä ja helppokäyttöinen sovellus, jonka avulla voit seurata Ruuvin anturien mittaustietoja."; "About.AboutHelp.header" = "Tietoa / Apua"; "About.TagsCount.text" = "Lisätyt anturit: %d"; "About.MeasurementsCount.text" = "Paikallisesti tallennettujen mittausten lukumäärä: %d"; "About.DatabaseSize.text" = "Tietokannan koko: %@"; "About.More.contents" = "Ruuvin kotisivu: ruuvi.com\nRuuvi Forum: f.ruuvi.com\nRuuvi Blog: ruuvi.com/fi/blogi\nRuuvi Twitter: twitter.com/ruuvicom"; "About.More.header" = "Lue lisää"; -"About.OpenSource.contents" = "Kuten muutkin Ruuvin tuotteet, myös Ruuvi Station perustuu avoimeen lähdekoodiin: github.com/ruuvi"; +"About.OpenSource.contents" = "Kuten Ruuvi-anturit, Ruuvi Station -sovellukset perustuvat avoimeen lähdekoodiin. Seuraa kehitystä ja osallistu osoitteessa: github.com/ruuvi"; "About.OpenSource.header" = "Avoin lähdekoodi"; -"About.OperationsManual.contents" = "Olemme suunnitelleet ohjelman ja laitteemme mahdollisimman helppokäyttöisiksi, mutta myös ohjeet ovat saatavilla osoitteessa ruuvi.com/fi/tuki"; +"About.OperationsManual.contents" = "Aloita Ruuvi Station -mobiilisovelluksen käyttö ohjeidemme avulla: ruuvi.com/fi/tuki/station-mobile-fi"; "About.OperationsManual.header" = "Käyttöohje"; "About.Privacy.contents" = "Sovelluksen käyttäminen vaatii Ruuvin käyttöehtojen hyväksymisen: ruuvi.com/terms"; "About.Privacy.header" = "Tietosuojakäytäntö"; -"About.Troubleshooting.contents" = "Jos törmäät ongelmiin ja ohjeet eivät auttaneet, tarkista vianetsintäkysymykset: ruuvi.com/fi/tuki"; +"About.Troubleshooting.contents" = "Löydä apua Ruuvi Station -sovellusten, Ruuvi-tuotteiden ja Ruuvi Cloud -palvelun käyttöön tukikeskuksestamme: ruuvi.com/fi/tuki"; "About.Troubleshooting.header" = "Vianetsintä"; "Interval.Hour.string" = "tuntia"; "Interval.Days.string" = "päivää"; @@ -287,6 +287,7 @@ Mikäli et näe Kieli-valintaa asetuksissa, varmista, että sinulla on vähintä "Menu.Label.AppSettings.text" = "Asetukset"; "Menu.Label.GetMoreSensors.text" = "Osta Ruuvi-antureita"; "Ruuvi.BuySensors.URL.IOS" = "https://ruuvi.com/fi/tuotteet?utm_campaign=app_ua&utm_medium=referral&utm_source=ios"; +"Ruuvi.BuySensors.Menu.URL.IOS" = "https://ruuvi.com/fi/tuotteet?utm_campaign=app_ua_nav&utm_medium=referral&utm_source=ios"; "Menu.Label.BuyRuuviGateway.text" = "Osta Ruuvi Gateway"; "Menu.BuyGateway.URL.IOS" = "https://ruuvi.com/fi/gateway?utm_campaign=app_ua&utm_medium=referral&utm_source=ios"; "Menu.Label.WhatToMeasure.text" = "Mitä mitata Ruuvilla?"; @@ -372,7 +373,6 @@ Mikäli et näe Kieli-valintaa asetuksissa, varmista, että sinulla on vähintä "SignIn.CodeHint" = "Koodi"; "TagsManagerPresenter.SignOutConfirmAlert.Message" = "Anturit, joiden omistajuuden olet vahvistanut asetussivulla poistetaan sovelluksesta automaattisesti uloskirjauduttaessa. Anturitiedot palautetaan pilvipalvelusta samalla sähköpostiosoitteella uudelleenkirjauduttaessa.\n\nHaluatko varmasti kirjautua ulos?"; "TagSettings.ClaimTagButton.Claim" = "Vahvista omistajuus"; -"TagSettings.ClaimTagButton.Unclaim" = "Vapauta omistajuus"; "TagSettings.ShareButton" = "Jaa"; "Syncing..." = "Ladataan historiaa pilvestä..."; "Synchronized" = "Synkronoitu"; @@ -512,7 +512,7 @@ RuuviTag on valmis käyttöön!"; "TagSettings.NotShared.title" = "Ei jaettu"; "Owner.title" = "Vahvista omistajuus"; "Owner.ClaimOwnership.button" = "Vahvista Omistajuus"; -"Owner.Claim.description" = "Omistatko tämän anturin? Vahvista omistajuus ja anturi lisätään Ruuvi-tilillesi. Jokaisella Ruuvi-anturilla voi olla vain yksi omistaja.\n\nHyödyt:\n\n ● Anturien nimet, taustakuvat sekä kalibrointi- ja hälytysasetukset tallentuvat turvallisesti tilillesi pilvipalveluun\n\n ● Lue anturitiedot etänä verkossa (vaatii Ruuvi Gateway -reitittimen)\n\n ● Jaa antureita ystävien ja perheen kesken (vaatii Ruuvi Gateway -reitittimen)\n\n ● Selaa jopa 2 vuoden historiatietoja osoitteessa station.ruuvi.com (vaatii Ruuvi Gateway -reitittimen)"; +"Owner.Claim.description" = "Omistatko tämän anturin? Vahvista omistajuus ja anturi lisätään Ruuvi-tilillesi. Jokaisella Ruuvi-anturilla voi olla vain yksi omistaja. Vahvistaaksesi omistajuuden, sinun täytyy olla kirjautuneena sisälle.\n\nHyödyt:\n\n ● Anturien nimet, taustakuvat sekä kalibrointi- ja hälytysasetukset tallentuvat turvallisesti tilillesi pilvipalveluun\n\n ● Lue anturitiedot etänä verkossa (vaatii Ruuvi Gateway -reitittimen)\n\n ● Jaa antureita ystävien ja perheen kesken (vaatii Ruuvi Gateway -reitittimen)\n\n ● Selaa jopa 2 vuoden historiatietoja osoitteessa station.ruuvi.com (vaatii Ruuvi Gateway -reitittimen)"; "TagSettings.confirmTagUnclaimAndRemoveDialog.message" = "Anturin poistaminen mitätöi omistajuuden. Kuka tahansa voi vaatia anturin omistajuutta poistamisen jälkeen. Jokaisella Ruuvi-anturilla voi olla vain yksi omistaja."; "TagSettings.confirmSharedTagRemovalDialog.message" = "Poistamisesta ilmoitetaan anturin omistajalle ja anturin tietojen lukuoikeus päättyy."; "TagSettings.General.Owner.none" = "Ei omistajaa"; @@ -593,7 +593,7 @@ RuuviTag on valmis käyttöön!"; "claim_in_progress" = "Omistajuutta vaihdetaan"; "force_claim_sensor" = "Vaihda omistajuus"; "force_claim_sensor_description1" = "Tällä anturilla on jo toinen omistaja. Omistajuus voidaan vaihtaa pakotetusti, mikäli anturi on lähettyvilläsi. Jokaisella Ruuvi-anturilla voi olla vain yksi omistaja."; -"force_claim_sensor_description2" = "Omistajuuden pakotettu vaihtaminen tapahtuu Near-Field Communication (NFC) ominaisuutta käyttämällä. Varmista, että NFC on käytössä mobiililaitteessasi.\n\n\t1. Aloita omistajuuden pakotettu vaihto koskettamalla Ruuvi-anturia mobiililaitteellasi.\n\n\t2. Sinut siirretään takaisin Asetukset-sivulle onnistuneen vaihdon päätteeksi.\n\nMikäli pakotettu vaihto epäonnistui tai NFC-ominaisuutta ei ole saatavilla mobiililaitteessasi:\n\n\t1. Avaa Ruuvi-anturin suojakotelo.\n\n\t2. Paikanna valkoisella piirilevyllä oleva pieni musta painike (tai painike \"B\", mikäli anturissasi on 2 painiketta) ja napauta painiketta lyhyesti pakotetun vaihdon aloittamiseksi.\n\n\t3. Sinut siirretään takaisin Asetukset-sivulle onnistuneen vaihdon päätteeksi."; +"force_claim_sensor_description2" = "Omistajuuden pakotettu vaihtaminen tapahtuu Near-Field Communication (NFC) ominaisuutta käyttämällä. Varmista, että NFC on käytössä mobiililaitteessasi.\n\n\t1. Aloita omistajuuden pakotettu vaihto koskettamalla Ruuvi-anturia mobiililaitteellasi.\n\n\t2. Sinut siirretään takaisin Asetukset-sivulle onnistuneen vaihdon päätteeksi.\n\nMikäli pakotettu vaihto epäonnistui tai NFC-ominaisuutta ei ole saatavilla mobiililaitteessasi:\n\n\t1. Avaa Ruuvi-anturin suojakotelo.\n\n\t2. Paikanna valkoisella piirilevyllä oleva pieni musta painike (tai painike \"B\", mikäli anturissasi on 2 painiketta), napauta painiketta ensin lyhyesti ja paina tämän jälkeen sivulla olevaa Käytä BT -nappia pakotetun vaihdon aloittamiseksi.\n\n\t3. Sinut siirretään takaisin Asetukset-sivulle onnistuneen vaihdon päätteeksi."; "force_claim" = "Vaadi omistajuutta"; "claim_wrong_sensor_scanned" = "Olet lukemassa väärää RuuviTagia"; "view" = "Näkymä"; @@ -673,22 +673,41 @@ RuuviTag on valmis käyttöön!"; "changelog_ios_url" = "https://f.ruuvi.com/t/3192"; "shared_to_x" = "Jaettu %d/%d"; "settings_alert_notifications" = "Hälytysilmoitukset"; -"settings_alert_sound" = "Alert Sound"; -"settings_alert_sound_description" = "Select push notification alert sound."; -"settings_alerts_footer_description" = "You can also adjust Notification settings under iOS Settings -> Notifications"; -"settings_alerts_footer_description_link_mask" = "iOS Settings -> Notifications"; -"settings_email_alerts" = "Email Alerts"; -"settings_email_alerts_description" = "If you are using Ruuvi Cloud and Ruuvi Gateway, you will be able to receive email alerts by enabling this."; -"settings_push_alerts" = "Push Alerts"; -"settings_push_alerts_description" = "If you are using Ruuvi Cloud and Ruuvi Gateway, you will be able to receive push alerts by enabling this."; +"settings_alert_sound" = "Hälytysääni"; +"settings_alert_sound_description" = "Valitse push-viestin hälytysääni."; +"settings_alerts_footer_description" = "Voit myös muokata ilmoitusasetuksia kohdassa iOS-asetukset -> Ilmoitukset"; +"settings_alerts_footer_description_link_mask" = "iOS-asetukset -> Ilmoitukset"; +"settings_email_alerts" = "Sähköpostihälytykset"; +"settings_email_alerts_description" = "Jos olet Ruuvi Cloud ja Ruuvi Gateway -käyttäjä, voit vastaanottaa sähköpostihälytyksiä ottamalla tämän käyttöön."; +"settings_push_alerts" = "Push-hälytykset"; +"settings_push_alerts_description" = "Jos olet Ruuvi Cloud ja Ruuvi Gateway -käyttäjä, voit vastaanottaa push-hälytyksiä ottamalla tämän käyttöön."; "synchronisation" = "Synkronointi"; "gatt_sync_description" = "Ruuvi Station lataa anturin sisäisen historian viimeiseltä 10 päivältä, jos historia on saatavilla.\n\nMittaushistoria ladataan Bluetooth-yhteydellä. Varmista, että olet anturin läheisyydessä."; "do_not_show_again" = "Älä näytä tätä uudestaan"; "sign_in_continue" = "Jatka"; "signing_in_is_optional" = "(Kirjautuminen ei ole pakollista)"; -"Defaults.UserAuthorized.title" = "User Authorized"; -"Defaults.DashboardTapActionChart.title" = "Show Chart on Dashboard Card Tap"; -"Defaults.DevServer.title" = "Use Dev Server"; -"Defaults.DevServer.message" = "Changing Ruuvi Cloud endpoint requires signing out from current session and restart the app. Are you sure?"; -"Defaults.ShowEmailAlertsSettings.title" = "Show email alerts settings"; -"Defaults.ShowPushAlertsSettings.title" = "Show push alerts settings"; +"Defaults.UserAuthorized.title" = "Käyttäjä autorisoitu"; +"Defaults.DashboardTapActionChart.title" = "Näytä kuvaaja napauttamalla koontinäytön korttia"; +"Defaults.DevServer.title" = "Käytä dev-palvelinta"; +"Defaults.DevServer.message" = "Ruuvi Cloud -päätepisteen muuttaminen vaatii kirjautumisen ulos nykyisestä istunnosta ja käynnistämään sovelluksen uudelleen. Oletko varma?"; +"Defaults.ShowEmailAlertsSettings.title" = "Näytä sähköpostihälytysasetukset"; +"Defaults.ShowPushAlertsSettings.title" = "Näytä push-hälytysasetukset"; +"use_nfc" = "Käytä NFC:tä"; +"use_bluetooth" = "Käytä BT:tä"; +"sensor_not_found_error" = "Anturia ei löytynyt. Yritä uudelleen."; +"Defaults.HideNFC.title" = "Piilota NFC-vaihtoehto pakotetusta omistajuudenvaihdosta"; +"settings_alert_sound_default" = "Järjestelmän oletus"; +"settings_alert_sound_ruuvi_speak" = "Ruuvi hälytys"; +"add_with_nfc" = "Add with NFC"; +"sensor_details" = "Sensor Details"; +"add_sensor" = "Add Sensor"; +"copy_details" = "Copy Details"; +"name" = "Name:"; +"mac_address" = "Mac Address:"; +"unique_id" = "Unique ID:"; +"firmware_version" = "Firmware Version:"; +"close" = "Close"; +"add_sensor_nfc_df3_error" = "This tag cannot be added with NFC due to old firmware. Please add the tag with Bluetooth and update firmware."; +"unclaim_sensor" = "Unclaim sensor"; +"unclaim" = "Unclaim"; +"unclaim_sensor_description" = "Ownership of this sensor has been claimed to your Ruuvi account. Press Unclaim to remove this sensor's settings and related data from your Ruuvi account."; diff --git a/station/Resources/Strings/fr.lproj/Localizable.strings b/station/Resources/Strings/fr.lproj/Localizable.strings index 6553d755a..67a15fc02 100644 --- a/station/Resources/Strings/fr.lproj/Localizable.strings +++ b/station/Resources/Strings/fr.lproj/Localizable.strings @@ -258,20 +258,20 @@ "TagSettings.ConnectStatus.Disconnected" = "Déconnecté"; "TagCharts.Export.title" = "EXPORTER"; "h" = "h"; -"About.AboutHelp.contents" = "Ruuvi Station est une application intuitive pour contôler vos capteurs Ruuvi. Commencez en jumelant des capteurs."; +"About.AboutHelp.contents" = "Ruuvi Station est une application facile à utiliser qui vous permet de surveiller les données de mesure des capteurs Ruuvi."; "About.AboutHelp.header" = "Infos / Aide"; "About.TagsCount.text" = "Capteurs jumelés : %d"; "About.MeasurementsCount.text" = "Données enregistrées : %d"; "About.DatabaseSize.text" = "Taille de la base de données : %@"; "About.More.contents" = "Site officiel de Ruuvi : ruuvi.com\nRuuvi Forum : f.ruuvi.com\nRuuvi Blog : ruuvi.com/blog\nRuuvi Twitter : twitter.com/ruuvicom"; "About.More.header" = "Plus d'infos"; -"About.OpenSource.contents" = "Comme tous les produits Ruuvi, Ruuvi Station est également open-source : github.com/ruuvi"; +"About.OpenSource.contents" = "Tout comme les capteurs Ruuvi, les applications Ruuvi Station sont open source. Suivez le développement et contribuez sur : github.com/ruuvi"; "About.OpenSource.header" = "Open-source"; -"About.OperationsManual.contents" = "Nous avons conçu nos produits et leur application pour qu'ils soient faciles à utiliser. Une notice est cependant disponible : ruuvi.com/support"; +"About.OperationsManual.contents" = "Commencez à utiliser l'application mobile Ruuvi Station avec nos guides en ligne : ruuvi.com/support/station-mobile"; "About.OperationsManual.header" = "Notice d'utilisation"; "About.Privacy.contents" = "L'utilisation de l'application nécessite l'approbation des conditions générales de Ruuvi : ruuvi.com/terms"; "About.Privacy.header" = "Protection des données personnelles"; -"About.Troubleshooting.contents" = "Si vous rencontrez des problèmes et que vous ne trouvez pas la réponse dans la notice, veuillez consulter la page d'assistance technique : ruuvi.com/support"; +"About.Troubleshooting.contents" = "Trouvez de l'aide en utilisant les applications Ruuvi Station, les produits Ruuvi et le service Ruuvi Cloud depuis notre centre d'assistance : ruuvi.com/support"; "About.Troubleshooting.header" = "Dépannage technique"; "Interval.Hour.string" = "Heures"; "Interval.Days.string" = "Jours"; @@ -286,6 +286,7 @@ "Menu.Label.AppSettings.text" = "Réglages"; "Menu.Label.GetMoreSensors.text" = "Acheter des capteurs"; "Ruuvi.BuySensors.URL.IOS" = "https://ruuvi.com/products?utm_campaign=app_ua&utm_medium=referral&utm_source=ios"; +"Ruuvi.BuySensors.Menu.URL.IOS" = "https://ruuvi.com/products?utm_campaign=app_ua_nav&utm_medium=referral&utm_source=ios"; "Menu.Label.BuyRuuviGateway.text" = "Acheter le routeur Ruuvi Gateway"; "Menu.BuyGateway.URL.IOS" = "https://ruuvi.com/gateway?utm_campaign=app_ua&utm_medium=referral&utm_source=ios"; "Menu.Label.WhatToMeasure.text" = "Que mesurer avec Ruuvi?"; @@ -371,7 +372,6 @@ "SignIn.CodeHint" = "Code"; "TagsManagerPresenter.SignOutConfirmAlert.Message" = "Les capteurs dont vous êtes propriétaire seront suspprimés de l'application lorsque vous vous déconnectez de votre compte Ruuvi. Les données seront importées du cloud lorsque vous vous connectez à nouveau.\n\nSouhaitez vous vous déconnecter ?"; "TagSettings.ClaimTagButton.Claim" = "Confirmer l'appropriation"; -"TagSettings.ClaimTagButton.Unclaim" = "Réinitialiser l'appropriation"; "TagSettings.ShareButton" = "Partager"; "Syncing..." = "Synchronisation..."; "Synchronized" = "Synchronisé"; @@ -511,7 +511,7 @@ RuuviTag est prêt à être utilisé !"; "TagSettings.NotShared.title" = "Non partagé"; "Owner.title" = "Confirmer l'appropriation"; "Owner.ClaimOwnership.button" = "Confirmer l'appropriation"; -"Owner.Claim.description" = "Êtes-vous propriétaire de ce capteur ? En devenant propriétaire, le capteur sera associé à votre compte Ruuvi. Chaque capteur ne peut avoir qu'un seul propriétaire.\n\nAvantages :\n\n ● Les noms, fonds d'écrans, les réglages de calibration, ainsi que les alertes sont enregistrés de façon sécurisée dans notre cloud\n\n ● Consultez les données à distance depuis n'importe quel navigateur (routeur Ruuvi Gateway requis)\n\n ● Partager les capteurs avec votre famille ou des amis (routeur Ruuvi Gateway requis)\n\n ● consultez jusqu'à deux ans d'historique de vos données sur le site station.ruuvi.com (routeur Ruuvi Gateway requis)"; +"Owner.Claim.description" = "Êtes-vous propriétaire de ce capteur ? En devenant propriétaire, le capteur sera associé à votre compte Ruuvi. Chaque capteur ne peut avoir qu'un seul propriétaire. Pour revendiquer la propriété, vous devez être connecté.\n\nAvantages:\n\n ● Les noms, fonds d'écrans, les réglages de calibration, ainsi que les alertes sont enregistrés de façon sécurisée dans notre cloud\n\n ● Consultez les données à distance depuis n'importe quel navigateur (routeur Ruuvi Gateway requis)\n\n ● Partager les capteurs avec votre famille ou des amis (routeur Ruuvi Gateway requis)\n\n ● consultez jusqu'à deux ans d'historique de vos données sur le site station.ruuvi.com (routeur Ruuvi Gateway requis)"; "TagSettings.confirmTagUnclaimAndRemoveDialog.message" = "Le propriétaire sera réinitialisé en supprimant le capteur. N'importe qui pourra alors s'approprier le capteur. Chaque capteur Ruuvi ne pouvant avoir qu'un propriétaire."; "TagSettings.confirmSharedTagRemovalDialog.message" = "En supprimant le capteur vous n'aurez plus accès aux données du capteur et une notification sera envoyée au propriétaire."; "TagSettings.General.Owner.none" = "Pas de propriétaire"; @@ -589,11 +589,11 @@ RuuviTag est prêt à être utilisé !"; "settings_and_alerts" = "Paramètres & alertes"; "change_background" = "Paramètres et alertes"; "check_claim_state" = "Vérification de l'état de la demande"; -"claim_in_progress" = "Claiming in progress"; +"claim_in_progress" = "Réclamation en cours"; "force_claim_sensor" = "Revendication en cours"; "force_claim_sensor_description1" = "Ce capteur a été réclamé par un autre utilisateur. Vous pouvez forcer la propriété à votre compte si vous avez un accès physique à ce capteur. Chaque capteur Ruuvi ne peut avoir qu'un seul propriétaire."; "force_claim_sensor_description2" = "La réclamation forcée est effectuée en utilisant la communication en champ proche (NFC). Assurez-vous que la NFC est activée sur votre appareil mobile. 1. Touchez le capteur de votre Ruuvi avec votre appareil mobile pour lancer le processus de réclamation. - 2. Une fois la réclamation réussie, vous serez renvoyé aux paramètres du capteur. Si la réclamation n'a pas abouti ou si la technologie NFC n'est pas disponible sur votre appareil : 1. Ouvrez le couvercle de votre capteur Ruuvi. 2. Localisez le bouton rond noir (ou le bouton \"B\" si votre capteur a 2 boutons) sur la carte de circuit imprimé blanche et appuyez dessus brièvement pour lancer le processus de réclamation. + 2. Une fois la réclamation réussie, vous serez renvoyé aux paramètres du capteur. Si la réclamation n'a pas abouti ou si la technologie NFC n'est pas disponible sur votre appareil : 1. Ouvrez le couvercle de votre capteur Ruuvi. 2. Localisez le bouton rond noir (ou le bouton \"B\" si votre capteur a 2 boutons) sur la carte de circuit imprimé blanche et appuyez dessus brièvement, puis appuyez sur le bouton Utiliser BT pour démarrer le processus de réclamation. 3. Une fois la réclamation réussie, vous serez renvoyé aux paramètres du capteur."; "force_claim" = "Revendication de force"; "claim_wrong_sensor_scanned" = "Vous scannez différents RuuviTag"; @@ -674,23 +674,42 @@ RuuviTag est prêt à être utilisé !"; "changelog_ios_url" = "https://f.ruuvi.com/t/3192"; "shared_to_x" = "Partagé %d/%d"; "settings_alert_notifications" = "Notifications d'alerte"; -"settings_alert_sound" = "Alert Sound"; -"settings_alert_sound_description" = "Select push notification alert sound."; -"settings_alerts_footer_description" = "You can also adjust Notification settings under iOS Settings -> Notifications"; -"settings_alerts_footer_description_link_mask" = "iOS Settings -> Notifications"; -"settings_email_alerts" = "Email Alerts"; -"settings_email_alerts_description" = "If you are using Ruuvi Cloud and Ruuvi Gateway, you will be able to receive email alerts by enabling this."; -"settings_push_alerts" = "Push Alerts"; -"settings_push_alerts_description" = "If you are using Ruuvi Cloud and Ruuvi Gateway, you will be able to receive push alerts by enabling this."; +"settings_alert_sound" = "Son d'alerte"; +"settings_alert_sound_description" = "Sélectionnez le son de notification push."; +"settings_alerts_footer_description" = "Vous pouvez également ajuster les paramètres de notification sous Paramètres iOS -> Notifications"; +"settings_alerts_footer_description_link_mask" = "Paramètres iOS -> Notifications"; +"settings_email_alerts" = "Alertes courrier électronique"; +"settings_email_alerts_description" = "Si vous utilisez Ruuvi Cloud et Ruuvi Gateway, vous pourrez recevoir des alertes par e-mail en activant ceci."; +"settings_push_alerts" = "Alertes push"; +"settings_push_alerts_description" = "Si vous utilisez Ruuvi Cloud et Ruuvi Gateway, vous pourrez recevoir des alertes push en activant ceci."; "synchronisation" = "Synchronisation"; "gatt_sync_description" = "Ruuvi Station télécharge l'historique interne du capteur pour les 10 derniers jours si l'historique des mesures est disponible.\n\nL'historique est téléchargé à l'aide d'une connexion Bluetooth. Assurez-vous d'être à proximité du capteur."; "do_not_show_again" = "Ne plus afficher ça"; "sign_in_continue" = "Continuer"; "signing_in_is_optional" = "(La connexion est facultative)"; -"Defaults.UserAuthorized.title" = "User Authorized"; -"Defaults.DashboardTapActionChart.title" = "Show Chart on Dashboard Card Tap"; -"Defaults.DevServer.title" = "Use Dev Server"; -"Defaults.DevServer.message" = "Changing Ruuvi Cloud endpoint requires signing out from current session and restart the app. Are you sure?"; -"Defaults.ShowEmailAlertsSettings.title" = "Show email alerts settings"; -"Defaults.ShowPushAlertsSettings.title" = "Show push alerts settings"; +"Defaults.UserAuthorized.title" = "Utilisateur autorisé"; +"Defaults.DashboardTapActionChart.title" = "Appuyez sur la carte du tableau de bord pour afficher le graphique"; +"Defaults.DevServer.title" = "Utiliser le serveur de développement"; +"Defaults.DevServer.message" = "La modification du point de terminaison Ruuvi Cloud nécessite de se déconnecter de la session en cours et de redémarrer l'application. Es-tu sûr?"; +"Defaults.ShowEmailAlertsSettings.title" = "Afficher les paramètres d'alertes par e-mail"; +"Defaults.ShowPushAlertsSettings.title" = "Afficher les paramètres des alertes push"; +"use_nfc" = "Utiliser NFC"; +"use_bluetooth" = "Utiliser BT"; +"sensor_not_found_error" = "Capteur introuvable. Essayer à nouveau."; +"Defaults.HideNFC.title" = "Masquer l'option NFC du changement de propriété forcé"; +"settings_alert_sound_default" = "Défaillance du système"; +"settings_alert_sound_ruuvi_speak" = "Ruuvi alarme"; +"add_with_nfc" = "Add with NFC"; +"sensor_details" = "Sensor Details"; +"add_sensor" = "Add Sensor"; +"copy_details" = "Copy Details"; +"name" = "Name:"; +"mac_address" = "Mac Address:"; +"unique_id" = "Unique ID:"; +"firmware_version" = "Firmware Version:"; +"close" = "Close"; +"add_sensor_nfc_df3_error" = "This tag cannot be added with NFC due to old firmware. Please add the tag with Bluetooth and update firmware."; +"unclaim_sensor" = "Unclaim sensor"; +"unclaim" = "Unclaim"; +"unclaim_sensor_description" = "Ownership of this sensor has been claimed to your Ruuvi account. Press Unclaim to remove this sensor's settings and related data from your Ruuvi account."; diff --git a/station/Resources/Strings/ru.lproj/Localizable.strings b/station/Resources/Strings/ru.lproj/Localizable.strings index ae82fde88..8257c478c 100644 --- a/station/Resources/Strings/ru.lproj/Localizable.strings +++ b/station/Resources/Strings/ru.lproj/Localizable.strings @@ -259,20 +259,20 @@ If you cannot see the Language option in the settings, make sure that you have a "TagSettings.ConnectStatus.Disconnected" = "Отсоединен"; "TagCharts.Export.title" = "ЭКСПОРТ"; "h" = "ч"; -"About.AboutHelp.contents" = "Ruuvi Station - простой в использовании, но мощный инструмент для работы с вашими Ruuvi сенсорами."; +"About.AboutHelp.contents" = "Ruuvi Station — это простое в использовании приложение, позволяющее отслеживать данные измерений датчиков Ruuvi."; "About.AboutHelp.header" = "О приложении"; "About.TagsCount.text" = "Добавленных датчиков: %d"; "About.MeasurementsCount.text" = "Кол-во сохраненных измерений: %d"; "About.DatabaseSize.text" = "Размер базы данных: %@"; "About.More.contents" = "Веб-сайт: ruuvi.com\nФорум: f.ruuvi.com\nБлог: ruuvi.com\nTwitter: twitter.com/ruuvicom"; "About.More.header" = "Почитать еще"; -"About.OpenSource.contents" = "Как и сенсоры Ruuvi, Ruuvi Station также доступна в исходных кодах: github.com/ruuvi"; +"About.OpenSource.contents" = "Как и датчики Ruuvi, приложения Ruuvi Station имеют открытый исходный код. Следите за развитием и вносите свой вклад на: github.com/ruuvi"; "About.OpenSource.header" = "Открытый исходный код"; -"About.OperationsManual.contents" = "Мы разработали приложение так, чтобы оно было интуитивно понятным. В то же время, руководства доступны здесь: ruuvi.com/support"; +"About.OperationsManual.contents" = "Начните использовать мобильное приложение Ruuvi Station с помощью наших онлайн-руководств: ruuvi.com/support/station-mobile"; "About.OperationsManual.header" = "Руководство"; "About.Privacy.contents" = "Используя это приложение, вы соглашаетесь со стандартными условиями и положениями Ruuvi: ruuvi.com/terms"; "About.Privacy.header" = "Политика конфиденциальности"; -"About.Troubleshooting.contents" = "Если вы испытываете какие либо проблемы или наши руководства не помогли вам, посмотрите наши многочисленные темы с решением проблем здесь: ruuvi.com/support"; +"About.Troubleshooting.contents" = "Получите помощь по использованию приложений Ruuvi Station, продуктов Ruuvi и облачной службы Ruuvi в нашем центре поддержки: ruuvi.com/support"; "About.Troubleshooting.header" = "Решение проблем"; "Interval.Hour.string" = "Часов"; "Interval.Days.string" = "Дней"; @@ -287,6 +287,7 @@ If you cannot see the Language option in the settings, make sure that you have a "Menu.Label.AppSettings.text" = "Настройки"; "Menu.Label.GetMoreSensors.text" = "Купить датчики Ruuvi"; "Ruuvi.BuySensors.URL.IOS" = "https://ruuvi.com/products?utm_campaign=app_ua&utm_medium=referral&utm_source=ios"; +"Ruuvi.BuySensors.Menu.URL.IOS" = "https://ruuvi.com/products?utm_campaign=app_ua_nav&utm_medium=referral&utm_source=ios"; "Menu.Label.BuyRuuviGateway.text" = "Купить Ruuvi Gateway"; "Menu.BuyGateway.URL.IOS" = "https://ruuvi.com/gateway?utm_campaign=app_ua&utm_medium=referral&utm_source=ios"; "Menu.Label.WhatToMeasure.text" = "Что измерять с помощью Ruuvi?"; @@ -372,7 +373,6 @@ If you cannot see the Language option in the settings, make sure that you have a "SignIn.CodeHint" = "Код для входа"; "TagsManagerPresenter.SignOutConfirmAlert.Message" = "После выхода все датчики, которые зарегистрованы на вас, будут удалены из приложения. Если вы повторно войдете с тем же email адресом, список ваших датчиков будет загружен из облака. \n\nВы хотите выйти из сети?"; "TagSettings.ClaimTagButton.Claim" = "Заявить о собственности"; -"TagSettings.ClaimTagButton.Unclaim" = "Отказаться от владения"; "TagSettings.ShareButton" = "Поделиться"; "Syncing..." = "Синхронизация..."; "Synchronized" = "Синхронизировано"; @@ -512,7 +512,7 @@ If you cannot see the Language option in the settings, make sure that you have a "TagSettings.NotShared.title" = "Не делились"; "Owner.title" = "Заявить о собственности"; "Owner.ClaimOwnership.button" = "Заявить о владении"; -"Owner.Claim.description" = "Это ваш датчик? Если да, то пожалуйста заявите о владении и датчик будет добавлен в ваш Ruuvi аккаунт. Каждый датчик может иметь только одного владельца.\n\nПреимущества:\n\n ● Название датчика, изображение фона, данные калибровки и настройки тревог будут безопасно храниться в облаке\n\n ● Получите доступ к датчикам через интернет (требуестся Ruuvi Gateway)\n\n ● Поделитесь данными датчиков с друзьями и членами семьи (требуется Ruuvi Gateway)\n\n ● Просматривайте до 2-х лет истории на сайте station.ruuvi.com (требуется a Ruuvi Gateway)"; +"Owner.Claim.description" = "Это ваш датчик? Если да, то пожалуйста заявите о владении и датчик будет добавлен в ваш Ruuvi аккаунт. Каждый датчик может иметь только одного владельца. Чтобы заявить права собственности, вам необходимо войти в систему.\n\nПреимущества:\n\n ● Название датчика, изображение фона, данные калибровки и настройки тревог будут безопасно храниться в облаке\n\n ● Получите доступ к датчикам через интернет (требуестся Ruuvi Gateway)\n\n ● Поделитесь данными датчиков с друзьями и членами семьи (требуется Ruuvi Gateway)\n\n ● Просматривайте до 2-х лет истории на сайте station.ruuvi.com (требуется a Ruuvi Gateway)"; "TagSettings.confirmTagUnclaimAndRemoveDialog.message" = "Удаляя датчик вы также отказываетесь от владения. Кто-то другой сможет заявить о владении этим датчиком. Каждый датчик может иметь только одного владельца."; "TagSettings.confirmSharedTagRemovalDialog.message" = "Если вы удалите датчик, то владелец будет уведомлен и вы более не сможете получать данные с него."; "TagSettings.General.Owner.none" = "Нет владельца"; @@ -593,7 +593,7 @@ If you cannot see the Language option in the settings, make sure that you have a "claim_in_progress" = "Claiming in progress"; "force_claim_sensor" = "Force Claim Sensor"; "force_claim_sensor_description1" = "This sensor has been claimed by another user. You can force the ownership to your account if you have physical access to this sensor. Each Ruuvi sensor can have only one owner."; -"force_claim_sensor_description2" = "Force Claim is done by using Near-Field Communication (NFC). Make sure NFC is enabled on your mobile device.\n\n\t1. Touch your Ruuvi sensor with your mobile device to start the claiming process.\n\n\t2. When successfully claimed, you will be sent back to Sensor Settings.\n\nIf claiming was unsuccessful or NFC is not available on your device:\n\n\t1. Open the cover of your Ruuvi sensor.\n\n\t2. Locate the round black button (or button \"B\" in case your sensor has 2 buttons) on the white circuit board and press it briefly to start the claiming process.\n\n\t3. When successfully claimed you will be sent back to Sensor Settings."; +"force_claim_sensor_description2" = "Force Claim is done by using Near-Field Communication (NFC). Make sure NFC is enabled on your mobile device.\n\n\t1. Touch your Ruuvi sensor with your mobile device to start the claiming process.\n\n\t2. When successfully claimed, you will be sent back to Sensor Settings.\n\nIf claiming was unsuccessful or NFC is not available on your device:\n\n\t1. Open the cover of your Ruuvi sensor.\n\n\t2. Locate the round black button (or button \"B\" in case your sensor has 2 buttons) on the white circuit board and press it briefly, then tap on Use BT button to start the claiming process.\n\n\t3. When successfully claimed you will be sent back to Sensor Settings."; "force_claim" = "Force Claim"; "claim_wrong_sensor_scanned" = "You are scanning different RuuviTag"; "view" = "Вид"; @@ -692,3 +692,22 @@ If you cannot see the Language option in the settings, make sure that you have a "Defaults.DevServer.message" = "Changing Ruuvi Cloud endpoint requires signing out from current session and restart the app. Are you sure?"; "Defaults.ShowEmailAlertsSettings.title" = "Show email alerts settings"; "Defaults.ShowPushAlertsSettings.title" = "Show push alerts settings"; +"use_nfc" = "Use NFC"; +"use_bluetooth" = "Use BT"; +"sensor_not_found_error" = "Sensor not found. Try again."; +"Defaults.HideNFC.title" = "Hide NFC Option for sensor contest"; +"settings_alert_sound_default" = "System Default"; +"settings_alert_sound_ruuvi_speak" = "Ruuvi Alert"; +"add_with_nfc" = "Add with NFC"; +"sensor_details" = "Sensor Details"; +"add_sensor" = "Add Sensor"; +"copy_details" = "Copy Details"; +"name" = "Name:"; +"mac_address" = "Mac Address:"; +"unique_id" = "Unique ID:"; +"firmware_version" = "Firmware Version:"; +"close" = "Close"; +"add_sensor_nfc_df3_error" = "This tag cannot be added with NFC due to old firmware. Please add the tag with Bluetooth and update firmware."; +"unclaim_sensor" = "Unclaim sensor"; +"unclaim" = "Unclaim"; +"unclaim_sensor_description" = "Ownership of this sensor has been claimed to your Ruuvi account. Press Unclaim to remove this sensor's settings and related data from your Ruuvi account."; diff --git a/station/Resources/Strings/sv.lproj/Localizable.strings b/station/Resources/Strings/sv.lproj/Localizable.strings index fe80e466f..a5c28093d 100644 --- a/station/Resources/Strings/sv.lproj/Localizable.strings +++ b/station/Resources/Strings/sv.lproj/Localizable.strings @@ -259,20 +259,20 @@ Om du inte kan se språkalternativet i inställningarna, se till att du har lagt "TagSettings.ConnectStatus.Disconnected" = "Frånkopplad"; "TagCharts.Export.title" = "EXPORTERA"; "h" = "h"; -"About.AboutHelp.contents" = "Ruuvi Station är en lättanvänt och kraftfullt program för Ruuvi sensorer. Sätt igång genom att lägga till sensorer."; +"About.AboutHelp.contents" = "Ruuvi Station är en lättanvänd applikation som låter dig övervaka mätdata från Ruuvi-sensorer."; "About.AboutHelp.header" = "Om / Hjälp"; "About.TagsCount.text" = "Tillagda sensorer: %d"; "About.MeasurementsCount.text" = "Sparade mätningar: %d"; "About.DatabaseSize.text" = "Databasens storlek: %@"; "About.More.contents" = "Ruuvis hemsida: ruuvi.com\nRuuvi Forum: f.ruuvi.com\nRuuvi Blog: ruuvi.com/blog\nRuuvi Twitter: twitter.com/ruuvicom"; "About.More.header" = "Läs mer"; -"About.OpenSource.contents" = "Likt Ruuvi sensor är också Ruuvi Station gjord med öppen källkod: github.com/ruuvi"; +"About.OpenSource.contents" = "Precis som Ruuvi-sensorer är Ruuvi Station-appar öppen källkod. Följ utvecklingen och bidra på: github.com/ruuvi"; "About.OpenSource.header" = "Öppen källkod"; -"About.OperationsManual.contents" = "Vi har designat appen så att den ska vara självförklarande men det finns också heltäckande manualer för både Ruuvi sensorer och Ruuvi Station: ruuvi.com/support"; +"About.OperationsManual.contents" = "Kom igång med Ruuvi Stations mobilapplikation med våra onlineguider: ruuvi.com/support/station-mobile"; "About.OperationsManual.header" = "Bruksanvisning"; "About.Privacy.contents" = "Användning av applikationen kräver godkännande av användarvillkoren\nruuvi.com/terms"; "About.Privacy.header" = "Integritetspolicy"; -"About.Troubleshooting.contents" = "Om du stöter på problem och våra manualer inte hjälper så kan det hjälpa att ta en titt på våra många felsökningsämnen: ruuvi.com/support"; +"About.Troubleshooting.contents" = "Hitta hjälp med att använda Ruuvi Station-appar, Ruuvi-produkter och Ruuvi Cloud-tjänst från vårt supportcenter: ruuvi.com/support"; "About.Troubleshooting.header" = "Felsökning"; "Interval.Hour.string" = "Timmar"; "Interval.Days.string" = "Dagar"; @@ -287,6 +287,7 @@ Om du inte kan se språkalternativet i inställningarna, se till att du har lagt "Menu.Label.AppSettings.text" = "App Inställningar"; "Menu.Label.GetMoreSensors.text" = "Köp Ruuvi-sensorer"; "Ruuvi.BuySensors.URL.IOS" = "https://ruuvi.com/products?utm_campaign=app_ua&utm_medium=referral&utm_source=ios"; +"Ruuvi.BuySensors.Menu.URL.IOS" = "https://ruuvi.com/products?utm_campaign=app_ua_nav&utm_medium=referral&utm_source=ios"; "Menu.Label.BuyRuuviGateway.text" = "Köp Ruuvi Gateway"; "Menu.BuyGateway.URL.IOS" = "https://ruuvi.com/gateway?utm_campaign=app_ua&utm_medium=referral&utm_source=ios"; "Menu.Label.WhatToMeasure.text" = "Vad ska man mäta med Ruuvi?"; @@ -372,7 +373,6 @@ Om du inte kan se språkalternativet i inställningarna, se till att du har lagt "SignIn.CodeHint" = "Kod"; "TagsManagerPresenter.SignOutConfirmAlert.Message" = "Sensorer som du har verifierat äganderätten till på inställningssidan tas automatiskt bort från appen när du loggar ut. Sensordata hämtas från molnet igen om du loggar in med samma e-postadress.\n\nÄr du säker på att du vill logga ut? "; "TagSettings.ClaimTagButton.Claim" = "Bekräfta ägandet"; -"TagSettings.ClaimTagButton.Unclaim" = "Hävda ägande"; "TagSettings.ShareButton" = "Dela"; "Syncing..." = "Synkroniserar..."; "Synchronized" = "Synkronisering klar"; @@ -512,7 +512,7 @@ RuuviTag-sensorn är redo att användas!"; "TagSettings.NotShared.title" = "Inte delad"; "Owner.title" = "Bekräfta ägandet"; "Owner.ClaimOwnership.button" = "Bekräfta Ägandet"; -"Owner.Claim.description" = "Äger du den här sensorn? Bekräfta ägandet och så läggs sensorn till på ditt Ruuvi-konto. Varje Ruuvi-sensor kan bara ha en ägare.\n\nFördelar:\n\n ● Sensornamn, bakgrundsbilder, kalibrering och larminformation från en säker molntjänst\n\n ● Läs sensorinformation på distans online ( kräver Ruuvi Gateway-router)\n\n ● Dela sensorer med vänner och familj (kräver Ruuvi Gateway)\n\n ● Upp till 2 års historik från station.ruuvi.com (kräver Ruuvi Gateway)"; +"Owner.Claim.description" = "Äger du den här sensorn? Bekräfta ägandet och så läggs sensorn till på ditt Ruuvi-konto. Varje Ruuvi-sensor kan bara ha en ägare. För att göra anspråk på äganderätt måste du vara inloggad.\n\nFördelar:\n\n ● Sensornamn, bakgrundsbilder, kalibrering och larminformation från en säker molntjänst\n\n ● Läs sensorinformation på distans online ( kräver Ruuvi Gateway-router)\n\n ● Dela sensorer med vänner och familj (kräver Ruuvi Gateway)\n\n ● Upp till 2 års historik från station.ruuvi.com (kräver Ruuvi Gateway)"; "TagSettings.confirmTagUnclaimAndRemoveDialog.message" = "Om du tar bort sensorn upphör ägarskapet. Vem som helst kan göra anspråk på ägandet av sensorn efter borttagning. Varje Ruuvi-sensor kan bara ha en ägare."; "TagSettings.confirmSharedTagRemovalDialog.message" = "Om du tar bort sensorn så meddelas ägaren till sensorn och du kan inte komma åt den längre."; "TagSettings.General.Owner.none" = "Ingen"; @@ -593,7 +593,7 @@ RuuviTag-sensorn är redo att användas!"; "claim_in_progress" = "Anspråk pågår"; "force_claim_sensor" = "Tvinga fram anspråk"; "force_claim_sensor_description1" = "Den här sensorn ägs av en annan användare. Du kan tvinga ägandeskap till ditt konto om du har fysisk tillgång till den här sensorn. Varje Ruuvi-sensor kan bara ha en ägare."; -"force_claim_sensor_description2" = "Tvingat anspråk görs genom att använda närfältskommunikation (NFC). Se till att NFC är aktiverat på din mobil.\n\n\t1. Rör din Ruuvi-givare med din mobila enhet för att starta kravprocessen.\n\n\t2. När det lyckats förs du tillbaka till sensorinställningar.\n\nOm det misslyckades eller om NFC inte är tillgängligt på din enhet:\n\n\t1. Öppna locket på sensorn. Leta upp den runda svarta knappen (eller knapp B om din sensor har två knappar) på det vita kretskortet och tryck kort för att starta processen.\n\n\t3. När det har gått igenom framgångsrikt förs du tillbaka till sensorinställningar."; +"force_claim_sensor_description2" = "Tvingat anspråk görs genom att använda närfältskommunikation (NFC). Se till att NFC är aktiverat på din mobil.\n\n\t1. Rör din Ruuvi-givare med din mobila enhet för att starta kravprocessen.\n\n\t2. När det lyckats förs du tillbaka till sensorinställningar.\n\nOm det misslyckades eller om NFC inte är tillgängligt på din enhet:\n\n\t1. Öppna locket på sensorn. Leta upp den runda svarta knappen (eller knapp B om din sensor har två knappar)på det vita kretskortet, tryck först kort på knappen och tryck sedan på knappen Använd BT på sidan för att starta processen.\n\n\t3. När det har gått igenom framgångsrikt förs du tillbaka till sensorinställningar."; "force_claim" = "Tvinga fram anspråk"; "claim_wrong_sensor_scanned" = "Du skannar fel RuuviTag"; "view" = "Vy"; @@ -673,22 +673,41 @@ RuuviTag-sensorn är redo att användas!"; "changelog_ios_url" = "https://f.ruuvi.com/t/3192"; "shared_to_x" = "Delad %d/%d"; "settings_alert_notifications" = "Varningsmeddelanden"; -"settings_alert_sound" = "Alert Sound"; -"settings_alert_sound_description" = "Select push notification alert sound."; -"settings_alerts_footer_description" = "You can also adjust Notification settings under iOS Settings -> Notifications"; -"settings_alerts_footer_description_link_mask" = "iOS Settings -> Notifications"; -"settings_email_alerts" = "Email Alerts"; -"settings_email_alerts_description" = "If you are using Ruuvi Cloud and Ruuvi Gateway, you will be able to receive email alerts by enabling this."; -"settings_push_alerts" = "Push Alerts"; -"settings_push_alerts_description" = "If you are using Ruuvi Cloud and Ruuvi Gateway, you will be able to receive push alerts by enabling this."; +"settings_alert_sound" = "Varningsljud"; +"settings_alert_sound_description" = "Välj varningsljudet för push-meddelandet."; +"settings_alerts_footer_description" = "Du kan även justera Aviseringsinställningar under iOS-inställningar -> Aviseringar"; +"settings_alerts_footer_description_link_mask" = "iOS-inställningar -> Aviseringar"; +"settings_email_alerts" = "E-postvarningar"; +"settings_email_alerts_description" = "Om du använder Ruuvi Cloud och Ruuvi Gateway kommer du att kunna ta emot e-postvarningar genom att aktivera detta."; +"settings_push_alerts" = "Push-varningar"; +"settings_push_alerts_description" = "Om du använder Ruuvi Cloud och Ruuvi Gateway kommer du att kunna ta emot push-varningar genom att aktivera detta."; "synchronisation" = "Synkronisering"; "gatt_sync_description" = "Ruuvi Station laddar ner sensorns interna historik för de senaste 10 dagarna om mäthistoriken är tillgänglig.\n\nHistorien laddas ner med en Bluetooth-anslutning. Se till att du är nära sensorn."; "do_not_show_again" = "Visa inte detta igen"; "sign_in_continue" = "Fortsätta"; "signing_in_is_optional" = "(Inloggning är inte obligatoriskt)"; -"Defaults.UserAuthorized.title" = "User Authorized"; -"Defaults.DashboardTapActionChart.title" = "Show Chart on Dashboard Card Tap"; -"Defaults.DevServer.title" = "Use Dev Server"; -"Defaults.DevServer.message" = "Changing Ruuvi Cloud endpoint requires signing out from current session and restart the app. Are you sure?"; -"Defaults.ShowEmailAlertsSettings.title" = "Show email alerts settings"; -"Defaults.ShowPushAlertsSettings.title" = "Show push alerts settings"; +"Defaults.UserAuthorized.title" = "Användare auktoriserad"; +"Defaults.DashboardTapActionChart.title" = "Tryck på instrumentpanelskortet för att visa diagrammet"; +"Defaults.DevServer.title" = "Använd dev-server"; +"Defaults.DevServer.message" = "För att ändra Ruuvi Cloud -slutpunkt måste du logga ut från den aktuella sessionen och starta om appen. Är du säker?"; +"Defaults.ShowEmailAlertsSettings.title" = "Visa inställningar för e-postvarningar"; +"Defaults.ShowPushAlertsSettings.title" = "Visa push-varningsinställningar"; +"use_nfc" = "Använd NFC"; +"use_bluetooth" = "Använd BT"; +"sensor_not_found_error" = "Sensorn hittades inte. Försök igen."; +"Defaults.HideNFC.title" = "Dölj NFC-alternativet från påtvingat ägarbyte"; +"settings_alert_sound_default" = "Systemfel"; +"settings_alert_sound_ruuvi_speak" = "Ruuvi larm"; +"add_with_nfc" = "Add with NFC"; +"sensor_details" = "Sensor Details"; +"add_sensor" = "Add Sensor"; +"copy_details" = "Copy Details"; +"name" = "Name:"; +"mac_address" = "Mac Address:"; +"unique_id" = "Unique ID:"; +"firmware_version" = "Firmware Version:"; +"close" = "Close"; +"add_sensor_nfc_df3_error" = "This tag cannot be added with NFC due to old firmware. Please add the tag with Bluetooth and update firmware."; +"unclaim_sensor" = "Unclaim sensor"; +"unclaim" = "Unclaim"; +"unclaim_sensor_description" = "Ownership of this sensor has been claimed to your Ruuvi account. Press Unclaim to remove this sensor's settings and related data from your Ruuvi account."; diff --git a/station/station.entitlements b/station/station.entitlements index 304527e0a..8e9efd5cd 100644 --- a/station/station.entitlements +++ b/station/station.entitlements @@ -11,6 +11,10 @@ com.apple.security.app-sandbox + com.apple.developer.nfc.readersession.formats + + TAG + com.apple.security.application-groups group.com.ruuvi.station.pnservice