From 407933024ebfff92d5c9bb6ca254fdb9b4c92021 Mon Sep 17 00:00:00 2001 From: Cainan Whelchel Date: Mon, 27 Oct 2025 18:21:34 -0400 Subject: [PATCH 1/4] Start of mobile logging mode. Adds Logging setting: mobile mode. When set, height of spots rows on spots screen is increased, the text enlarged and reaaranged for easier viewing while mobile. A long press event will log the spot and bring query the callsign. not working: multi-op spots dont log correctly and qsos in list dont have filled in data. Need to add rest of logging code to new modal. --- src/App.jsx | 6 + .../OpSpotsTab/OpSpotsModal.jsx | 304 ++++++++++++++++++ .../OpSpotsTab/OpSpotsTab.jsx | 18 +- .../OpSpotsTab/components/SpotItem.jsx | 154 ++++++--- .../OpSpotsTab/components/SpotList.jsx | 74 ++++- .../OpSpotsTab/components/SpotsPanel.jsx | 8 +- .../screens/LoggingSettingsScreen.jsx | 9 + src/styles/globalStyles.js | 28 ++ 8 files changed, 555 insertions(+), 46 deletions(-) create mode 100644 src/screens/OperationScreens/OpSpotsTab/OpSpotsModal.jsx diff --git a/src/App.jsx b/src/App.jsx index eb7337ad6..8a902f29f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -49,6 +49,7 @@ import SpotsScreen from './screens/SpotsScreen/SpotsScreen' import OpInfoScreen from './screens/OperationScreens/OpInfoScreen' import OperationDetailsScreen from './screens/OperationScreens/OpSettingsTab/OperationDetailsScreen' import OperationLocationScreen from './screens/OperationScreens/OpSettingsTab/OperationLocationScreen' +import OpSpotsModal from './screens/OperationScreens/OpSpotsTab/OpSpotsModal' const Stack = createNativeStackNavigator() @@ -192,6 +193,11 @@ function MainApp ({ navigationTheme }) { options={{ title: 'Settings', headerShown: false }} component={MainSettingsScreen} /> + + ) diff --git a/src/screens/OperationScreens/OpSpotsTab/OpSpotsModal.jsx b/src/screens/OperationScreens/OpSpotsTab/OpSpotsModal.jsx new file mode 100644 index 000000000..45af17c93 --- /dev/null +++ b/src/screens/OperationScreens/OpSpotsTab/OpSpotsModal.jsx @@ -0,0 +1,304 @@ +// cmw + +import { useEffect, useState, useRef, useCallback } from 'react' +import { View, Text, Animated } from 'react-native' +import { TouchableRipple, ProgressBar } from 'react-native-paper' +import { useDispatch, useSelector } from 'react-redux' +import cloneDeep from 'clone-deep' + +import { parseCallsign } from '@ham2k/lib-callsigns' +import { bandForFrequency } from '@ham2k/lib-operation-data' + +import { expandRSTValues, parseStackedCalls } from '../../../tools/callsignTools' +import { annotateQSO, useCallLookup } from '../OpLoggingTab/components/LoggingPanel/useCallLookup.js' +import { trackEvent } from '../../../distro' +import { findHooks } from '../../../extensions/registry' +import { addQSOs, selectQSOs } from '../../../store/qsos' +import { logTimer } from '../../../tools/perfTools' +// import { selectRuntimeOnline } from '../../../store/runtime' +import { selectSettings } from '../../../store/settings' +import { selectOperationCallInfo } from '../../../store/operations' +import { selectVFO } from '../../../store/station/stationSlice' +import { useThemedStyles } from '../../../styles/tools/useThemedStyles' + +const DEBUG = false + +function prepareStyles (themeStyles, themeColor) { + console.log(themeStyles) + + const white = '#fff' + const black = '#000' + const grey = '#bbb' + const grey2 = '#222' + const grey3 = '#333' + + const commonStyles = { + fontSize: themeStyles.normalFontSize, + lineHeight: themeStyles.normalFontSize * 1.3, + borderWidth: DEBUG ? 1 : 0, + color: (themeStyles.theme.dark) ? white : black + } + + const commonButton = { + alignItems: 'center', + flex: 0, + justifyContent: 'center', + color: themeStyles.theme.colors[`on${themeColor}`] + } + + return { + ...themeStyles, + panel: { + backgroundColor: themeStyles.theme.colors[`${themeColor}Container`], + borderBottomColor: themeStyles.theme.colors[`${themeColor}Light`], + borderTopColor: themeStyles.theme.colors[`${themeColor}Light`], + borderBottomWidth: 1, + paddingTop: themeStyles.oneSpace, + paddingBottom: themeStyles.oneSpace, + flexDirection: 'column', + color: themeStyles.theme.colors[`on${themeColor}`] + }, + container: { + paddingHorizontal: themeStyles.oneSpace, + paddingTop: themeStyles.oneSpace, + paddingBottom: themeStyles.oneSpace, + gap: themeStyles.halfSpace + }, + title: { + ...themeStyles.title, + color: (themeStyles.theme.dark) ? '#fff' : '#000', + marginBottom: 40 + }, + buttons: { + log: { + ...commonButton, + width: 200, + height: 175, + backgroundColor: themeStyles.theme.colors.tertiaryContainer, + borderRadius: 10 + }, + cancel: { + ...commonButton, + width: 200, + height: 175, + backgroundColor: (themeStyles.theme.dark) ? '#ff4444ff' : '#9f0101ff' + }, + text: { + fontSize: themeStyles.normalFontSize * 1.3 + } + }, + fields: { + callAndEmoji: { + ...commonStyles, + flexDirection: 'row', + alignItems: 'center', + marginLeft: themeStyles.oneSpace * 1.45, + minWidth: themeStyles.oneSpace * 5 + }, + call: { + ...commonStyles, + ...themeStyles.text.callsign, + fontWeight: 800, + fontSize: themeStyles.normalFontSize * 3.0, + lineHeight: themeStyles.normalFontSize * 2.5 + }, + band: { + ...commonStyles, + flex: 0, + marginBottom: themeStyles.oneSpace * 1 + }, + opName: { + ...commonStyles, + flex: 0, + marginBottom: themeStyles.oneSpace * 2.5, + fontWeight: 700, + fontSize: themeStyles.normalFontSize * 1.8, + lineHeight: themeStyles.normalFontSize * 2.5 + }, + note: { + ...commonStyles, + flex: 0, + marginBottom: themeStyles.oneSpace * 1, + fontWeight: 500, + fontSize: themeStyles.normalFontSize * 1.2, + color: (themeStyles.theme.dark) ? grey : grey3 + }, + mode: { + ...commonStyles, + flex: 0, + marginLeft: themeStyles.oneSpace * 0.2, + width: themeStyles.oneSpace * 5, + textAlign: 'right', + marginRight: themeStyles.oneSpace * 1.4, + color: (themeStyles.theme.dark) ? grey : grey3 + }, + icon: { + ...commonStyles, + flex: 0, + textAlign: 'left', + marginRight: themeStyles.oneSpace * 0.3, + marginLeft: themeStyles.oneSpace * -0.5, + marginTop: themeStyles.oneSpace * 0.2 + }, + label: { + ...commonStyles, + fontSize: themeStyles.normalFontSize * 1.2, + lineHeight: themeStyles.normalFontSize * 1.5, + textAlign: 'center', + marginTop: 10, + marginBottom: 10, + color: (themeStyles.theme.dark) ? grey : grey3 + } + } + } +} + +export default function OpSpotsModal ({ navigation, route }) { + const [isValidQSO, setIsValidQSO] = useState(false) + const themeColor = 'primary' + const styles = useThemedStyles(prepareStyles, themeColor) + const dispatch = useDispatch() + + const operation = route.params.operation + const qsos = useSelector(state => selectQSOs(state, route.params.operation.uuid)) + const settings = useSelector(selectSettings) + const ourInfo = useSelector(state => selectOperationCallInfo(state, operation?.uuid)) + const vfo = useSelector(state => selectVFO(state)) + + const { call, guess, lookup, refs, status, when } = useCallLookup(route.params.qso) + + // console.log('callLookup results') + // console.log([call, guess, lookup, refs, status, when]) + + useEffect(() => { // Validate and analyze the callsign + const { call } = parseStackedCalls(route.params.qso?.their?.call ?? '') + + const callInfo = parseCallsign(call) + + if (callInfo?.baseCall || call.indexOf('?') >= 0) { + setIsValidQSO(true) + } else { + setIsValidQSO(false) + } + }, [route.params.qso?.their?.call]) + + useEffect(() => { + const qso = route.params.qso + + if (isValidQSO && !qso.deleted) { + // setCurrentSecondaryControl(undefined) + + // if (qso?._isNew && qso?._manualTime && qso.startAtMillis) { + // let nextManualTime = qso.startAtMillis + (60 * 1000) + + // if (qsos.length > 0) { + // const diff = Math.abs(qso.startAtMillis - qsos[qsos.length - 1].startAtMillis) + // if (diff >= 1000) { + // nextManualTime = qso.startAtMillis + Math.min(diff, 60 * 5000) + // } + // } + // // No need to await this one, can happen in parallel + // dispatch(setOperationLocalData({ uuid: operation.uuid, _nextManualTime: nextManualTime })) + // } + + delete qso._isNew + delete qso._willBeDeleted + delete qso.deleted + + if (qso.freq) { + qso.band = bandForFrequency(qso.freq) + } + + if (!qso.startAtMillis) qso.startAtMillis = (new Date()).getTime() + qso.startAt = new Date(qso.startAtMillis).toISOString() + if (qso.endAtMillis) qso.endAt = new Date(qso.endAtMillis).toISOString() + qso.our = qso.our || {} + qso.our.call = qso.our.call || ourInfo?.call + qso.our.operatorCall = qso.our.operatorCall || operation.local?.operatorCall + qso.our.sent = expandRSTValues(qso.our.sent, qso.mode) + + qso.their = qso.their || {} + qso.their.sent = expandRSTValues(qso.their.sent, qso.mode) + let lastUUID + + const { call, allCalls } = parseStackedCalls(qso?.their?.call ?? '') + const multiQSOs = [] + for (let i = 0; i < allCalls.length; i++) { + let oneQSO = qso + qso.their.call = call + if (allCalls.length > 1) { // If this is a multi-call QSO, we need to clone and annotate the QSO for each call + oneQSO = cloneDeep(qso) + if (i > 0) oneQSO.uuid = null + oneQSO.their.call = allCalls[i]?.trim() + oneQSO.their.guess = {} + oneQSO.their.lookup = {} + oneQSO = annotateQSO({ qso: oneQSO, online: false, settings, dispatch }) + oneQSO._needsLookup = true + } + multiQSOs.push(oneQSO) + + const eventName = 'add_qso' + + trackEvent(eventName, { their_prefix: oneQSO.their?.entityPrefix ?? oneQSO.their?.guess?.entityPrefix, refs: (oneQSO.refs || []).map(r => r.type).join(',') }) + + // lastUUID = oneQSO.uuid + } + + const activities = findHooks('activity').filter(activity => activity.processQSOBeforeSaveWithDispatch || activity.processQSOBeforeSave) + for (const activity of activities) { + for (const q of multiQSOs) { + if (activity.processQSOBeforeSaveWithDispatch) { + activity.processQSOBeforeSaveWithDispatch({ qso: q, operation, qsos, vfo, settings, dispatch }) + } else { + activity.processQSOBeforeSave({ qso: q, operation, qsos, vfo, settings }) + } + } + } + + setTimeout(() => { + // Add the QSO to the operation, and set a new QSO + // But leave enough time for blur effects to take place before being overwritten by the new setQSO + // Just 10ms did not seemed to be enough in tests, but 50ms is fine. + + dispatch(addQSOs({ uuid: operation.uuid, qsos: multiQSOs })) + if (DEBUG) logTimer('submit', 'handleSubmit added QSOs') + + // Let queue management decide what to do next + // setQSO(undefined, { otherStateChanges: { lastUUID, callStack } }) + }, 50) + + // if (DEBUG) + // logTimer('submit', 'handleSubmit after setQSO') + } + }, [dispatch, isValidQSO, operation, ourInfo?.call, qsos, route.params.qso, settings, vfo]) + + return ( + + Spot Logged + {route.params.qso.band} : {route.params.qso.mode} + {/* {route.params.qso.their.call}{route.params.qso.their.guess.emoji} */} + + {route.params.qso.their?.call ?? '?'} + + {guess?.name} + {guess?.note && + <> + {guess?.note} + + } + {route.params.qso.spot.label} + navigation.goBack()}> + Press here to return to spots. + + + ) +} diff --git a/src/screens/OperationScreens/OpSpotsTab/OpSpotsTab.jsx b/src/screens/OperationScreens/OpSpotsTab/OpSpotsTab.jsx index dac8ac444..0a293feae 100644 --- a/src/screens/OperationScreens/OpSpotsTab/OpSpotsTab.jsx +++ b/src/screens/OperationScreens/OpSpotsTab/OpSpotsTab.jsx @@ -42,7 +42,23 @@ export default function OpSpotsTab ({ navigation, route }) { } }, [navigation, route?.params, extraSpotInfoHooks, dispatch, online, settings]) + const handleLongPress = useCallback(async ({ spot }) => { + if (spot._ourSpot) return + + for (const hook of extraSpotInfoHooks) { + await hook.extraSpotInfo({ online, settings, dispatch, spot }) + } + + if (settings.mobileMode === true) { + if (route?.params?.splitView) { + navigation.navigate('Operation', { ...route?.params, qso: { ...spot, our: undefined, _suggestedKey: spot.key, key: undefined } }) + } else { + navigation.navigate('OpSpotModal', { ...route?.params, qso: { ...spot, our: undefined, _suggestedKey: spot.key, key: undefined, _forceLog: true } }) + } + } + }, [navigation, route?.params, extraSpotInfoHooks, dispatch, online, settings]) + return ( - + ) } diff --git a/src/screens/OperationScreens/OpSpotsTab/components/SpotItem.jsx b/src/screens/OperationScreens/OpSpotsTab/components/SpotItem.jsx index 97fbcbbca..0136308be 100644 --- a/src/screens/OperationScreens/OpSpotsTab/components/SpotItem.jsx +++ b/src/screens/OperationScreens/OpSpotsTab/components/SpotItem.jsx @@ -10,10 +10,13 @@ import { Icon, Text } from 'react-native-paper' import { View } from 'react-native' import { partsForFreqInMHz } from '../../../../tools/frequencyFormats' -import { fmtDateTimeRelative } from '../../../../tools/timeFormats' +import { fmtDateTimeRelative, prepareTimeValue } from '../../../../tools/timeFormats' import { paperNameOrHam2KIcon, H2kPressable } from '../../../../ui' -const SpotItem = React.memo(function QSOItem({ spot, onPress, styles, extendedWidth }) { +export function guessItemHeight (qso, styles) { + return styles.doubleRow.height + styles.doubleRow.borderBottomWidth +} +const SpotItem = React.memo(function QSOItem ({ spot, onPress, styles, extendedWidth, onLongPress, settings }) { const freqParts = useMemo(() => partsForFreqInMHz(spot.freq), [spot.freq]) if (spot?.their?.call === 'W8WR') spot.their.call = 'N2Y' @@ -73,49 +76,120 @@ const SpotItem = React.memo(function QSOItem({ spot, onPress, styles, extendedWi return workedStyles }, [spot, styles]) + function getTimeColor (millis) { + const t1 = prepareTimeValue(millis) + const t2 = prepareTimeValue(new Date()) + + if (t1 && t2) { + const diff = t2 - t1 + + if (diff > (20 * 60 * 1000)) { + return styles.mobile.time.oldest + } else if (diff > (15 * 60 * 1000)) { + return styles.mobile.time.old + } else if (diff <= (2 * 60 * 1000)) { + return styles.mobile.time.new + } + } + return styles.mobile.time.normal + }; + return ( - onPress && onPress({ spot })}> - - - - {freqParts[0] && ( - {freqParts[0]} - )} - {freqParts[1] && ( - .{freqParts[1]} - )} - {freqParts[2] && ( - .{freqParts[2]} - )} - - - {spot.their?.call ?? '?'} - {spot.their?.guess?.emoji && ( - {spot.their?.guess?.emoji} - )} - {spot.spot.callLabel ?? ''} + onPress && onPress({ spot })} + onLongPress={() => onLongPress && onLongPress({ spot })} + // rippleColor="rgba(0, 255, 255, .32)" + rippleColor={ (settings.mobileMode === true) ? 'rgba(0, 255, 255, .32)' : '' } + // background={{ + // color: 'rgba(0, 255, 255, .32)', + // foreground: true + // }} + > + {settings.mobileMode === true ? ( + + + + + {freqParts[0]} + + + .{freqParts[1]} + .{freqParts[2]} + + + + + + + {spot.their?.call ?? '?'} + {spot.their?.guess?.emoji && ( + {spot.their?.guess?.emoji} + )} + + {fmtDateTimeRelative(spot.spot?.timeInMillis, { roundTo: 'minutes' })} + + + {spot.mode} + {spot.spots.filter(s => s?.icon).map(subSpot => ( + + + + ))} + + {spot.spot.emoji} + {spot.spot.label} + + - {fmtDateTimeRelative(spot.spot?.timeInMillis, { roundTo: 'minutes' })} - - {spot.band} - {spot.mode} - {spot.spots.filter(s => s?.icon).map(subSpot => ( - - + ) : ( + + + + {freqParts[0] && ( + {freqParts[0]} + )} + {freqParts[1] && ( + .{freqParts[1]} + )} + {freqParts[2] && ( + .{freqParts[2]} + )} + + + {spot.their?.call ?? '?'} + {spot.their?.guess?.emoji && ( + {spot.their?.guess?.emoji} + )} + {spot.spot.callLabel ?? ''} - ))} - - {spot.spot.emoji} - {spot.spot.label} - + {fmtDateTimeRelative(spot.spot?.timeInMillis, { roundTo: 'minutes' })} + + + {spot.band} + {spot.mode} + {spot.spots.filter(s => s?.icon).map(subSpot => ( + + + + ))} + + {spot.spot.emoji} + {spot.spot.label} + + - + )} ) }) diff --git a/src/screens/OperationScreens/OpSpotsTab/components/SpotList.jsx b/src/screens/OperationScreens/OpSpotsTab/components/SpotList.jsx index 3d298fd0a..a0fd7ad6d 100644 --- a/src/screens/OperationScreens/OpSpotsTab/components/SpotList.jsx +++ b/src/screens/OperationScreens/OpSpotsTab/components/SpotList.jsx @@ -16,7 +16,7 @@ import { useThemedStyles } from '../../../../styles/tools/useThemedStyles' import SpotItem from './SpotItem' import SpotHeader from './SpotHeader' -export default function SpotList ({ sections, loading, refresh, style, onPress }) { +export default function SpotList ({ sections, loading, refresh, style, onPress, onLongPress, settings }) { const styles = useThemedStyles(_prepareStyles, style) const safeArea = useSafeAreaInsets() @@ -42,9 +42,9 @@ export default function SpotList ({ sections, loading, refresh, style, onPress } const renderRow = useCallback(({ item, index }) => { const spot = item return ( - + ) - }, [styles, onPress, extendedWidth, paddingRight, paddingLeft]) + }, [onPress, onLongPress, styles, paddingRight, paddingLeft, extendedWidth, settings]) // eslint-disable-next-line react-hooks/exhaustive-deps const calculateLayout = useCallback( @@ -79,7 +79,7 @@ export default function SpotList ({ sections, loading, refresh, style, onPress } ) } -function _prepareStyles (themeStyles, style) { +function _prepareStyles (themeStyles, style, deviceColorScheme) { const DEBUG = false const commonStyles = { @@ -88,6 +88,16 @@ function _prepareStyles (themeStyles, style) { borderWidth: DEBUG ? 1 : 0 } + const mobileStyles = { + fontSize: themeStyles.normalFontSize * 1.2, + lineHeight: themeStyles.normalFontSize * 1.5, + borderWidth: 0 // debug + } + + // console.log(themeStyles) + // console.log(style) + // console.log(deviceColorScheme) + return { ...themeStyles, doubleRow: { @@ -103,6 +113,62 @@ function _prepareStyles (themeStyles, style) { paddingRight: 0, justifyContent: 'center' }, + mobile: { + freq: { + ...mobileStyles, + ...themeStyles.text.numbers, + ...themeStyles.text.lighter, + flexDirection: 'column', + width: themeStyles.oneSpace * 11.15, + textAlign: 'center', + alignItems: 'center' + }, + freqMHz: { + ...mobileStyles, + fontWeight: '600', + textAlign: 'right', + fontSize: themeStyles.normalFontSize * 1.0 + }, + freqKHz: { + ...mobileStyles, + textAlign: 'right', + fontWeight: '700' + }, + freqHz: { + ...mobileStyles, + fontWeight: '600', + textAlign: 'right', + fontSize: themeStyles.normalFontSize + }, + call: { + ...mobileStyles + }, + label: { + fontSize: themeStyles.normalFontSize * 0.9 + }, + mode: { + fontSize: themeStyles.normalFontSize * 0.9, + flex: 0, + marginLeft: themeStyles.oneSpace * 0.5, + width: themeStyles.oneSpace * 4, + textAlign: 'right', + marginRight: themeStyles.oneSpace * 1.0 + }, + time: { + oldest: { + color: (themeStyles.theme.dark) ? '#f11818ff' : '#e70606ff' + }, + old: { + color: (themeStyles.theme.dark) ? '#ff733fff' : '#9a4e0cff' + }, + normal: { + color: (themeStyles.theme.dark) ? '#CCC' : '#222' + }, + new: { + color: (themeStyles.theme.dark) ? '#11dda0ff' : '#128700ff' + } + } + }, fields: { freq: { ...commonStyles, diff --git a/src/screens/OperationScreens/OpSpotsTab/components/SpotsPanel.jsx b/src/screens/OperationScreens/OpSpotsTab/components/SpotsPanel.jsx index 99701a3b4..d1c2e9f53 100644 --- a/src/screens/OperationScreens/OpSpotsTab/components/SpotsPanel.jsx +++ b/src/screens/OperationScreens/OpSpotsTab/components/SpotsPanel.jsx @@ -66,7 +66,7 @@ function prepareStyles (baseStyles, themeColor, style) { } } -export default function SpotsPanel ({ operation, qsos, sections, onSelect, style }) { +export default function SpotsPanel ({ operation, qsos, sections, onSelect, style, onLongPress }) { const themeColor = 'tertiary' const styles = useThemedStyles(prepareStyles, themeColor, style) @@ -316,6 +316,10 @@ export default function SpotsPanel ({ operation, qsos, sections, onSelect, style onSelect && onSelect({ spot }) }, [onSelect]) + const handleLongPress = useCallback(({ spot }) => { + onLongPress && onLongPress({ spot }) + }, [onLongPress]) + return ( {showControls ? ( @@ -375,6 +379,8 @@ export default function SpotsPanel ({ operation, qsos, sections, onSelect, style loading={spotsState.loading} refresh={refresh} onPress={handlePress} + onLongPress={handleLongPress} + settings={settings} style={{ paddingBottom: style?.paddingBottom, paddingRight: style?.paddingRight, diff --git a/src/screens/SettingsScreens/screens/LoggingSettingsScreen.jsx b/src/screens/SettingsScreens/screens/LoggingSettingsScreen.jsx index f5c5f799e..65f2c3312 100644 --- a/src/screens/SettingsScreens/screens/LoggingSettingsScreen.jsx +++ b/src/screens/SettingsScreens/screens/LoggingSettingsScreen.jsx @@ -68,6 +68,7 @@ export default function LoggingSettingsScreen ({ navigation, splitView }) { onDialogDone={() => setCurrentDialog('')} /> )} + dispatch(setSettings({ jumpAfterRST: !settings.jumpAfterRST }))} /> + dispatch(setSettings({ mobileMode: value }))} + onPress={() => dispatch(setSettings({ mobileMode: !settings.mobileMode }))} + /> + { justifyContent: 'space-between', width: '100%' }, + doubleRowMobileMode: { + height: oneSpace * 10, + borderBottomWidth: 1, + borderBottomColor: theme.colors.outline, + flexDirection: 'row', + justifyContent: 'space-between', + width: '100%', + }, doubleRowInnerRow: { // borderWidth: 1, height: oneSpace * 2.6, flexDirection: 'row', width: '100%' }, + doubleRowInnerRowMobile: { + //borderWidth: 1, + height: oneSpace * 2.8, + flexDirection: 'row', + width: '80%' + }, + doubleRowMobileModeLeft: { + //borderWidth: 1, + height: '100%', + flexDirection: 'row', + width:'20%', + alignItems: 'center' + }, + doubleRowMobileModeRight: { + //borderWidth: 1, + height: '100%', + flexDirection: 'column', + width:'100%', + justifyContent: 'center' + }, rowText: { fontSize: normalFontSize, fontFamily: 'Roboto', From d975eb4b0355c3ae9dfe36114d1acec636b60fd4 Mon Sep 17 00:00:00 2001 From: Cainan Whelchel Date: Wed, 29 Oct 2025 18:48:14 -0400 Subject: [PATCH 2/4] Instead of having mobile mode logic in SpotItem.jsx, refactor the Mobile SpotItem component into its own Component Logic for picking the component to display is pulled up into SpotList. Also finish fixing the logging for Multiple-OP spots. --- .../OpSpotsTab/OpSpotsModal.jsx | 187 ++++++++++-------- .../OpSpotsTab/OpSpotsTab.jsx | 2 +- .../OpSpotsTab/components/MobileSpotItem.jsx | 153 ++++++++++++++ .../OpSpotsTab/components/SpotItem.jsx | 147 ++++---------- .../OpSpotsTab/components/SpotList.jsx | 9 +- 5 files changed, 304 insertions(+), 194 deletions(-) create mode 100644 src/screens/OperationScreens/OpSpotsTab/components/MobileSpotItem.jsx diff --git a/src/screens/OperationScreens/OpSpotsTab/OpSpotsModal.jsx b/src/screens/OperationScreens/OpSpotsTab/OpSpotsModal.jsx index 45af17c93..1d8919af6 100644 --- a/src/screens/OperationScreens/OpSpotsTab/OpSpotsModal.jsx +++ b/src/screens/OperationScreens/OpSpotsTab/OpSpotsModal.jsx @@ -1,27 +1,27 @@ // cmw -import { useEffect, useState, useRef, useCallback } from 'react' -import { View, Text, Animated } from 'react-native' -import { TouchableRipple, ProgressBar } from 'react-native-paper' +import { useEffect, useState, useCallback } from 'react' +import { View, Text } from 'react-native' import { useDispatch, useSelector } from 'react-redux' import cloneDeep from 'clone-deep' import { parseCallsign } from '@ham2k/lib-callsigns' import { bandForFrequency } from '@ham2k/lib-operation-data' -import { expandRSTValues, parseStackedCalls } from '../../../tools/callsignTools' import { annotateQSO, useCallLookup } from '../OpLoggingTab/components/LoggingPanel/useCallLookup.js' +import { H2kPressable, H2kMarkdown } from '../../../ui' import { trackEvent } from '../../../distro' import { findHooks } from '../../../extensions/registry' import { addQSOs, selectQSOs } from '../../../store/qsos' -import { logTimer } from '../../../tools/perfTools' -// import { selectRuntimeOnline } from '../../../store/runtime' import { selectSettings } from '../../../store/settings' import { selectOperationCallInfo } from '../../../store/operations' import { selectVFO } from '../../../store/station/stationSlice' +import { logTimer } from '../../../tools/perfTools' +import { expandRSTValues, parseStackedCalls } from '../../../tools/callsignTools' import { useThemedStyles } from '../../../styles/tools/useThemedStyles' const DEBUG = false +let submitTimeout function prepareStyles (themeStyles, themeColor) { console.log(themeStyles) @@ -29,7 +29,7 @@ function prepareStyles (themeStyles, themeColor) { const white = '#fff' const black = '#000' const grey = '#bbb' - const grey2 = '#222' + // const grey2 = '#222' const grey3 = '#333' const commonStyles = { @@ -165,10 +165,7 @@ export default function OpSpotsModal ({ navigation, route }) { const ourInfo = useSelector(state => selectOperationCallInfo(state, operation?.uuid)) const vfo = useSelector(state => selectVFO(state)) - const { call, guess, lookup, refs, status, when } = useCallLookup(route.params.qso) - - // console.log('callLookup results') - // console.log([call, guess, lookup, refs, status, when]) + const { guess } = useCallLookup(route.params.qso) useEffect(() => { // Validate and analyze the callsign const { call } = parseStackedCalls(route.params.qso?.their?.call ?? '') @@ -182,95 +179,113 @@ export default function OpSpotsModal ({ navigation, route }) { } }, [route.params.qso?.their?.call]) + // Since our fields and logic often perform some async work, + // we need to wait a few milliseconds before submitting to ensure all async work is complete. + // But we can't just use a timeout, because we need the function to bind to the latest values. + // So we use a state variable and a callback function to set it and an effect to actually submit.. + const [doSubmit, setDoSubmit] = useState(false) + + const handleSubmit = useCallback(() => { // + if (submitTimeout) clearTimeout(submitTimeout) + + submitTimeout = setTimeout(() => { + setDoSubmit(true) + }, 50) + }, [setDoSubmit]) + useEffect(() => { - const qso = route.params.qso - - if (isValidQSO && !qso.deleted) { - // setCurrentSecondaryControl(undefined) - - // if (qso?._isNew && qso?._manualTime && qso.startAtMillis) { - // let nextManualTime = qso.startAtMillis + (60 * 1000) - - // if (qsos.length > 0) { - // const diff = Math.abs(qso.startAtMillis - qsos[qsos.length - 1].startAtMillis) - // if (diff >= 1000) { - // nextManualTime = qso.startAtMillis + Math.min(diff, 60 * 5000) - // } - // } - // // No need to await this one, can happen in parallel - // dispatch(setOperationLocalData({ uuid: operation.uuid, _nextManualTime: nextManualTime })) - // } - - delete qso._isNew - delete qso._willBeDeleted - delete qso.deleted - - if (qso.freq) { - qso.band = bandForFrequency(qso.freq) - } + if (!doSubmit) return + + setDoSubmit(false) + + // copy out the params' qso to operate on it + const qso = cloneDeep(route.params.qso) - if (!qso.startAtMillis) qso.startAtMillis = (new Date()).getTime() - qso.startAt = new Date(qso.startAtMillis).toISOString() - if (qso.endAtMillis) qso.endAt = new Date(qso.endAtMillis).toISOString() - qso.our = qso.our || {} - qso.our.call = qso.our.call || ourInfo?.call - qso.our.operatorCall = qso.our.operatorCall || operation.local?.operatorCall - qso.our.sent = expandRSTValues(qso.our.sent, qso.mode) - - qso.their = qso.their || {} - qso.their.sent = expandRSTValues(qso.their.sent, qso.mode) - let lastUUID - - const { call, allCalls } = parseStackedCalls(qso?.their?.call ?? '') - const multiQSOs = [] - for (let i = 0; i < allCalls.length; i++) { - let oneQSO = qso - qso.their.call = call - if (allCalls.length > 1) { // If this is a multi-call QSO, we need to clone and annotate the QSO for each call - oneQSO = cloneDeep(qso) - if (i > 0) oneQSO.uuid = null - oneQSO.their.call = allCalls[i]?.trim() - oneQSO.their.guess = {} - oneQSO.their.lookup = {} - oneQSO = annotateQSO({ qso: oneQSO, online: false, settings, dispatch }) - oneQSO._needsLookup = true + setTimeout(async () => { // Run inside a setTimeout to allow for async functions + if (isValidQSO && !qso.deleted) { + delete qso._isNew + delete qso._willBeDeleted + delete qso.deleted + + if (qso.freq) { + qso.band = bandForFrequency(qso.freq) } - multiQSOs.push(oneQSO) - const eventName = 'add_qso' + if (!qso.startAtMillis) qso.startAtMillis = (new Date()).getTime() + qso.startAt = new Date(qso.startAtMillis).toISOString() + if (qso.endAtMillis) qso.endAt = new Date(qso.endAtMillis).toISOString() + qso.our = qso.our || {} + qso.our.call = qso.our.call || ourInfo?.call + qso.our.operatorCall = qso.our.operatorCall || operation.local?.operatorCall + qso.our.sent = expandRSTValues(qso.our.sent, qso.mode) + + qso.their = qso.their || {} + qso.their.sent = expandRSTValues(qso.their.sent, qso.mode) + // let lastUUID + + const { call, allCalls } = parseStackedCalls(qso?.their?.call ?? '') + + const multiQSOs = [] + + for (let i = 0; i < allCalls.length; i++) { + let oneQSO = qso + qso.their.call = call + if (allCalls.length > 1) { // If this is a multi-call QSO, we need to clone and annotate the QSO for each call + console.log('preclone ') + console.log(qso) + oneQSO = cloneDeep(qso) + console.log('postclone ') + console.log(oneQSO) + if (i > 0) oneQSO.uuid = null + oneQSO.their.call = allCalls[i]?.trim() + oneQSO.their.guess = {} + oneQSO.their.lookup = {} + oneQSO = await annotateQSO({ qso: oneQSO, online: false, settings, dispatch }) + console.log('this here is the problem') + console.log(oneQSO) + oneQSO._needsLookup = true + } + multiQSOs.push(oneQSO) + + const eventName = 'add_qso' - trackEvent(eventName, { their_prefix: oneQSO.their?.entityPrefix ?? oneQSO.their?.guess?.entityPrefix, refs: (oneQSO.refs || []).map(r => r.type).join(',') }) + trackEvent(eventName, { their_prefix: oneQSO.their?.entityPrefix ?? oneQSO.their?.guess?.entityPrefix, refs: (oneQSO.refs || []).map(r => r.type).join(',') }) // lastUUID = oneQSO.uuid - } + } - const activities = findHooks('activity').filter(activity => activity.processQSOBeforeSaveWithDispatch || activity.processQSOBeforeSave) - for (const activity of activities) { - for (const q of multiQSOs) { - if (activity.processQSOBeforeSaveWithDispatch) { - activity.processQSOBeforeSaveWithDispatch({ qso: q, operation, qsos, vfo, settings, dispatch }) - } else { - activity.processQSOBeforeSave({ qso: q, operation, qsos, vfo, settings }) + const activities = findHooks('activity').filter(activity => activity.processQSOBeforeSaveWithDispatch || activity.processQSOBeforeSave) + for (const activity of activities) { + for (const q of multiQSOs) { + if (activity.processQSOBeforeSaveWithDispatch) { + activity.processQSOBeforeSaveWithDispatch({ qso: q, operation, qsos, vfo, settings, dispatch }) + } else { + activity.processQSOBeforeSave({ qso: q, operation, qsos, vfo, settings }) + } } } - } - setTimeout(() => { + setTimeout(() => { // Add the QSO to the operation, and set a new QSO // But leave enough time for blur effects to take place before being overwritten by the new setQSO // Just 10ms did not seemed to be enough in tests, but 50ms is fine. - dispatch(addQSOs({ uuid: operation.uuid, qsos: multiQSOs })) - if (DEBUG) logTimer('submit', 'handleSubmit added QSOs') + console.log('checking multiQSOs') + console.log(multiQSOs) - // Let queue management decide what to do next - // setQSO(undefined, { otherStateChanges: { lastUUID, callStack } }) - }, 50) + dispatch(addQSOs({ uuid: operation.uuid, qsos: multiQSOs })) + if (DEBUG) logTimer('submit', 'handleSubmit added QSOs') + }, 50) // if (DEBUG) // logTimer('submit', 'handleSubmit after setQSO') - } - }, [dispatch, isValidQSO, operation, ourInfo?.call, qsos, route.params.qso, settings, vfo]) + } + }, 0) + }, [dispatch, doSubmit, isValidQSO, operation, ourInfo?.call, qsos, route.params.qso, settings, vfo]) + + useEffect(() => { + handleSubmit() + }, [handleSubmit]) return ( {guess?.name} {guess?.note && <> - {guess?.note} + {guess?.note} } {route.params.qso.spot.label} - navigation.goBack()}> + navigation.goBack()} + rippleColor='rgba(0, 255, 255, .32)' + > Press here to return to spots. - + ) } diff --git a/src/screens/OperationScreens/OpSpotsTab/OpSpotsTab.jsx b/src/screens/OperationScreens/OpSpotsTab/OpSpotsTab.jsx index 0a293feae..91f95904b 100644 --- a/src/screens/OperationScreens/OpSpotsTab/OpSpotsTab.jsx +++ b/src/screens/OperationScreens/OpSpotsTab/OpSpotsTab.jsx @@ -53,7 +53,7 @@ export default function OpSpotsTab ({ navigation, route }) { if (route?.params?.splitView) { navigation.navigate('Operation', { ...route?.params, qso: { ...spot, our: undefined, _suggestedKey: spot.key, key: undefined } }) } else { - navigation.navigate('OpSpotModal', { ...route?.params, qso: { ...spot, our: undefined, _suggestedKey: spot.key, key: undefined, _forceLog: true } }) + navigation.navigate('OpSpotModal', { operation, qso: { ...spot, our: undefined, _suggestedKey: spot.key, key: undefined } }) } } }, [navigation, route?.params, extraSpotInfoHooks, dispatch, online, settings]) diff --git a/src/screens/OperationScreens/OpSpotsTab/components/MobileSpotItem.jsx b/src/screens/OperationScreens/OpSpotsTab/components/MobileSpotItem.jsx new file mode 100644 index 000000000..f9cd13721 --- /dev/null +++ b/src/screens/OperationScreens/OpSpotsTab/components/MobileSpotItem.jsx @@ -0,0 +1,153 @@ +/* + * Copyright ©️ 2024-2025 Sebastian Delmont + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + * If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import React, { useMemo } from 'react' +import { Icon, Text } from 'react-native-paper' + +import { View } from 'react-native' +import { partsForFreqInMHz } from '../../../../tools/frequencyFormats' +import { fmtDateTimeRelative, prepareTimeValue } from '../../../../tools/timeFormats' +import { paperNameOrHam2KIcon, H2kPressable } from '../../../../ui' + +/** + * When settings Mobile Mode is true, this is used to render spots in SpotList. + * + * It's the same as SpotItem but with some padding and different layout for better viewing + * while mobile with a phone or tablet in a mounted holder. + */ +const MobileSpotItem = React.memo(function QSOItemMobile ({ spot, styles, onPress, onLongPress }) { + const freqParts = useMemo(() => partsForFreqInMHz(spot.freq), [spot.freq]) + + if (spot?.their?.call === 'W8WR') spot.their.call = 'N2Y' + + const { commonStyle, modeStyle, refStyle, callStyle } = useMemo(() => { + const workedStyles = {} + if (spot.spot?.type === 'self') { + workedStyles.commonStyle = { + color: styles.colors.tertiary, + opacity: 0.7 + } + } + if (spot.spot?.type === 'duplicate') { + workedStyles.commonStyle = { + textDecorationLine: 'line-through', + textDecorationColor: styles.colors.onBackground, + opacity: 0.6 + } + } + // no band on mobile spot item + // if (spot.spot?.flags?.newBand) { + // workedStyles.bandStyle = { + // fontWeight: 'bold', + // color: styles.colors.important + // } + // } + if (spot.spot?.flags?.newMode) { + workedStyles.modeStyle = { + fontWeight: 'bold', + color: styles.colors.important + } + } + if (spot.spot?.flags?.specialCall) { + workedStyles.callStyle = { + color: styles.colors.bands['40m'] + } + workedStyles.refStyle = { + color: styles.colors.bands['40m'] + } + } + if (spot.spot?.flags?.newRef || spot.spot?.flags?.newDay) { + workedStyles.refStyle = { + fontWeight: 'bold', + color: styles.colors.important + } + } + if (spot.spot?.flags?.newMult) { + workedStyles.callStyle = { + fontWeight: 'bold', + color: styles.colors.bands['10m'] + } + workedStyles.refStyle = { + fontWeight: 'bold', + color: styles.colors.bands['10m'] + } + } + + return workedStyles + }, [spot, styles]) + + function getTimeColor (millis) { + const t1 = prepareTimeValue(millis) + const t2 = prepareTimeValue(new Date()) + + if (t1 && t2) { + const diff = t2 - t1 + + if (diff > (20 * 60 * 1000)) { + return styles.mobile.time.oldest + } else if (diff > (15 * 60 * 1000)) { + return styles.mobile.time.old + } else if (diff <= (2 * 60 * 1000)) { + return styles.mobile.time.new + } + } + return styles.mobile.time.normal + }; + + return ( + onPress && onPress({ spot })} + onLongPress={() => onLongPress && onLongPress({ spot })} + rippleColor='rgba(0, 255, 255, .32)' + > + + + + + {freqParts[0]} + + + .{freqParts[1]} + {freqParts[2] !== '000' && ( + .{freqParts[2]} + )} + + + + + + + {spot.their?.call ?? '?'} + {spot.their?.guess?.emoji && ( + {spot.their?.guess?.emoji} + )} + + {fmtDateTimeRelative(spot.spot?.timeInMillis, { roundTo: 'minutes' })} + + + {spot.mode} + {spot.spots.filter(s => s?.icon).map(subSpot => ( + + + + ))} + + {spot.spot.emoji} + {spot.spot.label} + + + + + + ) +}) +export default MobileSpotItem diff --git a/src/screens/OperationScreens/OpSpotsTab/components/SpotItem.jsx b/src/screens/OperationScreens/OpSpotsTab/components/SpotItem.jsx index 0136308be..9e4f1f978 100644 --- a/src/screens/OperationScreens/OpSpotsTab/components/SpotItem.jsx +++ b/src/screens/OperationScreens/OpSpotsTab/components/SpotItem.jsx @@ -10,13 +10,13 @@ import { Icon, Text } from 'react-native-paper' import { View } from 'react-native' import { partsForFreqInMHz } from '../../../../tools/frequencyFormats' -import { fmtDateTimeRelative, prepareTimeValue } from '../../../../tools/timeFormats' +import { fmtDateTimeRelative } from '../../../../tools/timeFormats' import { paperNameOrHam2KIcon, H2kPressable } from '../../../../ui' export function guessItemHeight (qso, styles) { return styles.doubleRow.height + styles.doubleRow.borderBottomWidth } -const SpotItem = React.memo(function QSOItem ({ spot, onPress, styles, extendedWidth, onLongPress, settings }) { +const SpotItem = React.memo(function QSOItem ({ spot, onPress, styles, extendedWidth, settings }) { const freqParts = useMemo(() => partsForFreqInMHz(spot.freq), [spot.freq]) if (spot?.their?.call === 'W8WR') spot.their.call = 'N2Y' @@ -76,120 +76,51 @@ const SpotItem = React.memo(function QSOItem ({ spot, onPress, styles, extendedW return workedStyles }, [spot, styles]) - function getTimeColor (millis) { - const t1 = prepareTimeValue(millis) - const t2 = prepareTimeValue(new Date()) - - if (t1 && t2) { - const diff = t2 - t1 - - if (diff > (20 * 60 * 1000)) { - return styles.mobile.time.oldest - } else if (diff > (15 * 60 * 1000)) { - return styles.mobile.time.old - } else if (diff <= (2 * 60 * 1000)) { - return styles.mobile.time.new - } - } - return styles.mobile.time.normal - }; - return ( onPress && onPress({ spot })} - onLongPress={() => onLongPress && onLongPress({ spot })} - // rippleColor="rgba(0, 255, 255, .32)" - rippleColor={ (settings.mobileMode === true) ? 'rgba(0, 255, 255, .32)' : '' } - // background={{ - // color: 'rgba(0, 255, 255, .32)', - // foreground: true - // }} > - {settings.mobileMode === true ? ( - - - - - {freqParts[0]} - - - .{freqParts[1]} - .{freqParts[2]} - - - - - - - {spot.their?.call ?? '?'} - {spot.their?.guess?.emoji && ( - {spot.their?.guess?.emoji} - )} - - {fmtDateTimeRelative(spot.spot?.timeInMillis, { roundTo: 'minutes' })} - - - {spot.mode} - {spot.spots.filter(s => s?.icon).map(subSpot => ( - - - - ))} - - {spot.spot.emoji} - {spot.spot.label} - - + + + + {freqParts[0] && ( + {freqParts[0]} + )} + {freqParts[1] && ( + .{freqParts[1]} + )} + {freqParts[2] && ( + .{freqParts[2]} + )} + + + {spot.their?.call ?? '?'} + {spot.their?.guess?.emoji && ( + {spot.their?.guess?.emoji} + )} + {spot.spot.callLabel ?? ''} + {fmtDateTimeRelative(spot.spot?.timeInMillis, { roundTo: 'minutes' })} - ) : ( - - - - {freqParts[0] && ( - {freqParts[0]} - )} - {freqParts[1] && ( - .{freqParts[1]} - )} - {freqParts[2] && ( - .{freqParts[2]} - )} - - - {spot.their?.call ?? '?'} - {spot.their?.guess?.emoji && ( - {spot.their?.guess?.emoji} - )} - {spot.spot.callLabel ?? ''} + + {spot.band} + {spot.mode} + {spot.spots.filter(s => s?.icon).map(subSpot => ( + + - {fmtDateTimeRelative(spot.spot?.timeInMillis, { roundTo: 'minutes' })} - - - {spot.band} - {spot.mode} - {spot.spots.filter(s => s?.icon).map(subSpot => ( - - - - ))} - - {spot.spot.emoji} - {spot.spot.label} - - + ))} + + {spot.spot.emoji} + {spot.spot.label} + - )} + ) }) diff --git a/src/screens/OperationScreens/OpSpotsTab/components/SpotList.jsx b/src/screens/OperationScreens/OpSpotsTab/components/SpotList.jsx index a0fd7ad6d..47cd2ed9c 100644 --- a/src/screens/OperationScreens/OpSpotsTab/components/SpotList.jsx +++ b/src/screens/OperationScreens/OpSpotsTab/components/SpotList.jsx @@ -14,6 +14,7 @@ import getItemLayout from 'react-native-get-item-layout-section-list' import { useThemedStyles } from '../../../../styles/tools/useThemedStyles' import SpotItem from './SpotItem' +import MobileSpotItem from './MobileSpotItem' import SpotHeader from './SpotHeader' export default function SpotList ({ sections, loading, refresh, style, onPress, onLongPress, settings }) { @@ -42,7 +43,12 @@ export default function SpotList ({ sections, loading, refresh, style, onPress, const renderRow = useCallback(({ item, index }) => { const spot = item return ( - + (settings.mobileMode ? ( + + ) : ( + + ) + ) ) }, [onPress, onLongPress, styles, paddingRight, paddingLeft, extendedWidth, settings]) @@ -144,6 +150,7 @@ function _prepareStyles (themeStyles, style, deviceColorScheme) { ...mobileStyles }, label: { + flex: 1, fontSize: themeStyles.normalFontSize * 0.9 }, mode: { From 0b271067ef5fb46d53889ce7f126b25c4bc2ea36 Mon Sep 17 00:00:00 2001 From: Cainan Whelchel Date: Thu, 30 Oct 2025 21:36:11 -0400 Subject: [PATCH 3/4] Change of name to Big Thumb mode. Change log modal to make user press log or cancel. The new modal gives hunter chance to pull up info without logging. --- .../OpSpotsTab/OpSpotsModal.jsx | 60 ++++++++++++------- .../OpSpotsTab/OpSpotsTab.jsx | 4 +- .../OpSpotsTab/components/SpotList.jsx | 2 +- .../screens/LoggingSettingsScreen.jsx | 16 ++--- 4 files changed, 50 insertions(+), 32 deletions(-) diff --git a/src/screens/OperationScreens/OpSpotsTab/OpSpotsModal.jsx b/src/screens/OperationScreens/OpSpotsTab/OpSpotsModal.jsx index 1d8919af6..0a03e734a 100644 --- a/src/screens/OperationScreens/OpSpotsTab/OpSpotsModal.jsx +++ b/src/screens/OperationScreens/OpSpotsTab/OpSpotsModal.jsx @@ -24,7 +24,7 @@ const DEBUG = false let submitTimeout function prepareStyles (themeStyles, themeColor) { - console.log(themeStyles) + // console.log(themeStyles) const white = '#fff' const black = '#000' @@ -43,7 +43,9 @@ function prepareStyles (themeStyles, themeColor) { alignItems: 'center', flex: 0, justifyContent: 'center', - color: themeStyles.theme.colors[`on${themeColor}`] + borderRadius: 10, + borderWidth: 3, + borderColor: grey3 } return { @@ -72,19 +74,24 @@ function prepareStyles (themeStyles, themeColor) { buttons: { log: { ...commonButton, - width: 200, + width: 175, height: 175, - backgroundColor: themeStyles.theme.colors.tertiaryContainer, - borderRadius: 10 + backgroundColor: (themeStyles.theme.dark) ? '#149c21ff' : '#25582aff' }, cancel: { ...commonButton, - width: 200, + width: 175, height: 175, - backgroundColor: (themeStyles.theme.dark) ? '#ff4444ff' : '#9f0101ff' + backgroundColor: (themeStyles.theme.dark) ? '#b83202ff' : '#9f0101ff' }, text: { - fontSize: themeStyles.normalFontSize * 1.3 + ...commonStyles, + fontSize: themeStyles.normalFontSize * 1.3, + lineHeight: themeStyles.normalFontSize * 1.5, + textAlign: 'center', + marginTop: 10, + marginBottom: 10, + color: white } }, fields: { @@ -275,17 +282,16 @@ export default function OpSpotsModal ({ navigation, route }) { dispatch(addQSOs({ uuid: operation.uuid, qsos: multiQSOs })) if (DEBUG) logTimer('submit', 'handleSubmit added QSOs') + + // logging is done at this point. we can navigate away from popup + navigation.goBack() }, 50) // if (DEBUG) // logTimer('submit', 'handleSubmit after setQSO') } }, 0) - }, [dispatch, doSubmit, isValidQSO, operation, ourInfo?.call, qsos, route.params.qso, settings, vfo]) - - useEffect(() => { - handleSubmit() - }, [handleSubmit]) + }, [dispatch, doSubmit, isValidQSO, operation, ourInfo?.call, qsos, route.params.qso, settings, vfo, navigation]) return ( @@ -311,13 +316,26 @@ export default function OpSpotsModal ({ navigation, route }) { } {route.params.qso.spot.label} - navigation.goBack()} - rippleColor='rgba(0, 255, 255, .32)' - > - Press here to return to spots. - + + + { + // this triggers the log code above and navigates back to the spots list + handleSubmit() + }} + rippleColor='rgba(0, 255, 255, .32)' + > + Log it! + + navigation.goBack()} + rippleColor='rgba(218, 68, 3, 0.32)' + > + Cancel + + ) } diff --git a/src/screens/OperationScreens/OpSpotsTab/OpSpotsTab.jsx b/src/screens/OperationScreens/OpSpotsTab/OpSpotsTab.jsx index 91f95904b..8881248be 100644 --- a/src/screens/OperationScreens/OpSpotsTab/OpSpotsTab.jsx +++ b/src/screens/OperationScreens/OpSpotsTab/OpSpotsTab.jsx @@ -49,14 +49,14 @@ export default function OpSpotsTab ({ navigation, route }) { await hook.extraSpotInfo({ online, settings, dispatch, spot }) } - if (settings.mobileMode === true) { + if (settings.bigThumbMode === true) { if (route?.params?.splitView) { navigation.navigate('Operation', { ...route?.params, qso: { ...spot, our: undefined, _suggestedKey: spot.key, key: undefined } }) } else { navigation.navigate('OpSpotModal', { operation, qso: { ...spot, our: undefined, _suggestedKey: spot.key, key: undefined } }) } } - }, [navigation, route?.params, extraSpotInfoHooks, dispatch, online, settings]) + }, [navigation, route?.params, extraSpotInfoHooks, dispatch, online, settings, operation]) return ( diff --git a/src/screens/OperationScreens/OpSpotsTab/components/SpotList.jsx b/src/screens/OperationScreens/OpSpotsTab/components/SpotList.jsx index 47cd2ed9c..5a50458b0 100644 --- a/src/screens/OperationScreens/OpSpotsTab/components/SpotList.jsx +++ b/src/screens/OperationScreens/OpSpotsTab/components/SpotList.jsx @@ -43,7 +43,7 @@ export default function SpotList ({ sections, loading, refresh, style, onPress, const renderRow = useCallback(({ item, index }) => { const spot = item return ( - (settings.mobileMode ? ( + (settings.bigThumbMode ? ( ) : ( diff --git a/src/screens/SettingsScreens/screens/LoggingSettingsScreen.jsx b/src/screens/SettingsScreens/screens/LoggingSettingsScreen.jsx index 65f2c3312..1b1beb2cf 100644 --- a/src/screens/SettingsScreens/screens/LoggingSettingsScreen.jsx +++ b/src/screens/SettingsScreens/screens/LoggingSettingsScreen.jsx @@ -55,6 +55,14 @@ export default function LoggingSettingsScreen ({ navigation, splitView }) { onPress={() => dispatch(setSettings({ leftieMode: !settings.leftieMode }))} /> + dispatch(setSettings({ bigThumbMode: value }))} + onPress={() => dispatch(setSettings({ bigThumbMode: !settings.bigThumbMode }))} + /> + dispatch(setSettings({ jumpAfterRST: !settings.jumpAfterRST }))} /> - dispatch(setSettings({ mobileMode: value }))} - onPress={() => dispatch(setSettings({ mobileMode: !settings.mobileMode }))} - /> - Date: Tue, 11 Nov 2025 16:59:35 -0500 Subject: [PATCH 4/4] Rename fixes plus tweak to title on logging modal. Add myself to credits --- RELEASE-NOTES.json | 3 +- .../OpSpotsTab/OpSpotsModal.jsx | 9 +++-- .../OpSpotsTab/OpSpotsTab.jsx | 2 +- ...bileSpotItem.jsx => BigThumbsSpotItem.jsx} | 34 +++++++++---------- .../OpSpotsTab/components/SpotList.jsx | 20 +++++------ .../screens/CreditsSettingsScreen.jsx | 6 ++++ .../screens/LoggingSettingsScreen.jsx | 17 +++++----- 7 files changed, 52 insertions(+), 39 deletions(-) rename src/screens/OperationScreens/OpSpotsTab/components/{MobileSpotItem.jsx => BigThumbsSpotItem.jsx} (78%) diff --git a/RELEASE-NOTES.json b/RELEASE-NOTES.json index 555f5c35b..b7e53f77a 100644 --- a/RELEASE-NOTES.json +++ b/RELEASE-NOTES.json @@ -7,7 +7,8 @@ "Auto self-spotting every 10 minutes", "Allow spaces in commands (try 'SPOT HERE')", "Fix missing keystrokes on Android when pressing 'send' too fast", - "Note expansion now works with callsign stacking" + "Note expansion now works with callsign stacking", + "Added Big Thumbs mode to enable logging from the spot list" ] }, "25.9.3": { diff --git a/src/screens/OperationScreens/OpSpotsTab/OpSpotsModal.jsx b/src/screens/OperationScreens/OpSpotsTab/OpSpotsModal.jsx index 0a03e734a..8e5b7ec45 100644 --- a/src/screens/OperationScreens/OpSpotsTab/OpSpotsModal.jsx +++ b/src/screens/OperationScreens/OpSpotsTab/OpSpotsModal.jsx @@ -1,4 +1,9 @@ -// cmw +/* + * Copyright ©️ 2025 Cainan Whelchel + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + * If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ import { useEffect, useState, useCallback } from 'react' import { View, Text } from 'react-native' @@ -303,7 +308,7 @@ export default function OpSpotsModal ({ navigation, route }) { fontSize: 'large' }] } > - Spot Logged + Log Spot? {route.params.qso.band} : {route.params.qso.mode} {/* {route.params.qso.their.call}{route.params.qso.their.guess.emoji} */} diff --git a/src/screens/OperationScreens/OpSpotsTab/OpSpotsTab.jsx b/src/screens/OperationScreens/OpSpotsTab/OpSpotsTab.jsx index 8881248be..145aff6d3 100644 --- a/src/screens/OperationScreens/OpSpotsTab/OpSpotsTab.jsx +++ b/src/screens/OperationScreens/OpSpotsTab/OpSpotsTab.jsx @@ -49,7 +49,7 @@ export default function OpSpotsTab ({ navigation, route }) { await hook.extraSpotInfo({ online, settings, dispatch, spot }) } - if (settings.bigThumbMode === true) { + if (settings.bigThumbsMode === true) { if (route?.params?.splitView) { navigation.navigate('Operation', { ...route?.params, qso: { ...spot, our: undefined, _suggestedKey: spot.key, key: undefined } }) } else { diff --git a/src/screens/OperationScreens/OpSpotsTab/components/MobileSpotItem.jsx b/src/screens/OperationScreens/OpSpotsTab/components/BigThumbsSpotItem.jsx similarity index 78% rename from src/screens/OperationScreens/OpSpotsTab/components/MobileSpotItem.jsx rename to src/screens/OperationScreens/OpSpotsTab/components/BigThumbsSpotItem.jsx index f9cd13721..e0f7cc45a 100644 --- a/src/screens/OperationScreens/OpSpotsTab/components/MobileSpotItem.jsx +++ b/src/screens/OperationScreens/OpSpotsTab/components/BigThumbsSpotItem.jsx @@ -1,5 +1,5 @@ /* - * Copyright ©️ 2024-2025 Sebastian Delmont + * Copyright ©️ 2025 Cainan Whelchel * * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. * If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -14,12 +14,12 @@ import { fmtDateTimeRelative, prepareTimeValue } from '../../../../tools/timeFor import { paperNameOrHam2KIcon, H2kPressable } from '../../../../ui' /** - * When settings Mobile Mode is true, this is used to render spots in SpotList. + * When Big Thumbs Mode is enabled, this is used to render spots in SpotList. * * It's the same as SpotItem but with some padding and different layout for better viewing - * while mobile with a phone or tablet in a mounted holder. + * while the device is further away from the user. */ -const MobileSpotItem = React.memo(function QSOItemMobile ({ spot, styles, onPress, onLongPress }) { +const BigThumbsSpotItem = React.memo(function QSOItemMobile ({ spot, styles, onPress, onLongPress }) { const freqParts = useMemo(() => partsForFreqInMHz(spot.freq), [spot.freq]) if (spot?.their?.call === 'W8WR') spot.their.call = 'N2Y' @@ -39,7 +39,7 @@ const MobileSpotItem = React.memo(function QSOItemMobile ({ spot, styles, onPres opacity: 0.6 } } - // no band on mobile spot item + // no band on big thumbs spot item // if (spot.spot?.flags?.newBand) { // workedStyles.bandStyle = { // fontWeight: 'bold', @@ -88,14 +88,14 @@ const MobileSpotItem = React.memo(function QSOItemMobile ({ spot, styles, onPres const diff = t2 - t1 if (diff > (20 * 60 * 1000)) { - return styles.mobile.time.oldest + return styles.bigThumbs.time.oldest } else if (diff > (15 * 60 * 1000)) { - return styles.mobile.time.old + return styles.bigThumbs.time.old } else if (diff <= (2 * 60 * 1000)) { - return styles.mobile.time.new + return styles.bigThumbs.time.new } } - return styles.mobile.time.normal + return styles.bigThumbs.time.normal }; return ( @@ -106,14 +106,14 @@ const MobileSpotItem = React.memo(function QSOItemMobile ({ spot, styles, onPres > - + - {freqParts[0]} + {freqParts[0]} - .{freqParts[1]} + .{freqParts[1]} {freqParts[2] !== '000' && ( - .{freqParts[2]} + .{freqParts[2]} )} @@ -121,7 +121,7 @@ const MobileSpotItem = React.memo(function QSOItemMobile ({ spot, styles, onPres - {spot.their?.call ?? '?'} + {spot.their?.call ?? '?'} {spot.their?.guess?.emoji && ( {spot.their?.guess?.emoji} )} @@ -129,7 +129,7 @@ const MobileSpotItem = React.memo(function QSOItemMobile ({ spot, styles, onPres {fmtDateTimeRelative(spot.spot?.timeInMillis, { roundTo: 'minutes' })} - {spot.mode} + {spot.mode} {spot.spots.filter(s => s?.icon).map(subSpot => ( ))} - + {spot.spot.emoji} {spot.spot.label} @@ -150,4 +150,4 @@ const MobileSpotItem = React.memo(function QSOItemMobile ({ spot, styles, onPres ) }) -export default MobileSpotItem +export default BigThumbsSpotItem diff --git a/src/screens/OperationScreens/OpSpotsTab/components/SpotList.jsx b/src/screens/OperationScreens/OpSpotsTab/components/SpotList.jsx index 5a50458b0..2b5580aa5 100644 --- a/src/screens/OperationScreens/OpSpotsTab/components/SpotList.jsx +++ b/src/screens/OperationScreens/OpSpotsTab/components/SpotList.jsx @@ -14,7 +14,7 @@ import getItemLayout from 'react-native-get-item-layout-section-list' import { useThemedStyles } from '../../../../styles/tools/useThemedStyles' import SpotItem from './SpotItem' -import MobileSpotItem from './MobileSpotItem' +import BigThumbsSpotItem from './BigThumbsSpotItem' import SpotHeader from './SpotHeader' export default function SpotList ({ sections, loading, refresh, style, onPress, onLongPress, settings }) { @@ -43,8 +43,8 @@ export default function SpotList ({ sections, loading, refresh, style, onPress, const renderRow = useCallback(({ item, index }) => { const spot = item return ( - (settings.bigThumbMode ? ( - + (settings.bigThumbsMode ? ( + ) : ( ) @@ -94,7 +94,7 @@ function _prepareStyles (themeStyles, style, deviceColorScheme) { borderWidth: DEBUG ? 1 : 0 } - const mobileStyles = { + const bigThumbsStyles = { fontSize: themeStyles.normalFontSize * 1.2, lineHeight: themeStyles.normalFontSize * 1.5, borderWidth: 0 // debug @@ -119,9 +119,9 @@ function _prepareStyles (themeStyles, style, deviceColorScheme) { paddingRight: 0, justifyContent: 'center' }, - mobile: { + bigThumbs: { freq: { - ...mobileStyles, + ...bigThumbsStyles, ...themeStyles.text.numbers, ...themeStyles.text.lighter, flexDirection: 'column', @@ -130,24 +130,24 @@ function _prepareStyles (themeStyles, style, deviceColorScheme) { alignItems: 'center' }, freqMHz: { - ...mobileStyles, + ...bigThumbsStyles, fontWeight: '600', textAlign: 'right', fontSize: themeStyles.normalFontSize * 1.0 }, freqKHz: { - ...mobileStyles, + ...bigThumbsStyles, textAlign: 'right', fontWeight: '700' }, freqHz: { - ...mobileStyles, + ...bigThumbsStyles, fontWeight: '600', textAlign: 'right', fontSize: themeStyles.normalFontSize }, call: { - ...mobileStyles + ...bigThumbsStyles }, label: { flex: 1, diff --git a/src/screens/SettingsScreens/screens/CreditsSettingsScreen.jsx b/src/screens/SettingsScreens/screens/CreditsSettingsScreen.jsx index 008ef4725..f90397ccd 100644 --- a/src/screens/SettingsScreens/screens/CreditsSettingsScreen.jsx +++ b/src/screens/SettingsScreens/screens/CreditsSettingsScreen.jsx @@ -120,6 +120,12 @@ export default function CreditsSettingsScreen ({ navigation, splitView }) { leftIcon="account" onPress={() => navigation.navigate('CallInfo', { call: 'W8NI' })} /> + navigation.navigate('CallInfo', { call: 'N9FZ' })} + /> diff --git a/src/screens/SettingsScreens/screens/LoggingSettingsScreen.jsx b/src/screens/SettingsScreens/screens/LoggingSettingsScreen.jsx index 1b1beb2cf..5a5a46feb 100644 --- a/src/screens/SettingsScreens/screens/LoggingSettingsScreen.jsx +++ b/src/screens/SettingsScreens/screens/LoggingSettingsScreen.jsx @@ -55,14 +55,15 @@ export default function LoggingSettingsScreen ({ navigation, splitView }) { onPress={() => dispatch(setSettings({ leftieMode: !settings.leftieMode }))} /> - dispatch(setSettings({ bigThumbMode: value }))} - onPress={() => dispatch(setSettings({ bigThumbMode: !settings.bigThumbMode }))} - /> - + {settings.devMode && ( + dispatch(setSettings({ bigThumbsMode: value }))} + onPress={() => dispatch(setSettings({ bigThumbMode: !settings.bigThumbsMode }))} + /> + )}