Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions frontend/src/components/SyncBanner.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useStore } from '../store/useStore.js'
import { useUI } from '../store/useUI.js'
import { DEMO } from '../lib/demo.js'
import { connectionView, actionLabel, syncNowWithToast, pairAgain, connectServer, signInAgain, useOnline } from './ServerSync.jsx'
import Icon from './Icon.jsx'
Expand All @@ -10,10 +11,12 @@ import Icon from './Icon.jsx'
// every one of them would only teach people to ignore the line.
export const PENDING_GRACE_MS = 5000

/* The connection, always in view while the app is not connected to a server: offline, the server
unreachable or answering with an error (its HTTP code, for whoever runs it), a server that no
longer accepts this device, an answer that is not openGym's, and no server at all — a phone
kept local, a guest in a browser. It cannot be dismissed; it goes when the condition does.
/* The connection, in view while the app is not connected to a server unless hidden in Settings:
offline, the server unreachable or answering with an error (its HTTP code, for whoever runs
it), a server that no longer accepts this device, an answer that is not openGym's, and no
server at all — a phone kept local or a guest in a browser. The device-local display preference
can hide it; its details
remain in Settings → Server & sync.
The first ones say what is wrong and that the changes are kept here, with the one thing to do
about it (retry, pair again, sign in); the deliberate local setup only says so, quietly.

Expand All @@ -27,6 +30,7 @@ export default function SyncBanner() {
const sync = useStore(s => s.sync)
const guest = useStore(s => s.isGuest())
const onboarding = useStore(s => s.needsMobileOnboarding)
const showSyncBanner = useUI(s => s.showSyncBanner)
const online = useOnline()
const [waited, setWaited] = useState(false)
const row = useRef(null)
Expand All @@ -41,7 +45,7 @@ export default function SyncBanner() {
const view = connectionView(sync, { online })
// Signed out on the web, the sign-in screen is the whole app: it hears only that the server
// ended the session, and that the changes are still here.
const show = !DEMO && !onboarding && !!view?.banner && (!!user || guest || status === 'auth') && (status !== 'pending' || waited)
const show = showSyncBanner && !DEMO && !onboarding && !!view?.banner && (!!user || guest || status === 'auth') && (status !== 'pending' || waited)

useLayoutEffect(() => {
const root = document.documentElement
Expand Down
30 changes: 21 additions & 9 deletions frontend/src/components/SyncBanner.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,22 @@ import { connectionView } from './ServerSync.jsx'

globalThis.IS_REACT_ACT_ENVIRONMENT = true

/* The connection indicator: every way the app can be without its server gets a line that stays
while the condition lasts — offline, an error with its HTTP code, a server that refuses this
device, an answer that is not openGym's, no server at all — and says what to do about it.
Never in the public demo, never during the phone's first-launch choice, and a change merely
waiting for its push does not flash it. The store is a stand-in: its `sync` is what each test
sets; ServerSync.jsx (the words and the actions) is the real one. */
/* By default, the connection indicator gives every way the app can be without its server a line
that stays while the condition lasts — offline, an error with its HTTP code, a server that
refuses this device, an answer that is not openGym's, no server at all — and says what to do
about it. The device can hide it in Settings, where the full status remains. Never in the public
demo, never during the phone's first-launch choice, and a change merely waiting for its push
does not flash it. The store is a stand-in: its `sync` is what each test sets; ServerSync.jsx
(the words and the actions) is the real one. */
const mocks = vi.hoisted(() => {
const state = { MOBILE: false, DEMO: false, webauthn: true, user: null, guest: false, onboarding: false, sync: null, sheets: [], navs: [] }
const state = { MOBILE: false, DEMO: false, webauthn: true, user: null, guest: false, onboarding: false, sync: null, showSyncBanner: true, sheets: [], navs: [] }
state.toast = vi.fn()
state.syncNow = vi.fn(async () => state.sync)
state.passkeyLogin = vi.fn(async () => ({ id: 'u1', name: 'andi' }))
state.snapshot = () => ({
user: state.user, sync: state.sync, needsMobileOnboarding: state.onboarding,
isGuest: () => state.guest, syncNow: state.syncNow,
showSyncBanner: state.showSyncBanner,
setUser: vi.fn(), adoptProfile: vi.fn(async () => ({})),
})
return state
Expand All @@ -31,7 +33,7 @@ vi.mock('../store/useStore.js', () => {
return { useStore }
})
vi.mock('../store/useUI.js', () => {
const snap = () => ({ toast: (...a) => mocks.toast(...a), openSheet: (render, opts) => { mocks.sheets.push({ render, opts }); return {} } })
const snap = () => ({ showSyncBanner: mocks.showSyncBanner, toast: (...a) => mocks.toast(...a), openSheet: (render, opts) => { mocks.sheets.push({ render, opts }); return {} } })
const useUI = selector => selector ? selector(snap()) : snap()
useUI.getState = snap
return { useUI }
Expand All @@ -56,7 +58,7 @@ const network = on => {
let host, root
beforeEach(() => {
network(true)
Object.assign(mocks, { MOBILE: false, DEMO: false, webauthn: true, user: { id: 'u1', name: 'andi' }, guest: false, onboarding: false, sync: sync('ok') })
Object.assign(mocks, { MOBILE: false, DEMO: false, webauthn: true, user: { id: 'u1', name: 'andi' }, guest: false, onboarding: false, sync: sync('ok'), showSyncBanner: true })
mocks.sheets.length = 0
mocks.navs.length = 0
mocks.toast.mockClear(); mocks.syncNow.mockClear(); mocks.passkeyLogin.mockClear()
Expand Down Expand Up @@ -110,6 +112,16 @@ describe('connected and in step', () => {
})
})

describe('the device-local display preference', () => {
it('hides the banner and gives its space back when switched off', () => {
mocks.showSyncBanner = false
network(false)
render()
expect(bar()).toBeNull()
expect(conn()).toBe('')
})
})

describe('not connected — it says so, and what to do', () => {
it('offline with changes waiting: kept on this device, and a retry that reports back', async () => {
network(false)
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/ar.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export default {
'Exercises': 'التمارين',
'History': 'السجل',
'Settings': 'الإعدادات',
'Display': 'العرض',
'Show sync banner': 'إظهار شريط المزامنة',
'Show connection and sync status at the top of the screen.': 'إظهار حالة الاتصال والمزامنة أعلى الشاشة.',
'Freestyle': 'تمرين حر',
'Routine': 'روتين',
'routine': 'روتين',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/de.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export default {
'Also trains': 'Trainiert auch',
'History': 'Verlauf',
'Settings': 'Einstellungen',
'Display': 'Anzeige',
'Show sync banner': 'Sync-Banner anzeigen',
'Show connection and sync status at the top of the screen.': 'Verbindungs- und Synchronisierungsstatus oben auf dem Bildschirm anzeigen.',
'Freestyle': 'Freies Training',
'Routine': 'Routine',
'routine': 'Routine',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export default {
'Also trains': 'También trabaja',
'History': 'Historial',
'Settings': 'Ajustes',
'Display': 'Visualización',
'Show sync banner': 'Mostrar banner de sincronización',
'Show connection and sync status at the top of the screen.': 'Mostrar el estado de conexión y sincronización en la parte superior de la pantalla.',
'Freestyle': 'Libre',
'Routine': 'Rutina',
'routine': 'rutina',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/fr.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export default {
'Also trains': 'Sollicite aussi',
'History': 'Historique',
'Settings': 'Réglages',
'Display': 'Affichage',
'Show sync banner': 'Afficher la bannière de synchronisation',
'Show connection and sync status at the top of the screen.': 'Afficher l’état de la connexion et de la synchronisation en haut de l’écran.',
'Freestyle': 'Libre',
'Routine': 'Routine',
'routine': 'routine',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/hi.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export default {
'Also trains': 'यह भी प्रशिक्षित करता है',
'History': 'इतिहास',
'Settings': 'सेटिंग्स',
'Display': 'दिखावट',
'Show sync banner': 'सिंक बैनर दिखाएँ',
'Show connection and sync status at the top of the screen.': 'स्क्रीन के ऊपर कनेक्शन और सिंक की स्थिति दिखाएँ।',
'Freestyle': 'फ्रीस्टाइल',
'Routine': 'रूटीन',
'routine': 'रूटीन',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/hu.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export default {
'Also trains': 'Emellett edzi',
'History': 'Előzmények',
'Settings': 'Beállítások',
'Display': 'Megjelenítés',
'Show sync banner': 'Szinkronizálási sáv megjelenítése',
'Show connection and sync status at the top of the screen.': 'A kapcsolat és a szinkronizálás állapotának megjelenítése a képernyő tetején.',
'Freestyle': 'Szabad edzés',
'Routine': 'Rutin',
'routine': 'rutin',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/it.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export default {
'Also trains': 'Allena anche',
'History': 'Cronologia',
'Settings': 'Impostazioni',
'Display': 'Visualizzazione',
'Show sync banner': 'Mostra il banner di sincronizzazione',
'Show connection and sync status at the top of the screen.': 'Mostra lo stato della connessione e della sincronizzazione in alto sullo schermo.',
'Freestyle': 'Libero',
'Routine': 'Routine',
'routine': 'routine',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/ko.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export default {
'Also trains': '함께 훈련',
'History': '기록',
'Settings': '설정',
'Display': '화면 표시',
'Show sync banner': '동기화 배너 표시',
'Show connection and sync status at the top of the screen.': '화면 상단에 연결 및 동기화 상태를 표시합니다.',
'Freestyle': '자유 운동',
'Routine': '루틴',
'routine': '루틴',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/pl.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export default {
'Also trains': 'Angażuje również',
'History': 'Historia',
'Settings': 'Ustawienia',
'Display': 'Wyświetlanie',
'Show sync banner': 'Pokaż baner synchronizacji',
'Show connection and sync status at the top of the screen.': 'Pokazuj stan połączenia i synchronizacji u góry ekranu.',
'Freestyle': 'Dowolny',
'Routine': 'Plan treningowy',
'routine': 'planu',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/pt-BR.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ export const PT_BR_OVERRIDES = {
'Save': 'Salvar',
'Log': 'Registrar',
'Settings': 'Configurações',
'Display': 'Exibição',
'Show sync banner': 'Mostrar faixa de sincronização',
'Show connection and sync status at the top of the screen.': 'Mostrar o status da conexão e da sincronização no topo da tela.',
'English exercise names': 'Nomes de exercícios em inglês',
'Show the English name in parentheses next to the translated one.': 'Mostra o nome em inglês entre parênteses ao lado do traduzido.',
'English names only': 'Apenas nomes em inglês',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/pt.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export default {
'Also trains': 'Também trabalha',
'History': 'Histórico',
'Settings': 'Definições',
'Display': 'Apresentação',
'Show sync banner': 'Mostrar faixa de sincronização',
'Show connection and sync status at the top of the screen.': 'Mostrar o estado da ligação e da sincronização no topo do ecrã.',
'Freestyle': 'Livre',
'Routine': 'Rotina',
'routine': 'rotina',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/ru.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export default {
'Also trains': 'Также тренирует',
'History': 'История',
'Settings': 'Настройки',
'Display': 'Отображение',
'Show sync banner': 'Показывать баннер синхронизации',
'Show connection and sync status at the top of the screen.': 'Показывать состояние подключения и синхронизации вверху экрана.',
'Freestyle': 'Свободная',
'Routine': 'Программа',
'routine': 'программу',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/th.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export default {
'Also trains': 'ฝึกกล้ามเนื้อนี้ด้วย',
'History': 'ประวัติ',
'Settings': 'ตั้งค่า',
'Display': 'การแสดงผล',
'Show sync banner': 'แสดงแบนเนอร์การซิงค์',
'Show connection and sync status at the top of the screen.': 'แสดงสถานะการเชื่อมต่อและการซิงค์ที่ด้านบนของหน้าจอ',
'Freestyle': 'ฟรีสไตล์',
'Routine': 'รูทีน',
'routine': 'รูทีน',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/tr.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export default {
'Also trains': 'Ayrıca çalıştırır',
'History': 'Geçmiş',
'Settings': 'Ayarlar',
'Display': 'Görünüm',
'Show sync banner': 'Senkronizasyon bandını göster',
'Show connection and sync status at the top of the screen.': 'Bağlantı ve senkronizasyon durumunu ekranın üst kısmında göster.',
'Freestyle': 'Serbest',
'Routine': 'Rutin',
'routine': 'rutine',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/uk.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export default {
'Exercises': 'Вправи',
'History': 'Історія',
'Settings': 'Налаштування',
'Display': 'Вигляд',
'Show sync banner': 'Показувати банер синхронізації',
'Show connection and sync status at the top of the screen.': 'Показувати стан з’єднання та синхронізації у верхній частині екрана.',
'Freestyle': 'Довільна',
'Routine': 'Програма',
'routine': 'програму',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/zh.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export default {
'Also trains': '同时训练',
'History': '历史',
'Settings': '设置',
'Display': '显示',
'Show sync banner': '显示同步横幅',
'Show connection and sync status at the top of the screen.': '在屏幕顶部显示连接和同步状态。',
'Freestyle': '自由训练',
'Routine': '训练日',
'routine': '训练日',
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/store/useUI.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ let workInt = null
let workTick = null
let workDone = null
const MAX_WORK_OVERTIME_SEC = 15 * 60
const SYNC_BANNER_KEY = 'gym_show_sync_banner'
const readShowSyncBanner = () => {
try { return localStorage.getItem(SYNC_BANNER_KEY) !== '0' } catch { return true }
}

const stopRestTicking = () => {
if (timerInt) clearInterval(timerInt); timerInt = null
Expand Down Expand Up @@ -123,6 +127,7 @@ const runRest = (set, get) => {
export const useUI = create((set, get) => ({
sheets: [], // { id, render:(close)=>JSX, kind:'sheet'|'center', locked }
toastMsg: '',
showSyncBanner: readShowSyncBanner(), // device-local display choice; never part of the synced profile
timer: null, // rest countdown between sets — { left, total, endsAt, forIdx, ready?, paused? }
// forIdx: index of the active entry whose set started the rest (undefined when unknown)
// paused: held at `left`; `endsAt` means nothing until resumeRest sets it again
Expand All @@ -143,6 +148,13 @@ export const useUI = create((set, get) => ({
closeSheet(id) { set(s => ({ sheets: s.sheets.filter(x => x.id !== id) })) },
closeAll() { set({ sheets: [] }) },

setShowSyncBanner(show) {
const value = !!show
if (get().showSyncBanner === value) return
try { localStorage.setItem(SYNC_BANNER_KEY, value ? '1' : '0') } catch { /* keep the in-memory choice */ }
set({ showSyncBanner: value })
},

toast(msg) {
set({ toastMsg: msg })
clearTimeout(toastTm)
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/store/useUI.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest'
import { useUI } from './useUI.js'
import { useStore } from './useStore.js'

describe('device-local sync banner preference', () => {
afterEach(() => {
useUI.getState().setShowSyncBanner(true)
localStorage.removeItem('gym_show_sync_banner')
})

it('persists the display choice on this device', () => {
useUI.getState().setShowSyncBanner(false)
expect(useUI.getState().showSyncBanner).toBe(false)
expect(localStorage.getItem('gym_show_sync_banner')).toBe('0')
useUI.getState().setShowSyncBanner(true)
expect(localStorage.getItem('gym_show_sync_banner')).toBe('1')
})
})

// "Off" has to hold at the timer itself, not at the four places that start one — the same
// reason the rest-after-a-set rule is a shared condition rather than four copies.
describe('rest timer set to Off', () => {
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/views/Settings.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export default function Settings() {
const credsChanged = () => { passkeys.load(); setCredsV(v => v + 1) }
const { update, importConflict, importBackup, setUnit, resetEverything: resetAll, setUser, pullState, pushState, resetDemo } = useStore()
const toast = useUI(s => s.toast)
const showSyncBanner = useUI(s => s.showSyncBanner)
const setShowSyncBanner = useUI(s => s.setShowSyncBanner)
const fileRef = useRef(null)
const importRef = useRef(null)
const wakeOK = wakeLockSupported()
Expand Down Expand Up @@ -541,6 +543,14 @@ export default function Settings() {
</div>
</Section>

{/* The sync banner is a per-device display choice; the full status remains in Settings. */}
<Section title={t('Display')}>
<Row icon="cloud" iconTint="var(--blue)" title={t('Show sync banner')}
subtitle={t('Show connection and sync status at the top of the screen.')}>
<Switch checked={showSyncBanner} onChange={setShowSyncBanner} />
</Row>
</Section>

{/* ---------- data: fill it, bring things over, back it up, wipe it ---------- */}
<Section title={t('Data')}>
<Row icon="sparkles" iconTint="var(--acc)" title={t('Load starter plan')} accessory="chevron" onClick={starterPlanSheet} />
Expand Down
Loading