diff --git a/RELEASE-NOTES.json b/RELEASE-NOTES.json
index 555f5c35..b7e53f77 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/App.jsx b/src/App.jsx
index eb7337ad..8a902f29 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 00000000..8e5b7ec4
--- /dev/null
+++ b/src/screens/OperationScreens/OpSpotsTab/OpSpotsModal.jsx
@@ -0,0 +1,346 @@
+/*
+ * 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'
+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 { 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 { 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)
+
+ 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',
+ borderRadius: 10,
+ borderWidth: 3,
+ borderColor: grey3
+ }
+
+ 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: 175,
+ height: 175,
+ backgroundColor: (themeStyles.theme.dark) ? '#149c21ff' : '#25582aff'
+ },
+ cancel: {
+ ...commonButton,
+ width: 175,
+ height: 175,
+ backgroundColor: (themeStyles.theme.dark) ? '#b83202ff' : '#9f0101ff'
+ },
+ text: {
+ ...commonStyles,
+ fontSize: themeStyles.normalFontSize * 1.3,
+ lineHeight: themeStyles.normalFontSize * 1.5,
+ textAlign: 'center',
+ marginTop: 10,
+ marginBottom: 10,
+ color: white
+ }
+ },
+ 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 { guess } = useCallLookup(route.params.qso)
+
+ 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])
+
+ // 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(() => {
+ if (!doSubmit) return
+
+ setDoSubmit(false)
+
+ // copy out the params' qso to operate on it
+ const qso = cloneDeep(route.params.qso)
+
+ 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)
+ }
+
+ 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(',') })
+
+ // 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.
+
+ console.log('checking multiQSOs')
+ console.log(multiQSOs)
+
+ 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, navigation])
+
+ return (
+
+ Log Spot?
+ {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}
+
+
+ {
+ // 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 dac8ac44..145aff6d 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.bigThumbsMode === 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, operation])
+
return (
-
+
)
}
diff --git a/src/screens/OperationScreens/OpSpotsTab/components/BigThumbsSpotItem.jsx b/src/screens/OperationScreens/OpSpotsTab/components/BigThumbsSpotItem.jsx
new file mode 100644
index 00000000..e0f7cc45
--- /dev/null
+++ b/src/screens/OperationScreens/OpSpotsTab/components/BigThumbsSpotItem.jsx
@@ -0,0 +1,153 @@
+/*
+ * 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 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 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 the device is further away from the user.
+ */
+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'
+
+ 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 big thumbs 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.bigThumbs.time.oldest
+ } else if (diff > (15 * 60 * 1000)) {
+ return styles.bigThumbs.time.old
+ } else if (diff <= (2 * 60 * 1000)) {
+ return styles.bigThumbs.time.new
+ }
+ }
+ return styles.bigThumbs.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 BigThumbsSpotItem
diff --git a/src/screens/OperationScreens/OpSpotsTab/components/SpotItem.jsx b/src/screens/OperationScreens/OpSpotsTab/components/SpotItem.jsx
index 97fbcbbc..9e4f1f97 100644
--- a/src/screens/OperationScreens/OpSpotsTab/components/SpotItem.jsx
+++ b/src/screens/OperationScreens/OpSpotsTab/components/SpotItem.jsx
@@ -13,7 +13,10 @@ import { partsForFreqInMHz } from '../../../../tools/frequencyFormats'
import { fmtDateTimeRelative } 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, settings }) {
const freqParts = useMemo(() => partsForFreqInMHz(spot.freq), [spot.freq])
if (spot?.their?.call === 'W8WR') spot.their.call = 'N2Y'
@@ -74,7 +77,9 @@ const SpotItem = React.memo(function QSOItem({ spot, onPress, styles, extendedWi
}, [spot, styles])
return (
- onPress && onPress({ spot })}>
+ onPress && onPress({ spot })}
+ >
diff --git a/src/screens/OperationScreens/OpSpotsTab/components/SpotList.jsx b/src/screens/OperationScreens/OpSpotsTab/components/SpotList.jsx
index 3d298fd0..2b5580aa 100644
--- a/src/screens/OperationScreens/OpSpotsTab/components/SpotList.jsx
+++ b/src/screens/OperationScreens/OpSpotsTab/components/SpotList.jsx
@@ -14,9 +14,10 @@ import getItemLayout from 'react-native-get-item-layout-section-list'
import { useThemedStyles } from '../../../../styles/tools/useThemedStyles'
import SpotItem from './SpotItem'
+import BigThumbsSpotItem from './BigThumbsSpotItem'
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 +43,14 @@ export default function SpotList ({ sections, loading, refresh, style, onPress }
const renderRow = useCallback(({ item, index }) => {
const spot = item
return (
-
+ (settings.bigThumbsMode ? (
+
+ ) : (
+
+ )
+ )
)
- }, [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 +85,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 +94,16 @@ function _prepareStyles (themeStyles, style) {
borderWidth: DEBUG ? 1 : 0
}
+ const bigThumbsStyles = {
+ 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 +119,63 @@ function _prepareStyles (themeStyles, style) {
paddingRight: 0,
justifyContent: 'center'
},
+ bigThumbs: {
+ freq: {
+ ...bigThumbsStyles,
+ ...themeStyles.text.numbers,
+ ...themeStyles.text.lighter,
+ flexDirection: 'column',
+ width: themeStyles.oneSpace * 11.15,
+ textAlign: 'center',
+ alignItems: 'center'
+ },
+ freqMHz: {
+ ...bigThumbsStyles,
+ fontWeight: '600',
+ textAlign: 'right',
+ fontSize: themeStyles.normalFontSize * 1.0
+ },
+ freqKHz: {
+ ...bigThumbsStyles,
+ textAlign: 'right',
+ fontWeight: '700'
+ },
+ freqHz: {
+ ...bigThumbsStyles,
+ fontWeight: '600',
+ textAlign: 'right',
+ fontSize: themeStyles.normalFontSize
+ },
+ call: {
+ ...bigThumbsStyles
+ },
+ label: {
+ flex: 1,
+ 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 99701a3b..d1c2e9f5 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/CreditsSettingsScreen.jsx b/src/screens/SettingsScreens/screens/CreditsSettingsScreen.jsx
index 008ef472..f90397cc 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 f5c5f799..5a5a46fe 100644
--- a/src/screens/SettingsScreens/screens/LoggingSettingsScreen.jsx
+++ b/src/screens/SettingsScreens/screens/LoggingSettingsScreen.jsx
@@ -55,6 +55,15 @@ export default function LoggingSettingsScreen ({ navigation, splitView }) {
onPress={() => dispatch(setSettings({ leftieMode: !settings.leftieMode }))}
/>
+ {settings.devMode && (
+ dispatch(setSettings({ bigThumbsMode: value }))}
+ onPress={() => dispatch(setSettings({ bigThumbMode: !settings.bigThumbsMode }))}
+ />
+ )}
setCurrentDialog('')}
/>
)}
+
{
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',