diff --git a/Strand/Data/Units.swift b/Strand/Data/Units.swift index fb692b2eaa..80036ac0fa 100644 --- a/Strand/Data/Units.swift +++ b/Strand/Data/Units.swift @@ -116,8 +116,8 @@ enum UnitPrefs { } /// Whether the live-HR Live Activity (Lock Screen + Dynamic Island) may show, iOS only (#336). - /// Defaults to ON. The user can turn it off in Notifications settings without digging into iOS - /// Settings — `liveActivityEnabled()` reads it default-true so an unset key keeps the old behaviour. + /// Defaults to ON. The user can turn it off in Settings → Lock Screen & Dynamic Island without digging into + /// iOS Settings — `liveActivityEnabled()` reads it default-true so an unset key keeps the old behaviour. static let liveActivityKey = "liveActivity.enabled" static func liveActivityEnabled() -> Bool { UserDefaults.standard.object(forKey: liveActivityKey) == nil @@ -133,6 +133,15 @@ enum UnitPrefs { ? true : UserDefaults.standard.bool(forKey: syncLiveActivityKey) } + /// Whether the Lift Log session's Live Activity may show, iOS only. Its own switch: it used to follow the + /// live-HR one, so turning off the everyday heart-rate banner also took away the gym banner and the Lock + /// Screen light-up a strap step sends through it. Defaults to ON, read default-true like its siblings. + static let liftLiveActivityKey = "liveActivity.lift.enabled" + static func liftLiveActivityEnabled() -> Bool { + UserDefaults.standard.object(forKey: liftLiveActivityKey) == nil + ? true : UserDefaults.standard.bool(forKey: liftLiveActivityKey) + } + /// Resolve temperature, following body measurements when no explicit override is set. static func resolveTemperature(system: UnitSystem, override raw: String) -> TemperatureUnit { if let explicit = TemperatureUnit(rawValue: raw) { return explicit } diff --git a/Strand/Resources/Localizable.xcstrings b/Strand/Resources/Localizable.xcstrings index f48591a1fd..a4e863c465 100644 --- a/Strand/Resources/Localizable.xcstrings +++ b/Strand/Resources/Localizable.xcstrings @@ -283,6 +283,64 @@ } } }, + "Lift Log session": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Trainingsbuch-Einheit" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Sesión del Registro de fuerza" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Séance du Carnet de force" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sessione del Diario dei pesi" + } + }, + "pt-PT": { + "stringUnit": { + "state": "translated", + "value": "Sessão do Registo de força" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Тренировка «Дневника тренировок»" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "力量日志训练" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "力量日誌訓練" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Sesja Dziennika siłowni" + } + } + } + }, "Live heart rate is flowing from the ring.": { "localizations": { "de": { @@ -341,408 +399,408 @@ } } }, - "NOOP is connecting to your strap and will sync as soon as it's ready.": { + "Live notifications": { "localizations": { "de": { "stringUnit": { "state": "translated", - "value": "NOOP verbindet sich mit deinem Strap und synchronisiert, sobald er bereit ist." + "value": "Live-Mitteilungen" } }, "es": { "stringUnit": { "state": "translated", - "value": "NOOP se está conectando a tu pulsera y sincronizará en cuanto esté lista." + "value": "Notificaciones en vivo" } }, "fr": { "stringUnit": { "state": "translated", - "value": "NOOP se connecte à votre bracelet et synchronisera dès qu'il sera prêt." + "value": "Notifications en direct" } }, "it": { "stringUnit": { "state": "translated", - "value": "NOOP si sta collegando alla fascia e sincronizzerà non appena sarà pronta." + "value": "Notifiche in tempo reale" } }, "pt-PT": { "stringUnit": { "state": "translated", - "value": "O NOOP está a ligar-se à sua pulseira e sincronizará assim que estiver pronta." + "value": "Notificações em direto" } }, "ru": { "stringUnit": { "state": "translated", - "value": "NOOP подключается к браслету и синхронизирует, как только он будет готов." + "value": "Уведомления в реальном времени" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "NOOP 正在连接你的手环,准备就绪后会立即同步。" + "value": "实时通知" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "NOOP 正在連接你的手環,準備就緒後會立即同步。" + "value": "即時通知" } }, "pl": { "stringUnit": { "state": "translated", - "value": "NOOP łączy się z paskiem i zsynchronizuje go, gdy tylko będzie gotowy." + "value": "Powiadomienia na żywo" } } } }, - "Reconnect drops the current link, if any, and connects to the ring again. To pair or switch bands, open Devices.": { + "NOOP is connecting to your strap and will sync as soon as it's ready.": { "localizations": { "de": { "stringUnit": { "state": "translated", - "value": "„Neu verbinden“ trennt die aktuelle Verbindung, falls vorhanden, und verbindet den Ring erneut. Zum Koppeln oder Wechseln von Bändern öffne „Geräte“." + "value": "NOOP verbindet sich mit deinem Strap und synchronisiert, sobald er bereit ist." } }, "es": { "stringUnit": { "state": "translated", - "value": "Reconectar corta el enlace actual, si lo hay, y vuelve a conectar con el anillo. Para emparejar o cambiar de banda, abre Dispositivos." + "value": "NOOP se está conectando a tu pulsera y sincronizará en cuanto esté lista." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Reconnecter coupe le lien actuel, s'il existe, et se reconnecte à l'anneau. Pour associer ou changer de bande, ouvrez Appareils." + "value": "NOOP se connecte à votre bracelet et synchronisera dès qu'il sera prêt." } }, "it": { "stringUnit": { "state": "translated", - "value": "Riconnetti interrompe il collegamento attuale, se presente, e si ricollega all'anello. Per accoppiare o cambiare fascia, apri Dispositivi." - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Połącz ponownie zrywa bieżące połączenie, jeśli istnieje, i ponownie łączy się z pierścieniem. Aby sparować lub przełączyć pasma, otwórz Urządzenia." + "value": "NOOP si sta collegando alla fascia e sincronizzerà non appena sarà pronta." } }, "pt-PT": { "stringUnit": { "state": "translated", - "value": "Voltar a ligar termina a ligação atual, se existir, e liga-se novamente ao anel. Para emparelhar ou mudar de banda, abra Dispositivos." + "value": "O NOOP está a ligar-se à sua pulseira e sincronizará assim que estiver pronta." } }, "ru": { "stringUnit": { "state": "translated", - "value": "«Переподключить» разрывает текущее соединение, если оно есть, и снова подключается к кольцу. Чтобы выполнить сопряжение или сменить браслет, откройте раздел «Устройства»." + "value": "NOOP подключается к браслету и синхронизирует, как только он будет готов." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "重新连接会断开当前连接(如果有)并再次连接戒指。要配对或切换手环,请打开「设备」。" + "value": "NOOP 正在连接你的手环,准备就绪后会立即同步。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "重新連接會中斷目前連線(如果有)並再次連接戒指。要配對或切換手環,請打開「裝置」。" + "value": "NOOP 正在連接你的手環,準備就緒後會立即同步。" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "NOOP łączy się z paskiem i zsynchronizuje go, gdy tylko będzie gotowy." } } } }, - "Reconnect ring": { + "Progress while NOOP pulls history from the strap.": { "localizations": { "de": { "stringUnit": { "state": "translated", - "value": "Ring neu verbinden" + "value": "Fortschritt, während NOOP den Verlauf vom Strap lädt." } }, "es": { "stringUnit": { "state": "translated", - "value": "Reconectar anillo" + "value": "El progreso mientras NOOP descarga el historial de la pulsera." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Reconnecter l'anneau" + "value": "La progression pendant que NOOP récupère l'historique du bracelet." } }, "it": { "stringUnit": { "state": "translated", - "value": "Riconnetti anello" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Połącz ponownie pierścień" + "value": "L'avanzamento mentre NOOP scarica lo storico dalla fascia." } }, "pt-PT": { "stringUnit": { "state": "translated", - "value": "Voltar a ligar o anel" + "value": "O progresso enquanto o NOOP obtém o histórico da pulseira." } }, "ru": { "stringUnit": { "state": "translated", - "value": "Переподключить кольцо" + "value": "Прогресс, пока NOOP загружает историю с браслета." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "重新连接戒指" + "value": "NOOP 从手环读取历史数据时的进度。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "重新連接戒指" + "value": "NOOP 從手環讀取歷史資料時的進度。" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Postęp, gdy NOOP pobiera historię z paska." } } } }, - "Ring not connected.": { + "Reconnect drops the current link, if any, and connects to the ring again. To pair or switch bands, open Devices.": { "localizations": { "de": { "stringUnit": { "state": "translated", - "value": "Ring nicht verbunden." + "value": "„Neu verbinden“ trennt die aktuelle Verbindung, falls vorhanden, und verbindet den Ring erneut. Zum Koppeln oder Wechseln von Bändern öffne „Geräte“." } }, "es": { "stringUnit": { "state": "translated", - "value": "Anillo no conectado." + "value": "Reconectar corta el enlace actual, si lo hay, y vuelve a conectar con el anillo. Para emparejar o cambiar de banda, abre Dispositivos." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Anneau non connecté." + "value": "Reconnecter coupe le lien actuel, s'il existe, et se reconnecte à l'anneau. Pour associer ou changer de bande, ouvrez Appareils." } }, "it": { "stringUnit": { "state": "translated", - "value": "Anello non connesso." + "value": "Riconnetti interrompe il collegamento attuale, se presente, e si ricollega all'anello. Per accoppiare o cambiare fascia, apri Dispositivi." } }, "pl": { "stringUnit": { "state": "translated", - "value": "Pierścień niepołączony." + "value": "Połącz ponownie zrywa bieżące połączenie, jeśli istnieje, i ponownie łączy się z pierścieniem. Aby sparować lub przełączyć pasma, otwórz Urządzenia." } }, "pt-PT": { "stringUnit": { "state": "translated", - "value": "Anel não ligado." + "value": "Voltar a ligar termina a ligação atual, se existir, e liga-se novamente ao anel. Para emparelhar ou mudar de banda, abra Dispositivos." } }, "ru": { "stringUnit": { "state": "translated", - "value": "Кольцо не подключено." + "value": "«Переподключить» разрывает текущее соединение, если оно есть, и снова подключается к кольцу. Чтобы выполнить сопряжение или сменить браслет, откройте раздел «Устройства»." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "戒指未连接。" + "value": "重新连接会断开当前连接(如果有)并再次连接戒指。要配对或切换手环,请打开「设备」。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "戒指未連接。" + "value": "重新連接會中斷目前連線(如果有)並再次連接戒指。要配對或切換手環,請打開「裝置」。" } } } }, - "Shows Connecting… / Syncing… with the chunk count and elapsed time while NOOP pulls history from your strap, including a sync started by the Sync Strap shortcut. Independent of the live heart rate switch above.": { + "Reconnect ring": { "localizations": { "de": { "stringUnit": { "state": "translated", - "value": "Zeigt „Verbinde…“ / „Synchronisiere…“ mit Chunk-Anzahl und verstrichener Zeit, während NOOP Daten von deinem Strap abruft – auch bei einer Synchronisierung über den Kurzbefehl „Strap synchronisieren“. Unabhängig vom Schalter für den Live-Puls oben." + "value": "Ring neu verbinden" } }, "es": { "stringUnit": { "state": "translated", - "value": "Muestra Conectando… / Sincronizando… con el número de fragmentos y el tiempo transcurrido mientras NOOP descarga el historial de tu pulsera, incluida una sincronización iniciada con el atajo Sincronizar pulsera. Independiente del interruptor de frecuencia cardíaca en vivo de arriba." + "value": "Reconectar anillo" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Affiche Connexion… / Synchronisation… avec le nombre de blocs et le temps écoulé pendant que NOOP récupère l'historique de votre bracelet, y compris une synchronisation lancée par le raccourci Synchroniser le bracelet. Indépendant de l'interrupteur de fréquence cardiaque en direct ci-dessus." + "value": "Reconnecter l'anneau" } }, "it": { "stringUnit": { "state": "translated", - "value": "Mostra Connessione… / Sincronizzazione… con il numero di blocchi e il tempo trascorso mentre NOOP scarica la cronologia dalla fascia, anche per una sincronizzazione avviata dal comando rapido Sincronizza la fascia. Indipendente dall'interruttore della frequenza cardiaca in tempo reale qui sopra." + "value": "Riconnetti anello" } }, - "pt-PT": { + "pl": { "stringUnit": { "state": "translated", - "value": "Mostra A ligar… / A sincronizar… com o número de blocos e o tempo decorrido enquanto o NOOP transfere o histórico da sua pulseira, incluindo uma sincronização iniciada pelo atalho Sincronizar pulseira. Independente do interruptor de frequência cardíaca em direto acima." + "value": "Połącz ponownie pierścień" } }, - "ru": { + "pt-PT": { "stringUnit": { "state": "translated", - "value": "Показывает «Подключение…» / «Синхронизация…» с числом блоков и прошедшим временем, пока NOOP загружает историю с браслета, в том числе при синхронизации через команду «Синхронизировать браслет». Не зависит от переключателя пульса в реальном времени выше." + "value": "Voltar a ligar o anel" } }, - "zh-Hans": { + "ru": { "stringUnit": { "state": "translated", - "value": "NOOP 从手环读取历史数据时显示“连接中…”/“同步中…”、数据块数量和已用时间,包括通过“同步手环”快捷指令启动的同步。与上方的实时心率开关无关。" + "value": "Переподключить кольцо" } }, - "zh-Hant": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "NOOP 從手環讀取歷史資料時顯示「連接中…」/「同步中…」、資料塊數量和已用時間,包括透過「同步手環」捷徑啟動的同步。與上方的即時心率開關無關。" + "value": "重新连接戒指" } }, - "pl": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Pokazuje Łączenie… / Synchronizowanie… z liczbą fragmentów i czasem trwania, gdy NOOP pobiera historię z paska, także przy synchronizacji uruchomionej skrótem Synchronizuj pasek. Niezależne od przełącznika tętna na żywo powyżej." + "value": "重新連接戒指" } } } }, - "Shows sync progress on the Lock Screen and in the Dynamic Island": { + "Ring not connected.": { "localizations": { "de": { "stringUnit": { "state": "translated", - "value": "Zeigt den Synchronisierungsfortschritt auf dem Sperrbildschirm und in der Dynamic Island" + "value": "Ring nicht verbunden." } }, "es": { "stringUnit": { "state": "translated", - "value": "Muestra el progreso de la sincronización en la pantalla de bloqueo y en la Dynamic Island" + "value": "Anillo no conectado." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Affiche la progression de la synchronisation sur l'écran verrouillé et dans la Dynamic Island" + "value": "Anneau non connecté." } }, "it": { "stringUnit": { "state": "translated", - "value": "Mostra l'avanzamento della sincronizzazione nella schermata di blocco e nella Dynamic Island" + "value": "Anello non connesso." } }, - "pt-PT": { + "pl": { "stringUnit": { "state": "translated", - "value": "Mostra o progresso da sincronização no ecrã bloqueado e na Dynamic Island" + "value": "Pierścień niepołączony." } }, - "ru": { + "pt-PT": { "stringUnit": { "state": "translated", - "value": "Показывает ход синхронизации на экране блокировки и в Dynamic Island" + "value": "Anel não ligado." } }, - "zh-Hans": { + "ru": { "stringUnit": { "state": "translated", - "value": "在锁定屏幕和灵动岛中显示同步进度" + "value": "Кольцо не подключено." } }, - "zh-Hant": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "在鎖定畫面和動態島中顯示同步進度" + "value": "戒指未连接。" } }, - "pl": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Pokazuje postęp synchronizacji na ekranie blokady i w Dynamic Island" + "value": "戒指未連接。" } } } }, - "Strap sync in Dynamic Island": { + "Shown on the Lock Screen and in the Dynamic Island. A switch only hides one: NOOP still measures and records everything.": { "localizations": { "de": { "stringUnit": { "state": "translated", - "value": "Strap-Synchronisierung in der Dynamic Island" + "value": "Auf dem Sperrbildschirm und in der Dynamic Island. Ein Schalter blendet nur die Anzeige aus: NOOP misst und speichert weiterhin alles." } }, "es": { "stringUnit": { "state": "translated", - "value": "Sincronización de la pulsera en la Dynamic Island" + "value": "En la pantalla de bloqueo y en la Dynamic Island. Un interruptor solo oculta la notificación: NOOP sigue midiendo y registrando todo." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Synchronisation du bracelet dans la Dynamic Island" + "value": "Sur l'écran verrouillé et dans la Dynamic Island. Un interrupteur ne fait que masquer l'affichage : NOOP continue de tout mesurer et d'enregistrer." } }, "it": { "stringUnit": { "state": "translated", - "value": "Sincronizzazione della fascia nella Dynamic Island" + "value": "Sulla schermata di blocco e nella Dynamic Island. Un interruttore nasconde solo la notifica: NOOP continua a misurare e registrare tutto." } }, "pt-PT": { "stringUnit": { "state": "translated", - "value": "Sincronização da pulseira na Dynamic Island" + "value": "No ecrã de bloqueio e na Dynamic Island. Um interruptor apenas oculta a notificação: o NOOP continua a medir e a registar tudo." } }, "ru": { "stringUnit": { "state": "translated", - "value": "Синхронизация браслета в Dynamic Island" + "value": "На экране блокировки и в Dynamic Island. Переключатель только скрывает уведомление: NOOP продолжает всё измерять и записывать." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "灵动岛中显示手环同步" + "value": "显示在锁定屏幕和灵动岛上。开关只会隐藏通知:NOOP 仍会测量并记录所有数据。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "動態島中顯示手環同步" + "value": "顯示在鎖定畫面和動態島上。開關只會隱藏通知:NOOP 仍會測量並記錄所有資料。" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Synchronizacja paska w Dynamic Island" + "value": "Na ekranie blokady i w Dynamic Island. Przełącznik tylko ukrywa powiadomienie: NOOP nadal wszystko mierzy i zapisuje." } } } @@ -1690,6 +1748,122 @@ "Working sets": { "localizations": { "de": {"stringUnit": {"state": "translated", "value": "Arbeitssätze"}}, "es": {"stringUnit": {"state": "translated", "value": "Series efectivas"}}, "fr": {"stringUnit": {"state": "translated", "value": "Séries de travail"}}, "it": {"stringUnit": {"state": "translated", "value": "Serie di lavoro"}}, "pl": {"stringUnit": {"state": "translated", "value": "Serie robocze"}}, "pt-PT": {"stringUnit": {"state": "translated", "value": "Séries de trabalho"}}, "ru": {"stringUnit": {"state": "translated", "value": "Рабочие подходы"}}, "zh-Hans": {"stringUnit": {"state": "translated", "value": "正式组"}}, "zh-Hant": {"stringUnit": {"state": "translated", "value": "正式組"}} } }, + "While the strap is connected.": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Solange der Strap verbunden ist." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mientras la pulsera está conectada." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tant que le bracelet est connecté." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Finché la fascia è collegata." + } + }, + "pt-PT": { + "stringUnit": { + "state": "translated", + "value": "Enquanto a pulseira está ligada." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Пока браслет подключён." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "手环连接期间。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "手環連接期間。" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Gdy pasek jest połączony." + } + } + } + }, + "Your set, rest and heart rate, and the Lock Screen light-up on a double-tap.": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Dein Satz, deine Pause und deine Herzfrequenz, dazu das Aufleuchten des Sperrbildschirms beim Doppeltippen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tu serie, tu descanso y tu frecuencia cardíaca, y el encendido de la pantalla de bloqueo con un doble toque." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre série, votre repos et votre fréquence cardiaque, et l'allumage de l'écran verrouillé au double-tap." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La tua serie, il recupero e la frequenza cardiaca, e l'accensione della schermata di blocco al doppio tocco." + } + }, + "pt-PT": { + "stringUnit": { + "state": "translated", + "value": "A tua série, o teu descanso e a tua frequência cardíaca, e o acender do ecrã de bloqueio com um duplo toque." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Ваш подход, отдых и пульс, а также включение экрана блокировки по двойному касанию." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "你当前的组、休息和心率,以及双击时点亮锁定屏幕。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "你目前的組、休息和心率,以及雙擊時點亮鎖定畫面。" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Twoja seria, przerwa i tętno oraz podświetlenie ekranu blokady po podwójnym stuknięciu." + } + } + } + }, "Your strap is already syncing.": { "localizations": { "de": { @@ -94594,64 +94768,6 @@ } } }, - "Live heart rate in Dynamic Island": { - "localizations": { - "de": { - "stringUnit": { - "state": "translated", - "value": "Live-Herzfrequenz in der Dynamic Island" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Frecuencia cardíaca en vivo en la Dynamic Island" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Fréquence cardiaque en direct dans la Dynamic Island" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Frequenza cardiaca in diretta nella Dynamic Island" - } - }, - "pt-PT": { - "stringUnit": { - "state": "translated", - "value": "Frequência cardíaca em direto em Dynamic Island" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Пульс в реальном времени в Dynamic Island" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "在灵动岛中显示实时心率" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "在動態島中顯示即時心率" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Tętno na żywo w Dynamic Island" - } - } - } - }, "Live heart rate is flowing; full strap controls need an encrypted bond.": { "localizations": { "de": { @@ -146151,64 +146267,6 @@ } } }, - "Shows your live heart rate on the Lock Screen and in the Dynamic Island while the strap is connected. Turn it off to keep your live HR out of the Dynamic Island. (Any one already showing clears within a moment.)": { - "localizations": { - "de": { - "stringUnit": { - "state": "translated", - "value": "Zeigt deine Live-Herzfrequenz auf dem Sperrbildschirm und in der Dynamic Island, während der Strap verbunden ist. Schalte es aus, um deine Live-HF aus der Dynamic Island fernzuhalten. (Eine bereits angezeigte verschwindet gleich darauf.)" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Muestra tu frecuencia cardíaca en vivo en la pantalla de bloqueo y en la Dynamic Island mientras la pulsera está conectada. Desactívalo para mantener tu FC en vivo fuera de la Dynamic Island. (Cualquiera que ya se muestre desaparece en un momento.)" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Affiche votre fréquence cardiaque en direct sur l'écran verrouillé et dans la Dynamic Island tant que le bracelet est connecté. Désactivez-la pour garder votre FC en direct hors de la Dynamic Island. (Toute FC déjà affichée disparaît sous peu.)" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Mostra la tua frequenza cardiaca in diretta sulla schermata di blocco e nella Dynamic Island mentre la fascia è connessa. Disattivalo per tenere la tua FC in diretta fuori dalla Dynamic Island. (Quella eventualmente già mostrata sparisce in un attimo.)" - } - }, - "pt-PT": { - "stringUnit": { - "state": "translated", - "value": "Mostra a tua frequência cardíaca em direto no ecrã de bloqueio e na Ilha Dinâmica enquanto a bracelete está ligada. Desligue-o para manter o teu RH ativo fora da Ilha Dinâmica. (Qualquer um que já esteja a aparecer será eliminado daqui a pouco.)" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Показывает ваш текущий пульс на заблокированном экране и в Dynamic Island, пока браслет подключён. Отключите, чтобы пульс не отображался в Dynamic Island. (Уже показанный индикатор исчезнет через мгновение.)" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "在手环连接期间,于锁定屏幕和灵动岛上显示你的实时心率。将它关闭可让你的实时心率不出现在灵动岛中。(任何已在显示的都会在片刻内清除。)" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "在手環連接期間,於鎖定畫面和動態島上顯示你的即時心率。將它關閉可讓你的即時心率不出現在動態島中。(任何已在顯示的都會在片刻內清除。)" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Pokazuje Twoje tętno na żywo na ekranie blokady i na Dynamic Island, gdy pasek jest podłączony. Wyłącz tę opcję, aby Twoje tętno na żywo nie znajdowało się poza dynamiczną wyspą. (Każdy, kto już pokazał, znika w ciągu chwili.)" - } - } - } - }, "Signal Trust": { "localizations": { "de": { diff --git a/Strand/Screens/SettingsView.swift b/Strand/Screens/SettingsView.swift index e0f0cdf31f..63f00af533 100644 --- a/Strand/Screens/SettingsView.swift +++ b/Strand/Screens/SettingsView.swift @@ -120,6 +120,7 @@ struct SettingsView: View { @AppStorage(UnitPrefs.liveActivityKey) private var liveActivityEnabled = true // Strap-sync Live Activity, iOS only. Separate from the live-HR one on purpose. Default on. @AppStorage(UnitPrefs.syncLiveActivityKey) private var syncLiveActivityEnabled = true + @AppStorage(UnitPrefs.liftLiveActivityKey) private var liftLiveActivityEnabled = true @AppStorage(DayCycleMode.storageKey) private var dayCycleModeRaw = DayCycleMode.sleepOnset.rawValue // Alternate app icon (iOS only) — false = Titanium (primary AppIcon), true = Blue Titanium // ("AppIcon-Navy"). Display-only preference; the live switch goes through setAlternateIconName. @@ -263,6 +264,9 @@ struct SettingsView: View { unitsCard.staggeredAppear(index: 1) appearanceCard.staggeredAppear(index: 2) strapCard.staggeredAppear(index: 3) + #if os(iOS) + liveNotificationsCard.staggeredAppear(index: 3) + #endif streakCard.staggeredAppear(index: 4) featuresCard.staggeredAppear(index: 5) #if os(iOS) @@ -1461,36 +1465,6 @@ struct SettingsView: View { strapNameControl } - #if os(iOS) - rowDivider - // MARK: Live Activity — show live HR on the Lock Screen + Dynamic Island (#336). - Toggle(isOn: $liveActivityEnabled) { - Text("Live heart rate in Dynamic Island") - .font(StrandFont.subhead) - .foregroundStyle(StrandPalette.textPrimary) - } - .toggleStyle(.switch) - .tint(StrandPalette.accent) - Text("Shows your live heart rate on the Lock Screen and in the Dynamic Island while the strap is connected. Turn it off to keep your live HR out of the Dynamic Island. (Any one already showing clears within a moment.)") - .font(StrandFont.caption) - .foregroundStyle(StrandPalette.textTertiary) - .fixedSize(horizontal: false, vertical: true) - - rowDivider - // MARK: Strap-sync Live Activity — its own switch, independent of the live-HR one. - Toggle(isOn: $syncLiveActivityEnabled) { - Text("Strap sync in Dynamic Island") - .font(StrandFont.subhead) - .foregroundStyle(StrandPalette.textPrimary) - } - .toggleStyle(.switch) - .tint(StrandPalette.accent) - .accessibilityHint("Shows sync progress on the Lock Screen and in the Dynamic Island") - Text("Shows Connecting… / Syncing… with the chunk count and elapsed time while NOOP pulls history from your strap, including a sync started by the Sync Strap shortcut. Independent of the live heart rate switch above.") - .font(StrandFont.caption) - .foregroundStyle(StrandPalette.textTertiary) - .fixedSize(horizontal: false, vertical: true) - #endif } } } @@ -1583,6 +1557,48 @@ struct SettingsView: View { /// or an early reading that anchored too high). It writes now (epoch SECONDS) to BOTH the /// `noop.hrvBaselineEpoch` and `noop.recoveryBaselineEpoch` settings the recovery engine reads, then /// kicks a recompute the same way the sleep-edit path does (analyzeRecent → refresh). History stays. + #if os(iOS) + /// NOOP's live notifications — its Live Activities, on the Lock Screen and in the Dynamic Island — one switch + /// each: the live heart rate, a Lift Log session, a strap sync. These three are every Live Activity the app has. + /// A switch only decides whether its notification is SHOWN: the heart rate is still measured, recorded and + /// scored, a session still runs and buzzes, a sync still runs, with any of them off. + private var liveNotificationsCard: some View { + SettingsSection( + icon: "bell.badge", + title: "Live notifications", + blurb: "Shown on the Lock Screen and in the Dynamic Island. A switch only hides one: NOOP still measures and records everything." + ) { + VStack(alignment: .leading, spacing: NoopMetrics.rowSpacing) { + liveNotificationSwitch("Live heart rate", isOn: $liveActivityEnabled, + detail: "While the strap is connected.") + rowDivider + liveNotificationSwitch("Lift Log session", isOn: $liftLiveActivityEnabled, + detail: "Your set, rest and heart rate, and the Lock Screen light-up on a double-tap.") + rowDivider + liveNotificationSwitch("Strap sync", isOn: $syncLiveActivityEnabled, + detail: "Progress while NOOP pulls history from the strap.") + } + } + } + + private func liveNotificationSwitch(_ title: LocalizedStringKey, isOn: Binding, + detail: LocalizedStringKey) -> some View { + VStack(alignment: .leading, spacing: NoopMetrics.space1) { + Toggle(isOn: isOn) { + Text(title) + .font(StrandFont.subhead) + .foregroundStyle(StrandPalette.textPrimary) + } + .toggleStyle(.switch) + .tint(StrandPalette.accent) + Text(detail) + .font(StrandFont.caption) + .foregroundStyle(StrandPalette.textTertiary) + .fixedSize(horizontal: false, vertical: true) + } + } + #endif + private var recoveryCard: some View { SettingsSection( icon: "heart.text.square", diff --git a/StrandiOS/App/StrandiOSApp.swift b/StrandiOS/App/StrandiOSApp.swift index 3911ac645b..9f18792679 100644 --- a/StrandiOS/App/StrandiOSApp.swift +++ b/StrandiOS/App/StrandiOSApp.swift @@ -107,8 +107,10 @@ struct StrandiOSApp: App { })) // A gym session keeps ONE banner on the Lock Screen, its own — as the live-HR banner already // stands aside for it. A sync started in the foreground mid-session starts no sync banner. + // Held back only for a gym banner that will actually show: with its switch off, a session leaves the + // Lock Screen to the sync, rather than to nothing. SyncLiveActivityController.shared.holdsBackNewBanner = { [weak liftSession] in - liftSession?.isActive == true + liftSession?.isActive == true && UnitPrefs.liftLiveActivityEnabled() } // Before any view or publisher exists: the first push to the Lock Screen banner must find the // session already running, or it ends the banner iOS kept alive across the restart. diff --git a/StrandiOS/Widgets/LiftLiveActivityController.swift b/StrandiOS/Widgets/LiftLiveActivityController.swift index 1beefdb4c4..eaad2720d5 100644 --- a/StrandiOS/Widgets/LiftLiveActivityController.swift +++ b/StrandiOS/Widgets/LiftLiveActivityController.swift @@ -88,9 +88,9 @@ final class LiftLiveActivityController { ? Activity.activities.first(where: Self.isShowing) : nil if let adopted { activity = adopted } - // Shares the existing Live Activity opt-out rather than adding a second switch: a user who - // turned Live Activities off meant all of them. - guard UnitPrefs.liveActivityEnabled(), let state else { + // Its own switch (`UnitPrefs.liftLiveActivityEnabled`), so the everyday heart-rate banner can be off while + // the gym banner stays; turning this one off also ends a banner already showing. + guard UnitPrefs.liftLiveActivityEnabled(), let state else { if activity != nil { Task { await end() } } return alert ? .noBanner : nil } diff --git a/StrandiOS/Widgets/SyncLiveActivityController.swift b/StrandiOS/Widgets/SyncLiveActivityController.swift index 195dcb9b99..ad2c1ad4be 100644 --- a/StrandiOS/Widgets/SyncLiveActivityController.swift +++ b/StrandiOS/Widgets/SyncLiveActivityController.swift @@ -154,7 +154,7 @@ final class SyncLiveActivityController { return } guard UnitPrefs.syncLiveActivityEnabled() else { - live?.append(log: "Sync activity: not started — \"Strap sync in Dynamic Island\" is off in NOOP Settings") + live?.append(log: "Sync activity: not started — \"Strap sync\" is off in NOOP Settings → Live notifications") return } if let activity { push(activity, state); return }